Perform Model Predictive Control (MPC) on linear system dynamics by converting the MPC optimization problem to a quadratic cost problem. The new problem is optimized using the Operator Splitting Quadratic Program (OSQP).
Jake Miller and Kyle Johnsen
SIP Lab
Georgia Institute of Technology
Either clone repo and
pip install -e .
or install directly from GitHub:
pip install git+https://github.com/CLOCTools/lqmpc.git
We make use of OSQP in order to calculate optimal trajectories. OSQP does not have to be directly imported, but is used within LQMPC. Scipy is used for efficient sparse matrix operations.
import lqmpc
import numpy as np
from scipy import sparse
import matplotlib.pyplot as plt
In our example, we take a linear dynamical system describing the spiking behavior of a population of neurons.
We take
A = np.array([[1, -6.66e-13, -2.03e-9, -4.14e-6], # System matrix (n x n)
[9.83e-4, 1, -4.09e-8, -8.32e-5],
[4.83e-7, 9.83e-4, 1, -5.34e-4],
[1.58e-10, 4.83e-7, 9.83e-4, .9994]])
B = np.array([[9.83e-4, 4.83e-7, 1.58e-10, 3.89e-14]]).T # Control matrix (n x m)
C = np.array([[-.0096, .0135, .005, -.0095]]) # Output matrix (o x n)
n, m = B.shape
t_step = 0.001 # Dynamics for 1 ms
We define a cost function which includes the ability to have a state penalty, input penalty, and differential input penalty. The state penalties occur over the prediction horizon while the input penalties over the control horizon. MPC subjects this cost function to particular constraints:
where
N = 25 # Prediction horizon
Q = C.T@C # State penalty
R = sparse.eye(m)*1e-6 # Input penalty
umin = np.array([0]) # Lower input bound
umax = np.array([10]) # Upper input bound
x0 = np.zeros(n) # Initial state
u0 = np.zeros(m) # Initial input
When using LQMPC, state references can be supplied per step or for a full simulation loop. For this example, we aim to clamp the output spiking rate, represented with a peri-stimulus time histogram (PSTH), to a sine reference. We use a non-linear function to convert the control ouput to the PSTH.
We will define our reference for the full loop. The reference is defined on the
L = 120 # Number of simulation steps
t_sim = 0.25 # Simulation time step in seconds
ref_len = (L + N)*int(t_sim/t_step) # Length of reference
def z_to_y(z):
# From PSTH to output
return (np.log(z)+5.468)/61.4
def y_to_z(y):
# From output to PSTH
return np.exp(61.4*np.array(y)-5.468)
inv = np.linalg.inv
I = np.eye(4)
zr = 0.1 * np.sin(np.linspace(0,2*np.pi,ref_len) * 8) + 0.2 # Sine reference
yr = z_to_y([zr])
ur = inv(C@inv(I-A)@B)@yr
xr = inv(I-A)@B@ur # Full reference state
Now that we have prepared all the necessary inputs, we can simulate the system with control dynamics. There is also an option to print out how long it takes to run a simulation step or full loop.
mpc = lqmpc.LQMPC(t_step,A,B,C) # Instantiate object with dynamics
mpc.set_control(Q=Q,R=R,N=N,M=20) # Set the control parameters
mpc.set_constraints(umin=umin,umax=umax) # Set the constraints
mpc.simulate(t_sim,x0,u0,xr,L) # Perform a full simulation loop
mpc.plot(f=y_to_z) # Plot the results
Simulation Time: 5.69786501 s
Mean Step Time: 0.04748221 s
The simulate function utilizes the step function within the control loop, but what if you want to simulate one step at a time? Here, we explain the step function more closely. The step function takes as input a simulation step time
The step function returns the result of OSQP's solve() method.
n_sim = int(t_sim/t_step) # Number of points per simulation step
xr_step = xr[:,:N*n_sim] # Reference for step
zr_step = y_to_z(C@xr_step).T # Convert to output reference
result = mpc.step(t_sim, x0, u0, xr_step, out=False) # Step function
trajectory = result.x
state_traj = trajectory[0:N*n] # State trajectory
output_traj = y_to_z(C@np.reshape(state_traj,(N,4)).T).T # Convert to output trajectory
control_traj = trajectory[N*n:] # Control moves
cost = result.info.obj_val # Objective value
print('Cost:', cost)
t_r = np.arange(0,len(zr_step)) * t_step # Time series for reference
t_z = np.arange(0,len(output_traj)) * t_sim # Time series for state trajectory
plt.plot(t_r,zr_step) # Plot the optimal trajectory
plt.plot(t_z,output_traj,'--')
plt.xlim([0, N*t_sim])
plt.legend(['Reference','Trajectory'])
plt.xlabel('Time (s)')
plt.ylabel('Output')
plt.show()
Cost: -0.06743034519371201
This section will explain the conversion from the Model Predictive Control problem to a quadratic problem. We use OSQP to solve the cost function, but in order to utilize OSQP, we must provide the proper parameters for a quadratic problem. In contrast to our MPC definition shown previously, OSQP uses the following:
where
The variable
We will start with encoding the penalties Q, R, and S into P.
For
Our cost function also has a term linear in
where
We can compute the cost for
Now, if we compute
when
Observe that our two equations for
A final note for the cost function calculation is that in our implementation, we multiply
Now we must encode the four constraint conditions for the MPC problem into a single set of inequalities
which makes
This satisfies constraints on the values for the state and input, but we must also let the solver know that
where
If our simulation step takes
Now we are ready to define
How this produces the equality constraints is much clearer if we exemplify
We can see that as standard with a control horizon, after we have computed
To combine the equality constraints and inequality constraints into one, we simply stack vertically:
with
The source is the Jupyter notebook examples/README.ipynb
.
To generate markdown, run this from the project root directory:
jupyter nbconvert examples/README.ipynb --to markdown --output-dir .