Model Predictive Control
In this example we shall demonstrate an instance of using the box cone, as well as reusing a cached workspace and using warm-starting.
In model predictive control (MPC) the control action at each time-step is obtained by solving an optimization problem that simulates the dynamical system over some time horizon. Here, we consider the problem of controlling a linear, time-invariant dynamical systems with quadratic stage costs and box constraints on the state and action:
over variables corresponding to the states \(x_t \in \mathbf{R}^{n}\), \(t=0,\ldots,T\), and the inputs \(u_t \in \mathbf{R}^{m}\), \(t=0,\ldots,T-1\), where \(A \in \mathbf{R}^{n \times n}\), \(B \in \mathbf{R}^{n \times m}\) correspond to the linear dynamics and \(Q \in \mathbf{R}^{n \times n}\), \(R \in \mathbf{R}^{m \times m}\), \(q \in \mathbf{R}^{n}\) correspond to the positive semidefinite quadratic cost functions with terminal cost defined by \(Q_T \in \mathbf{R}^{n \times n}\) and \(q_T \in \mathbf{R}^{n}\).
The problem is solved repeatedly for varying initial state \(\bar{x} \in \mathbf{R}^{n}\). The upper and lower bound constraints can be expressed in SCS using the box cone.
Python code to solve this is below.
import scs
import numpy as np
import scipy as sp
from scipy import sparse
np.random.seed(1)
class MPC(object):
"""Model Predictive Contoller using SCS."""
def __init__(self, Ad, Bd, Q, R, q, QT, qT, xmin, xmax, umin, umax, T):
# State and action dimension
self.nx, self.nu = Bd.shape
# Stack variables as follows:
# [x_0, x_1, ..., x_{T-1}, x_T, u_0, u_1, ..., u_{T-1}]
# Quadratic objective
P = sparse.block_diag(
[sparse.kron(sparse.eye(T), Q), QT, sparse.kron(sparse.eye(T), R)],
format="csc",
)
# Linear objective
c = np.hstack([np.kron(np.ones(T), q), qT, np.zeros(T * self.nu)])
# Linear dynamics
Ax = sparse.kron(sparse.eye(T + 1), -sparse.eye(self.nx)) + sparse.kron(
sparse.eye(T + 1, k=-1), Ad
)
Bu = sparse.kron(
sparse.vstack([sparse.csc_matrix((1, T)), sparse.eye(T)]), Bd
)
Aeq = sparse.hstack([Ax, Bu])
# Will update this later with initial state
beq = np.zeros((T + 1) * self.nx)
# Box constraints on state and action
Aineq = sparse.eye((T + 1) * self.nx + T * self.nu)
box_lower = np.hstack(
[np.kron(np.ones(T + 1), xmin), np.kron(np.ones(T), umin)]
)
box_upper = np.hstack(
[np.kron(np.ones(T + 1), xmax), np.kron(np.ones(T), umax)]
)
A = sparse.vstack(
[
# zero cone
Aeq,
# Box cone {(t, s) | -t l <= s <= t u }
sparse.csc_matrix((1, (T + 1) * self.nx + T * self.nu)),
-Aineq,
],
format="csc",
)
# Box cone add constraint t=1
self.b = np.hstack([beq, 1, np.zeros((T + 1) * self.nx + T * self.nu)])
data = dict(P=P, A=A, b=self.b, c=c)
cone = dict(z=(T + 1) * self.nx, bu=box_upper, bl=box_lower)
# Create an SCS object
self.solver = scs.SCS(data, cone, eps_abs=1e-5, eps_rel=1e-5)
def control(self, x0):
# Overwrite b with new initial state
self.b[: self.nx] = -x0
# Update b
self.solver.update(b=self.b)
sol = self.solver.solve() # will warm-start automatically
if sol["info"]["status"] != "solved":
raise ValueError("SCS failed to solve the problem.")
# Return first action
return sol["x"][-T * self.nu : -(T - 1) * self.nu]
# States dimension
nx = 20
# Control dimension
nu = 5
# State dynamics matrices
Ad = 0.1 * np.random.randn(nx, nx) # State -> State
Bd = np.random.randn(nx, nu) # Control -> State
# Cost matrices
Q = sparse.eye(nx) # State
QT = 10 * Q # Terminal State
R = 0.1 * sparse.eye(nu) # Control
# Linear cost vector
q = 0.1 * np.random.randn(nx)
qT = q
# Initial state
x0 = 10 * np.random.randn(nx)
# Prediction horizon
T = 30
# Bounds on state
xmax = np.inf * np.ones(nx)
xmin = -np.inf * np.ones(nx)
# Bounds on control
umax = np.ones(nu)
umin = -np.ones(nu)
# Initialize Model Predictive Controller
mpc = MPC(Ad, Bd, Q, R, q, QT, qT, xmin, xmax, umin, umax, T)
# Simulate in closed loop
nsteps = 10 # Number of steps
for i in range(nsteps):
# Get control action
u = mpc.control(x0)
print(f"Control action: {u}")
# Apply first control input and update to next state
x0 = Ad @ x0 + Bd @ u + 0.01 * np.random.normal(nx, 1) # + noise
x0 = np.maximum(np.minimum(x0, xmax), xmin) # Bound to xmin, xmax
After following the python install instructions, we can run the code yielding output:
------------------------------------------------------------------
SCS v3.1.1 - Splitting Conic Solver
(c) Brendan O'Donoghue, Stanford University, 2012
------------------------------------------------------------------
problem: variables n: 770, constraints m: 1391
cones: z: primal zero / dual free vars: 620
b: box cone vars: 771
settings: eps_abs: 1.0e-05, eps_rel: 1.0e-05, eps_infeas: 1.0e-07
alpha: 1.50, scale: 1.00e-01, adaptive_scale: 1
max_iters: 100000, normalize: 1, rho_x: 1.00e-06
acceleration_lookback: 10, acceleration_interval: 10
lin-sys: sparse-direct
nnz(A): 16390, nnz(P): 770
------------------------------------------------------------------
iter | pri res | dua res | gap | obj | scale | time (s)
------------------------------------------------------------------
0| 1.74e+01 3.51e+00 3.73e+03 2.35e+03 1.00e-01 3.09e-04
225| 6.23e-11 1.21e-11 1.04e-09 1.09e+03 1.30e+00 2.33e-02
------------------------------------------------------------------
status: solved
timings: total: 2.34e-02s = setup: 9.72e-06s + solve: 2.34e-02s
lin-sys: 1.84e-02s, cones: 1.09e-03s, accel: 6.86e-04s
------------------------------------------------------------------
objective = 1088.948282
------------------------------------------------------------------
Control action: [ 0.41348794 -1. -0.38009805 -1. -0.42945415]
------------------------------------------------------------------
iter | pri res | dua res | gap | obj | scale | time (s)
------------------------------------------------------------------
0| 2.46e+01 6.40e+01 2.05e+03 9.35e+02 1.30e+00 4.82e-04
25| 5.67e-07 2.89e-05 4.55e-04 1.43e+02 1.30e+00 3.84e-03
------------------------------------------------------------------
status: solved
timings: total: 3.87e-03s = setup: 1.59e-05s + solve: 3.85e-03s
lin-sys: 2.81e-03s, cones: 2.09e-04s, accel: 1.12e-04s
------------------------------------------------------------------
objective = 142.600803
------------------------------------------------------------------
Control action: [-0.44966462 -0.03355008 0.52249607 0.19250174 0.14671164]
------------------------------------------------------------------
iter | pri res | dua res | gap | obj | scale | time (s)
------------------------------------------------------------------
0| 9.09e+00 2.36e+01 2.87e+02 9.13e+01 1.30e+00 3.74e-04
25| 1.28e-07 5.65e-06 2.59e-05 1.01e+01 1.30e+00 3.69e-03
------------------------------------------------------------------
status: solved
timings: total: 3.72e-03s = setup: 1.28e-05s + solve: 3.71e-03s
lin-sys: 2.79e-03s, cones: 2.11e-04s, accel: 2.78e-05s
------------------------------------------------------------------
objective = 10.124977
------------------------------------------------------------------
Control action: [ 0.18436581 0.01049508 -0.01660706 -0.10110899 -0.00797351]
------------------------------------------------------------------
iter | pri res | dua res | gap | obj | scale | time (s)
------------------------------------------------------------------
0| 2.97e+00 7.72e+00 2.21e+01 8.98e+00 1.30e+00 4.36e-04
25| 4.95e-08 2.08e-06 2.67e-06 6.17e-01 1.30e+00 3.85e-03
------------------------------------------------------------------
status: solved
timings: total: 3.88e-03s = setup: 1.18e-05s + solve: 3.87e-03s
lin-sys: 2.95e-03s, cones: 2.29e-04s, accel: 2.76e-05s
------------------------------------------------------------------
objective = 0.616727
------------------------------------------------------------------
Control action: [-0.05327529 0.02863548 0.02321702 0.05851298 0.05562412]
------------------------------------------------------------------
iter | pri res | dua res | gap | obj | scale | time (s)
------------------------------------------------------------------
0| 9.21e-01 2.39e+00 1.32e+00 2.03e+00 1.30e+00 3.92e-04
25| 1.79e-08 6.46e-07 6.26e-07 2.25e-01 1.30e+00 3.58e-03
------------------------------------------------------------------
status: solved
timings: total: 3.61e-03s = setup: 1.17e-05s + solve: 3.60e-03s
lin-sys: 2.76e-03s, cones: 1.88e-04s, accel: 2.94e-05s
------------------------------------------------------------------
objective = 0.224881
------------------------------------------------------------------
Control action: [ 0.00742414 0.01912228 -0.03393191 0.03964983 0.00193193]
------------------------------------------------------------------
iter | pri res | dua res | gap | obj | scale | time (s)
------------------------------------------------------------------
0| 3.06e-01 7.95e-01 4.69e-01 7.86e-02 1.30e+00 4.53e-04
25| 6.91e-09 2.38e-07 5.74e-08 4.16e-02 1.30e+00 3.61e-03
------------------------------------------------------------------
status: solved
timings: total: 3.65e-03s = setup: 2.32e-05s + solve: 3.63e-03s
lin-sys: 2.67e-03s, cones: 1.68e-04s, accel: 2.56e-05s
------------------------------------------------------------------
objective = 0.041573
------------------------------------------------------------------
Control action: [-0.01297334 0.02363413 -0.03044127 0.05802441 -0.00469033]
------------------------------------------------------------------
iter | pri res | dua res | gap | obj | scale | time (s)
------------------------------------------------------------------
0| 1.11e-01 2.89e-01 5.02e-02 -3.43e-02 1.30e+00 3.57e-04
25| 4.30e-09 1.81e-07 1.22e-08 -1.47e-02 1.30e+00 3.48e-03
------------------------------------------------------------------
status: solved
timings: total: 3.51e-03s = setup: 1.26e-05s + solve: 3.50e-03s
lin-sys: 2.55e-03s, cones: 1.59e-04s, accel: 1.49e-04s
------------------------------------------------------------------
objective = -0.014719
------------------------------------------------------------------
Control action: [-0.01673083 0.02410207 -0.03265513 0.05713659 0.00074876]
------------------------------------------------------------------
iter | pri res | dua res | gap | obj | scale | time (s)
------------------------------------------------------------------
0| 4.88e-02 1.27e-01 1.10e-01 1.95e-01 1.30e+00 4.38e-04
25| 1.05e-09 3.82e-08 5.28e-08 5.71e-02 1.30e+00 4.01e-03
------------------------------------------------------------------
status: solved
timings: total: 4.04e-03s = setup: 1.17e-05s + solve: 4.03e-03s
lin-sys: 3.09e-03s, cones: 2.11e-04s, accel: 1.81e-05s
------------------------------------------------------------------
objective = 0.057139
------------------------------------------------------------------
Control action: [-0.01873535 0.02513378 -0.03083648 0.0586056 0.00248981]
------------------------------------------------------------------
iter | pri res | dua res | gap | obj | scale | time (s)
------------------------------------------------------------------
0| 2.81e-02 7.30e-02 1.51e-01 3.06e-01 1.30e+00 4.49e-04
25| 5.55e-10 2.30e-08 6.88e-08 1.41e-01 1.30e+00 3.98e-03
------------------------------------------------------------------
status: solved
timings: total: 4.01e-03s = setup: 1.25e-05s + solve: 4.00e-03s
lin-sys: 3.05e-03s, cones: 2.10e-04s, accel: 3.11e-05s
------------------------------------------------------------------
objective = 0.140619
------------------------------------------------------------------
Control action: [-0.01807515 0.02511239 -0.03288644 0.0597179 0.00232987]
------------------------------------------------------------------
iter | pri res | dua res | gap | obj | scale | time (s)
------------------------------------------------------------------
0| 8.98e-03 2.33e-02 2.83e-02 1.86e-01 1.30e+00 3.14e-04
25| 1.54e-10 6.84e-09 1.30e-08 1.56e-01 1.30e+00 3.47e-03
------------------------------------------------------------------
status: solved
timings: total: 3.50e-03s = setup: 1.07e-05s + solve: 3.49e-03s
lin-sys: 2.68e-03s, cones: 1.64e-04s, accel: 2.98e-05s
------------------------------------------------------------------
objective = 0.156128
------------------------------------------------------------------
Control action: [-0.01797391 0.02512434 -0.03330189 0.06002231 0.00188589]