Skip to content

added: be more efficient with weights and conversion matrices for NonLinMPC #202

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
May 12, 2025
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name = "ModelPredictiveControl"
uuid = "61f9bdb8-6ae4-484a-811f-bbf86720c31c"
authors = ["Francis Gagnon"]
version = "1.6.0"
version = "1.6.1"

[deps]
ControlSystemsBase = "aaaaaaaa-a6ca-5380-bf3e-84a91bcd477e"
Expand Down
65 changes: 46 additions & 19 deletions src/controller/construct.jl
Original file line number Diff line number Diff line change
Expand Up @@ -33,41 +33,64 @@ function PredictiveControllerBuffer(
end

"Include all the objective function weights of [`PredictiveController`](@ref)"
struct ControllerWeights{NT<:Real}
M_Hp::Hermitian{NT, Matrix{NT}}
Ñ_Hc::Hermitian{NT, Matrix{NT}}
L_Hp::Hermitian{NT, Matrix{NT}}
struct ControllerWeights{
NT<:Real,
# parameters to support both dense and Diagonal matrices (with specialization):
MW<:AbstractMatrix{NT},
NW<:AbstractMatrix{NT},
LW<:AbstractMatrix{NT},
}
M_Hp::Hermitian{NT, MW}
Ñ_Hc::Hermitian{NT, NW}
L_Hp::Hermitian{NT, LW}
E ::NT
iszero_M_Hp::Vector{Bool}
iszero_Ñ_Hc::Vector{Bool}
iszero_L_Hp::Vector{Bool}
iszero_E::Bool
isinf_C ::Bool
function ControllerWeights{NT}(
model, Hp, Hc, M_Hp, N_Hc, L_Hp, Cwt=Inf, Ewt=0
) where NT<:Real
model, Hp, Hc, M_Hp::MW, N_Hc::NW, L_Hp::LW, Cwt=Inf, Ewt=0
) where {
NT<:Real,
MW<:AbstractMatrix{NT},
NW<:AbstractMatrix{NT},
LW<:AbstractMatrix{NT}
}
validate_weights(model, Hp, Hc, M_Hp, N_Hc, L_Hp, Cwt, Ewt)
# convert `Diagonal` to normal `Matrix` if required:
M_Hp = Hermitian(convert(Matrix{NT}, M_Hp), :L)
N_Hc = Hermitian(convert(Matrix{NT}, N_Hc), :L)
L_Hp = Hermitian(convert(Matrix{NT}, L_Hp), :L)
nΔU = size(N_Hc, 1)
C = Cwt
if !isinf(Cwt)
isinf_C = isinf(C)
if !isinf_C
# ΔŨ = [ΔU; ϵ] (ϵ is the slack variable)
Ñ_Hc = Hermitian([N_Hc zeros(NT, nΔU, 1); zeros(NT, 1, nΔU) C], :L)
Ñ_Hc = [N_Hc zeros(NT, nΔU, 1); zeros(NT, 1, nΔU) C]
isdiag(N_Hc) && (Ñ_Hc = Diagonal(Ñ_Hc)) # NW(Ñ_Hc) does not work on Julia 1.10
else
# ΔŨ = ΔU (only hard constraints)
Ñ_Hc = N_Hc
end
end
M_Hp = Hermitian(M_Hp, :L)
Ñ_Hc = Hermitian(Ñ_Hc, :L)
L_Hp = Hermitian(L_Hp, :L)
E = Ewt
iszero_M_Hp = [iszero(M_Hp)]
iszero_Ñ_Hc = [iszero(Ñ_Hc)]
iszero_L_Hp = [iszero(L_Hp)]
iszero_E = iszero(E)
return new{NT}(M_Hp, Ñ_Hc, L_Hp, E, iszero_M_Hp, iszero_Ñ_Hc, iszero_L_Hp, iszero_E)
return new{NT, MW, NW, LW}(
M_Hp, Ñ_Hc, L_Hp, E,
iszero_M_Hp, iszero_Ñ_Hc, iszero_L_Hp, iszero_E, isinf_C
)
end
end

"Outer constructor to convert weight matrix number type to `NT` if necessary."
function ControllerWeights{NT}(
model, Hp, Hc, M_Hp::MW, N_Hc::NW, L_Hp::LW, Cwt=Inf, Ewt=0
) where {NT<:Real, MW<:AbstractMatrix, NW<:AbstractMatrix, LW<:AbstractMatrix}
return ControllerWeights{NT}(model, Hp, Hc, NT.(M_Hp), NT.(N_Hc), NT.(L_Hp), Cwt, Ewt)
end

"Include all the data for the constraints of [`PredictiveController`](@ref)"
struct ControllerConstraint{NT<:Real, GCfunc<:Union{Nothing, Function}}
# matrices for the terminal constraints:
Expand Down Expand Up @@ -478,8 +501,10 @@ end

"""
init_defaultcon_mpc(
estim::StateEstimator, transcription::TranscriptionMethod,
Hp, Hc, C,
estim::StateEstimator,
weights::ControllerWeights
transcription::TranscriptionMethod,
Hp, Hc,
PΔu, Pu, E,
ex̂, fx̂, gx̂, jx̂, kx̂, vx̂, bx̂,
Eŝ, Fŝ, Gŝ, Jŝ, Kŝ, Vŝ, Bŝ,
Expand All @@ -491,16 +516,18 @@ Init `ControllerConstraint` struct with default parameters based on estimator `e
Also return `P̃Δu`, `P̃u`, `Ẽ` and `Ẽŝ` matrices for the the augmented decision vector `Z̃`.
"""
function init_defaultcon_mpc(
estim::StateEstimator{NT}, transcription::TranscriptionMethod,
Hp, Hc, C,
estim::StateEstimator{NT},
weights::ControllerWeights,
transcription::TranscriptionMethod,
Hp, Hc,
PΔu, Pu, E,
ex̂, fx̂, gx̂, jx̂, kx̂, vx̂, bx̂,
Eŝ, Fŝ, Gŝ, Jŝ, Kŝ, Vŝ, Bŝ,
gc!::GCfunc = nothing, nc = 0
) where {NT<:Real, GCfunc<:Union{Nothing, Function}}
model = estim.model
nu, ny, nx̂ = model.nu, model.ny, estim.nx̂
nϵ = isinf(C) ? 0 : 1
nϵ = weights.isinf_C ? 0 : 1
u0min, u0max = fill(convert(NT,-Inf), nu), fill(convert(NT,+Inf), nu)
Δumin, Δumax = fill(convert(NT,-Inf), nu), fill(convert(NT,+Inf), nu)
y0min, y0max = fill(convert(NT,-Inf), ny), fill(convert(NT,+Inf), ny)
Expand Down
6 changes: 3 additions & 3 deletions src/controller/execute.jl
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ function initpred!(mpc::PredictiveController, model::LinModel, d, D̂, R̂y, R̂
mul!(F, mpc.G, mpc.d0, 1, 1) # F = F + G*d0
mul!(F, mpc.J, mpc.D̂0, 1, 1) # F = F + J*D̂0
end
Cy, Cu, M_Hp_Ẽ, L_Hp_P̃U = mpc.buffer.Ŷ, mpc.buffer.U, mpc.buffer.Ẽ, mpc.buffer.P̃u
Cy, Cu, M_Hp_Ẽ, L_Hp_P̃u = mpc.buffer.Ŷ, mpc.buffer.U, mpc.buffer.Ẽ, mpc.buffer.P̃u
q̃, r = mpc.q̃, mpc.r
q̃ .= 0
r .= 0
Expand All @@ -226,8 +226,8 @@ function initpred!(mpc::PredictiveController, model::LinModel, d, D̂, R̂y, R̂
# --- input setpoint tracking term ---
if !mpc.weights.iszero_L_Hp[]
Cu .= mpc.Tu_lastu0 .+ mpc.Uop .- R̂u
mul!(L_Hp_P̃U, mpc.weights.L_Hp, mpc.P̃u)
mul!(q̃, L_Hp_P̃U', Cu, 1, 1) # q̃ = q̃ + L_Hp*P̃u'*Cu
mul!(L_Hp_P̃u, mpc.weights.L_Hp, mpc.P̃u)
mul!(q̃, L_Hp_P̃u', Cu, 1, 1) # q̃ = q̃ + L_Hp*P̃u'*Cu
r .+= dot(Cu, mpc.weights.L_Hp, Cu) # r = r + Cu'*L_Hp*Cu
end
# --- finalize ---
Expand Down
36 changes: 18 additions & 18 deletions src/controller/explicitmpc.jl
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
struct ExplicitMPC{NT<:Real, SE<:StateEstimator} <: PredictiveController{NT}
struct ExplicitMPC{
NT<:Real,
SE<:StateEstimator,
CW<:ControllerWeights
} <: PredictiveController{NT}
estim::SE
transcription::SingleShooting
Z̃::Vector{NT}
ŷ::Vector{NT}
Hp::Int
Hc::Int
nϵ::Int
weights::ControllerWeights{NT}
weights::CW
R̂u::Vector{NT}
R̂y::Vector{NT}
P̃Δu::Matrix{NT}
Expand Down Expand Up @@ -34,17 +38,12 @@ struct ExplicitMPC{NT<:Real, SE<:StateEstimator} <: PredictiveController{NT}
Dop::Vector{NT}
buffer::PredictiveControllerBuffer{NT}
function ExplicitMPC{NT}(
estim::SE, Hp, Hc, M_Hp, N_Hc, L_Hp
) where {NT<:Real, SE<:StateEstimator}
estim::SE, Hp, Hc, weights::CW
) where {NT<:Real, SE<:StateEstimator, CW<:ControllerWeights}
model = estim.model
nu, ny, nd, nx̂ = model.nu, model.ny, model.nd, estim.nx̂
ŷ = copy(model.yop) # dummy vals (updated just before optimization)
nϵ = 0 # no slack variable ϵ for ExplicitMPC
weights = ControllerWeights{NT}(model, Hp, Hc, M_Hp, N_Hc, L_Hp)
# convert `Diagonal` to normal `Matrix` if required:
M_Hp = Hermitian(convert(Matrix{NT}, M_Hp), :L)
N_Hc = Hermitian(convert(Matrix{NT}, N_Hc), :L)
L_Hp = Hermitian(convert(Matrix{NT}, L_Hp), :L)
# dummy vals (updated just before optimization):
R̂y, R̂u, Tu_lastu0 = zeros(NT, ny*Hp), zeros(NT, nu*Hp), zeros(NT, nu*Hp)
transcription = SingleShooting() # explicit MPC only supports SingleShooting
Expand All @@ -53,7 +52,7 @@ struct ExplicitMPC{NT<:Real, SE<:StateEstimator} <: PredictiveController{NT}
E, G, J, K, V, B = init_predmat(model, estim, transcription, Hp, Hc)
# dummy val (updated just before optimization):
F, fx̂ = zeros(NT, ny*Hp), zeros(NT, nx̂)
P̃Δu, P̃u, Ñ_Hc, Ẽ = PΔu, Pu, N_Hc, E # no slack variable ϵ for ExplicitMPC
P̃Δu, P̃u, Ẽ = PΔu, Pu, E # no slack variable ϵ for ExplicitMPC
H̃ = init_quadprog(model, weights, Ẽ, P̃Δu, P̃u)
# dummy vals (updated just before optimization):
q̃, r = zeros(NT, size(H̃, 1)), zeros(NT, 1)
Expand All @@ -65,7 +64,7 @@ struct ExplicitMPC{NT<:Real, SE<:StateEstimator} <: PredictiveController{NT}
nZ̃ = get_nZ(estim, transcription, Hp, Hc) + nϵ
Z̃ = zeros(NT, nZ̃)
buffer = PredictiveControllerBuffer(estim, transcription, Hp, Hc, nϵ)
mpc = new{NT, SE}(
mpc = new{NT, SE, CW}(
estim,
transcription,
Z̃, ŷ,
Expand Down Expand Up @@ -131,9 +130,9 @@ function ExplicitMPC(
Mwt = fill(DEFAULT_MWT, model.ny),
Nwt = fill(DEFAULT_NWT, model.nu),
Lwt = fill(DEFAULT_LWT, model.nu),
M_Hp = diagm(repeat(Mwt, Hp)),
N_Hc = diagm(repeat(Nwt, Hc)),
L_Hp = diagm(repeat(Lwt, Hp)),
M_Hp = Diagonal(repeat(Mwt, Hp)),
N_Hc = Diagonal(repeat(Nwt, Hc)),
L_Hp = Diagonal(repeat(Lwt, Hp)),
kwargs...
)
estim = SteadyKalmanFilter(model; kwargs...)
Expand All @@ -154,17 +153,18 @@ function ExplicitMPC(
Mwt = fill(DEFAULT_MWT, estim.model.ny),
Nwt = fill(DEFAULT_NWT, estim.model.nu),
Lwt = fill(DEFAULT_LWT, estim.model.nu),
M_Hp = diagm(repeat(Mwt, Hp)),
N_Hc = diagm(repeat(Nwt, Hc)),
L_Hp = diagm(repeat(Lwt, Hp)),
M_Hp = Diagonal(repeat(Mwt, Hp)),
N_Hc = Diagonal(repeat(Nwt, Hc)),
L_Hp = Diagonal(repeat(Lwt, Hp)),
) where {NT<:Real, SE<:StateEstimator{NT}}
isa(estim.model, LinModel) || error("estim.model type must be a LinModel")
nk = estimate_delays(estim.model)
if Hp ≤ nk
@warn("prediction horizon Hp ($Hp) ≤ estimated number of delays in model "*
"($nk), the closed-loop system may be unstable or zero-gain (unresponsive)")
end
return ExplicitMPC{NT}(estim, Hp, Hc, M_Hp, N_Hc, L_Hp)
weights = ControllerWeights{NT}(estim.model, Hp, Hc, M_Hp, N_Hc, L_Hp)
return ExplicitMPC{NT}(estim, Hp, Hc, weights)
end

setconstraint!(::ExplicitMPC; kwargs...) = error("ExplicitMPC does not support constraints.")
Expand Down
42 changes: 25 additions & 17 deletions src/controller/linmpc.jl
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const DEFAULT_LINMPC_TRANSCRIPTION = SingleShooting()
struct LinMPC{
NT<:Real,
SE<:StateEstimator,
CW<:ControllerWeights,
TM<:TranscriptionMethod,
JM<:JuMP.GenericModel
} <: PredictiveController{NT}
Expand All @@ -18,7 +19,7 @@ struct LinMPC{
Hp::Int
Hc::Int
nϵ::Int
weights::ControllerWeights{NT}
weights::CW
R̂u::Vector{NT}
R̂y::Vector{NT}
P̃Δu::Matrix{NT}
Expand All @@ -45,13 +46,18 @@ struct LinMPC{
Dop::Vector{NT}
buffer::PredictiveControllerBuffer{NT}
function LinMPC{NT}(
estim::SE, Hp, Hc, M_Hp, N_Hc, L_Hp, Cwt,
estim::SE, Hp, Hc, weights::CW,
transcription::TM, optim::JM
) where {NT<:Real, SE<:StateEstimator, TM<:TranscriptionMethod, JM<:JuMP.GenericModel}
) where {
NT<:Real,
SE<:StateEstimator,
CW<:ControllerWeights,
TM<:TranscriptionMethod,
JM<:JuMP.GenericModel
}
model = estim.model
nu, ny, nd, nx̂ = model.nu, model.ny, model.nd, estim.nx̂
ŷ = copy(model.yop) # dummy vals (updated just before optimization)
weights = ControllerWeights{NT}(model, Hp, Hc, M_Hp, N_Hc, L_Hp, Cwt)
# dummy vals (updated just before optimization):
R̂y, R̂u, Tu_lastu0 = zeros(NT, ny*Hp), zeros(NT, nu*Hp), zeros(NT, nu*Hp)
PΔu = init_ZtoΔU(estim, transcription, Hp, Hc)
Expand All @@ -63,8 +69,9 @@ struct LinMPC{
# dummy vals (updated just before optimization):
F, fx̂, Fŝ = zeros(NT, ny*Hp), zeros(NT, nx̂), zeros(NT, nx̂*Hp)
con, nϵ, P̃Δu, P̃u, Ẽ, Ẽŝ = init_defaultcon_mpc(
estim, transcription,
Hp, Hc, Cwt, PΔu, Pu, E,
estim, weights, transcription,
Hp, Hc,
PΔu, Pu, E,
ex̂, fx̂, gx̂, jx̂, kx̂, vx̂, bx̂,
Eŝ, Fŝ, Gŝ, Jŝ, Kŝ, Vŝ, Bŝ
)
Expand All @@ -78,7 +85,7 @@ struct LinMPC{
nZ̃ = get_nZ(estim, transcription, Hp, Hc) + nϵ
Z̃ = zeros(NT, nZ̃)
buffer = PredictiveControllerBuffer(estim, transcription, Hp, Hc, nϵ)
mpc = new{NT, SE, TM, JM}(
mpc = new{NT, SE, CW, TM, JM}(
estim, transcription, optim, con,
Z̃, ŷ,
Hp, Hc, nϵ,
Expand Down Expand Up @@ -140,9 +147,9 @@ arguments. This controller allocates memory at each time step for the optimizati
- `Mwt=fill(1.0,model.ny)` : main diagonal of ``\mathbf{M}`` weight matrix (vector).
- `Nwt=fill(0.1,model.nu)` : main diagonal of ``\mathbf{N}`` weight matrix (vector).
- `Lwt=fill(0.0,model.nu)` : main diagonal of ``\mathbf{L}`` weight matrix (vector).
- `M_Hp=diagm(repeat(Mwt,Hp))` : positive semidefinite symmetric matrix ``\mathbf{M}_{H_p}``.
- `N_Hc=diagm(repeat(Nwt,Hc))` : positive semidefinite symmetric matrix ``\mathbf{N}_{H_c}``.
- `L_Hp=diagm(repeat(Lwt,Hp))` : positive semidefinite symmetric matrix ``\mathbf{L}_{H_p}``.
- `M_Hp=Diagonal(repeat(Mwt,Hp))` : positive semidefinite symmetric matrix ``\mathbf{M}_{H_p}``.
- `N_Hc=Diagonal(repeat(Nwt,Hc))` : positive semidefinite symmetric matrix ``\mathbf{N}_{H_c}``.
- `L_Hp=Diagonal(repeat(Lwt,Hp))` : positive semidefinite symmetric matrix ``\mathbf{L}_{H_p}``.
- `Cwt=1e5` : slack variable weight ``C`` (scalar), use `Cwt=Inf` for hard constraints only.
- `transcription=SingleShooting()` : a [`TranscriptionMethod`](@ref) for the optimization.
- `optim=JuMP.Model(OSQP.MathOptInterfaceOSQP.Optimizer)` : quadratic optimizer used in
Expand Down Expand Up @@ -199,9 +206,9 @@ function LinMPC(
Mwt = fill(DEFAULT_MWT, model.ny),
Nwt = fill(DEFAULT_NWT, model.nu),
Lwt = fill(DEFAULT_LWT, model.nu),
M_Hp = diagm(repeat(Mwt, Hp)),
N_Hc = diagm(repeat(Nwt, Hc)),
L_Hp = diagm(repeat(Lwt, Hp)),
M_Hp = Diagonal(repeat(Mwt, Hp)),
N_Hc = Diagonal(repeat(Nwt, Hc)),
L_Hp = Diagonal(repeat(Lwt, Hp)),
Cwt = DEFAULT_CWT,
transcription::TranscriptionMethod = DEFAULT_LINMPC_TRANSCRIPTION,
optim::JuMP.GenericModel = JuMP.Model(DEFAULT_LINMPC_OPTIMIZER, add_bridges=false),
Expand Down Expand Up @@ -242,9 +249,9 @@ function LinMPC(
Mwt = fill(DEFAULT_MWT, estim.model.ny),
Nwt = fill(DEFAULT_NWT, estim.model.nu),
Lwt = fill(DEFAULT_LWT, estim.model.nu),
M_Hp = diagm(repeat(Mwt, Hp)),
N_Hc = diagm(repeat(Nwt, Hc)),
L_Hp = diagm(repeat(Lwt, Hp)),
M_Hp = Diagonal(repeat(Mwt, Hp)),
N_Hc = Diagonal(repeat(Nwt, Hc)),
L_Hp = Diagonal(repeat(Lwt, Hp)),
Cwt = DEFAULT_CWT,
transcription::TranscriptionMethod = DEFAULT_LINMPC_TRANSCRIPTION,
optim::JM = JuMP.Model(DEFAULT_LINMPC_OPTIMIZER, add_bridges=false),
Expand All @@ -255,7 +262,8 @@ function LinMPC(
@warn("prediction horizon Hp ($Hp) ≤ estimated number of delays in model "*
"($nk), the closed-loop system may be unstable or zero-gain (unresponsive)")
end
return LinMPC{NT}(estim, Hp, Hc, M_Hp, N_Hc, L_Hp, Cwt, transcription, optim)
weights = ControllerWeights{NT}(estim.model, Hp, Hc, M_Hp, N_Hc, L_Hp, Cwt)
return LinMPC{NT}(estim, Hp, Hc, weights, transcription, optim)
end

"""
Expand Down
Loading