From 6205caf3e4e1969dcca6af96d1bd4e5818325dec Mon Sep 17 00:00:00 2001 From: franckgaga Date: Wed, 10 Dec 2025 17:34:25 -0500 Subject: [PATCH 01/42] added: `LinearMPCext` extension --- Project.toml | 10 +++++++++- ext/LinearMPCext.jl | 9 +++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 ext/LinearMPCext.jl diff --git a/Project.toml b/Project.toml index feb433b48..6705afd74 100644 --- a/Project.toml +++ b/Project.toml @@ -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" @@ -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" @@ -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"] diff --git a/ext/LinearMPCext.jl b/ext/LinearMPCext.jl new file mode 100644 index 000000000..b182dc814 --- /dev/null +++ b/ext/LinearMPCext.jl @@ -0,0 +1,9 @@ +module LinearMPCext + +using ModelPredictiveControl, LinearMPC + +export hi + +hi() = println("hello world!") + +end # LinearMPCext \ No newline at end of file From c99311ccb3691cd344ff1566b16b3db83e14a34f Mon Sep 17 00:00:00 2001 From: franckgaga Date: Sun, 14 Dec 2025 14:18:46 -0500 Subject: [PATCH 02/42] wip: `LinearMPC` extension --- ext/LinearMPCext.jl | 3 +-- src/ModelPredictiveControl.jl | 7 +++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/ext/LinearMPCext.jl b/ext/LinearMPCext.jl index b182dc814..0c534ab4b 100644 --- a/ext/LinearMPCext.jl +++ b/ext/LinearMPCext.jl @@ -2,8 +2,7 @@ module LinearMPCext using ModelPredictiveControl, LinearMPC -export hi -hi() = println("hello world!") +ModelPredictiveControl.hi(::ModelPredictiveControl.Ext) = println("hello world!") end # LinearMPCext \ No newline at end of file diff --git a/src/ModelPredictiveControl.jl b/src/ModelPredictiveControl.jl index c33cc2c85..322fa496f 100644 --- a/src/ModelPredictiveControl.jl +++ b/src/ModelPredictiveControl.jl @@ -51,6 +51,13 @@ export PredictiveController, ExplicitMPC, LinMPC, NonLinMPC, setconstraint!, mov export TranscriptionMethod, SingleShooting, MultipleShooting, TrapezoidalCollocation export SimResult, getinfo, sim! +export hi + +struct NonExt end +struct Ext end + +hi(::NonExt) = println("hello wold with a number") + include("general.jl") include("sim_model.jl") include("state_estim.jl") From 788dca875b2e33c1059f00e096e634d2ee586caf Mon Sep 17 00:00:00 2001 From: franckgaga Date: Sun, 14 Dec 2025 17:11:43 -0500 Subject: [PATCH 03/42] wip: idem --- ext/LinearMPCext.jl | 56 ++++++++++++++++++++++++++++++++++- src/ModelPredictiveControl.jl | 7 ----- 2 files changed, 55 insertions(+), 8 deletions(-) diff --git a/ext/LinearMPCext.jl b/ext/LinearMPCext.jl index 0c534ab4b..5962ab6a7 100644 --- a/ext/LinearMPCext.jl +++ b/ext/LinearMPCext.jl @@ -2,7 +2,61 @@ module LinearMPCext using ModelPredictiveControl, LinearMPC +using JuMP + +function Base.convert(::Type{LinearMPC.MPC}, mpc::ModelPredictiveControl.LinMPC) + model, estim, weights = mpc.estim.model, mpc.estim, mpc.weights + validate_compatibility(mpc) + + F, G, Gd = estim.Â, estim.B̂u, estim.B̂d + C, Dd = estim.Ĉ, estim.D̂d + Np = mpc.Hp + Nc = mpc.Hc + + + newmpc = LinearMPC.MPC(F, G; Gd, C, Dd, Np, Nc) + + + Q, R = estim.cov.Q̂, estim.cov.R̂ + C = estim.Ĉm + set_state_observer!(newmpc; C, Q, R) + + Q = weights.M_Hp[1:model.ny, 1:model.ny] + Qf = weights.M_Hp[end-model.ny+1:end, end-model.ny+1:end] + Rr = weights.Ñ_Hc[1:model.nu, 1:model.nu] + R = weights.L_Hp[1:model.nu, 1:model.nu] + soft_weight = weights.Ñ_Hc[end, end] + + LinearMPC.set_objective!(newmpc; Q, Rr, R, Qf) + !weights.isinf_C && (newmpc.settings.soft_weight = soft_weight) + + Umin, Umax = mpc.con.U0min + mpc.Uop, mpc.con.U0max + mpc.Uop + Ymin, Ymax = mpc.con.Y0min + mpc.Yop, mpc.con.Y0max + mpc.Yop + + umin, umax = Umin[1:model.nu], Umax[1:model.nu] + ymin, ymax = Ymin[1:model.ny], Ymax[1:model.ny] + + LinearMPC.set_bounds!(newmpc; umin, umax, ymin, ymax) + + return newmpc +end + +function validate_compatibility(mpc::ModelPredictiveControl.LinMPC) + if mpc.transcription isa MultipleShooting + error("LinearMPC.MPC only supports SingleShooting transcription.") + end + if !(mpc.estim isa SteadyKalmanFilter) || !mpc.estim.direct + error("LinearMPC.MPC only supports SteadyKalmanFilter with direct=true option.") + end + if JuMP.solver_name(mpc.optim) != "DAQP" + @warn "LinearMPC.MPC 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 + return nothing +end + +LinearMPC.MPC(mpc::ModelPredictiveControl.LinMPC) = convert(LinearMPC.MPC, mpc) -ModelPredictiveControl.hi(::ModelPredictiveControl.Ext) = println("hello world!") end # LinearMPCext \ No newline at end of file diff --git a/src/ModelPredictiveControl.jl b/src/ModelPredictiveControl.jl index 322fa496f..c33cc2c85 100644 --- a/src/ModelPredictiveControl.jl +++ b/src/ModelPredictiveControl.jl @@ -51,13 +51,6 @@ export PredictiveController, ExplicitMPC, LinMPC, NonLinMPC, setconstraint!, mov export TranscriptionMethod, SingleShooting, MultipleShooting, TrapezoidalCollocation export SimResult, getinfo, sim! -export hi - -struct NonExt end -struct Ext end - -hi(::NonExt) = println("hello wold with a number") - include("general.jl") include("sim_model.jl") include("state_estim.jl") From 9a2eee0af6744333aa514ab765c7d5dee7adcf44 Mon Sep 17 00:00:00 2001 From: franckgaga Date: Sun, 14 Dec 2025 17:51:19 -0500 Subject: [PATCH 04/42] added: validation no input increment constraints --- ext/LinearMPCext.jl | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ext/LinearMPCext.jl b/ext/LinearMPCext.jl index 5962ab6a7..32f98ee29 100644 --- a/ext/LinearMPCext.jl +++ b/ext/LinearMPCext.jl @@ -53,6 +53,10 @@ function validate_compatibility(mpc::ModelPredictiveControl.LinMPC) "is currently $(JuMP.solver_name(mpc.optim)).\n"* "The results in closed-loop may be different." end + nΔU = mpc.Hc*mpc.estim.model.nu + if any(isfinite, mpc.con.ΔŨmin[1:nΔU]) || any(isfinite, mpc.con.ΔŨmin[1:nΔU]) + error("LinearMPC.MPC does not support constraints on input increments Δu") + end return nothing end From 7fd34e95086e4869373b1f63de1c6126da01ff5e Mon Sep 17 00:00:00 2001 From: franckgaga Date: Wed, 17 Dec 2025 18:30:29 -0500 Subject: [PATCH 05/42] wip: idem --- docs/Project.toml | 1 + ext/LinearMPCext.jl | 88 ++++++++++++++++++++++++++++++++++++--------- 2 files changed, 72 insertions(+), 17 deletions(-) diff --git a/docs/Project.toml b/docs/Project.toml index 6a821fa04..69b9c059f 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -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" diff --git a/ext/LinearMPCext.jl b/ext/LinearMPCext.jl index 32f98ee29..6c07787e6 100644 --- a/ext/LinearMPCext.jl +++ b/ext/LinearMPCext.jl @@ -1,30 +1,28 @@ module LinearMPCext using ModelPredictiveControl, LinearMPC - +using LinearAlgebra using JuMP function Base.convert(::Type{LinearMPC.MPC}, mpc::ModelPredictiveControl.LinMPC) model, estim, weights = mpc.estim.model, mpc.estim, mpc.weights + nu, ny, nd = model.nu, model.ny, model.nd validate_compatibility(mpc) F, G, Gd = estim.Â, estim.B̂u, estim.B̂d C, Dd = estim.Ĉ, estim.D̂d - Np = mpc.Hp - Nc = mpc.Hc - + Np = Hp = mpc.Hp + Nc = Hc = mpc.Hc newmpc = LinearMPC.MPC(F, G; Gd, C, Dd, Np, Nc) - Q, R = estim.cov.Q̂, estim.cov.R̂ - C = estim.Ĉm - set_state_observer!(newmpc; C, Q, R) + set_state_observer!(newmpc; C=estim.Ĉm, Q, R) - Q = weights.M_Hp[1:model.ny, 1:model.ny] - Qf = weights.M_Hp[end-model.ny+1:end, end-model.ny+1:end] - Rr = weights.Ñ_Hc[1:model.nu, 1:model.nu] - R = weights.L_Hp[1:model.nu, 1:model.nu] + 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] soft_weight = weights.Ñ_Hc[end, end] LinearMPC.set_objective!(newmpc; Q, Rr, R, Qf) @@ -32,11 +30,27 @@ function Base.convert(::Type{LinearMPC.MPC}, mpc::ModelPredictiveControl.LinMPC) Umin, Umax = mpc.con.U0min + mpc.Uop, mpc.con.U0max + mpc.Uop Ymin, Ymax = mpc.con.Y0min + mpc.Yop, mpc.con.Y0max + mpc.Yop - - umin, umax = Umin[1:model.nu], Umax[1:model.nu] - ymin, ymax = Ymin[1:model.ny], Ymax[1:model.ny] + C_u = -mpc.con.A_Umin[:, end] + C_y = -mpc.con.A_Ymin[:, end] + # ymin_k, y_max_k = Ymin[(k-1)*ny+1:k*ny], Ymax[(k-1)*ny+1:k*ny] + for k in 0:Hp-1 + umin_k, u_max_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] + Au_k = Matrix{Float64}(I, nu, nu) + for i in 1:nu + lb, ub = [umin_k[i]], [u_max_k[i]] + soft = (c_u_k[i] ≈ 1) + Au = Au_k[i:i, :] + add_constraint!(newmpc; Au, lb, ub, ks, soft) + end + end - LinearMPC.set_bounds!(newmpc; umin, umax, ymin, ymax) + + #umin, umax = Umin[1:model.nu], Umax[1:model.nu] + #ymin, ymax = Ymin[1:model.ny], Ymax[1:model.ny] + + #LinearMPC.set_bounds!(newmpc; umin, umax, ymin, ymax) return newmpc end @@ -53,8 +67,49 @@ function validate_compatibility(mpc::ModelPredictiveControl.LinMPC) "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 + M_Hp, N_Hc, L_Hp = mpc.weights.M_Hp, mpc.weights.Ñ_Hc, 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.MPC only supports identical weights for each stages in M_Hp.") + end + end + 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.MPC only supports identical weights for each stages in Ñ_Hc.") + end + end + 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.MPC only supports identical weights for each stages in L_Hp.") + end + end + return nothing +end + +function validate_constraints(mpc::ModelPredictiveControl.LinMPC) + ΔŨmin, ΔŨmax = mpc.con.ΔŨmin, mpc.con.ΔŨmax + C_umin, C_umax = -mpc.con.A_Umin[:, end], -mpc.con.A_Umax[:, end] + C_ymin, C_ymax = -mpc.con.A_Ymin[:, end], -mpc.con.A_Ymax[:, end] + is0or1(C) = all(x -> x ≈ 0 || x ≈ 1, C) + if !is0or1(C_umin) || !is0or1(C_umax) || !is0or1(C_ymin) || !is0or1(C_ymax) + error("LinearMPC.MPC does not support softness parameters c ≠ 0 or 1.") + end + if !isapprox(C_umin, C_umax) || !isapprox(C_ymin, C_ymax) + error("LinearMPC.MPC does not support different softness parameters for lower and upper bounds.") + end nΔU = mpc.Hc*mpc.estim.model.nu - if any(isfinite, mpc.con.ΔŨmin[1:nΔU]) || any(isfinite, mpc.con.ΔŨmin[1:nΔU]) + if any(isfinite, ΔŨmin[1:nΔU]) || any(isfinite, ΔŨmax[1:nΔU]) error("LinearMPC.MPC does not support constraints on input increments Δu") end return nothing @@ -62,5 +117,4 @@ end LinearMPC.MPC(mpc::ModelPredictiveControl.LinMPC) = convert(LinearMPC.MPC, mpc) - end # LinearMPCext \ No newline at end of file From 9a3f4d764e774fe4b7bacc0526adb621a5e53921 Mon Sep 17 00:00:00 2001 From: franckgaga Date: Thu, 18 Dec 2025 12:33:02 -0500 Subject: [PATCH 06/42] wip: idem --- ext/LinearMPCext.jl | 88 ++++++++++++++++++++++++++------------------- src/general.jl | 8 +++++ 2 files changed, 59 insertions(+), 37 deletions(-) diff --git a/ext/LinearMPCext.jl b/ext/LinearMPCext.jl index 6c07787e6..c360087f5 100644 --- a/ext/LinearMPCext.jl +++ b/ext/LinearMPCext.jl @@ -1,69 +1,78 @@ module LinearMPCext using ModelPredictiveControl, LinearMPC -using LinearAlgebra +using LinearAlgebra, SparseArrays using JuMP +import ModelPredictiveControl: isblockdiag + function Base.convert(::Type{LinearMPC.MPC}, mpc::ModelPredictiveControl.LinMPC) model, estim, weights = mpc.estim.model, mpc.estim, mpc.weights nu, ny, nd = model.nu, model.ny, model.nd validate_compatibility(mpc) - + # --- Model parameters --- F, G, Gd = estim.Â, estim.B̂u, estim.B̂d C, Dd = estim.Ĉ, estim.D̂d Np = Hp = mpc.Hp Nc = Hc = mpc.Hc - newmpc = LinearMPC.MPC(F, G; Gd, C, Dd, Np, Nc) - + # --- State observer parameters --- Q, R = estim.cov.Q̂, estim.cov.R̂ 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] - soft_weight = weights.Ñ_Hc[end, end] - LinearMPC.set_objective!(newmpc; Q, Rr, R, Qf) - !weights.isinf_C && (newmpc.settings.soft_weight = soft_weight) - + if !weights.isinf_C + Cwt = weights.Ñ_Hc[end, end] + newmpc.settings.soft_weight = Cwt + end + # --- Manipulated inputs constraints --- Umin, Umax = mpc.con.U0min + mpc.Uop, mpc.con.U0max + mpc.Uop - Ymin, Ymax = mpc.con.Y0min + mpc.Yop, mpc.con.Y0max + mpc.Yop C_u = -mpc.con.A_Umin[:, end] - C_y = -mpc.con.A_Ymin[:, end] - # ymin_k, y_max_k = Ymin[(k-1)*ny+1:k*ny], Ymax[(k-1)*ny+1:k*ny] - for k in 0:Hp-1 - umin_k, u_max_k = Umin[k*nu+1:(k+1)*nu], Umax[k*nu+1:(k+1)*nu] + Au = Matrix{Float64}(I, nu, nu) + for k in 0:Hc-1 # Hp-1 # TODO: modify this once debugged + 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] - Au_k = Matrix{Float64}(I, nu, nu) + ks = [k+1] # a `1` in ks argument corresponds to the present time step k+0 for i in 1:nu - lb, ub = [umin_k[i]], [u_max_k[i]] - soft = (c_u_k[i] ≈ 1) - Au = Au_k[i:i, :] - add_constraint!(newmpc; Au, lb, ub, ks, soft) + lb = isfinite(umin_k[i]) ? [umin_k[i]] : zeros(0) + ub = isfinite(umax_k[i]) ? [umax_k[i]] : zeros(0) + soft = (c_u_k[i] > 0) + Au_i = Au[i:i, :] + add_constraint!(newmpc; Au=Au_i, lb, ub, ks, soft) + end + end + # --- Output constraints --- + Ymin, Ymax = mpc.con.Y0min + mpc.Yop, mpc.con.Y0max + mpc.Yop + C_y = -mpc.con.A_Ymin[:, end] + Ax, Ad = C, Dd + for k in 1:Hp + ymin_k, ymax_k = Ymin[(k-1)*ny+1:k*ny], Ymax[(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 = (c_y_k[i] > 0) + Ax_i, Ad_i = Ax[i:i, :], Ad[i:i, :] + add_constraint!(newmpc; Ax=Ax_i, Ad=Ad_i, lb, ub, ks, soft) end end - - - #umin, umax = Umin[1:model.nu], Umax[1:model.nu] - #ymin, ymax = Ymin[1:model.ny], Ymax[1:model.ny] - - #LinearMPC.set_bounds!(newmpc; umin, umax, ymin, ymax) - return newmpc end function validate_compatibility(mpc::ModelPredictiveControl.LinMPC) if mpc.transcription isa MultipleShooting - error("LinearMPC.MPC only supports SingleShooting transcription.") + error("LinearMPC only supports SingleShooting transcription.") end if !(mpc.estim isa SteadyKalmanFilter) || !mpc.estim.direct - error("LinearMPC.MPC only supports SteadyKalmanFilter with direct=true option.") + error("LinearMPC only supports SteadyKalmanFilter with direct=true option.") end if JuMP.solver_name(mpc.optim) != "DAQP" - @warn "LinearMPC.MPC relies on DAQP, and the solver in the mpc object "* + @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 @@ -74,26 +83,31 @@ end function validate_weights(mpc::ModelPredictiveControl.LinMPC) ny, nu = mpc.estim.model.ny, mpc.estim.model.nu - M_Hp, N_Hc, L_Hp = mpc.weights.M_Hp, mpc.weights.Ñ_Hc, mpc.weights.L_Hp + 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.MPC only supports identical weights for each stages in M_Hp.") + 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.MPC only supports identical weights for each stages in Ñ_Hc.") + 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.MPC only supports identical weights for each stages in L_Hp.") + 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 @@ -103,14 +117,14 @@ function validate_constraints(mpc::ModelPredictiveControl.LinMPC) C_ymin, C_ymax = -mpc.con.A_Ymin[:, end], -mpc.con.A_Ymax[:, end] is0or1(C) = all(x -> x ≈ 0 || x ≈ 1, C) if !is0or1(C_umin) || !is0or1(C_umax) || !is0or1(C_ymin) || !is0or1(C_ymax) - error("LinearMPC.MPC does not support softness parameters c ≠ 0 or 1.") + error("LinearMPC does not support softness parameters c ≠ 0 or 1.") end if !isapprox(C_umin, C_umax) || !isapprox(C_ymin, C_ymax) - error("LinearMPC.MPC does not support different softness parameters for lower and upper bounds.") + error("LinearMPC does not support different softness parameters for lower and upper bounds.") end nΔU = mpc.Hc*mpc.estim.model.nu if any(isfinite, ΔŨmin[1:nΔU]) || any(isfinite, ΔŨmax[1:nΔU]) - error("LinearMPC.MPC does not support constraints on input increments Δu") + error("LinearMPC does not support constraints on input increments Δu") end return nothing end diff --git a/src/general.jl b/src/general.jl index 87f738e34..fc0a04717 100644 --- a/src/general.jl +++ b/src/general.jl @@ -173,6 +173,14 @@ function repeatdiag(A::Hermitian{NT, Diagonal{NT, Vector{NT}}}, n::Int) where {N return Hermitian(repeatdiag(A.data, n), :L) # to return hermitian of a `Diagonal` end +"Check if matrix `A` is block diagonal with `m` blocks, where each block is `n × n`." +function isblockdiag(A::AbstractMatrix, n::Int, m::Int) + @assert size(A) == (n*m, n*m) "A size does not match the specified block dimensions." + blocks = [A[(i-1)*n+1:i*n, (i-1)*n+1:i*n] for i in 1:m] + A_blockdiag = blockdiag(sparse.(blocks)...) + return isapprox(A, A_blockdiag) +end + "In-place version of `repeat` but for vectors only." function repeat!(Y::Vector, a::Vector, n::Int) na = length(a) From aa7054d2115e5e2454249c42e7f29d05378d31d1 Mon Sep 17 00:00:00 2001 From: franckgaga Date: Thu, 18 Dec 2025 12:40:37 -0500 Subject: [PATCH 07/42] added: terminal constraints in `LinearMPCext` --- ext/LinearMPCext.jl | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/ext/LinearMPCext.jl b/ext/LinearMPCext.jl index c360087f5..18d2e198d 100644 --- a/ext/LinearMPCext.jl +++ b/ext/LinearMPCext.jl @@ -8,7 +8,7 @@ import ModelPredictiveControl: isblockdiag function Base.convert(::Type{LinearMPC.MPC}, mpc::ModelPredictiveControl.LinMPC) model, estim, weights = mpc.estim.model, mpc.estim, mpc.weights - nu, ny, nd = model.nu, model.ny, model.nd + nu, ny, nd, nx̂ = model.nu, model.ny, model.nd, estim.nx̂ validate_compatibility(mpc) # --- Model parameters --- F, G, Gd = estim.Â, estim.B̂u, estim.B̂d @@ -32,7 +32,7 @@ function Base.convert(::Type{LinearMPC.MPC}, mpc::ModelPredictiveControl.LinMPC) # --- Manipulated inputs constraints --- Umin, Umax = mpc.con.U0min + mpc.Uop, mpc.con.U0max + mpc.Uop C_u = -mpc.con.A_Umin[:, end] - Au = Matrix{Float64}(I, nu, nu) + I_u = Matrix{Float64}(I, nu, nu) for k in 0:Hc-1 # Hp-1 # TODO: modify this once debugged 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] @@ -41,14 +41,13 @@ function Base.convert(::Type{LinearMPC.MPC}, mpc::ModelPredictiveControl.LinMPC) lb = isfinite(umin_k[i]) ? [umin_k[i]] : zeros(0) ub = isfinite(umax_k[i]) ? [umax_k[i]] : zeros(0) soft = (c_u_k[i] > 0) - Au_i = Au[i:i, :] - add_constraint!(newmpc; Au=Au_i, lb, ub, ks, soft) + Au = I_u[i:i, :] + add_constraint!(newmpc; Au, lb, ub, ks, soft) end end # --- Output constraints --- Ymin, Ymax = mpc.con.Y0min + mpc.Yop, mpc.con.Y0max + mpc.Yop C_y = -mpc.con.A_Ymin[:, end] - Ax, Ad = C, Dd for k in 1:Hp ymin_k, ymax_k = Ymin[(k-1)*ny+1:k*ny], Ymax[(k-1)*ny+1:k*ny] c_y_k = C_y[(k-1)*ny+1:k*ny] @@ -57,10 +56,22 @@ function Base.convert(::Type{LinearMPC.MPC}, mpc::ModelPredictiveControl.LinMPC) lb = isfinite(ymin_k[i]) ? [ymin_k[i]] : zeros(0) ub = isfinite(ymax_k[i]) ? [ymax_k[i]] : zeros(0) soft = (c_y_k[i] > 0) - Ax_i, Ad_i = Ax[i:i, :], Ad[i:i, :] - add_constraint!(newmpc; Ax=Ax_i, Ad=Ad_i, lb, ub, ks, soft) + Ax, Ad = C[i:i, :], Dd[i:i, :] + add_constraint!(newmpc; Ax, Ad, lb, ub, ks, soft) end end + # --- Terminal constraints --- + x̂min, x̂max = mpc.con.x̂0min + estim.x̂op, mpc.con.x̂0max + estim.x̂op + c_x̂ = -mpc.con.A_x̂min[:, end] + 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̂min[i]) ? [x̂min[i]] : zeros(0) + ub = isfinite(x̂max[i]) ? [x̂max[i]] : zeros(0) + soft = (c_x̂[i] > 0) + Ax = I_x̂[i:i, :] + add_constraint!(newmpc; Ax, lb, ub, ks, soft) + end return newmpc end @@ -115,11 +126,12 @@ function validate_constraints(mpc::ModelPredictiveControl.LinMPC) ΔŨmin, ΔŨmax = mpc.con.ΔŨmin, mpc.con.ΔŨmax C_umin, C_umax = -mpc.con.A_Umin[:, end], -mpc.con.A_Umax[:, 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_ymin) || !is0or1(C_ymax) error("LinearMPC does not support softness parameters c ≠ 0 or 1.") end - if !isapprox(C_umin, C_umax) || !isapprox(C_ymin, C_ymax) + if !isapprox(C_umin, C_umax) || !isapprox(C_ymin, C_ymax) || !isapprox(C_x̂min, C_x̂max) error("LinearMPC does not support different softness parameters for lower and upper bounds.") end nΔU = mpc.Hc*mpc.estim.model.nu From 51e28e5d73d4d07a5eed556bab480e87b4619866 Mon Sep 17 00:00:00 2001 From: franckgaga Date: Thu, 18 Dec 2025 14:17:04 -0500 Subject: [PATCH 08/42] added: commented-out move blocking --- ext/LinearMPCext.jl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ext/LinearMPCext.jl b/ext/LinearMPCext.jl index 18d2e198d..4f03d0fcc 100644 --- a/ext/LinearMPCext.jl +++ b/ext/LinearMPCext.jl @@ -29,6 +29,8 @@ function Base.convert(::Type{LinearMPC.MPC}, mpc::ModelPredictiveControl.LinMPC) Cwt = weights.Ñ_Hc[end, end] newmpc.settings.soft_weight = Cwt end + # --- Custom move blocking --- + # LinearMPC.move_block!(newmpc, mpc.nb) # un-comment when debugged # --- Manipulated inputs constraints --- Umin, Umax = mpc.con.U0min + mpc.Uop, mpc.con.U0max + mpc.Uop C_u = -mpc.con.A_Umin[:, end] From 940e04d1d3e1d7513e0bc41770c10c78a6f9a4b9 Mon Sep 17 00:00:00 2001 From: franckgaga Date: Thu, 18 Dec 2025 17:47:17 -0500 Subject: [PATCH 09/42] added: support bounds pass `Hc` with conversion --- ext/LinearMPCext.jl | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/ext/LinearMPCext.jl b/ext/LinearMPCext.jl index 4f03d0fcc..0eeb2e081 100644 --- a/ext/LinearMPCext.jl +++ b/ext/LinearMPCext.jl @@ -30,13 +30,23 @@ function Base.convert(::Type{LinearMPC.MPC}, mpc::ModelPredictiveControl.LinMPC) newmpc.settings.soft_weight = Cwt end # --- Custom move blocking --- - # LinearMPC.move_block!(newmpc, mpc.nb) # un-comment when debugged + #LinearMPC.move_block!(newmpc, mpc.nb) # un-comment when debugged # --- Manipulated inputs constraints --- Umin, Umax = mpc.con.U0min + mpc.Uop, mpc.con.U0max + mpc.Uop C_u = -mpc.con.A_Umin[:, end] I_u = Matrix{Float64}(I, nu, nu) - for k in 0:Hc-1 # Hp-1 # TODO: modify this once debugged - umin_k, umax_k = Umin[k*nu+1:(k+1)*nu], Umax[k*nu+1:(k+1)*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 @@ -77,6 +87,7 @@ function Base.convert(::Type{LinearMPC.MPC}, mpc::ModelPredictiveControl.LinMPC) return newmpc end + function validate_compatibility(mpc::ModelPredictiveControl.LinMPC) if mpc.transcription isa MultipleShooting error("LinearMPC only supports SingleShooting transcription.") From ba5496012d63cf221a52c71aae34d3302f6e8b99 Mon Sep 17 00:00:00 2001 From: franckgaga Date: Wed, 31 Dec 2025 02:37:38 -0500 Subject: [PATCH 10/42] doc: wip --- docs/make.jl | 4 ++++ docs/src/public/predictive_control.md | 8 +++++++- ext/LinearMPCext.jl | 29 +++++++++++++++------------ src/ModelPredictiveControl.jl | 23 +++++++++++++++++++++ 4 files changed, 50 insertions(+), 14 deletions(-) diff --git a/docs/make.jl b/docs/make.jl index 1107afc69..64de52dae 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -5,6 +5,9 @@ push!(LOAD_PATH,"../src/") using Documenter, DocumenterInterLinks using ModelPredictiveControl +using LinearMPC + +ExtModule = Base.get_extension(ModelPredictiveControl, :LinearMPCext) links = InterLinks( "Julia" => "https://docs.julialang.org/en/v1/objects.inv", @@ -25,6 +28,7 @@ DocMeta.setdocmeta!( makedocs( sitename = "ModelPredictiveControl.jl", #format = Documenter.LaTeX(platform = "none"), + modules = [ModelPredictiveControl, ExtModule], doctest = true, plugins = [links], format = Documenter.HTML( diff --git a/docs/src/public/predictive_control.md b/docs/src/public/predictive_control.md index 1e73553de..73bbade4f 100644 --- a/docs/src/public/predictive_control.md +++ b/docs/src/public/predictive_control.md @@ -72,6 +72,12 @@ PredictiveController LinMPC ``` +### Conversion to LinearMPC.jl (code generation) + +```@docs +ModelPredictiveControl.MPC +``` + ## ExplicitMPC ```@docs @@ -114,4 +120,4 @@ MultipleShooting ```@docs TrapezoidalCollocation -``` +``` \ No newline at end of file diff --git a/ext/LinearMPCext.jl b/ext/LinearMPCext.jl index 0eeb2e081..6fe3cee6e 100644 --- a/ext/LinearMPCext.jl +++ b/ext/LinearMPCext.jl @@ -20,17 +20,17 @@ function Base.convert(::Type{LinearMPC.MPC}, mpc::ModelPredictiveControl.LinMPC) Q, R = estim.cov.Q̂, estim.cov.R̂ set_state_observer!(newmpc; C=estim.Ĉm, Q, R) # --- Objective function weights --- - Q = weights.M_Hp[1:ny, 1:ny] + 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] + R = weights.L_Hp[1:nu, 1:nu] LinearMPC.set_objective!(newmpc; Q, Rr, R, Qf) if !weights.isinf_C Cwt = weights.Ñ_Hc[end, end] newmpc.settings.soft_weight = Cwt end # --- Custom move blocking --- - #LinearMPC.move_block!(newmpc, mpc.nb) # un-comment when debugged + LinearMPC.move_block!(newmpc, mpc.nb) # un-comment when debugged # --- Manipulated inputs constraints --- Umin, Umax = mpc.con.U0min + mpc.Uop, mpc.con.U0max + mpc.Uop C_u = -mpc.con.A_Umin[:, end] @@ -42,13 +42,13 @@ function Base.convert(::Type{LinearMPC.MPC}, mpc::ModelPredictiveControl.LinMPC) 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 + 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 + 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) @@ -63,7 +63,7 @@ function Base.convert(::Type{LinearMPC.MPC}, mpc::ModelPredictiveControl.LinMPC) for k in 1:Hp ymin_k, ymax_k = Ymin[(k-1)*ny+1:k*ny], Ymax[(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 + 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) @@ -76,7 +76,7 @@ function Base.convert(::Type{LinearMPC.MPC}, mpc::ModelPredictiveControl.LinMPC) x̂min, x̂max = mpc.con.x̂0min + estim.x̂op, mpc.con.x̂0max + estim.x̂op c_x̂ = -mpc.con.A_x̂min[:, end] I_x̂ = Matrix{Float64}(I, nx̂, nx̂) - ks = [Hp+1] # a `1` in ks argument corresponds to the present time step k+0 + ks = [Hp + 1] # a `1` in ks argument corresponds to the present time step k+0 for i in 1:nx̂ lb = isfinite(x̂min[i]) ? [x̂min[i]] : zeros(0) ub = isfinite(x̂max[i]) ? [x̂max[i]] : zeros(0) @@ -87,7 +87,6 @@ function Base.convert(::Type{LinearMPC.MPC}, mpc::ModelPredictiveControl.LinMPC) return newmpc end - function validate_compatibility(mpc::ModelPredictiveControl.LinMPC) if mpc.transcription isa MultipleShooting error("LinearMPC only supports SingleShooting transcription.") @@ -96,8 +95,8 @@ function validate_compatibility(mpc::ModelPredictiveControl.LinMPC) 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"* + @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) @@ -108,7 +107,7 @@ 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 + 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 @@ -141,19 +140,23 @@ function validate_constraints(mpc::ModelPredictiveControl.LinMPC) 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_ymin) || !is0or1(C_ymax) + if !is0or1(C_umin) || !is0or1(C_umax) || !is0or1(C_ymin) || !is0or1(C_ymax) error("LinearMPC does not support softness parameters c ≠ 0 or 1.") end if !isapprox(C_umin, C_umax) || !isapprox(C_ymin, C_ymax) || !isapprox(C_x̂min, C_x̂max) error("LinearMPC does not support different softness parameters for lower and upper bounds.") end - nΔU = mpc.Hc*mpc.estim.model.nu + nΔU = mpc.Hc * mpc.estim.model.nu if any(isfinite, ΔŨmin[1:nΔU]) || any(isfinite, ΔŨmax[1:nΔU]) error("LinearMPC does not support constraints on input increments Δu") end return nothing end +"test 2" LinearMPC.MPC(mpc::ModelPredictiveControl.LinMPC) = convert(LinearMPC.MPC, mpc) +"test 3" +MPC(mpc::ModelPredictiveControl.LinMPC) = convert(LinearMPC.MPC, mpc) + end # LinearMPCext \ No newline at end of file diff --git a/src/ModelPredictiveControl.jl b/src/ModelPredictiveControl.jl index c33cc2c85..a729218ad 100644 --- a/src/ModelPredictiveControl.jl +++ b/src/ModelPredictiveControl.jl @@ -51,6 +51,29 @@ export PredictiveController, ExplicitMPC, LinMPC, NonLinMPC, setconstraint!, mov export TranscriptionMethod, SingleShooting, MultipleShooting, TrapezoidalCollocation export SimResult, getinfo, sim! +""" + LinearMPC.MPC(mpc::ModelPredictiveControl.LinMPC) + +Convert a `ModelPredictiveControl.LinMPC` object to a `LinearMPC.MPC` object. + +The package need to be installed and available in the current Julia environment. Once +converted, the `LinearMPC.MPC` object can be used to generate lightweight C code for +embedded applications using the `codegen` function. Note that not all features of +[`LinMPC`] are supported, including these restrictions: + +- the solver is limited to `DAQP`. +- the transcription method must be [`SingleShooting`](@ref). +- the state estimator must be a [`SteadyKalmanFilter`](@ref) with `direct=true`. +- ``\\mathbf{Δu_{min}}`` and ``\\mathbf{Δu_{max}}`` constraints are not supported for now. + +e.g. the solver is limited to `DAQP` and only single shooting +transcription with a steady Kalman filter is supported. The weights and constraints + + See the `LinearMPC` documentation for +more details. +""" +function MPC end + include("general.jl") include("sim_model.jl") include("state_estim.jl") From 6eea3a2fc285bcaba8b74050ad23749a7fed526b Mon Sep 17 00:00:00 2001 From: franckgaga Date: Mon, 5 Jan 2026 17:20:01 -0500 Subject: [PATCH 11/42] doc: `LinearMPC.MPC` docstring in online doc --- docs/Project.toml | 1 + docs/make.jl | 5 +---- docs/src/public/predictive_control.md | 4 ++-- ext/LinearMPCext.jl | 25 ++++++++++++++++++++++--- src/ModelPredictiveControl.jl | 23 ----------------------- 5 files changed, 26 insertions(+), 32 deletions(-) diff --git a/docs/Project.toml b/docs/Project.toml index 69b9c059f..f3223373d 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -16,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" diff --git a/docs/make.jl b/docs/make.jl index 64de52dae..0a92b89fd 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -5,9 +5,7 @@ push!(LOAD_PATH,"../src/") using Documenter, DocumenterInterLinks using ModelPredictiveControl -using LinearMPC - -ExtModule = Base.get_extension(ModelPredictiveControl, :LinearMPCext) +import LinearMPC links = InterLinks( "Julia" => "https://docs.julialang.org/en/v1/objects.inv", @@ -28,7 +26,6 @@ DocMeta.setdocmeta!( makedocs( sitename = "ModelPredictiveControl.jl", #format = Documenter.LaTeX(platform = "none"), - modules = [ModelPredictiveControl, ExtModule], doctest = true, plugins = [links], format = Documenter.HTML( diff --git a/docs/src/public/predictive_control.md b/docs/src/public/predictive_control.md index 73bbade4f..88f454010 100644 --- a/docs/src/public/predictive_control.md +++ b/docs/src/public/predictive_control.md @@ -72,10 +72,10 @@ PredictiveController LinMPC ``` -### Conversion to LinearMPC.jl (code generation) +### Conversion to `LinearMPC.jl` (code generation) ```@docs -ModelPredictiveControl.MPC +LinearMPC.MPC ``` ## ExplicitMPC diff --git a/ext/LinearMPCext.jl b/ext/LinearMPCext.jl index 6fe3cee6e..c2fae9a8d 100644 --- a/ext/LinearMPCext.jl +++ b/ext/LinearMPCext.jl @@ -153,10 +153,29 @@ function validate_constraints(mpc::ModelPredictiveControl.LinMPC) return nothing end -"test 2" +""" + LinearMPC.MPC(mpc::ModelPredictiveControl.LinMPC) + +Convert a `ModelPredictiveControl.LinMPC` object to a `LinearMPC.MPC` object. + +The package need to be installed and available in the current Julia environment. Once +converted, the `LinearMPC.MPC` object can be used to generate lightweight C code for +embedded applications using the `codegen` function. Note that not all features of +[`LinMPC`] are supported, including these restrictions: + +- the solver is limited to `DAQP`. +- the transcription method must be [`SingleShooting`](@ref). +- the state estimator must be a [`SteadyKalmanFilter`](@ref) with `direct=true`. +- ``\\mathbf{Δu_{min}}`` and ``\\mathbf{Δu_{max}}`` constraints are not supported for now. + +e.g. the solver is limited to `DAQP` and only single shooting +transcription with a steady Kalman filter is supported. The weights and constraints + + See the `LinearMPC` documentation for +more details. +""" LinearMPC.MPC(mpc::ModelPredictiveControl.LinMPC) = convert(LinearMPC.MPC, mpc) -"test 3" -MPC(mpc::ModelPredictiveControl.LinMPC) = convert(LinearMPC.MPC, mpc) + end # LinearMPCext \ No newline at end of file diff --git a/src/ModelPredictiveControl.jl b/src/ModelPredictiveControl.jl index a729218ad..c33cc2c85 100644 --- a/src/ModelPredictiveControl.jl +++ b/src/ModelPredictiveControl.jl @@ -51,29 +51,6 @@ export PredictiveController, ExplicitMPC, LinMPC, NonLinMPC, setconstraint!, mov export TranscriptionMethod, SingleShooting, MultipleShooting, TrapezoidalCollocation export SimResult, getinfo, sim! -""" - LinearMPC.MPC(mpc::ModelPredictiveControl.LinMPC) - -Convert a `ModelPredictiveControl.LinMPC` object to a `LinearMPC.MPC` object. - -The package need to be installed and available in the current Julia environment. Once -converted, the `LinearMPC.MPC` object can be used to generate lightweight C code for -embedded applications using the `codegen` function. Note that not all features of -[`LinMPC`] are supported, including these restrictions: - -- the solver is limited to `DAQP`. -- the transcription method must be [`SingleShooting`](@ref). -- the state estimator must be a [`SteadyKalmanFilter`](@ref) with `direct=true`. -- ``\\mathbf{Δu_{min}}`` and ``\\mathbf{Δu_{max}}`` constraints are not supported for now. - -e.g. the solver is limited to `DAQP` and only single shooting -transcription with a steady Kalman filter is supported. The weights and constraints - - See the `LinearMPC` documentation for -more details. -""" -function MPC end - include("general.jl") include("sim_model.jl") include("state_estim.jl") From 167e6140da7a079e9ce1fb627743ee1edb496732 Mon Sep 17 00:00:00 2001 From: franckgaga Date: Mon, 5 Jan 2026 18:14:45 -0500 Subject: [PATCH 12/42] doc: `LinearMPC.MPC` doc improvement --- docs/make.jl | 1 + docs/src/public/predictive_control.md | 2 +- ext/LinearMPCext.jl | 44 +++++++++++++++++++-------- 3 files changed, 34 insertions(+), 13 deletions(-) diff --git a/docs/make.jl b/docs/make.jl index 0a92b89fd..4a1c3092b 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -15,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!( diff --git a/docs/src/public/predictive_control.md b/docs/src/public/predictive_control.md index 88f454010..4250a4b14 100644 --- a/docs/src/public/predictive_control.md +++ b/docs/src/public/predictive_control.md @@ -72,7 +72,7 @@ PredictiveController LinMPC ``` -### Conversion to `LinearMPC.jl` (code generation) +### Conversion to LinearMPC.jl (code generation) ```@docs LinearMPC.MPC diff --git a/ext/LinearMPCext.jl b/ext/LinearMPCext.jl index c2fae9a8d..583dcee6e 100644 --- a/ext/LinearMPCext.jl +++ b/ext/LinearMPCext.jl @@ -153,29 +153,49 @@ function validate_constraints(mpc::ModelPredictiveControl.LinMPC) return nothing end -""" +@doc raw""" LinearMPC.MPC(mpc::ModelPredictiveControl.LinMPC) Convert a `ModelPredictiveControl.LinMPC` object to a `LinearMPC.MPC` object. -The package need to be installed and available in the current Julia environment. Once -converted, the `LinearMPC.MPC` object can be used to generate lightweight C code for -embedded applications using the `codegen` function. Note that not all features of -[`LinMPC`] are supported, including these restrictions: +The package need to be installed and available in the current Julia environment. The +converted object can be used to generate lightweight C-code for embedded applications using +the `codegen` function. Note that not all features of [`LinMPC`] are supported, including +these restrictions: -- the solver is limited to `DAQP`. +- 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`. - ``\\mathbf{Δu_{min}}`` and ``\\mathbf{Δu_{max}}`` constraints are not supported for now. +- only block-diagonal weights are allowed. + +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> mpc1 = setconstraint!(mpc1, ymax=[10.1]); -e.g. the solver is limited to `DAQP` and only single shooting -transcription with a steady Kalman filter is supported. The weights and constraints - - See the `LinearMPC` documentation for -more details. +julia> preparestate!(mpc1, [1.0]); + +julia> u1 = round.(moveinput!(mpc1, [10.0]), digits=3) +1-element Vector{Float64}: + 18.813 + +julia> mpc2 = LinearMPC.MPC(mpc1); + +julia> x̂2 = LinearMPC.correct_state!(mpc2, [1.0]); + +julia> u2 = round.(LinearMPC.compute_control(mpc2, x̂2, r=[10.0]), digits=3) +1-element Vector{Float64}: + 18.813 +``` """ LinearMPC.MPC(mpc::ModelPredictiveControl.LinMPC) = convert(LinearMPC.MPC, mpc) - end # LinearMPCext \ No newline at end of file From 80d7042d6bb941b9bf3557822e7f1be890ef4dfd Mon Sep 17 00:00:00 2001 From: franckgaga Date: Mon, 5 Jan 2026 18:20:42 -0500 Subject: [PATCH 13/42] doc: idem --- ext/LinearMPCext.jl | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/ext/LinearMPCext.jl b/ext/LinearMPCext.jl index 583dcee6e..06f56b4fb 100644 --- a/ext/LinearMPCext.jl +++ b/ext/LinearMPCext.jl @@ -158,15 +158,15 @@ end Convert a `ModelPredictiveControl.LinMPC` object to a `LinearMPC.MPC` object. -The package need to be installed and available in the current Julia environment. The -converted object can be used to generate lightweight C-code for embedded applications using -the `codegen` function. Note that not all features of [`LinMPC`] are supported, including -these restrictions: +The `LinearMPC` package need to be installed and available in the current 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`. -- ``\\mathbf{Δu_{min}}`` and ``\\mathbf{Δu_{max}}`` constraints are not supported for now. +- ``\mathbf{Δu_{min}}`` and ``\mathbf{Δu_{max}}`` constraints are not supported for now. - only block-diagonal weights are allowed. See the [LinearMPC.jl](@extref LinearMPC) documentation for more details on the supported From f302879c89b9b258bece1997dc3249b0699cc787 Mon Sep 17 00:00:00 2001 From: franckgaga Date: Mon, 5 Jan 2026 18:28:33 -0500 Subject: [PATCH 14/42] doc: idem --- ext/LinearMPCext.jl | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/ext/LinearMPCext.jl b/ext/LinearMPCext.jl index 06f56b4fb..f8a56e609 100644 --- a/ext/LinearMPCext.jl +++ b/ext/LinearMPCext.jl @@ -158,19 +158,20 @@ end Convert a `ModelPredictiveControl.LinMPC` object to a `LinearMPC.MPC` object. -The `LinearMPC` package need to be installed and available in the current 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 `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`. -- ``\mathbf{Δu_{min}}`` and ``\mathbf{Δu_{max}}`` constraints are not supported for now. - only block-diagonal weights are allowed. +- ``\mathbf{Δu_{min}}`` and ``\mathbf{Δu_{max}}`` constraints are not supported for now. -See the [LinearMPC.jl](@extref LinearMPC) documentation for more details on the supported -features and how to generate code. +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 From 15e1515423204e375b8e6fcc0c4406983a41341f Mon Sep 17 00:00:00 2001 From: franckgaga Date: Mon, 5 Jan 2026 18:29:14 -0500 Subject: [PATCH 15/42] doc: idem --- ext/LinearMPCext.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/LinearMPCext.jl b/ext/LinearMPCext.jl index f8a56e609..03f97a03d 100644 --- a/ext/LinearMPCext.jl +++ b/ext/LinearMPCext.jl @@ -154,7 +154,7 @@ function validate_constraints(mpc::ModelPredictiveControl.LinMPC) end @doc raw""" - LinearMPC.MPC(mpc::ModelPredictiveControl.LinMPC) + LinearMPC.MPC(mpc::LinMPC) Convert a `ModelPredictiveControl.LinMPC` object to a `LinearMPC.MPC` object. From f2d872fa2f12b0e2ecf727ba37c658023a7428d7 Mon Sep 17 00:00:00 2001 From: franckgaga Date: Mon, 5 Jan 2026 18:43:51 -0500 Subject: [PATCH 16/42] doc: idem --- ext/LinearMPCext.jl | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ext/LinearMPCext.jl b/ext/LinearMPCext.jl index 03f97a03d..728948189 100644 --- a/ext/LinearMPCext.jl +++ b/ext/LinearMPCext.jl @@ -167,10 +167,11 @@ are supported, including these restrictions: - the transcription method must be [`SingleShooting`](@ref). - the state estimator must be a [`SteadyKalmanFilter`](@ref) with `direct=true`. - only block-diagonal weights are allowed. -- ``\mathbf{Δu_{min}}`` and ``\mathbf{Δu_{max}}`` constraints are not supported for now. +- input increment constraints ``\mathbf{Δu_{min}}`` and ``\mathbf{Δu_{max}}`` are not + supported for now. 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) +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 From 4d186f6806481394eb93959bac00bcbee5f825ea Mon Sep 17 00:00:00 2001 From: franckgaga Date: Tue, 6 Jan 2026 10:40:00 -0500 Subject: [PATCH 17/42] added: operating point conversion --- ext/LinearMPCext.jl | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/ext/LinearMPCext.jl b/ext/LinearMPCext.jl index 728948189..341e6ea4d 100644 --- a/ext/LinearMPCext.jl +++ b/ext/LinearMPCext.jl @@ -16,6 +16,12 @@ function Base.convert(::Type{LinearMPC.MPC}, mpc::ModelPredictiveControl.LinMPC) Np = Hp = mpc.Hp Nc = Hc = mpc.Hc newmpc = LinearMPC.MPC(F, G; Gd, C, Dd, Np, Nc) + # --- Operating points --- + xo = estim.x̂op + uo = model.uop + yo = model.yop + !iszero(y0) && error("LinearMPC does not support non-zero output operating points.") + LinearMPC.set_operating_point!(newmpc; xo, uo, relinearize=false) # --- State observer parameters --- Q, R = estim.cov.Q̂, estim.cov.R̂ set_state_observer!(newmpc; C=estim.Ĉm, Q, R) @@ -180,21 +186,19 @@ julia> import LinearMPC, JuMP, DAQP; julia> mpc1 = LinMPC(LinModel(tf(2, [10, 1]), 1.0); optim=JuMP.Model(DAQP.Optimizer)); -julia> mpc1 = setconstraint!(mpc1, ymax=[10.1]); - julia> preparestate!(mpc1, [1.0]); -julia> u1 = round.(moveinput!(mpc1, [10.0]), digits=3) +julia> u1 = moveinput!(mpc1, [10.0]); round.(u1, digits=6) 1-element Vector{Float64}: - 18.813 + 17.577311 julia> mpc2 = LinearMPC.MPC(mpc1); julia> x̂2 = LinearMPC.correct_state!(mpc2, [1.0]); -julia> u2 = round.(LinearMPC.compute_control(mpc2, x̂2, r=[10.0]), digits=3) +julia> u2 = LinearMPC.compute_control(mpc2, x̂2, r=[10.0]); round.(u2, digits=6) 1-element Vector{Float64}: - 18.813 + 17.577311 ``` """ LinearMPC.MPC(mpc::ModelPredictiveControl.LinMPC) = convert(LinearMPC.MPC, mpc) From 3ad3e59bcf0f2ba5414cc6c0f1f618998f2b20c3 Mon Sep 17 00:00:00 2001 From: franckgaga Date: Tue, 6 Jan 2026 10:40:23 -0500 Subject: [PATCH 18/42] debug: correct variable --- ext/LinearMPCext.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/LinearMPCext.jl b/ext/LinearMPCext.jl index 341e6ea4d..f2417820f 100644 --- a/ext/LinearMPCext.jl +++ b/ext/LinearMPCext.jl @@ -20,7 +20,7 @@ function Base.convert(::Type{LinearMPC.MPC}, mpc::ModelPredictiveControl.LinMPC) xo = estim.x̂op uo = model.uop yo = model.yop - !iszero(y0) && error("LinearMPC does not support non-zero output operating points.") + !iszero(yo) && error("LinearMPC does not support non-zero output operating points.") LinearMPC.set_operating_point!(newmpc; xo, uo, relinearize=false) # --- State observer parameters --- Q, R = estim.cov.Q̂, estim.cov.R̂ From f7bf86e4cba2ce9d308c833443f2600dc95dcd4d Mon Sep 17 00:00:00 2001 From: franckgaga Date: Tue, 6 Jan 2026 13:57:37 -0500 Subject: [PATCH 19/42] added: warning about measured disturbance operating point `dop` --- ext/LinearMPCext.jl | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/ext/LinearMPCext.jl b/ext/LinearMPCext.jl index f2417820f..14c3788e7 100644 --- a/ext/LinearMPCext.jl +++ b/ext/LinearMPCext.jl @@ -20,7 +20,11 @@ function Base.convert(::Type{LinearMPC.MPC}, mpc::ModelPredictiveControl.LinMPC) xo = estim.x̂op uo = model.uop yo = model.yop - !iszero(yo) && error("LinearMPC does not support non-zero output operating points.") + !iszero(yo) && error("LinearMPC does not support non-zero output operating points yop.") + if !iszero(model.dop) + @warn "LinearMPC does not support measured disturbance operating points dop.\n" * + "Ensure to subtract the operating point from the measurement at each time "* + "step before solving the MPC problem." LinearMPC.set_operating_point!(newmpc; xo, uo, relinearize=false) # --- State observer parameters --- Q, R = estim.cov.Q̂, estim.cov.R̂ From c3e19f8ac1c8a1d07664f258aa3a3814646bc82d Mon Sep 17 00:00:00 2001 From: franckgaga Date: Tue, 6 Jan 2026 16:38:24 -0500 Subject: [PATCH 20/42] debug: forgot an `end` --- ext/LinearMPCext.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/ext/LinearMPCext.jl b/ext/LinearMPCext.jl index 14c3788e7..ccb9d0d6d 100644 --- a/ext/LinearMPCext.jl +++ b/ext/LinearMPCext.jl @@ -25,6 +25,7 @@ function Base.convert(::Type{LinearMPC.MPC}, mpc::ModelPredictiveControl.LinMPC) @warn "LinearMPC does not support measured disturbance operating points dop.\n" * "Ensure to subtract the operating point from the measurement at each time "* "step before solving the MPC problem." + end LinearMPC.set_operating_point!(newmpc; xo, uo, relinearize=false) # --- State observer parameters --- Q, R = estim.cov.Q̂, estim.cov.R̂ From 321e4475dd5b2c394a1538b2999cb4f083880443 Mon Sep 17 00:00:00 2001 From: franckgaga Date: Fri, 9 Jan 2026 15:31:38 -0500 Subject: [PATCH 21/42] wip: offsets --- ext/LinearMPCext.jl | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/ext/LinearMPCext.jl b/ext/LinearMPCext.jl index ccb9d0d6d..0bb6e0a11 100644 --- a/ext/LinearMPCext.jl +++ b/ext/LinearMPCext.jl @@ -17,16 +17,12 @@ function Base.convert(::Type{LinearMPC.MPC}, mpc::ModelPredictiveControl.LinMPC) Nc = Hc = mpc.Hc newmpc = LinearMPC.MPC(F, G; Gd, C, Dd, Np, Nc) # --- Operating points --- - xo = estim.x̂op - uo = model.uop - yo = model.yop - !iszero(yo) && error("LinearMPC does not support non-zero output operating points yop.") - if !iszero(model.dop) - @warn "LinearMPC does not support measured disturbance operating points dop.\n" * - "Ensure to subtract the operating point from the measurement at each time "* - "step before solving the MPC problem." - end - LinearMPC.set_operating_point!(newmpc; xo, uo, relinearize=false) + 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) # --- State observer parameters --- Q, R = estim.cov.Q̂, estim.cov.R̂ set_state_observer!(newmpc; C=estim.Ĉm, Q, R) From f66c105d8b23316fc751ae52a75bbada276b00ac Mon Sep 17 00:00:00 2001 From: franckgaga Date: Sat, 10 Jan 2026 11:08:47 -0500 Subject: [PATCH 22/42] debug --- ext/LinearMPCext.jl | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/ext/LinearMPCext.jl b/ext/LinearMPCext.jl index ccb9d0d6d..d7ba42e0d 100644 --- a/ext/LinearMPCext.jl +++ b/ext/LinearMPCext.jl @@ -20,13 +20,11 @@ function Base.convert(::Type{LinearMPC.MPC}, mpc::ModelPredictiveControl.LinMPC) xo = estim.x̂op uo = model.uop yo = model.yop - !iszero(yo) && error("LinearMPC does not support non-zero output operating points yop.") if !iszero(model.dop) @warn "LinearMPC does not support measured disturbance operating points dop.\n" * "Ensure to subtract the operating point from the measurement at each time "* "step before solving the MPC problem." end - LinearMPC.set_operating_point!(newmpc; xo, uo, relinearize=false) # --- State observer parameters --- Q, R = estim.cov.Q̂, estim.cov.R̂ set_state_observer!(newmpc; C=estim.Ĉm, Q, R) @@ -69,10 +67,10 @@ function Base.convert(::Type{LinearMPC.MPC}, mpc::ModelPredictiveControl.LinMPC) end end # --- Output constraints --- - Ymin, Ymax = mpc.con.Y0min + mpc.Yop, mpc.con.Y0max + mpc.Yop + Y0min, Y0max = mpc.con.Y0min, mpc.con.Y0max C_y = -mpc.con.A_Ymin[:, end] for k in 1:Hp - ymin_k, ymax_k = Ymin[(k-1)*ny+1:k*ny], Ymax[(k-1)*ny+1:k*ny] + 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 @@ -84,13 +82,13 @@ function Base.convert(::Type{LinearMPC.MPC}, mpc::ModelPredictiveControl.LinMPC) end end # --- Terminal constraints --- - x̂min, x̂max = mpc.con.x̂0min + estim.x̂op, mpc.con.x̂0max + estim.x̂op + x̂0min, x̂0max = mpc.con.x̂0min, mpc.con.x̂0max c_x̂ = -mpc.con.A_x̂min[:, end] 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̂min[i]) ? [x̂min[i]] : zeros(0) - ub = isfinite(x̂max[i]) ? [x̂max[i]] : zeros(0) + lb = isfinite(x̂0min[i]) ? [x̂0min[i]] : zeros(0) + ub = isfinite(x̂0max[i]) ? [x̂0max[i]] : zeros(0) soft = (c_x̂[i] > 0) Ax = I_x̂[i:i, :] add_constraint!(newmpc; Ax, lb, ub, ks, soft) From 35dc7a3980a3bad1c185df4b9a4b3edec93397ad Mon Sep 17 00:00:00 2001 From: franckgaga Date: Sat, 10 Jan 2026 12:12:11 -0500 Subject: [PATCH 23/42] debug: all offsets --- ext/LinearMPCext.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/LinearMPCext.jl b/ext/LinearMPCext.jl index 4deb4af61..71e8f2f0e 100644 --- a/ext/LinearMPCext.jl +++ b/ext/LinearMPCext.jl @@ -22,7 +22,7 @@ function Base.convert(::Type{LinearMPC.MPC}, mpc::ModelPredictiveControl.LinMPC) yoff = model.yop xoff = estim.x̂op foff = estim.f̂op - LinearMPC.set_offset!(newmpc; uo=uoff, ho=yoff) + LinearMPC.set_offset!(newmpc; uo=uoff, yo=yoff, dooff=doff, xo=xoff, fo=foff) # --- State observer parameters --- Q, R = estim.cov.Q̂, estim.cov.R̂ set_state_observer!(newmpc; C=estim.Ĉm, Q, R) From e0c4dd35c97256cac1fda8bd7ce98b646d5fe306 Mon Sep 17 00:00:00 2001 From: franckgaga Date: Sat, 10 Jan 2026 13:14:44 -0500 Subject: [PATCH 24/42] debug: correct kwarg --- ext/LinearMPCext.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/LinearMPCext.jl b/ext/LinearMPCext.jl index 71e8f2f0e..654d47e6e 100644 --- a/ext/LinearMPCext.jl +++ b/ext/LinearMPCext.jl @@ -22,7 +22,7 @@ function Base.convert(::Type{LinearMPC.MPC}, mpc::ModelPredictiveControl.LinMPC) yoff = model.yop xoff = estim.x̂op foff = estim.f̂op - LinearMPC.set_offset!(newmpc; uo=uoff, yo=yoff, dooff=doff, xo=xoff, fo=foff) + 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̂ set_state_observer!(newmpc; C=estim.Ĉm, Q, R) From 055c150c1ee6f2f636a4aa062ef3f866bef2abc0 Mon Sep 17 00:00:00 2001 From: Francis Gagnon Date: Sun, 11 Jan 2026 11:15:11 -0500 Subject: [PATCH 25/42] doc: correct bound in MHE manual example --- docs/src/manual/linmpc.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/manual/linmpc.md b/docs/src/manual/linmpc.md index 8d11aa9fa..a866896a9 100644 --- a/docs/src/manual/linmpc.md +++ b/docs/src/manual/linmpc.md @@ -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: From 752fb7e47e8bf503ff6491c5f3c0950de6eff47c Mon Sep 17 00:00:00 2001 From: Francis Gagnon Date: Sun, 11 Jan 2026 11:31:50 -0500 Subject: [PATCH 26/42] doc: minor change in `jldoctest` --- ext/LinearMPCext.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ext/LinearMPCext.jl b/ext/LinearMPCext.jl index 654d47e6e..113a943f8 100644 --- a/ext/LinearMPCext.jl +++ b/ext/LinearMPCext.jl @@ -189,15 +189,15 @@ julia> mpc1 = LinMPC(LinModel(tf(2, [10, 1]), 1.0); optim=JuMP.Model(DAQP.Optimi julia> preparestate!(mpc1, [1.0]); -julia> u1 = moveinput!(mpc1, [10.0]); round.(u1, digits=6) +julia> u = moveinput!(mpc1, [10.0]); round.(u, digits=6) 1-element Vector{Float64}: 17.577311 julia> mpc2 = LinearMPC.MPC(mpc1); -julia> x̂2 = LinearMPC.correct_state!(mpc2, [1.0]); +julia> x̂ = LinearMPC.correct_state!(mpc2, [1.0]); -julia> u2 = LinearMPC.compute_control(mpc2, x̂2, r=[10.0]); round.(u2, digits=6) +julia> u = LinearMPC.compute_control(mpc2, x̂, r=[10.0]); round.(u, digits=6) 1-element Vector{Float64}: 17.577311 ``` From ad4147cf02f055c726b5872bb98d693759b306d9 Mon Sep 17 00:00:00 2001 From: Francis Gagnon Date: Sun, 11 Jan 2026 13:39:50 -0500 Subject: [PATCH 27/42] test: comparison with `LinMPC` and `LinearMPC.MPC` --- Project.toml | 2 +- test/5_test_extensions.jl | 50 +++++++++++++++++++ test/{5_test_doctest.jl => 6_test_doctest.jl} | 0 test/runtests.jl | 11 ++-- 4 files changed, 57 insertions(+), 6 deletions(-) create mode 100644 test/5_test_extensions.jl rename test/{5_test_doctest.jl => 6_test_doctest.jl} (100%) diff --git a/Project.toml b/Project.toml index d87e8699b..70e091680 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "ModelPredictiveControl" uuid = "61f9bdb8-6ae4-484a-811f-bbf86720c31c" -version = "1.14.4" +version = "1.15.0" authors = ["Francis Gagnon"] [deps] diff --git a/test/5_test_extensions.jl b/test/5_test_extensions.jl new file mode 100644 index 000000000..3691f8485 --- /dev/null +++ b/test/5_test_extensions.jl @@ -0,0 +1,50 @@ +@testitem "LinearMPCext extension" setup=[SetupMPCtests] begin + using .SetupMPCtests, ControlSystemsBase, LinearAlgebra, JuMP, DAQP + import LinearMPC + model = LinModel(sys, Ts, i_u=1:2) + model = setop!(model, uop=[20, 20], yop=[50, 30]) + optim = JuMP.Model(DAQP.Optimizer) + mpc1 = LinMPC(model, Hp=15, Hc=[2, 3, 10], optim=optim) + mpc1 = setconstraint!(mpc1, ymin=[48, -Inf], umax=[Inf, 30]) + mpc2 = LinearMPC.MPC(mpc1) + function sim_both(model, mpc1, mpc2, N) + r = [55.0; 30.0] + u1 = [20.0, 20.0] + u2 = [20.0, 20.0] + model.x0 .= 0 + y_data = zeros(model.ny, N) + u_data1, u_data2 = zeros(model.nu, N), zeros(model.nu, N) + for k in 0:N-1 + k == 10 && (r .= [45; 30.0]) + k == 25 && (r .= [50; 45.0]) + y = model() + y_data[:, k+1] = y + preparestate!(mpc1, y) + x̂ = LinearMPC.correct_state!(mpc2, y) + u1 = moveinput!(mpc1, r) + u2 = LinearMPC.compute_control(mpc2, x̂, r=r, uprev=u2) + u_data1[:, k+1], u_data2[:, k+1] = u1, u2 + updatestate!(model, u1) + updatestate!(mpc1, u1, y) + LinearMPC.predict_state!(mpc2, u2) + end + return y_data, u_data1, u_data2 + end + N = 50 + y_data, u_data1, u_data2 = sim_both(model, mpc1, mpc2, N) + @test u_data1 ≈ u_data2 atol=1e-6 + + mpc_ms = LinMPC(model; transcription=MultipleShooting(), optim) + @test_throws ErrorException LinearMPC.MPC(mpc_ms) + mpc_kf = LinMPC(KalmanFilter(model, direct=false); optim) + @test_throws ErrorException LinearMPC.MPC(mpc_kf) + mpc_osqp = LinMPC(model) + "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." + @test_logs( + (:warn, "LinearMPC relies on DAQP, and the solver in the mpc object is currently "* + "OSQP.\nThe results in closed-loop may be different."), + LinearMPC.MPC(mpc_osqp) + ) +end diff --git a/test/5_test_doctest.jl b/test/6_test_doctest.jl similarity index 100% rename from test/5_test_doctest.jl rename to test/6_test_doctest.jl diff --git a/test/runtests.jl b/test/runtests.jl index 9be719de0..4d7de9002 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -4,10 +4,11 @@ using Test, TestItems, TestItemRunner @run_package_tests(verbose=true) include("0_test_module.jl") -include("1_test_sim_model.jl") -include("2_test_state_estim.jl") -include("3_test_predictive_control.jl") -include("4_test_plot_sim.jl") -include("5_test_doctest.jl") +#include("1_test_sim_model.jl") +#include("2_test_state_estim.jl") +#include("3_test_predictive_control.jl") +#include("4_test_plot_sim.jl") +include("5_test_extensions.jl") +include("6_test_doctest.jl") nothing \ No newline at end of file From 5eb8642f345a0725641764985b9a3ed9bb84dd68 Mon Sep 17 00:00:00 2001 From: Francis Gagnon Date: Sun, 11 Jan 2026 13:49:24 -0500 Subject: [PATCH 28/42] test: looser tolerances --- test/5_test_extensions.jl | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/test/5_test_extensions.jl b/test/5_test_extensions.jl index 3691f8485..aaf3ee95b 100644 --- a/test/5_test_extensions.jl +++ b/test/5_test_extensions.jl @@ -32,16 +32,13 @@ end N = 50 y_data, u_data1, u_data2 = sim_both(model, mpc1, mpc2, N) - @test u_data1 ≈ u_data2 atol=1e-6 + @test u_data1 ≈ u_data2 atol=1e-3 rtol=1e-3 mpc_ms = LinMPC(model; transcription=MultipleShooting(), optim) @test_throws ErrorException LinearMPC.MPC(mpc_ms) mpc_kf = LinMPC(KalmanFilter(model, direct=false); optim) @test_throws ErrorException LinearMPC.MPC(mpc_kf) mpc_osqp = LinMPC(model) - "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." @test_logs( (:warn, "LinearMPC relies on DAQP, and the solver in the mpc object is currently "* "OSQP.\nThe results in closed-loop may be different."), From 45ca4587529e4ce9e4c64ffefc390c5d1f6f5aa7 Mon Sep 17 00:00:00 2001 From: Francis Gagnon Date: Sun, 11 Jan 2026 13:56:24 -0500 Subject: [PATCH 29/42] changed: minor change in error messages --- ext/LinearMPCext.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ext/LinearMPCext.jl b/ext/LinearMPCext.jl index 113a943f8..d699c8f77 100644 --- a/ext/LinearMPCext.jl +++ b/ext/LinearMPCext.jl @@ -148,10 +148,10 @@ function validate_constraints(mpc::ModelPredictiveControl.LinMPC) 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_ymin) || !is0or1(C_ymax) - error("LinearMPC does not support softness parameters c ≠ 0 or 1.") + error("LinearMPC only supports softness parameters c = 0 or 1.") end if !isapprox(C_umin, C_umax) || !isapprox(C_ymin, C_ymax) || !isapprox(C_x̂min, C_x̂max) - error("LinearMPC does not support different softness parameters for lower and upper bounds.") + error("LinearMPC only supports identical softness parameters for lower and upper bounds.") end nΔU = mpc.Hc * mpc.estim.model.nu if any(isfinite, ΔŨmin[1:nΔU]) || any(isfinite, ΔŨmax[1:nΔU]) From 0bcf07cd44b864139723680dde3d8eb00cc7be9a Mon Sep 17 00:00:00 2001 From: Francis Gagnon Date: Sun, 11 Jan 2026 15:57:23 -0500 Subject: [PATCH 30/42] doc: C code gen in `README.md` --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index b4f7167a0..14a062023 100644 --- a/README.md +++ b/README.md @@ -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 with `LinearMPC.jl` ### 🔭 State Estimation Features From f420bdfab2be9acc491ff0a2ce0769bc90b31482 Mon Sep 17 00:00:00 2001 From: Francis Gagnon Date: Sun, 11 Jan 2026 15:57:45 -0500 Subject: [PATCH 31/42] doc: minor change --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 14a062023..bbe274473 100644 --- a/README.md +++ b/README.md @@ -91,7 +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 with `LinearMPC.jl` +- 📟️ **Embedded**: Lightweight C code generation via `LinearMPC.jl` ### 🔭 State Estimation Features From 6b42b92b35db33b75c6f8e2c8960c9f8cc5d476d Mon Sep 17 00:00:00 2001 From: Francis Gagnon Date: Tue, 13 Jan 2026 15:34:28 -0500 Subject: [PATCH 32/42] doc: return args of `init_predmat` and `init_defectmat` --- src/controller/transcription.jl | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/controller/transcription.jl b/src/controller/transcription.jl index b8dc0c17d..a7b503bea 100644 --- a/src/controller/transcription.jl +++ b/src/controller/transcription.jl @@ -256,7 +256,7 @@ end @doc raw""" init_predmat( model::LinModel, estim, transcription::SingleShooting, Hp, Hc, nb - ) -> E, G, J, K, V, ex̂, gx̂, jx̂, kx̂, vx̂ + ) -> E, G, J, K, V, B, ex̂, gx̂, jx̂, kx̂, vx̂, bx̂ Construct the prediction matrices for [`LinModel`](@ref) and [`SingleShooting`](@ref). @@ -484,7 +484,9 @@ function init_predmat( end """ - init_predmat(model::NonLinModel, estim, transcription::SingleShooting, Hp, Hc, nb) + init_predmat( + model::NonLinModel, estim, transcription::SingleShooting, Hp, Hc, nb + ) -> E, G, J, K, V, B, ex̂, gx̂, jx̂, kx̂, vx̂, bx̂ Return empty matrices for [`SingleShooting`](@ref) of [`NonLinModel`](@ref) """ @@ -504,7 +506,9 @@ function init_predmat( end @doc raw""" - init_predmat(model::NonLinModel, estim, transcription::TranscriptionMethod, Hp, Hc, nb) + init_predmat( + model::NonLinModel, estim, transcription::TranscriptionMethod, Hp, Hc, nb + ) -> E, G, J, K, V, B, ex̂, gx̂, jx̂, kx̂, vx̂, bx̂ Return the terminal state matrices for [`NonLinModel`](@ref) and other [`TranscriptionMethod`](@ref). @@ -537,7 +541,9 @@ function init_predmat( end @doc raw""" - init_defectmat(model::LinModel, estim, transcription::MultipleShooting, Hp, Hc, nb) + init_defectmat( + model::LinModel, estim, transcription::MultipleShooting, Hp, Hc, nb + ) -> Eŝ, Gŝ, Jŝ, Kŝ, Vŝ, Bŝ Init the matrices for computing the defects over the predicted states. @@ -645,7 +651,9 @@ function init_defectmat( end """ - init_defectmat(model::SimModel, estim, transcription::TranscriptionMethod, Hp, Hc, nb) + init_defectmat( + model::SimModel, estim, transcription::TranscriptionMethod, Hp, Hc, nb + ) -> Eŝ, Gŝ, Jŝ, Kŝ, Vŝ, Bŝ Return empty matrices for all other cases (N/A). """ From 92bb1b05b58ab42829bd797eccbd51a719150050 Mon Sep 17 00:00:00 2001 From: franckgaga Date: Tue, 13 Jan 2026 20:41:02 -0500 Subject: [PATCH 33/42] added: input increment constraint conversion --- ext/LinearMPCext.jl | 80 +++++++++++++++++++++++++++++++++------------ 1 file changed, 59 insertions(+), 21 deletions(-) diff --git a/ext/LinearMPCext.jl b/ext/LinearMPCext.jl index d699c8f77..2a9fd7446 100644 --- a/ext/LinearMPCext.jl +++ b/ext/LinearMPCext.jl @@ -1,9 +1,10 @@ module LinearMPCext -using ModelPredictiveControl, LinearMPC +using ModelPredictiveControl using LinearAlgebra, SparseArrays using JuMP +import LinearMPC import ModelPredictiveControl: isblockdiag function Base.convert(::Type{LinearMPC.MPC}, mpc::ModelPredictiveControl.LinMPC) @@ -25,16 +26,20 @@ function Base.convert(::Type{LinearMPC.MPC}, mpc::ModelPredictiveControl.LinMPC) 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̂ - set_state_observer!(newmpc; C=estim.Ĉm, Q, 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) - if !weights.isinf_C + only_hard = weights.isinf_C + if !only_hard + # LinearMPC relies on a different softening mechanism, so we apply + # an approximate conversion factor on the softening weight: Cwt = weights.Ñ_Hc[end, end] - newmpc.settings.soft_weight = Cwt + conversion_factor = 0.1 #0.09066 + newmpc.settings.soft_weight = conversion_factor*Cwt end # --- Custom move blocking --- LinearMPC.move_block!(newmpc, mpc.nb) # un-comment when debugged @@ -59,9 +64,26 @@ function Base.convert(::Type{LinearMPC.MPC}, mpc::ModelPredictiveControl.LinMPC) 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 = (c_u_k[i] > 0) + soft = !only_hard && c_u_k[i] > 0 Au = I_u[i:i, :] - add_constraint!(newmpc; Au, lb, ub, ks, soft) + LinearMPC.add_constraint!(newmpc; Au, lb, ub, ks, soft) + end + end + # --- Input increment constraints --- + nΔU = Hc * nu + ΔUmin, ΔUmax = mpc.con.ΔŨmin[1:nΔU], mpc.con.ΔŨmax[1:nΔU] + C_Δu = -mpc.con.A_ΔŨmin[1:nΔU, end] + 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 --- @@ -74,9 +96,9 @@ function Base.convert(::Type{LinearMPC.MPC}, mpc::ModelPredictiveControl.LinMPC) 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 = (c_y_k[i] > 0) + soft = !only_hard && c_y_k[i] > 0 Ax, Ad = C[i:i, :], Dd[i:i, :] - add_constraint!(newmpc; Ax, Ad, lb, ub, ks, soft) + LinearMPC.add_constraint!(newmpc; Ax, Ad, lb, ub, ks, soft) end end # --- Terminal constraints --- @@ -87,9 +109,9 @@ function Base.convert(::Type{LinearMPC.MPC}, mpc::ModelPredictiveControl.LinMPC) 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 = (c_x̂[i] > 0) + soft = !only_hard && c_x̂[i] > 0 Ax = I_x̂[i:i, :] - add_constraint!(newmpc; Ax, lb, ub, ks, soft) + LinearMPC.add_constraint!(newmpc; Ax, lb, ub, ks, soft) end return newmpc end @@ -142,20 +164,36 @@ function validate_weights(mpc::ModelPredictiveControl.LinMPC) end function validate_constraints(mpc::ModelPredictiveControl.LinMPC) - ΔŨmin, ΔŨmax = mpc.con.ΔŨmin, mpc.con.ΔŨmax - C_umin, C_umax = -mpc.con.A_Umin[:, end], -mpc.con.A_Umax[:, 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] + nΔU = mpc.Hc * mpc.estim.model.nu + 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_ymin) || !is0or1(C_ymax) + 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_ymin, C_ymax) || !isapprox(C_x̂min, C_x̂max) + 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 - nΔU = mpc.Hc * mpc.estim.model.nu - if any(isfinite, ΔŨmin[1:nΔU]) || any(isfinite, ΔŨmax[1:nΔU]) - error("LinearMPC does not support constraints on input increments Δu") + issoft(C) = any(x -> x > 0, C) + if !mpc.weights.isinf_C && sum(mpc.con.i_b) > 1 # ignore the slack variable ϵ bound + if issoft(C_umin) || issoft(C_Δumin) || issoft(C_ymin) || issoft(C_x̂min) + @warn "The LinearMPC conversion applies an approximate conversion " * + "of the soft constraints.\n You may need to adjust the soft_weight "* + "field of the LinearMPC.MPC object to replicate behaviors." + end end return nothing end @@ -174,8 +212,8 @@ are supported, including these restrictions: - the transcription method must be [`SingleShooting`](@ref). - the state estimator must be a [`SteadyKalmanFilter`](@ref) with `direct=true`. - only block-diagonal weights are allowed. -- input increment constraints ``\mathbf{Δu_{min}}`` and ``\mathbf{Δu_{max}}`` are not - supported for now. +- 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) From 9194d4fcdc33861fbf3f05943f4de91b4a6a389f Mon Sep 17 00:00:00 2001 From: franckgaga Date: Wed, 14 Jan 2026 10:30:50 -0500 Subject: [PATCH 34/42] added: heuristic conversion factor of soft constraints Since DAQP introduces implicitly a new variable for each softened constraints, it make sense to apply a conversion factor inversely proportional to the number of softened constraints. I tested on a simple SISO model and it works OK for various scenario of softened constraints. Note that the issue is less apparent when the only soft constraints is the output variable, which is the most common case in practice. --- ext/LinearMPCext.jl | 33 +++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/ext/LinearMPCext.jl b/ext/LinearMPCext.jl index 2a9fd7446..d61985e4d 100644 --- a/ext/LinearMPCext.jl +++ b/ext/LinearMPCext.jl @@ -9,13 +9,15 @@ import ModelPredictiveControl: isblockdiag function Base.convert(::Type{LinearMPC.MPC}, mpc::ModelPredictiveControl.LinMPC) model, estim, weights = mpc.estim.model, mpc.estim, mpc.weights - nu, ny, nd, nx̂ = model.nu, model.ny, model.nd, estim.nx̂ + 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 = mpc.Hp - Nc = Hc = mpc.Hc + Np = Hp + Nc = Hc newmpc = LinearMPC.MPC(F, G; Gd, C, Dd, Np, Nc) # --- Operating points --- uoff = model.uop @@ -33,19 +35,29 @@ function Base.convert(::Type{LinearMPC.MPC}, mpc::ModelPredictiveControl.LinMPC) 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 # LinearMPC relies on a different softening mechanism, so we apply - # an approximate conversion factor on the softening weight: + # an approximate conversion factor on the softening weight Cwt: + nsoft = sum((mpc.con.A[:,end] .< 0) .& (mpc.con.i_b)) - 1 + conversion_factor = 1/2/nsoft Cwt = weights.Ñ_Hc[end, end] - conversion_factor = 0.1 #0.09066 newmpc.settings.soft_weight = conversion_factor*Cwt + 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] + else + C_u = zeros(nu*Hp) + C_Δu = zeros(nu*Hc) + C_y = zeros(ny*Hp) + c_x̂ = zeros(nx̂) end - # --- Custom move blocking --- - LinearMPC.move_block!(newmpc, mpc.nb) # un-comment when debugged # --- Manipulated inputs constraints --- Umin, Umax = mpc.con.U0min + mpc.Uop, mpc.con.U0max + mpc.Uop - C_u = -mpc.con.A_Umin[:, end] 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 @@ -70,9 +82,7 @@ function Base.convert(::Type{LinearMPC.MPC}, mpc::ModelPredictiveControl.LinMPC) end end # --- Input increment constraints --- - nΔU = Hc * nu ΔUmin, ΔUmax = mpc.con.ΔŨmin[1:nΔU], mpc.con.ΔŨmax[1:nΔU] - C_Δu = -mpc.con.A_ΔŨmin[1:nΔU, end] 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] @@ -88,7 +98,6 @@ function Base.convert(::Type{LinearMPC.MPC}, mpc::ModelPredictiveControl.LinMPC) end # --- Output constraints --- Y0min, Y0max = mpc.con.Y0min, mpc.con.Y0max - C_y = -mpc.con.A_Ymin[:, end] 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] @@ -103,7 +112,6 @@ function Base.convert(::Type{LinearMPC.MPC}, mpc::ModelPredictiveControl.LinMPC) end # --- Terminal constraints --- x̂0min, x̂0max = mpc.con.x̂0min, mpc.con.x̂0max - c_x̂ = -mpc.con.A_x̂min[:, end] 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̂ @@ -165,6 +173,7 @@ 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] From ca5b230ea9cc252091390db502ea571c38da42ac Mon Sep 17 00:00:00 2001 From: franckgaga Date: Wed, 14 Jan 2026 10:52:55 -0500 Subject: [PATCH 35/42] changed: more details in comment --- ext/LinearMPCext.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ext/LinearMPCext.jl b/ext/LinearMPCext.jl index d61985e4d..349ab5276 100644 --- a/ext/LinearMPCext.jl +++ b/ext/LinearMPCext.jl @@ -40,8 +40,8 @@ function Base.convert(::Type{LinearMPC.MPC}, mpc::ModelPredictiveControl.LinMPC) # ---- Constraint softening --- only_hard = weights.isinf_C if !only_hard - # LinearMPC relies on a different softening mechanism, so we apply - # an approximate conversion factor on the softening weight Cwt: + # LinearMPC relies on a different softening mechanism (new implicit slack for each + # softened bounds), so we apply an approximate conversion factor on the Cwt weight: nsoft = sum((mpc.con.A[:,end] .< 0) .& (mpc.con.i_b)) - 1 conversion_factor = 1/2/nsoft Cwt = weights.Ñ_Hc[end, end] From fcfc2fe1e741706c640a2be67195fd20eb829daf Mon Sep 17 00:00:00 2001 From: franckgaga Date: Wed, 14 Jan 2026 13:11:48 -0500 Subject: [PATCH 36/42] =?UTF-8?q?changed:=20better=20conversion=20factor?= =?UTF-8?q?=20for=20`soft=5Fweight`=20The=20"Prioritized=20Constraints=20i?= =?UTF-8?q?n=20Optimization-Based=20Control"=20paper=20indicated=20that=20?= =?UTF-8?q?the=20=CF=81=20weight=20is=20squared,=20and=20there=20is=20one?= =?UTF-8?q?=20term=20for=20each=20soft=20bound.=20So=20a=20`sqrt`=20relati?= =?UTF-8?q?on=20is=20more=20logical=20here=20(Cwt=20is=20not=20squared=20i?= =?UTF-8?q?n=20MPC.jl).=20And=20it=20is=20indeed=20a=20better=20approximat?= =?UTF-8?q?e=20conversion=20factor,=20according=20to=20various=20simulatio?= =?UTF-8?q?ns=20on=20a=20SISO=20system.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ext/LinearMPCext.jl | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/ext/LinearMPCext.jl b/ext/LinearMPCext.jl index 349ab5276..d5a4ff8dd 100644 --- a/ext/LinearMPCext.jl +++ b/ext/LinearMPCext.jl @@ -40,12 +40,11 @@ function Base.convert(::Type{LinearMPC.MPC}, mpc::ModelPredictiveControl.LinMPC) # ---- Constraint softening --- only_hard = weights.isinf_C if !only_hard - # LinearMPC relies on a different softening mechanism (new implicit slack for each + # 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: - nsoft = sum((mpc.con.A[:,end] .< 0) .& (mpc.con.i_b)) - 1 - conversion_factor = 1/2/nsoft Cwt = weights.Ñ_Hc[end, end] - newmpc.settings.soft_weight = conversion_factor*Cwt + nsoft = sum((mpc.con.A[:,end] .< 0) .& (mpc.con.i_b)) - 1 + newmpc.settings.soft_weight = 10*sqrt(nsoft*Cwt) C_u = -mpc.con.A_Umin[:, end] C_Δu = -mpc.con.A_ΔŨmin[1:nΔU, end] C_y = -mpc.con.A_Ymin[:, end] From bc3869bf51066948b91c8057acb62bc9a235a9dc Mon Sep 17 00:00:00 2001 From: franckgaga Date: Wed, 14 Jan 2026 16:42:49 -0500 Subject: [PATCH 37/42] doc: minor modification in `jldoctest` --- src/plot_sim.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plot_sim.jl b/src/plot_sim.jl index 5e010a73d..737153dd2 100644 --- a/src/plot_sim.jl +++ b/src/plot_sim.jl @@ -45,7 +45,7 @@ julia> model = LinModel(tf(1, [1, 1]), 1.0); julia> N = 5; U_data = fill(1.0, 1, N); Y_data = zeros(1, N); -julia> for i=1:N; updatestate!(model, U_data[:, i]); Y_data[:, i] = model(); end; Y_data +julia> foreach(i->(updatestate!(model, U_data[:, i]); Y_data[:, i] = model()), 1:N); Y_data 1×5 Matrix{Float64}: 0.632121 0.864665 0.950213 0.981684 0.993262 From 54dcc38b80d3e9ee14c2e4826f92bd746ac507e7 Mon Sep 17 00:00:00 2001 From: franckgaga Date: Fri, 16 Jan 2026 11:09:51 -0500 Subject: [PATCH 38/42] changed: moving `@warn` near the conversion code --- ext/LinearMPCext.jl | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/ext/LinearMPCext.jl b/ext/LinearMPCext.jl index d5a4ff8dd..ce395802d 100644 --- a/ext/LinearMPCext.jl +++ b/ext/LinearMPCext.jl @@ -40,15 +40,23 @@ function Base.convert(::Type{LinearMPC.MPC}, mpc::ModelPredictiveControl.LinMPC) # ---- 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 applies an approximate conversion " * + "of 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) - 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] else C_u = zeros(nu*Hp) C_Δu = zeros(nu*Hc) @@ -195,14 +203,6 @@ function validate_constraints(mpc::ModelPredictiveControl.LinMPC) ) error("LinearMPC only supports identical softness parameters for lower and upper bounds.") end - issoft(C) = any(x -> x > 0, C) - if !mpc.weights.isinf_C && sum(mpc.con.i_b) > 1 # ignore the slack variable ϵ bound - if issoft(C_umin) || issoft(C_Δumin) || issoft(C_ymin) || issoft(C_x̂min) - @warn "The LinearMPC conversion applies an approximate conversion " * - "of the soft constraints.\n You may need to adjust the soft_weight "* - "field of the LinearMPC.MPC object to replicate behaviors." - end - end return nothing end From 141d162ff9818b2f88ba2ccb13c530019cc54d12 Mon Sep 17 00:00:00 2001 From: franckgaga Date: Fri, 16 Jan 2026 21:43:04 -0500 Subject: [PATCH 39/42] changed: reformulation --- ext/LinearMPCext.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ext/LinearMPCext.jl b/ext/LinearMPCext.jl index ce395802d..ba89654bb 100644 --- a/ext/LinearMPCext.jl +++ b/ext/LinearMPCext.jl @@ -47,9 +47,9 @@ function Base.convert(::Type{LinearMPC.MPC}, mpc::ModelPredictiveControl.LinMPC) 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 applies an approximate conversion " * - "of the soft constraints.\n You may need to adjust the soft_weight "* - "field of the LinearMPC.MPC object to replicate behaviors." + @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 From 2a4d5d64e727e6ba474265766387731815a20f3c Mon Sep 17 00:00:00 2001 From: franckgaga Date: Sun, 18 Jan 2026 15:22:52 -0500 Subject: [PATCH 40/42] changed: update `compat` of `LinearMPC` --- Project.toml | 2 +- docs/Project.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Project.toml b/Project.toml index 70e091680..55bb3680f 100644 --- a/Project.toml +++ b/Project.toml @@ -38,7 +38,7 @@ ForwardDiff = "0.10, 1" Ipopt = "1" JuMP = "1.21" LinearAlgebra = "1.10" -LinearMPC = "0.7.0" +LinearMPC = "0.8.0" Logging = "1.10" MathOptInterface = "1.46" OSQP = "0.8" diff --git a/docs/Project.toml b/docs/Project.toml index f3223373d..32049a0fa 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -16,7 +16,7 @@ DAQP = "0.6, 0.7.1" Documenter = "1" JuMP = "1" LinearAlgebra = "1.10" -LinearMPC = "0.7.0" +LinearMPC = "0.8.0" Logging = "1.10" ModelingToolkit = "10" Plots = "1" From a954c3fcf0fca9a4ff2cc1f07322f3341b79ca9c Mon Sep 17 00:00:00 2001 From: franckgaga Date: Sun, 18 Jan 2026 15:58:29 -0500 Subject: [PATCH 41/42] test: `LinearMPC` with hard bounds and tight tolerances --- test/5_test_extensions.jl | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/test/5_test_extensions.jl b/test/5_test_extensions.jl index aaf3ee95b..65b5a1cdf 100644 --- a/test/5_test_extensions.jl +++ b/test/5_test_extensions.jl @@ -12,13 +12,11 @@ u1 = [20.0, 20.0] u2 = [20.0, 20.0] model.x0 .= 0 - y_data = zeros(model.ny, N) u_data1, u_data2 = zeros(model.nu, N), zeros(model.nu, N) for k in 0:N-1 k == 10 && (r .= [45; 30.0]) k == 25 && (r .= [50; 45.0]) y = model() - y_data[:, k+1] = y preparestate!(mpc1, y) x̂ = LinearMPC.correct_state!(mpc2, y) u1 = moveinput!(mpc1, r) @@ -28,11 +26,19 @@ updatestate!(mpc1, u1, y) LinearMPC.predict_state!(mpc2, u2) end - return y_data, u_data1, u_data2 + return u_data1, u_data2 end N = 50 - y_data, u_data1, u_data2 = sim_both(model, mpc1, mpc2, N) - @test u_data1 ≈ u_data2 atol=1e-3 rtol=1e-3 + u_data1, u_data2 = sim_both(model, mpc1, mpc2, N) + @test u_data1 ≈ u_data2 atol=1e-3 rtol=1e-3 # looser tol due to different softening + + mpc1_hard = LinMPC(model, Hp=15, Cwt=Inf, optim=optim) + mpc1_hard = setconstraint!(mpc1_hard, ymin=[48, -Inf], umax=[Inf, 30]) + mpc2_hard = LinearMPC.MPC(mpc1_hard) + u_data1_hard, u_data2_hard = sim_both( + model, mpc1_hard, mpc2_hard, N + ) + @test u_data1_hard ≈ u_data2_hard atol=1e-12 # exact match for hard constraints mpc_ms = LinMPC(model; transcription=MultipleShooting(), optim) @test_throws ErrorException LinearMPC.MPC(mpc_ms) From 8b5e6b341703da7b18c74cc45fcdcb2a34cfe5b1 Mon Sep 17 00:00:00 2001 From: franckgaga Date: Sun, 18 Jan 2026 18:00:18 -0500 Subject: [PATCH 42/42] test: increase tolerance with hard constraints --- test/5_test_extensions.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/5_test_extensions.jl b/test/5_test_extensions.jl index 65b5a1cdf..68e74c6ce 100644 --- a/test/5_test_extensions.jl +++ b/test/5_test_extensions.jl @@ -30,7 +30,7 @@ end N = 50 u_data1, u_data2 = sim_both(model, mpc1, mpc2, N) - @test u_data1 ≈ u_data2 atol=1e-3 rtol=1e-3 # looser tol due to different softening + @test u_data1 ≈ u_data2 atol=1e-3 rtol=1e-3 # looser tols due to different softening mpc1_hard = LinMPC(model, Hp=15, Cwt=Inf, optim=optim) mpc1_hard = setconstraint!(mpc1_hard, ymin=[48, -Inf], umax=[Inf, 30]) @@ -38,7 +38,7 @@ u_data1_hard, u_data2_hard = sim_both( model, mpc1_hard, mpc2_hard, N ) - @test u_data1_hard ≈ u_data2_hard atol=1e-12 # exact match for hard constraints + @test u_data1_hard ≈ u_data2_hard atol=1e-10 rtol=1e-10 # tighter tols for hard constraints mpc_ms = LinMPC(model; transcription=MultipleShooting(), optim) @test_throws ErrorException LinearMPC.MPC(mpc_ms)