Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
6205caf
added: `LinearMPCext` extension
franckgaga Dec 10, 2025
c99311c
wip: `LinearMPC` extension
franckgaga Dec 14, 2025
788dca8
wip: idem
franckgaga Dec 14, 2025
9a2eee0
added: validation no input increment constraints
franckgaga Dec 14, 2025
7cfe2d0
Merge branch 'main' into extension
franckgaga Dec 17, 2025
7fd34e9
wip: idem
franckgaga Dec 17, 2025
9a3f4d7
wip: idem
franckgaga Dec 18, 2025
aa7054d
added: terminal constraints in `LinearMPCext`
franckgaga Dec 18, 2025
e3d48b2
Merge branch 'debug_terminal_softness' into extension
franckgaga Dec 18, 2025
51e28e5
added: commented-out move blocking
franckgaga Dec 18, 2025
940e04d
added: support bounds pass `Hc` with conversion
franckgaga Dec 18, 2025
ba54960
doc: wip
franckgaga Dec 31, 2025
58b5fe2
Merge branch 'main' into extension
franckgaga Jan 4, 2026
6c96df2
Merge branch 'main' into extension
franckgaga Jan 5, 2026
c81216c
Merge branch 'main' into extension
franckgaga Jan 5, 2026
7cc107c
Merge branch 'main' into extension
franckgaga Jan 5, 2026
1ecc976
Merge branch 'main' into extension
franckgaga Jan 5, 2026
6eea3a2
doc: `LinearMPC.MPC` docstring in online doc
franckgaga Jan 5, 2026
167e614
doc: `LinearMPC.MPC` doc improvement
franckgaga Jan 5, 2026
80d7042
doc: idem
franckgaga Jan 5, 2026
f302879
doc: idem
franckgaga Jan 5, 2026
15e1515
doc: idem
franckgaga Jan 5, 2026
f2d872f
doc: idem
franckgaga Jan 5, 2026
4d186f6
added: operating point conversion
franckgaga Jan 6, 2026
3ad3e59
debug: correct variable
franckgaga Jan 6, 2026
f7bf86e
added: warning about measured disturbance operating point `dop`
franckgaga Jan 6, 2026
c3e19f8
debug: forgot an `end`
franckgaga Jan 6, 2026
321e447
wip: offsets
franckgaga Jan 9, 2026
f66c105
debug
franckgaga Jan 10, 2026
9442a30
Merge origin/extension into extension
franckgaga Jan 10, 2026
35dc7a3
debug: all offsets
franckgaga Jan 10, 2026
e0c4dd3
debug: correct kwarg
franckgaga Jan 10, 2026
055c150
doc: correct bound in MHE manual example
franckgaga Jan 11, 2026
752fb7e
doc: minor change in `jldoctest`
franckgaga Jan 11, 2026
ad4147c
test: comparison with `LinMPC` and `LinearMPC.MPC`
franckgaga Jan 11, 2026
5eb8642
test: looser tolerances
franckgaga Jan 11, 2026
45ca458
changed: minor change in error messages
franckgaga Jan 11, 2026
0bcf07c
doc: C code gen in `README.md`
franckgaga Jan 11, 2026
f420bdf
doc: minor change
franckgaga Jan 11, 2026
049bbc7
Merge remote-tracking branch 'origin/main' into extension
franckgaga Jan 13, 2026
6b42b92
doc: return args of `init_predmat` and `init_defectmat`
franckgaga Jan 13, 2026
92bb1b0
added: input increment constraint conversion
franckgaga Jan 14, 2026
9194d4f
added: heuristic conversion factor of soft constraints
franckgaga Jan 14, 2026
ca5b230
changed: more details in comment
franckgaga Jan 14, 2026
fcfc2fe
changed: better conversion factor for `soft_weight`
franckgaga Jan 14, 2026
bc3869b
doc: minor modification in `jldoctest`
franckgaga Jan 14, 2026
54dcc38
changed: moving `@warn` near the conversion code
franckgaga Jan 16, 2026
141d162
changed: reformulation
franckgaga Jan 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions Project.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name = "ModelPredictiveControl"
uuid = "61f9bdb8-6ae4-484a-811f-bbf86720c31c"
version = "1.14.4"
version = "1.15.0"
authors = ["Francis Gagnon"]

[deps]
Expand All @@ -22,6 +22,12 @@ SparseConnectivityTracer = "9f842d2f-2579-4b1d-911e-f412cf18a3f5"
SparseMatrixColorings = "0a514795-09f3-496d-8182-132a7b665d35"
StableRNGs = "860ef19b-820b-49d6-a774-d7a799459cd3"

[weakdeps]
LinearMPC = "82e1c212-e1a2-49d2-b26a-a31d6968e3bd"

[extensions]
LinearMPCext = "LinearMPC"

[compat]
ControlSystemsBase = "1.18.2"
DAQP = "0.6, 0.7.1"
Expand All @@ -32,6 +38,7 @@ ForwardDiff = "0.10, 1"
Ipopt = "1"
JuMP = "1.21"
LinearAlgebra = "1.10"
LinearMPC = "0.7.0"
Logging = "1.10"
MathOptInterface = "1.46"
OSQP = "0.8"
Expand All @@ -52,10 +59,11 @@ julia = "1.10"
DAQP = "c47d62df-3981-49c8-9651-128b1cd08617"
Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4"
FiniteDiff = "6a86dc24-6348-571c-b903-95158fe2bd41"
LinearMPC = "82e1c212-e1a2-49d2-b26a-a31d6968e3bd"
Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
TestItemRunner = "f8b46487-2199-4994-9208-9a1283c18c0a"
TestItems = "1c621080-faea-4a02-84b6-bbd5e436b8fe"

[targets]
test = ["Test", "TestItems", "TestItemRunner", "Documenter", "Plots", "DAQP", "FiniteDiff"]
test = ["Test", "TestItems", "TestItemRunner", "Documenter", "Plots", "DAQP", "FiniteDiff", "LinearMPC"]
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ for more detailed examples.
- 📝 **Transcription**: Direct single/multiple shooting and trapezoidal collocation.
- 🩺 **Troubleshooting**: Detailed diagnostic information about optimum.
- ⏱️ **Real-Time**: Optimized for low memory allocations with soft real-time utilities.
- 📟️ **Embedded**: Lightweight C code generation via `LinearMPC.jl`

### 🔭 State Estimation Features

Expand Down
2 changes: 2 additions & 0 deletions docs/Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4"
DocumenterInterLinks = "d12716ef-a0f6-4df4-a9f1-a5a34e75c656"
JuMP = "4076af6c-e467-56ae-b986-b466b2749572"
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
LinearMPC = "82e1c212-e1a2-49d2-b26a-a31d6968e3bd"
Logging = "56ddb016-857b-54e1-b83d-db4d58db5568"
ModelingToolkit = "961ee093-0014-501f-94e3-6117800e7a78"
Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80"
Expand All @@ -15,6 +16,7 @@ DAQP = "0.6, 0.7.1"
Documenter = "1"
JuMP = "1"
LinearAlgebra = "1.10"
LinearMPC = "0.7.0"
Logging = "1.10"
ModelingToolkit = "10"
Plots = "1"
2 changes: 2 additions & 0 deletions docs/make.jl
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ push!(LOAD_PATH,"../src/")

using Documenter, DocumenterInterLinks
using ModelPredictiveControl
import LinearMPC

links = InterLinks(
"Julia" => "https://docs.julialang.org/en/v1/objects.inv",
Expand All @@ -14,6 +15,7 @@ links = InterLinks(
"DifferentiationInterface" => "https://juliadiff.org/DifferentiationInterface.jl/DifferentiationInterface/stable/objects.inv",
"ForwardDiff" => "https://juliadiff.org/ForwardDiff.jl/stable/objects.inv",
"LowLevelParticleFilters" => "https://baggepinnen.github.io/LowLevelParticleFilters.jl/stable/objects.inv",
"LinearMPC" => "https://darnstrom.github.io/LinearMPC.jl/stable/objects.inv",
)

DocMeta.setdocmeta!(
Expand Down
2 changes: 1 addition & 1 deletion docs/src/manual/linmpc.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ For the CSTR, we will bound the innovation term ``\mathbf{y}(k) - \mathbf{ŷ}(k
estim = MovingHorizonEstimator(model, He=10, nint_u=[1, 1], σQint_u = [1, 2])
estim = setconstraint!(estim, v̂min=[-1, -0.5], v̂max=[+1, +0.5])
mpc_mhe = LinMPC(estim, Hp=10, Hc=2, Mwt=[1, 1], Nwt=[0.1, 0.1])
mpc_mhe = setconstraint!(mpc_mhe, ymin=[45, -Inf])
mpc_mhe = setconstraint!(mpc_mhe, ymin=[48, -Inf])
```

The rejection is indeed improved:
Expand Down
8 changes: 7 additions & 1 deletion docs/src/public/predictive_control.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,12 @@ PredictiveController
LinMPC
```

### Conversion to LinearMPC.jl (code generation)

```@docs
LinearMPC.MPC
```

## ExplicitMPC

```@docs
Expand Down Expand Up @@ -114,4 +120,4 @@ MultipleShooting

```@docs
TrapezoidalCollocation
```
```
254 changes: 254 additions & 0 deletions ext/LinearMPCext.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
module LinearMPCext

using ModelPredictiveControl
using LinearAlgebra, SparseArrays
using JuMP

import LinearMPC
import ModelPredictiveControl: isblockdiag

function Base.convert(::Type{LinearMPC.MPC}, mpc::ModelPredictiveControl.LinMPC)
model, estim, weights = mpc.estim.model, mpc.estim, mpc.weights
nu, ny, nx̂ = model.nu, model.ny, estim.nx̂
Hp, Hc = mpc.Hp, mpc.Hc
nΔU = Hc * nu
validate_compatibility(mpc)
# --- Model parameters ---
F, G, Gd = estim.Â, estim.B̂u, estim.B̂d
C, Dd = estim.Ĉ, estim.D̂d
Np = Hp
Nc = Hc
newmpc = LinearMPC.MPC(F, G; Gd, C, Dd, Np, Nc)
# --- Operating points ---
uoff = model.uop
doff = model.dop
yoff = model.yop
xoff = estim.x̂op
foff = estim.f̂op
LinearMPC.set_offset!(newmpc; uo=uoff, ho=yoff, doff=doff, xo=xoff, fo=foff)
# --- State observer parameters ---
Q, R = estim.cov.Q̂, estim.cov.R̂
LinearMPC.set_state_observer!(newmpc; C=estim.Ĉm, Q, R)
# --- Objective function weights ---
Q = weights.M_Hp[1:ny, 1:ny]
Qf = weights.M_Hp[end-ny+1:end, end-ny+1:end]
Rr = weights.Ñ_Hc[1:nu, 1:nu]
R = weights.L_Hp[1:nu, 1:nu]
LinearMPC.set_objective!(newmpc; Q, Rr, R, Qf)
# --- Custom move blocking ---
LinearMPC.move_block!(newmpc, mpc.nb) # un-comment when debugged
# ---- Constraint softening ---
only_hard = weights.isinf_C
if !only_hard
issoft(C) = any(x -> x > 0, C)
C_u = -mpc.con.A_Umin[:, end]
C_Δu = -mpc.con.A_ΔŨmin[1:nΔU, end]
C_y = -mpc.con.A_Ymin[:, end]
c_x̂ = -mpc.con.A_x̂min[:, end]
if sum(mpc.con.i_b) > 1 # ignore the slack variable ϵ bound
if issoft(C_u) || issoft(C_Δu) || issoft(C_y) || issoft(C_x̂)
@warn "The LinearMPC conversion is approximate for the soft constraints.\n"*
"You may need to adjust the soft_weight field of the "*
"LinearMPC.MPC object to replicate behaviors."
end
end
# LinearMPC relies on a different softening mechanism (new implicit slacks for each
# softened bounds), so we apply an approximate conversion factor on the Cwt weight:
Cwt = weights.Ñ_Hc[end, end]
nsoft = sum((mpc.con.A[:,end] .< 0) .& (mpc.con.i_b)) - 1
newmpc.settings.soft_weight = 10*sqrt(nsoft*Cwt)
else
C_u = zeros(nu*Hp)
C_Δu = zeros(nu*Hc)
C_y = zeros(ny*Hp)
c_x̂ = zeros(nx̂)
end
# --- Manipulated inputs constraints ---
Umin, Umax = mpc.con.U0min + mpc.Uop, mpc.con.U0max + mpc.Uop
I_u = Matrix{Float64}(I, nu, nu)
# add_constraint! does not support u bounds pass the control horizon Hc
# so we compute the extremum bounds from k=Hc-1 to Hp, and apply them at k=Hc-1
Umin_finals = reshape(Umin[nu*(Hc-1)+1:end], nu, :)
Umax_finals = reshape(Umax[nu*(Hc-1)+1:end], nu, :)
umin_end = mapslices(maximum, Umin_finals; dims=2)
umax_end = mapslices(minimum, Umax_finals; dims=2)
for k in 0:Hc-1
if k < Hc - 1
umin_k, umax_k = Umin[k*nu+1:(k+1)*nu], Umax[k*nu+1:(k+1)*nu]
else
umin_k, umax_k = umin_end, umax_end
end
c_u_k = C_u[k*nu+1:(k+1)*nu]
ks = [k + 1] # a `1` in ks argument corresponds to the present time step k+0
for i in 1:nu
lb = isfinite(umin_k[i]) ? [umin_k[i]] : zeros(0)
ub = isfinite(umax_k[i]) ? [umax_k[i]] : zeros(0)
soft = !only_hard && c_u_k[i] > 0
Au = I_u[i:i, :]
LinearMPC.add_constraint!(newmpc; Au, lb, ub, ks, soft)
end
end
# --- Input increment constraints ---
ΔUmin, ΔUmax = mpc.con.ΔŨmin[1:nΔU], mpc.con.ΔŨmax[1:nΔU]
I_Δu = Matrix{Float64}(I, nu, nu)
for k in 0:Hc-1
Δumin_k, Δumax_k = ΔUmin[k*nu+1:(k+1)*nu], ΔUmax[k*nu+1:(k+1)*nu]
c_Δu_k = C_Δu[k*nu+1:(k+1)*nu]
ks = [k + 1] # a `1` in ks argument corresponds to the present time step k+0
for i in 1:nu
lb = isfinite(Δumin_k[i]) ? [Δumin_k[i]] : zeros(0)
ub = isfinite(Δumax_k[i]) ? [Δumax_k[i]] : zeros(0)
soft = !only_hard && c_Δu_k[i] > 0
Au, Aup = I_Δu[i:i, :], -I_Δu[i:i, :]
LinearMPC.add_constraint!(newmpc; Au, Aup, lb, ub, ks, soft)
end
end
# --- Output constraints ---
Y0min, Y0max = mpc.con.Y0min, mpc.con.Y0max
for k in 1:Hp
ymin_k, ymax_k = Y0min[(k-1)*ny+1:k*ny], Y0max[(k-1)*ny+1:k*ny]
c_y_k = C_y[(k-1)*ny+1:k*ny]
ks = [k + 1] # a `1` in ks argument corresponds to the present time step k+0
for i in 1:ny
lb = isfinite(ymin_k[i]) ? [ymin_k[i]] : zeros(0)
ub = isfinite(ymax_k[i]) ? [ymax_k[i]] : zeros(0)
soft = !only_hard && c_y_k[i] > 0
Ax, Ad = C[i:i, :], Dd[i:i, :]
LinearMPC.add_constraint!(newmpc; Ax, Ad, lb, ub, ks, soft)
end
end
# --- Terminal constraints ---
x̂0min, x̂0max = mpc.con.x̂0min, mpc.con.x̂0max
I_x̂ = Matrix{Float64}(I, nx̂, nx̂)
ks = [Hp + 1] # a `1` in ks argument corresponds to the present time step k+0
for i in 1:nx̂
lb = isfinite(x̂0min[i]) ? [x̂0min[i]] : zeros(0)
ub = isfinite(x̂0max[i]) ? [x̂0max[i]] : zeros(0)
soft = !only_hard && c_x̂[i] > 0
Ax = I_x̂[i:i, :]
LinearMPC.add_constraint!(newmpc; Ax, lb, ub, ks, soft)
end
return newmpc
end

function validate_compatibility(mpc::ModelPredictiveControl.LinMPC)
if mpc.transcription isa MultipleShooting
error("LinearMPC only supports SingleShooting transcription.")
end
if !(mpc.estim isa SteadyKalmanFilter) || !mpc.estim.direct
error("LinearMPC only supports SteadyKalmanFilter with direct=true option.")
end
if JuMP.solver_name(mpc.optim) != "DAQP"
@warn "LinearMPC relies on DAQP, and the solver in the mpc object " *
"is currently $(JuMP.solver_name(mpc.optim)).\n" *
"The results in closed-loop may be different."
end
validate_weights(mpc)
validate_constraints(mpc)
return nothing
end

function validate_weights(mpc::ModelPredictiveControl.LinMPC)
ny, nu = mpc.estim.model.ny, mpc.estim.model.nu
Hp, Hc = mpc.Hp, mpc.Hc
nΔU = Hc * nu
M_Hp, N_Hc, L_Hp = mpc.weights.M_Hp, mpc.weights.Ñ_Hc[1:nΔU, 1:nΔU], mpc.weights.L_Hp
M_1, N_1, L_1 = M_Hp[1:ny, 1:ny], N_Hc[1:nu, 1:nu], L_Hp[1:nu, 1:nu]
for i in 2:mpc.Hp-1 # last block is terminal weight, can be different
M_i = M_Hp[(i-1)*ny+1:i*ny, (i-1)*ny+1:i*ny]
if !isapprox(M_i, M_1)
error("LinearMPC only supports identical weights for each stages in M_Hp.")
end
end
isblockdiag(M_Hp, ny, Hp) || error("M_Hp must be block diagonal.")
for i in 2:mpc.Hc
N_i = N_Hc[(i-1)*nu+1:i*nu, (i-1)*nu+1:i*nu]
if !isapprox(N_i, N_1)
error("LinearMPC only supports identical weights for each stages in Ñ_Hc.")
end
end
isblockdiag(N_Hc, nu, Hc) || error("Ñ_Hc must be block diagonal.")
for i in 2:mpc.Hp
L_i = L_Hp[(i-1)*nu+1:i*nu, (i-1)*nu+1:i*nu]
if !isapprox(L_i, L_1)
error("LinearMPC only supports identical weights for each stages in L_Hp.")
end
end
isblockdiag(L_Hp, nu, Hp) || error("L_Hp must be block diagonal.")
return nothing
end

function validate_constraints(mpc::ModelPredictiveControl.LinMPC)
nΔU = mpc.Hc * mpc.estim.model.nu
mpc.weights.isinf_C && return nothing # only hard constraints are entirely supported
C_umin, C_umax = -mpc.con.A_Umin[:, end], -mpc.con.A_Umax[:, end]
C_Δumin, C_Δumax = -mpc.con.A_ΔŨmin[1:nΔU, end], -mpc.con.A_ΔŨmax[1:nΔU, end]
C_ymin, C_ymax = -mpc.con.A_Ymin[:, end], -mpc.con.A_Ymax[:, end]
C_x̂min, C_x̂max = -mpc.con.A_x̂min[:, end], -mpc.con.A_x̂max[:, end]
is0or1(C) = all(x -> x ≈ 0 || x ≈ 1, C)
if (
!is0or1(C_umin) || !is0or1(C_umax) ||
!is0or1(C_Δumin) || !is0or1(C_Δumax) ||
!is0or1(C_ymin) || !is0or1(C_ymax) ||
!is0or1(C_x̂min) || !is0or1(C_x̂max)

)
error("LinearMPC only supports softness parameters c = 0 or 1.")
end
if (
!isapprox(C_umin, C_umax) ||
!isapprox(C_Δumin, C_Δumax) ||
!isapprox(C_ymin, C_ymax) ||
!isapprox(C_x̂min, C_x̂max)
)
error("LinearMPC only supports identical softness parameters for lower and upper bounds.")
end
return nothing
end

@doc raw"""
LinearMPC.MPC(mpc::LinMPC)

Convert a `ModelPredictiveControl.LinMPC` object to a `LinearMPC.MPC` object.

The `LinearMPC` package needs to be installed and available in the activated Julia
environment. The converted object can be used to generate lightweight C-code for embedded
applications using the `LinearMPC.codegen` function. Note that not all features of [`LinMPC`](@ref)
are supported, including these restrictions:

- the solver is limited to [`DAQP`](https://darnstrom.github.io/daqp/).
- the transcription method must be [`SingleShooting`](@ref).
- the state estimator must be a [`SteadyKalmanFilter`](@ref) with `direct=true`.
- only block-diagonal weights are allowed.
- the constraint relaxation mechanism is different, so a 1-on-1 conversion of the soft
constraints is impossible (use `Cwt=Inf` to disable relaxation).

But the package has also several exclusive functionalities, such as pre-stabilization,
constrained explicit MPC, and binary manipulated inputs. See the [`LinearMPC.jl`](@extref LinearMPC)
documentation for more details on the supported features and how to generate code.

# Examples
```jldoctest
julia> import LinearMPC, JuMP, DAQP;

julia> mpc1 = LinMPC(LinModel(tf(2, [10, 1]), 1.0); optim=JuMP.Model(DAQP.Optimizer));

julia> preparestate!(mpc1, [1.0]);

julia> u = moveinput!(mpc1, [10.0]); round.(u, digits=6)
1-element Vector{Float64}:
17.577311

julia> mpc2 = LinearMPC.MPC(mpc1);

julia> x̂ = LinearMPC.correct_state!(mpc2, [1.0]);

julia> u = LinearMPC.compute_control(mpc2, x̂, r=[10.0]); round.(u, digits=6)
1-element Vector{Float64}:
17.577311
```
"""
LinearMPC.MPC(mpc::ModelPredictiveControl.LinMPC) = convert(LinearMPC.MPC, mpc)


end # LinearMPCext
Loading
Loading