Skip to content

added: move blocking feature in LinMPC, ExplicitMPC and NonLinMPC #201

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 20 commits into from
May 29, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.7.0"
version = "1.8.0"

[deps]
ControlSystemsBase = "aaaaaaaa-a6ca-5380-bf3e-84a91bcd477e"
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ for more detailed examples.
- input setpoint tracking
- terminal costs
- custom economic costs (economic model predictive control)
- control horizon distinct from prediction horizon and custom move blocking
- adaptive linear model predictive controller
- manual model modification
- automatic successive linearization of a nonlinear model
Expand Down
1 change: 1 addition & 0 deletions docs/src/internals/predictive_control.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ The prediction methodology of this module is mainly based on Maciejowski textboo
## Controller Construction

```@docs
ModelPredictiveControl.move_blocking
ModelPredictiveControl.init_ZtoΔU
ModelPredictiveControl.init_ZtoU
ModelPredictiveControl.init_predmat
Expand Down
3 changes: 2 additions & 1 deletion docs/src/public/predictive_control.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ are shifted by one time step:
in which ``\mathbf{U}`` and ``\mathbf{R̂_u}`` are both vectors of `nu*Hp` elements. Defining
the manipulated input increment as ``\mathbf{Δu}(k+j) = \mathbf{u}(k+j) - \mathbf{u}(k+j-1)``,
the control horizon ``H_c`` enforces that ``\mathbf{Δu}(k+j) = \mathbf{0}`` for ``j ≥ H_c``.
For this reason, the vector that collects them is truncated up to ``k+H_c-1``:
For this reason, the vector that collects them is truncated up to ``k+H_c-1`` (without
any custom move blocking):

```math
\mathbf{ΔU} =
Expand Down
66 changes: 66 additions & 0 deletions src/controller/construct.jl
Original file line number Diff line number Diff line change
Expand Up @@ -456,6 +456,72 @@ end
"Return `0` when model is not a [`LinModel`](@ref)."
estimate_delays(::SimModel) = 0


@doc raw"""
move_blocking(Hp::Int, Hc::AbstractVector{Int}) -> nb

Get the move blocking vector `nb` from the `Hc` argument, and modify it to match `Hp`.

This feature is also known as manipulated variable blocking. The argument `Hc` is
interpreted as the move blocking vector `nb`. It specifies the length of each step (or
"block") in the ``\mathbf{ΔU}`` vector, to customize the pattern (in time steps, thus
strictly positive integers):
```math
\mathbf{n_b} = \begin{bmatrix} n_1 & n_2 & \cdots & n_{H_c} \end{bmatrix}'
```
The vector that includes all the manipulated input increments ``\mathbf{Δu}`` is then
defined as:
```math
\mathbf{ΔU} = \begin{bmatrix}
\mathbf{Δu}(k + 0) \\[0.1em]
\mathbf{Δu}(k + ∑_{i=1}^1 n_i) \\[0.1em]
\mathbf{Δu}(k + ∑_{i=1}^2 n_i) \\[0.1em]
\vdots \\[0.1em]
\mathbf{Δu}(k + ∑_{i=1}^{H_c-1} n_i)
\end{bmatrix}
```
The provided `nb` vector is modified to ensure `sum(nb) == Hp`:
- If `sum(nb) < Hp`, a new element is pushed to `nb` with the value `Hp - sum(nb)`.
- If `sum(nb) > Hp`, the intervals are truncated until `sum(nb) == Hp`. For example, if
`Hp = 10` and `nb = [1, 2, 3, 6, 7]`, then `nb` is truncated to `[1, 2, 3, 4]`.
"""
function move_blocking(Hp_arg::Int, Hc_arg::AbstractVector{Int})
Hp = Hp_arg
nb = Hc_arg
all(>(0), nb) || throw(ArgumentError("Move blocking vector must be strictly positive integers."))
if sum(nb) < Hp
newblock = [Hp - sum(nb)]
nb = [nb; newblock]
elseif sum(nb) > Hp
nb = nb[begin:findfirst(≥(Hp), cumsum(nb))]
if sum(nb) > Hp
# if the last block is too large, it is truncated to fit Hp:
nb[end] = Hp - @views sum(nb[begin:end-1])
end
end
return nb
end

"""
move_blocking(Hp::Int, Hc::Int) -> nb

Construct a move blocking vector `nb` that match the provided `Hp` and `Hc` integers.

The vector is filled with `1`s, except for the last element which is `Hp - Hc + 1`.
"""
function move_blocking(Hp_arg::Int, Hc_arg::Int)
Hp, Hc = Hp_arg, Hc_arg
nb = fill(1, Hc)
if Hc > 0 # if Hc < 1, it will crash later with a clear error message
nb[end] = Hp - Hc + 1
end
return nb
end

"Get the actual control Horizon `Hc` (integer) from the move blocking vector `nb`."
get_Hc(nb::AbstractVector{Int}) = length(nb)


"""
validate_args(mpc::PredictiveController, ry, d, D̂, R̂y, R̂u)

Expand Down
19 changes: 11 additions & 8 deletions src/controller/explicitmpc.jl
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ struct ExplicitMPC{
Hp::Int
Hc::Int
nϵ::Int
nb::Vector{Int}
weights::CW
R̂u::Vector{NT}
R̂y::Vector{NT}
Expand Down Expand Up @@ -39,7 +40,7 @@ struct ExplicitMPC{
Dop::Vector{NT}
buffer::PredictiveControllerBuffer{NT}
function ExplicitMPC{NT}(
estim::SE, Hp, Hc, weights::CW
estim::SE, Hp, Hc, nb, weights::CW
) where {NT<:Real, SE<:StateEstimator, CW<:ControllerWeights}
model = estim.model
nu, ny, nd, nx̂ = model.nu, model.ny, model.nd, estim.nx̂
Expand All @@ -50,7 +51,7 @@ struct ExplicitMPC{
lastu0 = zeros(NT, nu)
transcription = SingleShooting() # explicit MPC only supports SingleShooting
PΔu = init_ZtoΔU(estim, transcription, Hp, Hc)
Pu, Tu = init_ZtoU(estim, transcription, Hp, Hc)
Pu, Tu = init_ZtoU(estim, transcription, Hp, Hc, nb)
E, G, J, K, V, B = init_predmat(model, estim, transcription, Hp, Hc)
# dummy val (updated just before optimization):
F = zeros(NT, ny*Hp)
Expand All @@ -70,7 +71,7 @@ struct ExplicitMPC{
estim,
transcription,
Z̃, ŷ,
Hp, Hc, nϵ,
Hp, Hc, nϵ, nb,
weights,
R̂u, R̂y,
lastu0,
Expand Down Expand Up @@ -129,12 +130,12 @@ ExplicitMPC controller with a sample time Ts = 4.0 s, SteadyKalmanFilter estimat
function ExplicitMPC(
model::LinModel;
Hp::Int = default_Hp(model),
Hc::Int = DEFAULT_HC,
Hc::IntVectorOrInt = DEFAULT_HC,
Mwt = fill(DEFAULT_MWT, model.ny),
Nwt = fill(DEFAULT_NWT, model.nu),
Lwt = fill(DEFAULT_LWT, model.nu),
M_Hp = Diagonal(repeat(Mwt, Hp)),
N_Hc = Diagonal(repeat(Nwt, Hc)),
N_Hc = Diagonal(repeat(Nwt, get_Hc(move_blocking(Hp, Hc)))),
L_Hp = Diagonal(repeat(Lwt, Hp)),
kwargs...
)
Expand All @@ -152,12 +153,12 @@ Use custom state estimator `estim` to construct `ExplicitMPC`.
function ExplicitMPC(
estim::SE;
Hp::Int = default_Hp(estim.model),
Hc::Int = DEFAULT_HC,
Hc::IntVectorOrInt = DEFAULT_HC,
Mwt = fill(DEFAULT_MWT, estim.model.ny),
Nwt = fill(DEFAULT_NWT, estim.model.nu),
Lwt = fill(DEFAULT_LWT, estim.model.nu),
M_Hp = Diagonal(repeat(Mwt, Hp)),
N_Hc = Diagonal(repeat(Nwt, Hc)),
N_Hc = Diagonal(repeat(Nwt, get_Hc(move_blocking(Hp, Hc)))),
L_Hp = Diagonal(repeat(Lwt, Hp)),
) where {NT<:Real, SE<:StateEstimator{NT}}
isa(estim.model, LinModel) || error(MSG_LINMODEL_ERR)
Expand All @@ -166,8 +167,10 @@ function ExplicitMPC(
@warn("prediction horizon Hp ($Hp) ≤ estimated number of delays in model "*
"($nk), the closed-loop system may be unstable or zero-gain (unresponsive)")
end
nb = move_blocking(Hp, Hc)
Hc = get_Hc(nb)
weights = ControllerWeights{NT}(estim.model, Hp, Hc, M_Hp, N_Hc, L_Hp)
return ExplicitMPC{NT}(estim, Hp, Hc, weights)
return ExplicitMPC{NT}(estim, Hp, Hc, nb, weights)
end

setconstraint!(::ExplicitMPC; kwargs...) = error("ExplicitMPC does not support constraints.")
Expand Down
27 changes: 16 additions & 11 deletions src/controller/linmpc.jl
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ struct LinMPC{
Hp::Int
Hc::Int
nϵ::Int
nb::Vector{Int}
weights::CW
R̂u::Vector{NT}
R̂y::Vector{NT}
Expand Down Expand Up @@ -47,7 +48,7 @@ struct LinMPC{
Dop::Vector{NT}
buffer::PredictiveControllerBuffer{NT}
function LinMPC{NT}(
estim::SE, Hp, Hc, weights::CW,
estim::SE, Hp, Hc, nb, weights::CW,
transcription::TM, optim::JM
) where {
NT<:Real,
Expand All @@ -63,7 +64,7 @@ struct LinMPC{
R̂y, R̂u, Tu_lastu0 = zeros(NT, ny*Hp), zeros(NT, nu*Hp), zeros(NT, nu*Hp)
lastu0 = zeros(NT, nu)
PΔu = init_ZtoΔU(estim, transcription, Hp, Hc)
Pu, Tu = init_ZtoU(estim, transcription, Hp, Hc)
Pu, Tu = init_ZtoU(estim, transcription, Hp, Hc, nb)
E, G, J, K, V, B, ex̂, gx̂, jx̂, kx̂, vx̂, bx̂ = init_predmat(
model, estim, transcription, Hp, Hc
)
Expand All @@ -90,7 +91,7 @@ struct LinMPC{
mpc = new{NT, SE, CW, TM, JM}(
estim, transcription, optim, con,
Z̃, ŷ,
Hp, Hc, nϵ,
Hp, Hc, nϵ, nb,
weights,
R̂u, R̂y,
lastu0,
Expand Down Expand Up @@ -145,8 +146,9 @@ arguments. This controller allocates memory at each time step for the optimizati

# Arguments
- `model::LinModel` : model used for controller predictions and state estimations.
- `Hp=10+nk` : prediction horizon ``H_p``, `nk` is the number of delays in `model`.
- `Hc=2` : control horizon ``H_c``.
- `Hp::Int=10+nk` : prediction horizon ``H_p``, `nk` is the number of delays in `model`.
- `Hc::Union{Int, Vector{Int}}=2` : control horizon ``H_c``, custom move blocking pattern is
specified with a vector of integers (see [`move_blocking`](@ref) for details).
- `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).
Expand Down Expand Up @@ -205,12 +207,12 @@ LinMPC controller with a sample time Ts = 4.0 s, OSQP optimizer, SteadyKalmanFil
function LinMPC(
model::LinModel;
Hp::Int = default_Hp(model),
Hc::Int = DEFAULT_HC,
Hc::IntVectorOrInt = DEFAULT_HC,
Mwt = fill(DEFAULT_MWT, model.ny),
Nwt = fill(DEFAULT_NWT, model.nu),
Lwt = fill(DEFAULT_LWT, model.nu),
M_Hp = Diagonal(repeat(Mwt, Hp)),
N_Hc = Diagonal(repeat(Nwt, Hc)),
N_Hc = Diagonal(repeat(Nwt, get_Hc(move_blocking(Hp, Hc)))),
L_Hp = Diagonal(repeat(Lwt, Hp)),
Cwt = DEFAULT_CWT,
transcription::TranscriptionMethod = DEFAULT_LINMPC_TRANSCRIPTION,
Expand All @@ -227,7 +229,8 @@ end

Use custom state estimator `estim` to construct `LinMPC`.

`estim.model` must be a [`LinModel`](@ref). Else, a [`NonLinMPC`](@ref) is required.
`estim.model` must be a [`LinModel`](@ref). Else, a [`NonLinMPC`](@ref) is required. See
[`ManualEstimator`](@ref) for linear controllers with nonlinear state estimation.

# Examples
```jldoctest
Expand All @@ -248,12 +251,12 @@ LinMPC controller with a sample time Ts = 4.0 s, OSQP optimizer, KalmanFilter es
function LinMPC(
estim::SE;
Hp::Int = default_Hp(estim.model),
Hc::Int = DEFAULT_HC,
Hc::IntVectorOrInt = DEFAULT_HC,
Mwt = fill(DEFAULT_MWT, estim.model.ny),
Nwt = fill(DEFAULT_NWT, estim.model.nu),
Lwt = fill(DEFAULT_LWT, estim.model.nu),
M_Hp = Diagonal(repeat(Mwt, Hp)),
N_Hc = Diagonal(repeat(Nwt, Hc)),
N_Hc = Diagonal(repeat(Nwt, get_Hc(move_blocking(Hp, Hc)))),
L_Hp = Diagonal(repeat(Lwt, Hp)),
Cwt = DEFAULT_CWT,
transcription::TranscriptionMethod = DEFAULT_LINMPC_TRANSCRIPTION,
Expand All @@ -265,8 +268,10 @@ 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
nb = move_blocking(Hp, Hc)
Hc = get_Hc(nb)
weights = ControllerWeights{NT}(estim.model, Hp, Hc, M_Hp, N_Hc, L_Hp, Cwt)
return LinMPC{NT}(estim, Hp, Hc, weights, transcription, optim)
return LinMPC{NT}(estim, Hp, Hc, nb, weights, transcription, optim)
end

"""
Expand Down
25 changes: 15 additions & 10 deletions src/controller/nonlinmpc.jl
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ struct NonLinMPC{
Hp::Int
Hc::Int
nϵ::Int
nb::Vector{Int}
weights::CW
JE::JEfunc
p::PT
Expand Down Expand Up @@ -63,7 +64,7 @@ struct NonLinMPC{
Dop::Vector{NT}
buffer::PredictiveControllerBuffer{NT}
function NonLinMPC{NT}(
estim::SE, Hp, Hc, weights::CW,
estim::SE, Hp, Hc, nb, weights::CW,
JE::JEfunc, gc!::GCfunc, nc, p::PT,
transcription::TM, optim::JM,
gradient::GB, jacobian::JB
Expand All @@ -86,7 +87,7 @@ struct NonLinMPC{
R̂y, R̂u, Tu_lastu0 = zeros(NT, ny*Hp), zeros(NT, nu*Hp), zeros(NT, nu*Hp)
lastu0 = zeros(NT, nu)
PΔu = init_ZtoΔU(estim, transcription, Hp, Hc)
Pu, Tu = init_ZtoU(estim, transcription, Hp, Hc)
Pu, Tu = init_ZtoU(estim, transcription, Hp, Hc, nb)
E, G, J, K, V, B, ex̂, gx̂, jx̂, kx̂, vx̂, bx̂ = init_predmat(
model, estim, transcription, Hp, Hc
)
Expand Down Expand Up @@ -116,7 +117,7 @@ struct NonLinMPC{
estim, transcription, optim, con,
gradient, jacobian,
Z̃, ŷ,
Hp, Hc, nϵ,
Hp, Hc, nϵ, nb,
weights,
JE, p,
R̂u, R̂y,
Expand Down Expand Up @@ -188,8 +189,10 @@ This controller allocates memory at each time step for the optimization.

# Arguments
- `model::SimModel` : model used for controller predictions and state estimations.
- `Hp=nothing`: prediction horizon ``H_p``, must be specified for [`NonLinModel`](@ref).
- `Hc=2` : control horizon ``H_c``.
- `Hp::Int=10+nk` : prediction horizon ``H_p``, `nk` is the number of delays if `model` is a
[`LinModel`](@ref) (must be specified otherwise).
- `Hc::Union{Int, Vector{Int}}=2` : control horizon ``H_c``, custom move blocking pattern is
specified with a vector of integers (see [`move_blocking`](@ref) for details).
- `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).
Expand Down Expand Up @@ -281,12 +284,12 @@ NonLinMPC controller with a sample time Ts = 10.0 s, Ipopt optimizer, UnscentedK
function NonLinMPC(
model::SimModel;
Hp::Int = default_Hp(model),
Hc::Int = DEFAULT_HC,
Hc::IntVectorOrInt = DEFAULT_HC,
Mwt = fill(DEFAULT_MWT, model.ny),
Nwt = fill(DEFAULT_NWT, model.nu),
Lwt = fill(DEFAULT_LWT, model.nu),
M_Hp = Diagonal(repeat(Mwt, Hp)),
N_Hc = Diagonal(repeat(Nwt, Hc)),
N_Hc = Diagonal(repeat(Nwt, get_Hc(move_blocking(Hp, Hc)))),
L_Hp = Diagonal(repeat(Lwt, Hp)),
Cwt = DEFAULT_CWT,
Ewt = DEFAULT_EWT,
Expand Down Expand Up @@ -338,12 +341,12 @@ NonLinMPC controller with a sample time Ts = 10.0 s, Ipopt optimizer, UnscentedK
function NonLinMPC(
estim::SE;
Hp::Int = default_Hp(estim.model),
Hc::Int = DEFAULT_HC,
Hc::IntVectorOrInt = DEFAULT_HC,
Mwt = fill(DEFAULT_MWT, estim.model.ny),
Nwt = fill(DEFAULT_NWT, estim.model.nu),
Lwt = fill(DEFAULT_LWT, estim.model.nu),
M_Hp = Diagonal(repeat(Mwt, Hp)),
N_Hc = Diagonal(repeat(Nwt, Hc)),
N_Hc = Diagonal(repeat(Nwt, get_Hc(move_blocking(Hp, Hc)))),
L_Hp = Diagonal(repeat(Lwt, Hp)),
Cwt = DEFAULT_CWT,
Ewt = DEFAULT_EWT,
Expand All @@ -365,11 +368,13 @@ function NonLinMPC(
@warn("prediction horizon Hp ($Hp) ≤ estimated number of delays in model "*
"($nk), the closed-loop system may be unstable or zero-gain (unresponsive)")
end
nb = move_blocking(Hp, Hc)
Hc = get_Hc(nb)
validate_JE(NT, JE)
gc! = get_mutating_gc(NT, gc)
weights = ControllerWeights{NT}(estim.model, Hp, Hc, M_Hp, N_Hc, L_Hp, Cwt, Ewt)
return NonLinMPC{NT}(
estim, Hp, Hc, weights, JE, gc!, nc, p, transcription, optim, gradient, jacobian
estim, Hp, Hc, nb, weights, JE, gc!, nc, p, transcription, optim, gradient, jacobian
)
end

Expand Down
Loading