using LinearAlgebra, SparseArrays, COSMO
# You might find these other packages useful
# using ToeplitzMatrices, BlockArrays
mutable struct MPCProblem{T <: AbstractFloat}
::COSMO.Workspace{T}
model::Matrix{T}
F::Vector{T}
W::Matrix{T}
Send
"""
Sets up the necessary matrices for a tracking Model Predictive Controller (MPC).
Given a discrete-time linear system with state-space representation:
x(k+1) = A*x(k) + B*u(k)
y(k) = C*x(k)
This function constructs the required matrices to reformulate the MPC problem as a quadratic program (QP):
min_z (1/2) * z' * H * z + [x_t', u_(t-1)', r_(t+1:t+N)'] * F * z
subject to: G * z ≤ W + S * [x_t, u_(t-1)]
where:
- z = [Δu_t; ...; Δu_(t+N-1)] (control input changes over the horizon)
- x_t: current state
- u_(t-1): most recently applied input
- r_(t+1:t+N): reference trajectory
Arguments:
- A, B, C: System matrices defining dynamics and output equations
- Q: State tracking cost matrix
- R: Control input cost matrix
- N: Prediction horizon length
- u_min, u_max: Input constraints
Returns:
A dictionary containing the constructed QP matrices:
- H: Quadratic cost matrix
- F: Linear cost term matrix
- S: Constraint matrix for state and past input
- W: Constraint bounds vector
- G: Inequality constraint matrix
"""
function setup_mpc(A::Matrix{T}, B::Matrix{T}, C::Matrix{T}, Q::Matrix{T}, R::Matrix{T}, N::Int, u_min::Vector{T}, u_max::Vector{T}) where T <: AbstractFloat
= size(A, 1)
nx = size(B, 2)
nu = size(C, 1)
ny
# TODO Construct the matrices H, F, S, W, and G
= zeros(T, N * nu, N * nu)
H = zeros(T, nx + nu + N * ny, N * nu)
F = zeros(T, 2N * nu, nx + nu)
S = zeros(T, 2N * nu)
W = zeros(T, 2N * nu, N * nu)
G
return H, F, S, W, G
end
"""
Constructs a tracking Model Predictive Controller (MPC) problem.
Arguments:
- A, B, C: System matrices defining dynamics and output equations
- Q: State tracking cost matrix
- R: Control input cost matrix
- N: Prediction horizon length
- u_min, u_max: Input constraints
Returns:
An instance of `MPCProblem` containing the COSMO model and the matrices F, W, and S.
"""
function MPCProblem(A::Matrix{T}, B::Matrix{T}, C::Matrix{T}, Q::Matrix{T}, R::Matrix{T}, N::Int, u_min::Vector{T}, u_max::Vector{T}) where T <: AbstractFloat
= setup_mpc(A, B, C, Q, R, N, u_min, u_max)
H, F, S, W, G
= COSMO.Model{T}() # COSMO model
model
## We use COSMO with the following QP formulation:
## min 1/2 x' * P * x + q' * x
## s.t. A_constr * x - b_constr ≥ 0
# Dummy variables - just for illustration and to provide correct stuff to COSMO
= zeros(T, size(A, 1))
x₀ = zeros(T, N * size(C, 1))
r = zeros(T, size(B, 2))
u₀
= [x₀; u₀; r]' * F
q
= sparse(H)
P
= -G
A_constr = W + S * [x₀; u₀]
b_constr
= COSMO.Constraint(A_constr, b_constr, COSMO.Nonnegatives);
constr
assemble!(model, sparse(H), q, constr, settings = COSMO.Settings(verbose=true)) # Assemble the QP
COSMO.
return MPCProblem(model, F, W, S)
end
"""
Solves the MPC problem for the given initial state, input, and reference trajectory.
Arguments:
- mpc: An instance of `MPCProblem` containing the COSMO model and the matrices F, W, and S.
- xₖ: The current state vector.
- uₖ₋₁: The most recently applied input vector.
- r: The reference trajectory vector.
Returns:
The optimal control input vector at the current time step.
"""
function solve!(mpc::MPCProblem{T}, xₖ::Vector{T}, uₖ₋₁::Vector{T}, r::Matrix{T}) where T <: AbstractFloat
# TODO implement the updates for the COSMO model (q and b_constr)
= zeros(T, size(mpc.F, 2))
q = zeros(T, size(mpc.S, 1))
b_constr
update!(mpc.model, q = q, b = b_constr)
COSMO.
= COSMO.optimize!(mpc.model)
result
# TODO Extract the optimal control input and return it
= result.x
Δu_opt
return zeros(T, size(uₖ₋₁, 1))
end
Homework
In this homework, you will be implementing a tracking Model Predictive Controller (MPC) for a linear model in Julia. Your task is to take the Optimal Control Problem of the tracking MPC \begin{align*} \underset{\mathbf{u}_k}{\text{minimize}} \quad & \frac{1}{2}\sum_{k=t}^{t+N-1} (\mathbf{y}_{k+1} - r_{k+1})^T\mathbf{Q}(\mathbf{y}_{k+1} - r_{k+1}) + \Delta\mathbf{u}_k^T\mathbf{R}\Delta\mathbf{u}_k\\ \text{subject to} \quad & \mathbf{x}_{k+1} = \mathbf{A}\mathbf{x}_k + \mathbf{B}\mathbf{u}_k, \qquad k = t,\dots,t+N-1,\\ & \mathbf{y}_k = \mathbf{C}\mathbf{x}_k, \qquad k = t+1,\dots,t+N,\\ &\mathbf{u}_\mathrm{min} \leq \mathbf{u}_{k} \leq \mathbf{u}_\mathrm{max}, \qquad k =t,\dots,t+N-1, \end{align*} where \Delta\mathbf{u}_k = \mathbf{u}_k - \mathbf{u}_{k-1}, and reformulate it as a Quadratic Program (QP) of the form \begin{array}{rl} \underset{\mathbf{z}}{\text{minimize}} \quad & \frac{1}{2}\mathbf{z}^T \mathbf{H} \mathbf{z} + [\mathbf{x}_t^T \: \mathbf{u}_{t-1}^T \: \mathbf{r}_{t+1,\dots,t+N}^T]\,\mathbf{F}\,\mathbf{z} \\ \text{subject to} \quad &\mathbf{G}\mathbf{z} \leq \mathbf{W} + \mathbf{S}\left[\begin{array}{c}\mathbf{x}_t\\ \mathbf{u}_{t-1}\end{array}\right]. \end{array} where \mathbf{z}=\left[\begin{array}{c}\Delta \mathbf{u}_t^\mathrm{T} & \ldots & \Delta \mathbf{u}_{t+N-1}^\mathrm{T}\end{array}\right]^\mathrm{T}, \mathbf{x}_t is the current state value of the model, \mathbf{u}_{t-1} is the most recently applied input and \mathbf{r}_{t+1,\dots,t+N} is the reference over the current prediction horizon. You should then implement the MPC controller using this QP formulation.
We recommend starting by writing down the QP formulation on paper and only then proceeding to implement the MPC construction in code. In case of doubts, we advise you to consult the lecture notes, especially the section on the Sequential (Dense) formulation of direct discrete-time optimal control problems, as well as the related MPC video lectures. Another source which may serve you well is slides from a doctoral course on MPC by Bemporad, which can be found here.
The MPC controller should be based on the template provided below. Your goal is to complete the implementation by filling in the missing parts, specifically
- Complete the
setup_mpc
function that constructs the matrices \mathbf{H}, \mathbf{F}, \mathbf{S}, \mathbf{W}, and \mathbf{G} for the QP formulation. - Complete the
solve!
function that does the single MPC step, that is, it solves the QP for the given initial state, input, and reference trajectory, and returns the optimal control input.
Your solution should be contained in a single file named hw.jl
, which you will upload to the BRUTE system. You should use the COSMO.jl package for solving the QP.
You should test your implementation using the following example we prepared for you.
using ControlSystemsBase
using Plots
= [-.0151 -60.5651 0 -32.174;
Ac -.0001 -1.3411 .9929 0;
.00018 43.2541 -.86939 0;
0 0 1 0];
= [-2.516 -13.136;
Bc -.1689 -.2514;
-17.251 -1.5766;
0 0];
= [0 1 0 0;
Cc 0 0 0 1];
= [0 0;
Dc 0 0];
=ss(Ac,Bc,Cc,Dc)
sys
= .05; # Sampling time
Ts = c2d(sys,Ts)
model
= model.A
A = model.B
B = model.C
C
= 10 # Prediction horizon
N = diagm([10.0, 10.0]); # Tracking weight matrix
Q = Matrix(0.1I, 2, 2); # Input increment weight matrix
R
= 25.0*[1, 1];
u_max = -25.0*[1, 1];
u_min
= MPCProblem(A, B, C, Q, R, N, u_min, u_max)
mpc
= 75*Ts;
Tf = 0:Ts:Tf;
t
= [2*ones(1, length(t)+N); 10*ones(1, length(t)+N)] # Reference trajectory
ref
:, Int(round(end/2)):end] ./= 2
ref[
= zeros(4, length(t)+1)
xs = zeros(2, length(t))
us = zeros(2, length(t))
ys
for k = 2:length(t)
= solve!(mpc, xs[:, k], us[:, k-1], ref[:, k:k+N-1])
u :, k+1] = A*xs[:, k] + B*u
xs[:, k] = C*xs[:, k]
ys[:, k] = u
us[end
## Visualize the results
using Plots
= plot(t, ys[1, :], label="y₁", linetype=:steppre, linewidth=2)
p1plot!(t, ys[2, :], label="y₂", linetype=:steppre, linewidth=2)
plot!(t, ref[1, 1:end-N], linestyle=:dash, label="r₁", linetype=:steppre, linewidth=2)
plot!(t, ref[2, 1:end-N], linestyle=:dash, label="r₂", linetype=:steppre, linewidth=2)
xlabel!("Time [s]")
ylabel!("Output")
= plot(t[1:end], us[1, :], label="u₁", linetype=:steppre, linewidth=2)
p2 plot!(t[1:end], us[2, :], label="u₂", linetype=:steppre, linewidth=2)
plot!(t, u_max[1]*ones(length(t)), linestyle=:dash, label="u max", linetype=:steppre, linewidth=2)
plot!(t, u_min[1]*ones(length(t)), linestyle=:dash, label="u min", linetype=:steppre, linewidth=2)
xlabel!("Time [s]")
ylabel!("Input")
plot(p1, p2, layout=(2,1), size=(800, 600))