Skip to content

Commit 9ca9e43

Browse files
committed
add LinearMPC extension
1 parent c725cf6 commit 9ca9e43

File tree

5 files changed

+200
-1
lines changed

5 files changed

+200
-1
lines changed

Project.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,12 @@ 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"
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
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+
12+
13+
"""
14+
LinearMPC.MPC(prob::LQGProblem; N, Nc=N, Q3=nothing, Qf=nothing,
15+
umin=nothing, umax=nothing, ymin=nothing, ymax=nothing, kwargs...)
16+
17+
Convert an `LQGProblem` from RobustAndOptimalControl.jl to a `LinearMPC.MPC` object.
18+
19+
# Arguments
20+
- `prob::LQGProblem`: The LQG problem to convert
21+
- `N::Int`: Prediction horizon (required)
22+
- `Nc::Int`: Control horizon (default: `N`)
23+
- `Q3`: Input rate cost matrix (default: zeros)
24+
- `Qf`: Terminal state cost matrix (default: none)
25+
- `umin`, `umax`: Input bounds (default: none)
26+
- `ymin`, `ymax`: Output bounds (default: none)
27+
- `kwargs...`: Additional arguments passed to LinearMPC.MPC
28+
29+
# Notes
30+
- Only discrete-time systems are supported
31+
- The Kalman filter/observer from LQGProblem is not converted
32+
- Uses C1 (performance output) as the controlled output matrix
33+
- Cost matrices Q1, Q2 from LQGProblem map to Q, R in LinearMPC
34+
35+
# Example
36+
```julia
37+
using RobustAndOptimalControl, LinearMPC, LinearAlgebra
38+
39+
sys = ss([1 0.1; 0 1], [0; 0.1], [1 0], 0, 0.1)
40+
lqg = LQGProblem(sys, I(2), I(1), I(2), 0.01*I(1))
41+
mpc = LinearMPC.MPC(lqg; N=20, umin=[-0.3], umax=[0.3])
42+
43+
sim = LinearMPC.Simulation(mpc, N=100, r=[1.0, 0])
44+
45+
using Plots
46+
plot(sim)
47+
```
48+
"""
49+
function LinearMPC.MPC(prob::LQGProblem;
50+
N::Int,
51+
Nc::Int = N,
52+
Q3 = zeros(0,0),
53+
Qf = zeros(0,0),
54+
umin = zeros(0),
55+
umax = zeros(0),
56+
ymin = zeros(0),
57+
ymax = zeros(0),
58+
kwargs...
59+
)
60+
# Validate discrete-time system
61+
if !ControlSystemsBase.isdiscrete(prob)
62+
error("Only discrete-time systems are supported. Got a continuous-time system.")
63+
end
64+
65+
# Extract system matrices
66+
F = Matrix(prob.A) # State transition matrix
67+
G = Matrix(prob.B2) # Control input matrix
68+
C = Matrix(prob.C1) # Performance output matrix
69+
70+
# Get sampling time
71+
Ts = prob.Ts
72+
73+
# Get dimensions
74+
nu = size(G, 2)
75+
ny = size(C, 1)
76+
77+
# Create the LinearMPC.MPC object
78+
mpc = LinearMPC.MPC(F, G; Ts, C, Np=N, Nc, kwargs...)
79+
80+
# Set objective
81+
Q = Matrix(prob.Q1)
82+
R = Matrix(prob.Q2)
83+
Rr = Matrix(Q3)
84+
85+
LinearMPC.set_objective!(mpc; Q, R, Rr, Qf)
86+
LinearMPC.set_bounds!(mpc; umin, umax, ymin, ymax)
87+
88+
# Set labels from system names
89+
# sys = prob.sys
90+
# set_labels!(mpc; x=Symbol.(state_names(sys)), u=Symbol.(input_names(sys)))
91+
92+
return mpc
93+
end
94+
95+
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,

test/runtests.jl

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,4 +131,9 @@ using Test
131131
include("test_mcm_nugap.jl")
132132
end
133133

134+
@testset "LinearMPC extension" begin
135+
@info "Testing LinearMPC extension"
136+
include("test_linearmpc_ext.jl")
137+
end
138+
134139
end

test/test_linearmpc_ext.jl

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
using Test
2+
using LinearAlgebra
3+
using LinearMPC
4+
using RobustAndOptimalControl
5+
6+
# Test basic LQGProblem conversion (from docstring example)
7+
Ts = 0.1
8+
sys = ss([1 Ts; 0 1], [0; Ts], [1 0], 0, Ts)
9+
10+
# Create LQGProblem: Q1=state cost, Q2=input cost, R1=process noise, R2=measurement noise
11+
lqg = LQGProblem(sys, I(2), I(1), I(2), 0.01*I(1))
12+
13+
# Convert to LinearMPC with constraints
14+
mpc = LinearMPC.MPC(lqg; N=20, umin=[-0.3], umax=[0.3])
15+
16+
# Check basic properties
17+
@test mpc.model.Ts == Ts
18+
@test mpc.model.F == sys.A
19+
@test mpc.model.G == sys.B
20+
@test mpc.settings.Np == 20
21+
22+
# Simulate and verify it works
23+
sim = LinearMPC.Simulation(mpc; N=100, r=[1.0, 0])
24+
25+
# Check that simulation ran
26+
@test size(sim.xs, 2) == 101 # 100 steps + initial state
27+
@test size(sim.us, 2) == 100
28+
29+
# Check constraint satisfaction
30+
@test all(sim.us .>= -0.3 - 1e-6)
31+
@test all(sim.us .<= 0.3 + 1e-6)
32+
33+
# Check that output converges toward reference
34+
final_output = (sys.C * sim.xs[:, end])[1]
35+
@test abs(final_output - 1.0) < 0.1 # Should be close to reference
36+
37+
@testset "LQGProblem with Q3 (input rate penalty)" begin
38+
# Test with input rate cost
39+
Q3 = 0.5 * I(1)
40+
mpc_q3 = LinearMPC.MPC(lqg; N=15, Q3, umin=[-0.5], umax=[0.5])
41+
42+
sim_q3 = LinearMPC.Simulation(mpc_q3; N=50, r=[0.5, 0])
43+
44+
# With Q3, control should be smoother (smaller rate of change)
45+
du = diff(sim_q3.us, dims=2)
46+
max_rate = maximum(abs.(du))
47+
@test max_rate < 0.3 # Rate should be limited due to Q3 penalty
48+
end
49+
50+
@testset "LQGProblem with terminal cost Qf" begin
51+
# Test with terminal cost
52+
Qf = 10.0 * I(2) # Higher terminal cost
53+
mpc_qf = LinearMPC.MPC(lqg; N=10, Qf, umin=[-1.0], umax=[1.0])
54+
55+
sim_qf = LinearMPC.Simulation(mpc_qf; N=30, r=[0.3, 0])
56+
57+
# Should still work and converge
58+
final_output = (sys.C * sim_qf.xs[:, end])[1]
59+
@test abs(final_output - 0.3) < 0.1
60+
end
61+
62+
@testset "LQGProblem MIMO system" begin
63+
# Test with 2-input 2-output system
64+
A = [0.9 0.1; 0.05 0.95]
65+
B = [1.0 0.0; 0.0 1.0]
66+
C = [1.0 0.0; 0.0 1.0]
67+
D = zeros(2, 2)
68+
sys_mimo = ss(A, B, C, D, Ts)
69+
70+
lqg_mimo = LQGProblem(sys_mimo, I(2), I(2), I(2), 0.01*I(2))
71+
mpc_mimo = LinearMPC.MPC(lqg_mimo; N=15,
72+
umin=[-0.5, -0.5], umax=[0.5, 0.5])
73+
74+
sim_mimo = LinearMPC.Simulation(mpc_mimo; N=50, r=[0.3, -0.2])
75+
76+
# Check dimensions
77+
@test size(sim_mimo.us, 1) == 2
78+
@test size(sim_mimo.xs, 1) == 2
79+
80+
# Check constraints on both inputs
81+
@test all(sim_mimo.us[1, :] .>= -0.5 - 1e-6)
82+
@test all(sim_mimo.us[1, :] .<= 0.5 + 1e-6)
83+
@test all(sim_mimo.us[2, :] .>= -0.5 - 1e-6)
84+
@test all(sim_mimo.us[2, :] .<= 0.5 + 1e-6)
85+
end
86+
87+
@testset "LQGProblem continuous-time error" begin
88+
# Test that continuous-time systems throw an error
89+
sys_cont = ss([0 1; -1 -1], [0; 1], [1 0], 0) # Continuous-time
90+
lqg_cont = LQGProblem(sys_cont, I(2), I(1), I(2), 0.01*I(1))
91+
92+
@test_throws ErrorException LinearMPC.MPC(lqg_cont; N=10)
93+
end

0 commit comments

Comments
 (0)