Simulating Dynamical Systems I

Week 10

Integrating ODEs & Simulating Dynamical Systems

What is a dynamical system?

Systems can be characterized by the specific relation between their input(s) and output(s). A static system has an output that only depends on its input. A mechanical example of such a system is an idealized, massless spring. The length of the spring depends only on the force (the input) that acts upon it. Change the input force, and the length of the spring will change, and this will happen instantaneously (obviously a massless spring is a theoretical construct). A system becomes dynamical (it is said to have dynamics) when a mass is attached to the spring (Figure 1 below). Now the position of the mass (and equivalently, the length of the spring) is no longer directly dependent on the input force, but is also tied to the acceleration of the mass, which in turn depends on the sum of all forces acting upon it (the sum of the input force and the force due to the spring). The net force depends on the position of the mass, which depends on the length of the spring, which depends on the spring force. The property that acceleration of the mass depends on its position makes this a dynamical system.

Figure 1: A simple mass-spring system

Dynamical systems can be characterized by differential equations that relate the state derivatives (e.g. velocity—the first derivative, or acceleration—the second derivative) to the state variables (e.g. position). The differential equation for the spring-mass system depicted above is:

\[m\ddot{x} = -kx + mg \tag{1}\]

Where \(x\) is the position of the mass \(m\) (the length of the spring), \(\ddot{x}\) is the second derivative of position (i.e. acceleration), \(k\) is a constant (related to the stiffness of the spring), and \(g\) is the graviational constant (9.81 m/s/s).

The system is said to be a second order system, as the highest derivative that appears in the differential equation describing the system, is two. The position \(x\) and its time derivative \(\dot{x}\) are called states of the system, and \(\dot{x}\) and \(\ddot{x}\) are called state derivatives.

Most systems out there in nature are dynamical systems. For example most chemical reactions under natural circumstances are dynamical: the rate of change of a chemical reaction depends on the amount of chemical present, in other words the state derivative is proportional to the state. Dynamical systems exist in biology as well. For example the rate of change of the population size of a certain species likely depends on its population size.

Dynamical equations are often described by a set of coupled differential equations. For example, the reproduction rate of rabbits (state derivative 1) depends on the population of rabbits (state 1, \(x\)) and on the population size of foxes (state 2, \(y\)). The reproduction rate of foxes (state derivative 2, \(\dot{y}\)) depends on the population of foxes (state 2, \(y\)) and also on the population of rabbits (state 1, \(x\)). In this case we have two coupled first-order differential equations, and hence a system of order two. The so-called predator-prey model is also known as the Lotka-Volterra equations:

\[\begin{aligned} \dot{x} &= x(\alpha - \beta y)\\ \label{eq:lotkavolterra1} \dot{y} &= -y(\gamma - \delta x) \end{aligned} \tag{2}\]

Where \(x\) is the population of rabbits, \(y\) is the population of foxes,

Why make models?

There are two main reasons why we would want to model a physical system using mathematical equations, one being practical and one mostly theoretical. The practical use is prediction. A typical example of a dynamical system that is modelled for prediction is the weather. The weather is a very complex, (high-order, nonlinear, coupled and chaotic) system. More theoretically, one reason to make models is to test the validity of a functional hypothesis of an observed phenomenon. A beautiful example is the model made by Hodgkin and Huxley to understand how action potentials arise and propagate in neurons (see Figure 2 and Figure 3 below). They modelled the different (voltage-gated) ion channels in an axon membrane and showed using mathematical models that indeed the changes in ion concentrations were responsible for the electrical spikes observed experimentally 7 years earlier.

Figure 2: Hodgkin-Huxley model of voltage-gated ion channels

Figure 3: Action potentials across the membrane

A second theoretical reason to make models is that it is sometimes very difficult, if not impossible, to answer a certain question empirically. As an example we take the following biomechanical question: Would you be able to jump higher if your biceps femoris (part of your hamstrings) were two separate muscles each crossing only one joint rather than being one muscle crossing both the hip and knee joint? Not a strange question as one could then independently control the torques around each joint.

In order to answer this question empirically, one would like to do the following experiment:

  • measure the maximal jump height of a subject

  • change only the musculoskeletal properties in question

  • measure the jump height again

Of course, such an experiment would yield several major ethical, practical and theoretical drawbacks. It is unlikely that an ethics committee would approve the transplantation of the origin and insertion of the hamstrings in order to examine its effect on jump height. And even so, one would have some difficulties finding subjects. Even with a volunteer for such a surgery it would not bring us any closer to an answer. After such a surgery, the subject would not be able to jump right away, but would have to undergo significant rehabilitation, and surely during such a period many factors will undesirably change like maximal contractile forces. And even if the subject would fully recover (apart from the hamstrings transplantation), his or her nervous system would have to find the new optimal muscle stimulation pattern.

If one person jumps lower than another person, is that because she cannot jump as high with her particular muscles, or was it just that her CNS was not able to find the optimal muscle activation pattern? Ultimately, one wants to know through what mechanism the subject’s jump performance changes. To investigate this, one would need to know, for example, the forces produced by the hamstrings as a function of time, something that is impossible to obtain experimentally. Of course, this example is somewhat ridiculous, but its message is hopefully clear that for several questions a strict empirical approach is not suitable. An alternative is provided by mathematical modelling.

Here we will be examining three systems—a mass-spring system, a two-link double pendulum system, and a system representing weather patterns. In each case we will see how to go from differential equations characterizing the dynamics of the system, to Python code, and run that code to simulate the behaviour of the system over time. We will see the great power of simulation, namely the ability to change aspects of the system at will, and simulate to explore the resulting change in system behaviour.

Modelling Dynamical Systems

Characterizing a System Using Differential Equations

A dynamical system such as the mass-spring system we saw before, can be characterized by the relationship between state variables \(s\) and their (time) derivatives \(\dot{s}\). How do we arrive at the correct characterization of this relationship? The short answer is, we figure it out using our knowledge of physics, or we are simply given the equations by someone else. Let’s look at a simple mass-spring system again, shown in Figure 4.

Figure 4: A spring with a mass attached

We know a couple of things about this system. We know from Hooke’s law of elasticity that the extension of a spring is directly and linearly proportional to the load applied to it. More precisely, the force that a spring applies in response to a perturbation from it’s resting length (the length at which it doesn’t generate any force), is linearly proportional, through a constant \(k\), to the difference in length between its current length and its resting length (let’s call this distance \(x\)). For convention let’s assume positive values of \(x\) correspond to lengthening the spring beyond its resting length, and negative values of \(x\) correspond to shortening the spring from its resting length.

\[F = -kx \tag{3}\]

Let’s decide that the state variable that we are interested in for our system is \(x\).

We also know from Newton’s laws of motion (specifically Newton’s second law) that the net force on an object is equal to its mass \(m\) multiplied by its acceleration \(a\) (the second derivative of position).

\[F = ma \tag{4}\]

Instead of using \(a\) to denote acceleration let’s use a different notation, in terms of the spring’s perturbed length \(x\). The rate of change (velocity) is denoted \(\dot{x}\) and the rate of change of the velocity (i.e. the acceleration) is denoted \(\ddot{x}\).

\[F = m \ddot{x} \tag{5}\]

We also know that the mass is affected by two forces: the force due to the spring (\(-kx\)) and also the gravitational force \(g\). So the equation characterizing the net forces on the mass is

\[\sum{F} = m\ddot{x} = -kx + mg \tag{6}\]

or just

\[m\ddot{x} = -kx + mg \tag{7}\]

This equation is a second-order differential equation, because the highest state derivative is a second derivative (i.e. \(\ddot{x}\), the second derivative, i.e. the acceleration, of \(x\)). The equation specifies the relationship between the state variables (in this case a single state variable \(x\)) and its derivatives (in this case a single derivative, \(\ddot{x}\)).

The reason we want an equation like this, from a practical point of view, is that we will be using numerical solvers in Python to integrate this differential equation over time, so that we can simulate the behaviour of the system. What these solvers need is a Python function that returns state derivatives, given current states. We can re-arrange the equation above so that it specifies how to compute the state derivative \(\ddot{x}\) given the current state \(\ddot{x}\).

\[\ddot{x} = \frac{-kx}{m} + g \tag{8}\]

Now we have what we need in order to simulate this system in Python. At any time point, we can compute the acceleration of the mass by the formula above.

Integrating Differential Equations in Python

Here is a Python function that we will be using to simulate the mass-spring system. All it does, really, is compute the equation above: what is the value of \(\ddot{x}\), given \(x\)? The one addition we have is that we are going to keep track not just of one state variable \(x\) but also its first derivative \(\dot{x}\) (the rate of change of \(x\), i.e. velocity).

def MassSpring(state,t):
  # unpack the state vector
  x = state[0]
  xd = state[1]

  # these are our constants
  k = 2.5 # Newtons per metre
  m = 1.5 # Kilograms
  g = 9.8 # metres per second

  # compute acceleration xdd
  xdd = ((-k*x)/m) + g

  # return the two state derivatives
  return [xd, xdd]

Note that the function we wrote takes two arguments as inputs: state and t, which corresponds to time. This is necessary for the numerical solver that we will use in Python. The state variable is actually an array of two values corresponding to \(x\) and \(\dot{x}\).

How does numerical integration (simulation) work?

The simplest method is called the Euler method named after Leonhard Euler, the 18th century Swiss mathematician. In the Euler method, the state estimate for the next time period \(x_{n+1}\) is equal to the state from the previous time period \(x_{n}\) plus the product of the state derivative \(\dot{x_{n}}\) and the change-in-time (step size) \(\Delta{t}\):

\[ x_{n+1} = x_{n} + \Delta{t}(\dot{x_{n}}) \]

In this way, numerical solvers esimate how the system states \((x,\dot{x})\) unfold over time, given the initial conditions, and the known relationship between state derivatives and system states.

The Euler method is an approximation, and its accuracy increases as the step size decreases. It turns out that for many dynamical systems, other methods are far more accurate, and more stable, for example the Runge-Kutta family of methods.

Runge-Kutta and other integration algorithms are readily available in scipy.integrate and are known as ODE solvers (ODE stands for ordinary differential equation).

Here’s how we would simulate the mass-spring system above, using the Runge-Kutta method. Assume that we have the function MassSpring.m defined as above:

import matplotlib.pyplot as plt
import numpy as np
import scipy as sp

def MassSpring(state,t):
  # unpack the state vector
  x = state[0]
  xd = state[1]

  # these are our constants
  k = -2.5 # Newtons per metre
  m = 1.5 # Kilograms
  g = 9.8 # metres per second

  # compute acceleration xdd
  xdd = ((k*x)/m) + g

  # return the two state derivatives
  return np.array([xd, xdd])

state0 = np.array([0.0, 0.0]) # initial state (x, xdot)
t = np.arange(0.0, 10.0, 0.1) # time vector

state = sp.integrate.odeint(MassSpring, state0, t) # integrate!

plt.plot(t, state)
plt.xlabel('TIME (sec)')
plt.title('Mass-Spring System')
plt.legend(('$x$ (m)', '$\dot{x}$ (m/sec)'))

A couple of notes about the code. I have simply chosen, out of the blue, values for the constants \(k\) and \(m\). The gravitational constant \(g\) is of course known. I have also chosen to simulate the system for 10 seconds, and I have chosen a time resolution of 100 milliseconds (0.1 seconds). We will talk later about the issue of what is an appropriate time resolution for simulation.

The blue line shows the position \(x\) of the mass (the length of the spring) over time, and the orange line shows the rate of change of \(x\), in other words the velocity \(\dot{x}\), over time. These are the two states of the system, simulated over time.

The way to interpret this simulation is, if we start the system at \(x=0\) and \(\dot{x}=0\), and simulate for 10 seconds, this is how the system would behave.

The power of modelling and simulation

Now you can appreciate the power of mathematical models and simulation: given a model that characterizes (to some degree of accuracy) the behaviour of a system we are interested in, we can use simulation to perform experiments in simulation instead of in reality. This can be very powerful. We can ask questions of the model, in simulation, that may be too difficult, or expensive, or time consuming, or just plain impossible, to do in real-life empirical studies. The degree to which we regard the results of simulations as interpretable, is a direct reflection of the degree to which we believe that our mathematical model is a reasonable characterization of the behaviour of the real system.

Simulating Motion of a Two-Joint Arm

Here is a Python function called twojointarm() that implements the forward dynamics of a double pendulum. You can look up the equations of motion on the Wikipedia page. The code below simply implements these equations. It’s not important right now to understand where the equations came from, but know that they can be derived from a relatively basic knowledge of physics. One property of a double pendulum is that the motion about one joint affects the motion of the adjacent joint, and vice-versa. These interaction forces can result in complex behaviour. Indeed, experimental work has demonstrated that the nervous system is capable of predicting these interaction forces and compensating for them (or in some cases exploiting them) during voluntary movements of the upper limb.

The four state variables are the angle of joint 1, the angle of joint 2, the velocity of joint 1 and the velocity of joint 2.

def twojointarm(state,t,aparams):
        passive two-joint arm in a vertical plane
        X is fwd(+) and back(-)
        Y is up(+) and down(-)
        gravity acts down
        shoulder angle a1 relative to Y vert, +ve counter-clockwise
        elbow angle a2 relative to upper arm, +ve counter-clockwise
        a1,a2,a1d,a2d = state
        l1,l2 = aparams['l1'], aparams['l2']
        m1,m2 = aparams['m1'], aparams['m2']
        i1,i2 = aparams['i1'], aparams['i2']
        r1,r2 = aparams['r1'], aparams['r2']
        g = 9.81
        M11 = i1 + i2 + (m1*r1*r1) + (m2*((l1*l1) + (r2*r2) + (2*l1*r2*np.cos(a2))))
        M12 = i2 + (m2*((r2*r2) + (l1*r2*np.cos(a2))))
        M21 = M12
        M22 = i2 + (m2*r2*r2)
        M = np.array([[M11,M12],[M21,M22]])
        C1 = -(m2*l1*a2d*a2d*r2*np.sin(a2)) - (2*m2*l1*a1d*a2d*r2*np.sin(a2))
        C2 = m2*l1*a1d*a1d*r2*np.sin(a2)
        C = np.array([[C1],[C2]])
        G1 = (g*np.sin(a1)*((m2*l1)+(m1*r1))) + (g*m2*r2*np.sin(a1+a2))
        G2 = g*m2*r2*np.sin(a1+a2)
        G = np.array([[G1],[G2]])
        ACC = np.linalg.inv(M) * (-C-G)
        a1dd,a2dd = ACC[0,0],ACC[1,0]
        return np.array([a1d, a2d, a1dd, a2dd])

Now that we have a function that implements the equations of motion, now we can run a simulation, by starting the arm out at some initial condition (initial joint angles and initial joint velocities) and integrating the differential equations over time. Here is some Python code for running a simulation from a given set of initial state conditions.

# anthropometric parameters of the arm
aparams = {
    'l1' : 0.3384, # metres
    'l2' : 0.4554,
    'r1' : 0.1692,
    'r2' : 0.2277,
    'm1' : 2.10,   # kg
    'm2' : 1.65,
    'i1' : 0.025,  # kg*m*m
    'i2' : 0.075

# forward kinematics
def joints_to_hand(A,aparams):
    Given joint angles A=(a1,a2) and anthropometric params aparams,
    returns hand position H=(hx,hy) and elbow position E=(ex,ey)
    l1 = aparams['l1']
    l2 = aparams['l2']
    n = np.shape(A)[0]
    E = np.zeros((n,2))
    H = np.zeros((n,2))
    for i in range(n):
        E[i,0] = l1 * np.cos(A[i,0])
        E[i,1] = l1 * np.sin(A[i,0])
        H[i,0] = E[i,0] + (l2 * np.cos(A[i,0]+A[i,1]))
        H[i,1] = E[i,1] + (l2 * np.sin(A[i,0]+A[i,1]))
    return H,E

# run a simulation
state0 = np.array([0*np.pi/180, 90*np.pi/180, 0, 0]) # initial joint angles and vels
t = np.arange(2001)/200                                 # 10 seconds at 200 Hz
state = sp.integrate.odeint(twojointarm, state0, t, args=(aparams,))

Here is what the joint angles (top) and joint velocities (bottom) look like:

fig,ax = plt.subplots(2,1)
ax[0].set_ylabel('joint angles (deg)')
ax[1].set_ylabel('joint velocities (deg/s)')
ax[1].set_xlabel('time (s)')

Play with the simulation code and see what happens when you start the simulation from different initial conditions, for example different initial joint angles.

The simple model described here has only passive motion, there are no active forces (for example generated by muscles). One can relatively easily add mathematical models of muscle force generation, and other neurophysiologically relevant properties such as muscle mechanics, spinal reflex circuitry, reflex time delays, and so on. One can then use such a model to test hypotheses about how the brain controls voluntary arm movement.

Lorenz Attractor

The Lorenz system is a dynamical system that we will look at briefly, as it will allow us to discuss several interesting issues around dynamical systems. It is a system often used to illustrate non-linear systems theory and chaos theory. It’s sometimes used as a simple demonstration of the butterfly effect (sensitivity to initial conditions). See here for a YouTube video that explains the meaning of the \(x\), \(y\) and \(z\) coordinates.

The Lorenz system is a simplified mathematical model for atmospheric convection. Let’s not worry about the details of what it represents, for now the important things to note are that it is a system of three coupled differential equations, and characterizes a system with three state variables \((x,y,z\)).

\[\begin{aligned} \dot{x} &= \sigma(y-x)\\ \dot{y} &= (\rho-z)x - y\\ \dot{z} &= xy-\beta z\end{aligned} \tag{9}\]

If you set the three constants \((\sigma,\rho,\beta)\) to specific values, the system exhibits chaotic behaviour.

\[\begin{aligned} \sigma &= 10\\ \rho &= 28\\ \beta &= \frac{8}{3}\end{aligned} \tag{10}\]

Let’s implement this system in Python. We have been given above the three equations that characterize how the state derivatives \((\dot{x},\dot{y},\dot{z})\) depend on \((x,y,z)\) and the constants \((\sigma,\rho,\beta)\). All we have to do is write a function that implements this, set some initial conditions, decide on a time array to simulate over, and run the simulation using scipy.integrate.odeint().

Here is our function that implements the state derivatives of the Lorenz system:

def Lorenz(state,t):
  # unpack the state vector
  x = state[0]
  y = state[1]
  z = state[2]
  # these are our constants
  sigma = 10.0
  rho = 28.0
  beta = 8.0/3.0
  # compute state derivatives
  xd = sigma * (y-x)
  yd = (rho-z)*x - y
  zd = x*y - beta*z
  # return the state derivatives
  return np.array([xd, yd, zd])

Here is code to integrate the system over time:

state0 = np.array([2.0, 3.0, 4.0])
t = np.arange(0.0, 30.0, 0.01)

state = sp.integrate.odeint(Lorenz, state0, t)

# do some fancy 3D plotting
from mpl_toolkits.mplot3d import Axes3D
fig = plt.figure()
ax = plt.axes(projection='3d')

The three axes on the plot represent the three states \((x,y,z)\) plotted over the 30 seconds of simulated time. We started the system with three particular values of \((x,y,z)\) (I chose them arbitrarily), and we set the simulation in motion. This is the trajectory, in state-space, of the Lorenz system.

You can see an interesting thing … the system seems to have two stable equilibrium states, or attractors: those circular paths. The system circles around in one “neighborhood” in state-space, and then flips over and circles around the second neighborhood. The number of times it circles in a given neighborhood, and the time at which it switches, displays chaotic behaviour, in the sense that they are exquisitly sensitive to initial conditions.

For example let’s re-run the simulation but change the initial conditions. Let’s change them by a very small amount, say 0.0001 … and let’s only change the \(x\) initial state by that very small amount. We will simulate again for 30 seconds.

state0 = np.array([2.0, 3.0, 4.0]);
t = np.arange(0.0, 30.0, 0.01)
state = sp.integrate.odeint(Lorenz, state0, t)

fig = plt.figure()
ax = plt.axes(projection='3d')

# re-run with very small change in initial conditions
delta = 0.0001
state0 = np.array([2.0+delta, 3.0, 4.0]);
t = np.arange(0.0, 30.0, 0.01)
state2 = sp.integrate.odeint(Lorenz, state0, t)


You can sort of see that at some point the two state-space trajectories diverge. You can better visualize this by plotting each state against time as shown in Figure 5.

Figure 5: Lorenz attractor changing initial conditions

After about 15 seconds into the simulation, the two systems are in different neighborhoods entirely!

This has illustrated how systems with relatively simple differential equations characterizing their behaviour, can turn out to be exquisitely sensitive to initial conditions. Just imagine if the initial conditions of your simulation were gathered from empirical observations (like the weather, for example). Now imagine you use a model simulation to predict whether it will be sunny (left-hand neighborhood of the plot above) or thunderstorms (right-hand neighborhood), 30 days from now. If the answer can flip between one prediction and the other, based on a 1/10,000 different in measurement, you had better be sure of your empirical measurement instruments, when you make a prediction 30 days out! Actually this won’t even solve the problem, no matter how precise your measurements. The point is that the system as a whole is very sensitive to even tiny changes in initial conditions. This is one reason why short-term weather forecasts are relatively accurate, but forecasts past a couple of days can turn out to be dead wrong.

MATLAB version

There is a MATLAB version of these notes here that includes animations of the two joint arm pendulum system and of the Lorenz system.

Other Dynamical Systems

In a previous version of this course we looked at a hanging chain dynamical system (coded in MATLAB), here is a link to that demo.