In this post, we provide an introduction to state-space models and explain how to simulate linear ordinary differential equations (ODEs) using the Python programming language. State-space modeling and numerical simulations are demonstrated using an example of a mass-spring system. A detailed video accompanying this post is given below.
Mass-spring systems are widely used to model the dynamics of actuators and other electromechanical devices. A basic sketch of a mass-spring system is given in Figure 1 below.
An object of mass is attached to a spring and a damper. The spring constant is and the damper constant is . The purpose of the damper is to damp the mechanical oscillations. Let us assume that the object is at its equilibrium position. If an external force is acting on the system, then the object will move in the direction of the force. How do we mathematically describe this motion?
To mathematically describe the oscillations, we first need to construct a free-body diagram. The free-body diagram is given in Figure 2 below.
In the equilibrium point, the spring is in its neutral position, and consequently, it does not exert a force on the object. In Fig.2, is the distance from the wall to the equilibrium point. The variable is used to describe the distance of the object from the equilibrium point. The spring force is given by the following equation
(1)
That is, the spring force is proportional to the displacement from the equilibrium point. The sign is opposite from the sign of the variable . If is positive, the spring is expanded and the spring force tends to bring the object to its equilibrium position. The damper force is given by the following equation:
(2)
The damper force is proportional to the object’s velocity that is denoted by and the sign is opposite from the sign of the velocity. That is, the damper force sign is opposite from the direction of the movement. In Figure 2, we assumed that the direction of the movement is in the direction of the -axis. The dynamics of the object is governed by Newton’s second law:
(3)
where is the acceleration vector and is the -th force acting on the system. Projecting this equation onto the axis, and using the equations (1) and (2) we obtain the following equation:
(4)
The last equation can be written as follows:
(5)
This equation mathematically describes the effect of the force onto the movement of the object. This is a second-order, linear ODE. This equation is linear since the variable and its derivatives and appear as linear functions. The equation’s order is determined by the highest derivative, which in our case is equal to . The general solution of this equation is defined as follows:
(6)
where and are the initial position and velocity, respectively, is time, and is a force as a function of time. Our goal is not to find an exact analytical form of the solution given by equation (6). Instead, our goal is to numerically solve the equation (5).
To numerically solve this equation, we will write it as a system of first-order ODEs. More generally, an -th order ODE can be written as a system of first-order ODEs. This system actually defines a state-space model of the system.
To define a state-space model, we first need to introduce state variables. The state variables are denoted by and . We define the state variables as follows:
(7)
By differentiating the last two equations, we obtain:
(8)
where we have used the equation (5) to express as a function of and . The last equation can be written in a vector form:
(9)
or
(10)
where is the state vector, is the control input, and and are system matrices defined in equation (9). With the state equation (10) we need to associate an output equation. The output equation maps the state vector into the output vector (in our case it will be a scalar). A system output is a system variable or a function of system variables whose dynamical behavior we want to track. We usually measure the system output using sensors. In our case, the output equation has the following form:
(11)
That is, we are measuring the state variable that corresponds to the displacement from the equilibrium position, denoted by . The state-space and output equations are usually written next to each other:
(12)
The main question is how to simulate the system (12) for given initial conditions and . The first approach is to use a forward Euler method. Namely, using the forward Euler method, we can approximate the derivative as follows:
(13)
where is a discretization time constant (usually a small real number), denotes a discrete-time instant , , is an approximation of the state vector at the time instant , that is an approximation of . Subsituting the equation (13) in the state-space model (12), we obtain
(14)
where and are defined in the same manner as the discretized state variable . From the equation (14) we see that we can obtain the discretized state sequence by simply propagating the equation
(15)
from the initial condition and using the control input sequence . However, this approach might produce numerical instabilities of the discretized state vector (the state vector might diverge to infinity). To avoid this problem, it is preferable to discretize the system using the backward Euler method. The backward Euler method discretizes the state sequence as follows:
(16)
Subsituting the equation (16) in the state equation of the state-space model (12), we obtain:
(17)
From this equation, we obtain:
(18)
where
(19)
Starting from the initial condition and using the control input sequence , we can generate the discretized state sequence , by propagating the equation (18). Once the state sequence is generated, we can easily generate the output sequence by using the equation . We use this method to discretize the state-space model.
The Python code for discretizing the state-space model is given below.
import numpy as np
import matplotlib.pyplot as plt
# define the continuous-time system matrices
A=np.matrix([[0, 1],[- 0.1, -0.05]])
B=np.matrix([[0],[1]])
C=np.matrix([[1, 0]])
#define an initial state for simulation
x0=np.random.rand(2,1)
#define the number of time-samples used for the simulation and the sampling time for the discretization
time=300
sampling=0.5
#define an input sequence for the simulation
#input_seq=np.random.rand(time,1)
input_seq=np.ones(time)
#plt.plot(input_sequence)
# the following function simulates the state-space model using the backward Euler method
# the input parameters are:
# -- A,B,C - continuous time system matrices
# -- initial_state - the initial state of the system
# -- time_steps - the total number of simulation time steps
# -- sampling_perios - the sampling period for the backward Euler discretization
# this function returns the state sequence and the output sequence
# they are stored in the vectors Xd and Yd respectively
def simulate(A,B,C,initial_state,input_sequence, time_steps,sampling_period):
from numpy.linalg import inv
I=np.identity(A.shape[0]) # this is an identity matrix
Ad=inv(I-sampling_period*A)
Bd=Ad*sampling_period*B
Xd=np.zeros(shape=(A.shape[0],time_steps+1))
Yd=np.zeros(shape=(C.shape[0],time_steps+1))
for i in range(0,time_steps):
if i==0:
Xd[:,[i]]=initial_state
Yd[:,[i]]=C*initial_state
x=Ad*initial_state+Bd*input_sequence[i]
else:
Xd[:,[i]]=x
Yd[:,[i]]=C*x
x=Ad*x+Bd*input_sequence[i]
Xd[:,[-1]]=x
Yd[:,[-1]]=C*x
return Xd, Yd
state,output=simulate(A,B,C,x0,input_seq, time ,sampling)
plt.plot(output[0,:])
plt.xlabel('Discrete time instant-k')
plt.ylabel('Position- d')
plt.title('System step response')
A few comments are in order. The code lines 6-8 are used to define an arbitrary mass-spring system. The code line 10 is used to define an initial condition. The code line 13 is used to define the total number of discrete-time steps. The code line 14 defines the discretization time constant . The code line 18 defines a control input sequence. The control input sequence is equal to at every time sample. This is a so-called step input sequence, and the corresponding output is called the step response of the system. The code lines 30-49 define a function for discretizing the state-space model. The code lines 51-57 are used to generate the system output and to plot it. The result is given in Figure 3 below.