Skip to content
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

debug: terminal constraint now works with MultipleShooting + NonLinModel #176

Merged
merged 9 commits into from
Mar 16, 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.4.2"
version = "1.4.3"

[deps]
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
Expand Down
2 changes: 1 addition & 1 deletion docs/src/internals/predictive_control.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ ModelPredictiveControl.init_nonlincon!

```@docs
ModelPredictiveControl.initpred!(::PredictiveController, ::LinModel, ::Any, ::Any, ::Any, ::Any)
ModelPredictiveControl.linconstraint!(::PredictiveController, ::LinModel)
ModelPredictiveControl.linconstraint!(::PredictiveController, ::LinModel, ::TranscriptionMethod)
ModelPredictiveControl.linconstrainteq!
```

Expand Down
2 changes: 1 addition & 1 deletion src/ModelPredictiveControl.jl
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export SteadyKalmanFilter, KalmanFilter, UnscentedKalmanFilter, ExtendedKalmanFi
export MovingHorizonEstimator
export default_nint, initstate!
export PredictiveController, ExplicitMPC, LinMPC, NonLinMPC, setconstraint!, moveinput!
export SingleShooting, MultipleShooting
export TranscriptionMethod, SingleShooting, MultipleShooting
export SimResult, getinfo, sim!

include("general.jl")
Expand Down
90 changes: 1 addition & 89 deletions src/controller/construct.jl
Original file line number Diff line number Diff line change
Expand Up @@ -386,7 +386,7 @@ function setconstraint!(
JuMP.delete(optim, optim[:linconstraint])
JuMP.unregister(optim, :linconstraint)
@constraint(optim, linconstraint, A*Z̃var .≤ b)
set_nonlincon!(mpc, model, optim)
set_nonlincon!(mpc, model, transcription, optim)
else
i_b, i_g = init_matconstraint_mpc(
model, transcription, nc,
Expand All @@ -400,94 +400,6 @@ function setconstraint!(
return mpc
end


@doc raw"""
init_matconstraint_mpc(
model::LinModel, transcription::TranscriptionMethod, nc::Int,
i_Umin, i_Umax, i_ΔŨmin, i_ΔŨmax, i_Ymin, i_Ymax, i_x̂min, i_x̂max,
args...
) -> i_b, i_g, A, Aeq, neq

Init `i_b`, `i_g`, `neq`, and `A` and `Aeq` matrices for the all the MPC constraints.

The linear and nonlinear constraints are respectively defined as:
```math
\begin{aligned}
\mathbf{A Z̃ } &≤ \mathbf{b} \\
\mathbf{A_{eq} Z̃} &= \mathbf{b_{eq}} \\
\mathbf{g(Z̃)} &≤ \mathbf{0} \\
\mathbf{g_{eq}(Z̃)} &= \mathbf{0} \\
\end{aligned}
```
The argument `nc` is the number of custom nonlinear inequality constraints in
``\mathbf{g_c}``. `i_b` is a `BitVector` including the indices of ``\mathbf{b}`` that are
finite numbers. `i_g` is a similar vector but for the indices of ``\mathbf{g}``. The method
also returns the ``\mathbf{A, A_{eq}}`` matrices and `neq` if `args` is provided. In such a
case, `args` needs to contain all the inequality and equality constraint matrices:
`A_Umin, A_Umax, A_ΔŨmin, A_ΔŨmax, A_Ymin, A_Ymax, A_x̂min, A_x̂max, A_ŝ`. The integer `neq`
is the number of nonlinear equality constraints in ``\mathbf{g_{eq}}``.
"""
function init_matconstraint_mpc(
::LinModel{NT}, ::TranscriptionMethod, nc::Int,
i_Umin, i_Umax, i_ΔŨmin, i_ΔŨmax, i_Ymin, i_Ymax, i_x̂min, i_x̂max,
args...
) where {NT<:Real}
if isempty(args)
A, Aeq, neq = nothing, nothing, nothing
else
A_Umin, A_Umax, A_ΔŨmin, A_ΔŨmax, A_Ymin, A_Ymax, A_x̂min, A_x̂max, A_ŝ = args
A = [A_Umin; A_Umax; A_ΔŨmin; A_ΔŨmax; A_Ymin; A_Ymax; A_x̂min; A_x̂max]
Aeq = A_ŝ
neq = 0
end
i_b = [i_Umin; i_Umax; i_ΔŨmin; i_ΔŨmax; i_Ymin; i_Ymax; i_x̂min; i_x̂max]
i_g = trues(nc)
return i_b, i_g, A, Aeq, neq
end

"Init `i_b` without output constraints if [`NonLinModel`](@ref) & [`MultipleShooting`](@ref)."
function init_matconstraint_mpc(
::NonLinModel{NT}, ::MultipleShooting, nc::Int,
i_Umin, i_Umax, i_ΔŨmin, i_ΔŨmax, i_Ymin, i_Ymax, i_x̂min, i_x̂max,
args...
) where {NT<:Real}
if isempty(args)
A, Aeq, neq = nothing, nothing, nothing
else
A_Umin, A_Umax, A_ΔŨmin, A_ΔŨmax, A_Ymin, A_Ymax, A_x̂min, A_x̂max, A_ŝ = args
A = [A_Umin; A_Umax; A_ΔŨmin; A_ΔŨmax; A_Ymin; A_Ymax; A_x̂min; A_x̂max]
Aeq = A_ŝ
nΔŨ, nZ̃ = size(A_ΔŨmin)
neq = nZ̃ - nΔŨ
end
i_b = [i_Umin; i_Umax; i_ΔŨmin; i_ΔŨmax; i_x̂min; i_x̂max]
i_g = [i_Ymin; i_Ymax; trues(nc)]
return i_b, i_g, A, Aeq, neq
end

"Init `i_b` without output & terminal constraints if [`NonLinModel`](@ref) & [`SingleShooting`](@ref)."
function init_matconstraint_mpc(
::NonLinModel{NT}, ::SingleShooting, nc::Int,
i_Umin, i_Umax, i_ΔŨmin, i_ΔŨmax, i_Ymin, i_Ymax, i_x̂min, i_x̂max,
args...
) where {NT<:Real}
if isempty(args)
A, Aeq, neq = nothing, nothing, nothing
else
A_Umin, A_Umax, A_ΔŨmin, A_ΔŨmax, A_Ymin, A_Ymax, A_x̂min, A_x̂max, A_ŝ = args
A = [A_Umin; A_Umax; A_ΔŨmin; A_ΔŨmax; A_Ymin; A_Ymax; A_x̂min; A_x̂max]
Aeq = A_ŝ
neq = 0
end
i_b = [i_Umin; i_Umax; i_ΔŨmin; i_ΔŨmax]
i_g = [i_Ymin; i_Ymax; i_x̂min; i_x̂max; trues(nc)]
return i_b, i_g, A, Aeq, neq
end


"By default, there is no nonlinear constraint, thus do nothing."
set_nonlincon!(::PredictiveController, ::SimModel, ::JuMP.GenericModel) = nothing

"""
default_Hp(model::LinModel)

Expand Down
62 changes: 1 addition & 61 deletions src/controller/execute.jl
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ function moveinput!(
end
validate_args(mpc, ry, d, D̂, R̂y, R̂u)
initpred!(mpc, mpc.estim.model, d, D̂, R̂y, R̂u)
linconstraint!(mpc, mpc.estim.model)
linconstraint!(mpc, mpc.estim.model, mpc.transcription)
linconstrainteq!(mpc, mpc.estim.model, mpc.transcription)
Z̃ = optim_objective!(mpc)
return getinput(mpc, Z̃)
Expand Down Expand Up @@ -269,66 +269,6 @@ end
"Fill `Ŷs` vector with 0 values when `estim` is not an [`InternalModel`](@ref)."
predictstoch!(Ŷs, mpc::PredictiveController, ::StateEstimator) = (Ŷs .= 0; nothing)

@doc raw"""
linconstraint!(mpc::PredictiveController, model::LinModel)

Set `b` vector for the linear model inequality constraints (``\mathbf{A Z̃ ≤ b}``).

Also init ``\mathbf{f_x̂} = \mathbf{g_x̂ d_0}(k) + \mathbf{j_x̂ D̂_0} + \mathbf{k_x̂ x̂_0}(k) +
\mathbf{v_x̂ u_0}(k-1) + \mathbf{b_x̂}`` vector for the terminal constraints, see
[`init_predmat`](@ref).
"""
function linconstraint!(mpc::PredictiveController, model::LinModel)
nU, nΔŨ, nY = length(mpc.con.U0min), length(mpc.con.ΔŨmin), length(mpc.con.Y0min)
nx̂, fx̂ = mpc.estim.nx̂, mpc.con.fx̂
fx̂ .= mpc.con.bx̂
mul!(fx̂, mpc.con.kx̂, mpc.estim.x̂0, 1, 1)
mul!(fx̂, mpc.con.vx̂, mpc.estim.lastu0, 1, 1)
if model.nd ≠ 0
mul!(fx̂, mpc.con.gx̂, mpc.d0, 1, 1)
mul!(fx̂, mpc.con.jx̂, mpc.D̂0, 1, 1)
end
n = 0
mpc.con.b[(n+1):(n+nU)] .= @. -mpc.con.U0min + mpc.Tu_lastu0
n += nU
mpc.con.b[(n+1):(n+nU)] .= @. +mpc.con.U0max - mpc.Tu_lastu0
n += nU
mpc.con.b[(n+1):(n+nΔŨ)] .= @. -mpc.con.ΔŨmin
n += nΔŨ
mpc.con.b[(n+1):(n+nΔŨ)] .= @. +mpc.con.ΔŨmax
n += nΔŨ
mpc.con.b[(n+1):(n+nY)] .= @. -mpc.con.Y0min + mpc.F
n += nY
mpc.con.b[(n+1):(n+nY)] .= @. +mpc.con.Y0max - mpc.F
n += nY
mpc.con.b[(n+1):(n+nx̂)] .= @. -mpc.con.x̂0min + fx̂
n += nx̂
mpc.con.b[(n+1):(n+nx̂)] .= @. +mpc.con.x̂0max - fx̂
if any(mpc.con.i_b)
lincon = mpc.optim[:linconstraint]
JuMP.set_normalized_rhs(lincon, mpc.con.b[mpc.con.i_b])
end
return nothing
end

"Set `b` excluding predicted output constraints when `model` is not a [`LinModel`](@ref)."
function linconstraint!(mpc::PredictiveController, ::SimModel)
nU, nΔŨ = length(mpc.con.U0min), length(mpc.con.ΔŨmin)
n = 0
mpc.con.b[(n+1):(n+nU)] .= @. -mpc.con.U0min + mpc.Tu_lastu0
n += nU
mpc.con.b[(n+1):(n+nU)] .= @. +mpc.con.U0max - mpc.Tu_lastu0
n += nU
mpc.con.b[(n+1):(n+nΔŨ)] .= @. -mpc.con.ΔŨmin
n += nΔŨ
mpc.con.b[(n+1):(n+nΔŨ)] .= @. +mpc.con.ΔŨmax
if any(mpc.con.i_b)
lincon = mpc.optim[:linconstraint]
@views JuMP.set_normalized_rhs(lincon, mpc.con.b[mpc.con.i_b])
end
return nothing
end

"""
extended_vectors!(Ue, Ŷe, mpc::PredictiveController, U0, Ŷ0) -> Ue, Ŷe

Expand Down
2 changes: 1 addition & 1 deletion src/controller/explicitmpc.jl
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ function Base.show(io::IO, mpc::ExplicitMPC)
print_estim_dim(io, mpc.estim, n)
end

linconstraint!(::ExplicitMPC, ::LinModel) = nothing
linconstraint!(::ExplicitMPC, ::LinModel, ::TranscriptionMethod) = nothing

@doc raw"""
optim_objective!(mpc::ExplicitMPC) -> Z̃
Expand Down
56 changes: 1 addition & 55 deletions src/controller/nonlinmpc.jl
Original file line number Diff line number Diff line change
Expand Up @@ -509,7 +509,7 @@ function init_optimization!(mpc::NonLinMPC, model::SimModel, optim)
@operator(optim, J, nZ̃, Jfunc, ∇Jfunc!)
@objective(optim, Min, J(Z̃var...))
init_nonlincon!(mpc, model, transcription, gfuncs, ∇gfuncs!, geqfuncs, ∇geqfuncs!)
set_nonlincon!(mpc, model, optim)
set_nonlincon!(mpc, model, transcription, optim)
return nothing
end

Expand Down Expand Up @@ -692,60 +692,6 @@ function get_optim_functions(mpc::NonLinMPC, ::JuMP.GenericModel{JNT}) where JNT
return Jfunc, ∇Jfunc!, gfuncs, ∇gfuncs!, geqfuncs, ∇geqfuncs!
end

"""
set_nonlincon!(mpc::NonLinMPC, ::LinModel, optim)

Set the custom nonlinear inequality constraints for `LinModel`.
"""
function set_nonlincon!(
mpc::NonLinMPC, ::LinModel, optim::JuMP.GenericModel{JNT}
) where JNT<:Real
Z̃var = optim[:Z̃var]
con = mpc.con
nonlin_constraints = JuMP.all_constraints(optim, JuMP.NonlinearExpr, MOI.LessThan{JNT})
map(con_ref -> JuMP.delete(optim, con_ref), nonlin_constraints)
for i in 1:con.nc
gfunc_i = optim[Symbol("g_c_$i")]
@constraint(optim, gfunc_i(Z̃var...) <= 0)
end
return nothing
end

"""
set_nonlincon!(mpc::NonLinMPC, ::NonLinModel, optim)

Also set output prediction `Ŷ` and terminal state `x̂end` constraints when not a `LinModel`.
"""
function set_nonlincon!(
mpc::NonLinMPC, ::SimModel, optim::JuMP.GenericModel{JNT}
) where JNT<:Real
Z̃var = optim[:Z̃var]
con = mpc.con
nonlin_constraints = JuMP.all_constraints(optim, JuMP.NonlinearExpr, MOI.LessThan{JNT})
map(con_ref -> JuMP.delete(optim, con_ref), nonlin_constraints)
for i in findall(.!isinf.(con.Y0min))
gfunc_i = optim[Symbol("g_Y0min_$(i)")]
@constraint(optim, gfunc_i(Z̃var...) <= 0)
end
for i in findall(.!isinf.(con.Y0max))
gfunc_i = optim[Symbol("g_Y0max_$(i)")]
@constraint(optim, gfunc_i(Z̃var...) <= 0)
end
for i in findall(.!isinf.(con.x̂0min))
gfunc_i = optim[Symbol("g_x̂0min_$(i)")]
@constraint(optim, gfunc_i(Z̃var...) <= 0)
end
for i in findall(.!isinf.(con.x̂0max))
gfunc_i = optim[Symbol("g_x̂0max_$(i)")]
@constraint(optim, gfunc_i(Z̃var...) <= 0)
end
for i in 1:con.nc
gfunc_i = optim[Symbol("g_c_$i")]
@constraint(optim, gfunc_i(Z̃var...) <= 0)
end
return nothing
end

"""
con_nonlinprog!(g, mpc::NonLinMPC, model::LinModel, _ , _ , gc, ϵ) -> g

Expand Down
Loading