Skip to content

Commit 67f3081

Browse files
authored
add LinearMPC extension (#132)
* add LinearMPC extension * add test dep * add mention in docs * add LinMPC to docs env * fix * add example file * global
1 parent 493d939 commit 67f3081

File tree

9 files changed

+344
-2
lines changed

9 files changed

+344
-2
lines changed

Project.toml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,20 @@ Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2"
2323
UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4"
2424
UnPack = "3a884ed6-31ef-47d7-9d2a-63182c4928ed"
2525

26+
[weakdeps]
27+
LinearMPC = "82e1c212-e1a2-49d2-b26a-a31d6968e3bd"
28+
29+
[extensions]
30+
RobustAndOptimalControlLinearMPCExt = "LinearMPC"
31+
2632
[compat]
2733
ChainRulesCore = "1"
2834
ComponentArrays = "0.9, 0.10, 0.11, 0.12, 0.13, 0.14, 0.15"
2935
ControlSystemsBase = "1.17"
3036
DescriptorSystems = "1.2"
3137
Distributions = "0.25"
3238
GenericSchur = "0.5.2"
39+
LinearMPC = "0.7"
3340
MatrixEquations = "2"
3441
MatrixPencils = "1"
3542
MonteCarloMeasurements = "1.0"
@@ -44,9 +51,10 @@ julia = "1.7"
4451
[extras]
4552
FiniteDiff = "6a86dc24-6348-571c-b903-95158fe2bd41"
4653
GenericLinearAlgebra = "14197337-ba66-59df-a3e3-ca00e7dcff7a"
54+
LinearMPC = "82e1c212-e1a2-49d2-b26a-a31d6968e3bd"
4755
Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80"
4856
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
4957
Zygote = "e88e6eb3-aa80-5325-afca-941959d7151f"
5058

5159
[targets]
52-
test = ["Test", "Plots", "Zygote", "FiniteDiff", "GenericLinearAlgebra"]
60+
test = ["Test", "Plots", "LinearMPC", "Zygote", "FiniteDiff", "GenericLinearAlgebra"]

docs/Project.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ ControlSystemsBase = "aaaaaaaa-a6ca-5380-bf3e-84a91bcd477e"
33
DisplayAs = "0b91fe84-8a4c-11e9-3e1d-67c38462b6d6"
44
Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4"
55
ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210"
6+
LinearMPC = "82e1c212-e1a2-49d2-b26a-a31d6968e3bd"
67
MonteCarloMeasurements = "0987c9cc-fe09-11e8-30f0-b96dd679fdca"
78
Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80"
89
RobustAndOptimalControl = "21fd56a4-db03-40ee-82ee-a87907bee541"

docs/src/index.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,3 +297,23 @@ A video tutorial on how to perform closed-loop analysis is available [here](http
297297
- [`hess_form`](@ref)
298298
- [`frequency_separation`](@ref)
299299
- [`RobustAndOptimalControl.blockdiagonalize`](@ref)
300+
301+
## MPC
302+
This package includes an extension for [LinearMPC.jl](https://darnstrom.github.io/LinearMPC.jl/stable/) that allows you to convert an [`LQGProblem`](@ref) into a linear MPC controller with added constraints. Example:
303+
```@example lqg_mpc
304+
using RobustAndOptimalControl, LinearMPC, LinearAlgebra
305+
306+
sys = ss([1 0.1; 0 1], [0; 0.1], [1 0], 0, 0.1)
307+
lqg = LQGProblem(sys, I(2), I(1), I(2), 0.01*I(1))
308+
309+
mpc = LinearMPC.MPC(lqg; N=20, umin=[-0.3], umax=[0.3])
310+
311+
sim = LinearMPC.Simulation(mpc, N=100, r=[1.0, 0])
312+
313+
using Plots
314+
plot(sim)
315+
```
316+
317+
See [Example: `lqg_mpc_disturbance.jl`](https://github.com/JuliaControl/RobustAndOptimalControl.jl/blob/master/examples/lqg_mpc_disturbance.jl) for a more detailed example.
318+
319+
See the [docs for LinearMPC.jl](https://darnstrom.github.io/LinearMPC.jl/stable/) for more details on how to use the MPC controller and modify its settings.

examples/lqg_mpc_disturbance.jl

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
#=
2+
This file demonstrates
3+
- Adding integral action to an LQG controller by means of state augmentation
4+
- Conversion of the LQG controller into an MPC contorller with added constraints
5+
=#
6+
using RobustAndOptimalControl, ControlSystemsBase, Plots, LinearAlgebra
7+
Ts = 1 # Sample time
8+
G = c2d(ss(tf(1, [10, 1])), Ts) # Process model
9+
10+
nx = G.nx
11+
nu = G.nu
12+
ny = G.ny
13+
x0 = zeros(G.nx) # Initial condition
14+
15+
Q1 = 100diagm(ones(G.nx)) # state cost matrix
16+
Q2 = 0.01diagm(ones(nu)) # control cost matrix
17+
18+
R1 = 0.001I(nx) # State noise covariance
19+
R2 = I(ny) # measurement noise covariance
20+
prob = LQGProblem(G, Q1, Q2, R1, R2)
21+
22+
disturbance = (x, t) -> t * Ts 10 # This is our load disturbance, a step at ``t = 10``
23+
Gcl = G_PS(prob) # This forms the transfer function from load disturbance to output
24+
res = lsim(Gcl, disturbance, 100)
25+
plot(res)
26+
27+
Gd1 = add_low_frequency_disturbance(G, ϵ = 1e-6, measurement=false)
28+
Gd2 = add_low_frequency_disturbance(G, ϵ = 1e-6, measurement=true) # The ϵ moves the integrator pole slightly into the stable region
29+
plots = map([Gd1, Gd2]) do Gd
30+
nx = Gd.nx
31+
32+
C = Gd.C
33+
Q1 = 100C'diagm(ones(G.nx)) * C # state cost matrix
34+
x0 = zeros(nx)
35+
36+
R1 = diagm([0.001, 1])
37+
R2 = I(ny)
38+
prob = LQGProblem(Gd, Q1, Q2, R1, R2)
39+
Gcl = [G_PS(prob); -comp_sensitivity(prob)] # -comp_sensitivity(prob) is the same as the transfer function from load disturbance to control signal
40+
res = lsim(Gcl, disturbance, 100)
41+
f1 = plot(res, ylabel=["y" "u"]); ylims!((-0.05, 0.3), sp = 1)
42+
43+
w = exp10.(LinRange(-3, log10(pi / Ts), 200))
44+
f2 = gangoffourplot(prob, w, lab = "", legend = :bottomright)
45+
46+
R1 = diagm([0.001, 0.2]) # Reduce the noise on the integrator state from 1 to 0.2
47+
R2 = I(ny)
48+
prob = LQGProblem(Gd, Q1, Q2, R1, R2)
49+
50+
Gcl = [G_PS(prob); -comp_sensitivity(prob)]
51+
res = lsim(Gcl, disturbance, 100)
52+
f3 = plot(res, ylabel=["y" "u"]); ylims!((-0.05, 0.3), sp = 1)
53+
f4 = gangoffourplot(prob, w, lab = "", legend = :bottomright)
54+
55+
plot(f1, f2, f3, f4, titlefontsize=10)
56+
end
57+
58+
plot(plots..., size=(1200,1000))
59+
60+
61+
62+
# ==============================================================================
63+
## LinearMPC
64+
# The example below illustrate how we can convert the LQGProblem into an MPC contorller by loading LinearMPC.jl
65+
# We then perform a rather low-level simulation with a manual loop, where we form the observer `obs` and step the plant
66+
# ==============================================================================
67+
68+
using LinearMPC
69+
70+
Gd = add_low_frequency_disturbance(G, ϵ = 1e-6, measurement=false)
71+
72+
C = Gd.C
73+
Q1 = 100diagm([1.0]) # output cost matrix
74+
R1 = diagm([0.001, 1]) # Dynamics noise covariance
75+
R2 = I(ny) # Measurement noise covariance
76+
Gde = ExtendedStateSpace(Gd, B1=I) # Since B1=I, R1 has size determined by state dimension, but C1=C, so Q1 has size determined by the output dimension
77+
prob = LQGProblem(Gde, Q1, Q2, R1, R2)
78+
obs = observer_predictor(prob, direct=false) # If a predictor is used, the observer update should be carried out in the end of the loop, if we use the filter below, we should instead perform the observer update in the beginning of the loop directly after obtaining the new measurement but before computing a new control signal.
79+
obs = observer_filter(prob)
80+
81+
mpc = LinearMPC.MPC(prob; N=20, umin=[-3], umax=[3])
82+
x = zeros(G.nx) # True plant state
83+
xh = zeros(Gd.nx) # Observer state
84+
X = [x[]] # For storage
85+
U = Float64[]
86+
u_mpc = [0.0]
87+
for i = 1:50
88+
global x, xh, u_mpc
89+
y = G.C*x # Compute the true measurement output
90+
xh = obs.A * xh + obs.B*[u_mpc; y] # Predict one step with the observer, u here is the control signal from the previous iteration, if using the predictor, use u from the current iteration and perform the observer update in the end of the loop instead
91+
u_disturbance = i * Ts 10 ? 1.0 : 0.0
92+
r = [1.0] # output reference
93+
u_mpc = LinearMPC.compute_control(mpc, xh; r) # Call MPC optimizer with estimated state
94+
u_tot = u_mpc .+ u_disturbance # Total input is control signal + disturbance
95+
x = G.A*x + G.B*u_tot # Advance the true plant state
96+
push!(X, x[]) # Store data for plotting
97+
push!(U, u_mpc[])
98+
end
99+
100+
using Test
101+
@test (G.C*X[end])[] 1 rtol=1e-4
102+
103+
plot(X*G.C', layout=2, sp=1, label="\$y\$")
104+
plot!(U, sp=2, label="\$u\$")
105+
hline!([1.0 3.0], linestyle=:dash, color=:black, label=["Reference" "\$u_{max}\$"], sp=[1 2])
106+
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
"""
2+
RobustAndOptimalControlLinearMPCExt
3+
4+
This extension allows you to convert an `LQGProblem` from RobustAndOptimalControl.jl to a `LinearMPC.MPC` object from LinearMPC.jl.
5+
"""
6+
module RobustAndOptimalControlLinearMPCExt
7+
8+
using LinearMPC
9+
using LinearAlgebra
10+
using RobustAndOptimalControl: LQGProblem
11+
using ControlSystemsBase: isdiscrete
12+
13+
14+
"""
15+
LinearMPC.MPC(prob::LQGProblem; N, Nc=N, Q3=nothing, Qf=nothing,
16+
umin=nothing, umax=nothing, ymin=nothing, ymax=nothing, kwargs...)
17+
18+
Convert an `LQGProblem` from RobustAndOptimalControl.jl to a `LinearMPC.MPC` object.
19+
20+
# Arguments
21+
- `prob::LQGProblem`: The LQG problem to convert
22+
- `N::Int`: Prediction horizon (required)
23+
- `Nc::Int`: Control horizon (default: `N`)
24+
- `Q3`: Input rate cost matrix (default: zeros)
25+
- `Qf`: Terminal state cost matrix (default: none)
26+
- `umin`, `umax`: Input bounds (default: none)
27+
- `ymin`, `ymax`: Output bounds (default: none)
28+
- `kwargs...`: Additional arguments passed to LinearMPC.MPC
29+
30+
# Notes
31+
- Only discrete-time systems are supported
32+
- The Kalman filter/observer from LQGProblem is not converted
33+
- Uses C1 (performance output) as the controlled output matrix
34+
- Cost matrices Q1, Q2 from LQGProblem map to Q, R in LinearMPC
35+
36+
# Example
37+
```julia
38+
using RobustAndOptimalControl, LinearMPC, LinearAlgebra
39+
40+
sys = ss([1 0.1; 0 1], [0; 0.1], [1 0], 0, 0.1)
41+
lqg = LQGProblem(sys, I(2), I(1), I(2), 0.01*I(1))
42+
mpc = LinearMPC.MPC(lqg; N=20, umin=[-0.3], umax=[0.3])
43+
44+
sim = LinearMPC.Simulation(mpc, N=100, r=[1.0, 0])
45+
46+
using Plots
47+
plot(sim)
48+
```
49+
"""
50+
function LinearMPC.MPC(prob::LQGProblem;
51+
N::Int,
52+
Nc::Int = N,
53+
Q3 = zeros(0,0),
54+
Qf = zeros(0,0),
55+
umin = zeros(0),
56+
umax = zeros(0),
57+
ymin = zeros(0),
58+
ymax = zeros(0),
59+
kwargs...
60+
)
61+
# Validate discrete-time system
62+
if !isdiscrete(prob)
63+
error("Only discrete-time systems are supported. Got a continuous-time system.")
64+
end
65+
66+
# Extract system matrices
67+
F = Matrix(prob.A) # State transition matrix
68+
G = Matrix(prob.B2) # Control input matrix
69+
C = Matrix(prob.C1) # Performance output matrix
70+
71+
# Get sampling time
72+
Ts = prob.Ts
73+
74+
# Get dimensions
75+
nu = size(G, 2)
76+
ny = size(C, 1)
77+
78+
# Create the LinearMPC.MPC object
79+
mpc = LinearMPC.MPC(F, G; Ts, C, Np=N, Nc, kwargs...)
80+
81+
# Set objective
82+
Q = Matrix(prob.Q1)
83+
R = Matrix(prob.Q2)
84+
Rr = Matrix(Q3)
85+
86+
LinearMPC.set_objective!(mpc; Q, R, Rr, Qf)
87+
LinearMPC.set_bounds!(mpc; umin, umax, ymin, ymax)
88+
89+
# Set labels from system names
90+
# sys = prob.sys
91+
# set_labels!(mpc; x=Symbol.(state_names(sys)), u=Symbol.(input_names(sys)))
92+
93+
return mpc
94+
end
95+
96+
end # module

src/ExtendedStateSpace.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -599,7 +599,7 @@ The conversion from a regular statespace object to an `ExtendedStateSpace` creat
599599
i.e., the system and performance mappings are identical, `system_mapping(se) == performance_mapping(se) == s`.
600600
However, all matrices `B1, B2, C1, C2; D11, D12, D21, D22` are overridable by a corresponding keyword argument. In this case, the controlled outputs are the same as measured outputs.
601601
602-
Related: `se = convert(ExtendedStateSpace, s::StateSpace)` produces an `ExtendedStateSpace` with empty `performance_mapping` from w->z such that `ss(se) == s`.
602+
Related: `se = convert(ExtendedStateSpace, s::StateSpace)` produces an `ExtendedStateSpace` with empty `performance_mapping` from w->z such that `ss(se) == s`. `ExtendedStateSpace(sys, B1=I, C1=I)` leads to a system where all state variables are affected by noise, and all are considered performance outputs, this corresponds to the traditional use of the functions `lqr` and `kalman`.
603603
"""
604604
function ExtendedStateSpace(s::AbstractStateSpace;
605605
A = s.A,

src/lqg.jl

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,18 @@ function ControlSystemsBase.observer_controller(l::LQGProblem, L::AbstractMatrix
354354
ss(Ac, Bc, Cc, Dc, l.timeevol)
355355
end
356356

357+
function ControlSystemsBase.observer_predictor(l::LQGProblem, K::Union{AbstractMatrix, Nothing} = nothing; direct = false, kwargs...)
358+
P = system_mapping(l, identity)
359+
if K === nothing
360+
K = kalman(l; direct)
361+
end
362+
observer_predictor(P, K; kwargs...)
363+
end
364+
365+
function ControlSystemsBase.observer_filter(l::LQGProblem, K = kalman(l); kwargs...)
366+
P = system_mapping(l, identity)
367+
observer_filter(P, K; kwargs...)
368+
end
357369

358370
"""
359371
ff_controller(sys, L, K; comp_dc = true)

test/runtests.jl

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,9 +126,19 @@ using Test
126126
include("../examples/uncertain.jl")
127127
end
128128

129+
@testset "lqg_mpc_disturbance" begin
130+
@info "Testing LQG MPC example"
131+
include("../examples/lqg_mpc_disturbance.jl")
132+
end
133+
129134
@testset "mcm_nugap" begin
130135
@info "Testing mcm_nugap"
131136
include("test_mcm_nugap.jl")
132137
end
133138

139+
@testset "LinearMPC extension" begin
140+
@info "Testing LinearMPC extension"
141+
include("test_linearmpc_ext.jl")
142+
end
143+
134144
end

0 commit comments

Comments
 (0)