import numpy as np
import matplotlib.pyplot as plt
from scipy.integrate import odeint
from sympy import *
Studying one-dimensional and two-dimensional systems
Non-linear dynamics is broadly the study of systems whose behaviour is described by complicated non-linear functions. Since most of the world is non-linear, this is a very useful field of mathematics for biology. In this notebook, we will see how graphical methods can be used to study non-linear systems of equations. Those interested in details can refer to Strogatz’s excellent book for details.
One dimensional systems
One-dimensional systems are equations of the form
\[
\dot{x} = f(x)
\]
where \(\dot{x}\) denotes the derivative of \(x\) with respect to time and \(f\) is a (non-linear) function. For instance, the logistic equation
\[ \dot{x} = x(1-x) \]
is an example of a one-dimensional non-linear system. Let us see how we may study this system.
def logistic(x,t):
return x*(1-x)
Finding fixed points diagrammatically
Often, we are only interesed in the behavior of our biological system at equilibrium (or over very long timescales). In this case, we are interested in finding points at which \(\dot{x} = 0\) (the quantity \(x\) is no longer changing). Such points are called fixed points of the system. To find fixed points of a one-dimensional system, we can plot \(\dot{x}\) vs \(x\). The points at which this curve intersects the x-axis are then the fixed points of our system.
= np.arange(-0.01,1.01,0.001)
xarray = [logistic(x,0) for x in xarray]
xdotarray
plt.plot(xarray,xdotarray)=0,color='g',linestyle='--')
plt.axhline(y'x')
plt.xlabel('x dot')
plt.ylabel( plt.show()
Thus, we see that the only fixed points of this system are \(x=0\) and \(x=1\).
We can also ask which fixed points of the system are attractors. If we begin at, say, \(x=0.01\), will the system move towards \(x=0\) or \(x=1\)? This too can be answered diagramatically. Notice, in the plot above, that \(\dot{x}\) is positive when \(x \in [0,1]\). Thus, if \(x\) is between \(0\) and \(1\), it will increase (since the rate of change is positive) towards \(x = 1\). However, if \(x>1\), then \(\dot{x}\) is negative, meaning that \(x\) tends to decrease. This means that systems with \(x>1\) tend to decrease towards \(x=1\) - the point \(x=1\) is an attractor!
We can confirm this by numerically integrating the logistic equation for a variety of initial conditions
def logistic_trajectory(x0list,t=10,tstep=0.01):
for x0 in x0list:
= np.arange(0,t,tstep)
xlist = odeint(func=logistic,y0=[x0],t=np.arange(0,t,tstep))
ylist ='x0 = '+str(x0))
plt.plot(xlist,ylist,label
='upper right')
plt.legend(loc'time')
plt.xlabel('x')
plt.ylabel(=1,color='g',linestyle='--')
plt.axhline(y
plt.show()
0.02,0.1,0.25,1.5,2]) logistic_trajectory([
A more complicated example with a single tunable parameter
Consider now the more complicated system
\[ \dot{x} = rx - x^3 \]
where \(r\) is a model parameter.
def xdot(x,t,r):
return r*x - x**3
The fixed points
To begin, let us study the fixed points of the system. As before, we do it graphically
def xdot_vs_x(rlist):
for r in np.arange(rlist[0],rlist[1],rlist[2]):
= np.arange(-10,10,0.01)
x = []
xdotlist for val in x:
0,r))
xdotlist.append(xdot(val,
='r = '+str(r))
plt.plot(x,xdotlist,label=0,color='g',linestyle='--')
plt.axhline(y=0,color='g',linestyle='--')
plt.axvline(x='upper right')
plt.legend(loc'x')
plt.xlabel('x dot')
plt.ylabel(-5,5])
plt.xlim([-10,10])
plt.ylim([ plt.show()
-6,6.1,6]) xdot_vs_x([
The number of fixed points changes depending on the parameter!
from celluloid import Camera
= np.arange(-10,10,0.1)
rlist = plt.figure(figsize = (8,8))
fig -0.1,1.1)
plt.ylim("x")
plt.xlabel("x dot")
plt.ylabel(= Camera(fig)
camera for r in rlist:
= np.arange(-10,10,0.1)
xlist = [xdot(x,0,r) for x in xlist]
ylist ='r')
plt.plot(xlist,ylist,color=0,color='g',linestyle='--')
plt.axhline(y=0,color='g',linestyle='--')
plt.axvline(x'$r = '+str(np.round(r,1))+'$'],loc='upper right')
plt.legend(['x')
plt.xlabel('x dot')
plt.ylabel(-5,5])
plt.xlim([-10,10])
plt.ylim([
camera.snap() plt.close()
You can play around with it here:
"animation.html"] = "jshtml"
plt.rcParams[= camera.animate()
animation animation
Solving the ODE
Obtaining an analytic solution
This can also be seen by actually solving the differential equation. I will use sympy to analytically solve the differential equation
#Solving the given ODE symbolically
= symbols('n r')
n,r
init_printing()= r*n - n**3
n_dot
solve(n_dot,n)
\(\displaystyle \left[ 0, \ - \sqrt{r}, \ \sqrt{r}\right]\)
Numeric integration to find trajectories
Since solving ODEs analytically is not always feasible, we could also use numerical integration to obtain the trajectory, i.e the values of \(x(t)\) as \(t\) varies, given an initial value \(x_0\). Here, I use the odeint package from scipy.
def trajectory(rlist,x0,t=10,tstep=0.01):
= np.arange(0,t,tstep)
xlist for r in rlist:
= odeint(func=xdot,y0=[x0],t=np.arange(0,t,tstep),args=tuple([r]))
ylist ='r = '+str(r))
plt.plot(xlist,ylist,label
='upper right')
plt.legend(loc'time')
plt.xlabel('x')
plt.ylabel( plt.show()
-4,4.1,4),1) trajectory(np.arange(
Dependence on parameters
Finally, we can examine how the number of fixed points varies as we vary parameters. In our case, the system has a single parameter: \(r\).
We examine the relationship between fixed points and parameter values by plotting a bifurcation diagram. In this case, we plot \(r\) against the corresponding equilibrium values of \(x\), for various initial values \(x(0)\).
def bifurcation(r_range):
= np.arange(r_range[0],r_range[1],r_range[2])
rvals = []
eq_list = []
r_list for r in rvals:
for x in np.arange(-10,10.1,5):
= np.unique(odeint(func=xdot,y0=[x],t=np.arange(0,1000,1),args=tuple([r]))[900:1000])
unique_sols for sol in unique_sols:
eq_list.append(sol)
r_list.append(r)
='g',s=2)
plt.scatter(r_list,eq_list,color'r')
plt.xlabel('x*')
plt.ylabel(
plt.show()
-4,4.1,0.1]) bifurcation([
This diagram tells us that when r is less than 0, there is a single fixed point present at x = 0. At r = 0, the behaviour changes, and two new fixed points appear for all values of r > 0. Values of \(r\) at which such changes occur are known as bifurcation points.
Two dimensional systems
Two dimensional systems are systems of the form:
\[
\begin{align*}
\dot{x} = f(x,y) \\
\dot{y} = g(x,y)
\end{align*}
\]
As before, we can begin by plotting the phase portrait. Here, the phase portrait is two-dimensional, and you can think of \(f(x,y)\) and \(g(x,y)\) as representing the flow on a plane.
def twoD_phase_portrait(var,var_dot,xlims=[-10,10,40],ylims=[-10,10,40],plot_nullclines=True,save=False,filename='2D_portrait.png'):
#convert the symbolic math into a python function
= lambdify(var,var_dot,'numpy')
dvar
#The range of values in which you want the phase portrait
= np.linspace(xlims[0],xlims[1],xlims[2])
x_in = np.linspace(ylims[0],ylims[1],ylims[2])
y_in
= np.meshgrid(x_in,y_in)
X_in, Y_in
#Compute and store the derivatives at each point
= np.zeros(X_in.shape), np.zeros(Y_in.shape)
u,v = X_in.shape
shape_1, shape_2 for i in range(shape_1):
for j in range(shape_2):
= X_in[i,j]
pt_x = Y_in[i,j]
pt_y = dvar(pt_x,pt_y)
derivative = derivative[0]
u[i,j] = derivative[1]
v[i,j]
= np.hypot(u,v)
colors ==0] = 1 #To avoid division by 0
colors[colors
#Normalize arrow lengths
#u /= colors
#v /= colors
=(8,8))
plt.figure(figsize='Reds',pivot='mid')
plt.quiver(X_in,Y_in,u,v,colors,cmap
if plot_nullclines:
#Compute and plot the nullclines
= solve(var_dot[0])
x_nullcline = solve(var_dot[1])
y_nullcline
= "x nullcline"
xnull_label for sol in range(len(x_nullcline)):
try:
= lambdify(var[1],x_nullcline[sol][x],'numpy')
null_func = []
null for i in range(len(y_in)):
null.append(null_func(y_in[i]))'b',label=xnull_label)
plt.plot(null,y_in,= '_nolegend_' #To avoid duplicate labels
xnull_label except KeyError:
= lambdify(var[0],y_nullcline[sol][y],'numpy')
null_func = []
null for i in range(len(x_in)):
null.append(null_func(x_in[i]))'b',label=xnull_label)
plt.plot(x_in,null,= '_nolegend_' #To avoid duplicate labels
xnull_label
= 'y nullcline'
ynull_label for sol in range(len(y_nullcline)):
try:
= lambdify(var[1],y_nullcline[sol][x],'numpy')
null_func = []
null for i in range(len(y_in)):
null.append(null_func(y_in[i]))'r',label=ynull_label)
plt.plot(null,y_in,= '_nolegend_' #To avoid duplicate labels
ynull_label except KeyError:
= lambdify(var[0],y_nullcline[sol][y],'numpy')
null_func = []
null for i in range(len(x_in)):
null.append(null_func(x_in[i]))'r',label=ynull_label)
plt.plot(x_in,null,= '_nolegend_' #To avoid duplicate labels
ynull_label
str(var[0]))
plt.xlabel(str(var[1]))
plt.ylabel(0:2])
plt.xlim(xlims[0:2])
plt.ylim(ylims[="Derivative")
plt.colorbar(label
if plot_nullclines:
=[1.3,1])
plt.legend(bbox_to_anchorif save:
plt.savefig(filename)
= symbols('x y')
x,y
init_printing()
#write the differential equations here
= cos(x-y), sin(x*y+1)
x_dot, y_dot
x_dot, y_dot
\(\displaystyle \left( \cos{\left(x - y \right)}, \ \sin{\left(x y + 1 \right)}\right)\)
=[-10,10,50],ylims=[-10,10,50],plot_nullclines=False) twoD_phase_portrait([x,y],[x_dot,y_dot],xlims
Nullclines
One useful notion is that of a nullcline. Nullclines are defined as the curves where either \(\dot{x} = 0\) or \(\dot{y} = 0\). Fixed points are points at which nullclines intersect.
=[-10,10,100],ylims=[-10,10,100]) twoD_phase_portrait([x,y],[x_dot,y_dot],xlims
#Number of fixed points should equal the number of times the nullclines intersect
= solve([x_dot,y_dot],[x,y])
fixed_points len(fixed_points)
\(\displaystyle 8\)