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
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,
(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
(2)
The damper force is proportional to the object’s velocity that is denoted by
(3)
where
(4)
The last equation can be written as follows:
(5)
This equation mathematically describes the effect of the force
(6)
where
To numerically solve this equation, we will write it as a system of first-order ODEs. More generally, an
To define a state-space model, we first need to introduce state variables. The state variables are denoted by
(7)
By differentiating the last two equations, we obtain:
(8)
where we have used the equation (5) to express
(9)
or
(10)
where
(11)
That is, we are measuring the state variable
(12)
The main question is how to simulate the system (12) for given initial conditions
(13)
where
(14)
where
(15)
from the initial condition
(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
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