Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
9b0bda7
added: `OrthogonalCollocation` type
franckgaga Feb 10, 2026
5a66aae
debug: syntax for `OrthogonalCollocation` type
franckgaga Feb 10, 2026
5376c1b
debug: forgot `end`
franckgaga Feb 10, 2026
0cb98e6
added: `FastGaussQuadrature` dependency
franckgaga Feb 11, 2026
64e9adf
wip: `OrthgonalCollation`
franckgaga Feb 12, 2026
7e96575
changed: renamed `nc` to `np`
franckgaga Feb 18, 2026
dc4f555
changed: renamed `np` to `no`
franckgaga Feb 19, 2026
9b620d1
changed: moving `init_ZtoΔU` to `construct.jl`
franckgaga Feb 19, 2026
907d3ec
changed: same for `init_ZtoU`
franckgaga Feb 19, 2026
a73dee0
wip: orthogonal collocation
franckgaga Feb 19, 2026
a812b0d
debug: correct `spzeros` size in `init_ZtoU`
franckgaga Feb 19, 2026
b63f57d
debug: correct prediction matrices for `OrthogonalCollocation`
franckgaga Feb 22, 2026
ea6447b
doc: warning admonition for collocation and state estimators
franckgaga Feb 22, 2026
78ad007
doc: idem
franckgaga Feb 22, 2026
eb91f87
doc: minor details
franckgaga Feb 23, 2026
cef2bed
Merge branch 'main' into orthogonal_colloc
franckgaga Feb 24, 2026
d416675
changed: renamed `K0` to `K` and `k0` to `k`
franckgaga Feb 25, 2026
66c88c1
changed: minor formatting modification
franckgaga Feb 25, 2026
fef9bfa
wip: `OrthogonalCollocation`
franckgaga Feb 26, 2026
8c719f8
wip
franckgaga Feb 27, 2026
4d39181
doc
franckgaga Feb 27, 2026
ebe9df5
doc: cleanup in `OrthogonalCollocation` equations
franckgaga Feb 27, 2026
ea5c1e5
doc: idem
franckgaga Feb 27, 2026
d0789af
doc: implicit product
franckgaga Feb 28, 2026
8e6ac65
doc: cleanup `OrthogonalCollocation`
franckgaga Mar 1, 2026
8d07cce
changed: renaming `K` arg to `K̇` for clarity.
franckgaga Mar 1, 2026
8b4197c
debug: correct field location
franckgaga Mar 1, 2026
bed52aa
added: `OrthogonalCollocation` now works 🍾
franckgaga Mar 1, 2026
0e605cf
removed: useless print
franckgaga Mar 1, 2026
ae0c22f
doc: minor correction
franckgaga Mar 1, 2026
29e12c4
doc: moving collocation matrices to seperate docstring
franckgaga Mar 2, 2026
7e3afe6
changed: default `no` in `OrthogonalCollocation`
franckgaga Mar 2, 2026
530a1be
doc: details on `d` interpolation
franckgaga Mar 2, 2026
2510575
doc: going back to `k_i` notation
franckgaga Mar 2, 2026
d3cad37
doc: `sk` instead of `sknext`
franckgaga Mar 2, 2026
b71a2f0
changed: clearer code in `TrapezoidalCollocation`
franckgaga Mar 2, 2026
17c1f71
doc: cleanup
franckgaga Mar 2, 2026
94f86e7
debug: also warm-start the collocation points
franckgaga Mar 2, 2026
5e6ceeb
doc: minor correction
franckgaga Mar 2, 2026
51d3e43
added: reduce allocations
franckgaga Mar 2, 2026
fcc824f
changed: allocate before the loop
franckgaga Mar 2, 2026
a3fd269
changed: using `i` counter for colloc. points
franckgaga Mar 2, 2026
7537b91
doc: modify continuity constraint equation
franckgaga Mar 3, 2026
a1b7b62
test: NMPC with `AutoFiniteDiff`
franckgaga Mar 3, 2026
d67d407
added: linearly interpolated `d` in `OrthogonalCollocation`
franckgaga Mar 3, 2026
2493daa
test: unit case with `OrthogonalCollocation`
franckgaga Mar 3, 2026
51727ec
bump
franckgaga Mar 3, 2026
d7971e5
test: improve coverage with IMC
franckgaga Mar 3, 2026
8877d0f
test: improve coverage
franckgaga Mar 3, 2026
0ac337c
test: improve coverage
franckgaga Mar 3, 2026
File filter

Filter by extension

Filter by extension

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

[deps]
ControlSystemsBase = "aaaaaaaa-a6ca-5380-bf3e-84a91bcd477e"
DifferentiationInterface = "a0c0ee7d-e4b9-4e03-894e-1c5f64a51d63"
FastGaussQuadrature = "442a2c76-b920-505d-bb47-c5924d526838"
ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210"
Ipopt = "b6b21f68-93f8-5de0-b562-5493be1d77c9"
JuMP = "4076af6c-e467-56ae-b986-b466b2749572"
Expand Down Expand Up @@ -33,6 +34,7 @@ ControlSystemsBase = "1.18.2"
DAQP = "0.6, 0.7.1"
DifferentiationInterface = "0.7.11"
Documenter = "1"
FastGaussQuadrature = "1.1.0"
FiniteDiff = "2"
ForwardDiff = "0.10, 1"
Ipopt = "1"
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 @@ -25,6 +25,7 @@ ModelPredictiveControl.relaxterminal
ModelPredictiveControl.augmentdefect
ModelPredictiveControl.init_quadprog
ModelPredictiveControl.init_stochpred
ModelPredictiveControl.init_orthocolloc
ModelPredictiveControl.init_matconstraint_mpc
ModelPredictiveControl.get_nonlinobj_op(::NonLinMPC, ::ModelPredictiveControl.GenericModel)
ModelPredictiveControl.get_nonlincon_oracle(::NonLinMPC, ::ModelPredictiveControl.GenericModel)
Expand Down
1 change: 1 addition & 0 deletions docs/src/internals/state_estim.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ ModelPredictiveControl.get_nonlincon_oracle(::MovingHorizonEstimator, ::ModelPre
```@docs
ModelPredictiveControl.f̂!
ModelPredictiveControl.ĥ!
ModelPredictiveControl.f̂_input!
```

## Update Quadratic Optimization
Expand Down
8 changes: 7 additions & 1 deletion docs/src/public/predictive_control.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,4 +120,10 @@ MultipleShooting

```@docs
TrapezoidalCollocation
```
```

### OrthogonalCollocation

```@docs
OrthogonalCollocation
```
5 changes: 4 additions & 1 deletion src/ModelPredictiveControl.jl
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ import JuMP: @variable, @operator, @constraint, @objective

import OSQP, Ipopt

import FastGaussQuadrature

export SimModel, LinModel, NonLinModel
export DiffSolver, RungeKutta, ForwardEuler
export setop!, setname!
Expand All @@ -48,7 +50,8 @@ export MovingHorizonEstimator
export ManualEstimator
export default_nint, initstate!
export PredictiveController, ExplicitMPC, LinMPC, NonLinMPC, setconstraint!, moveinput!
export TranscriptionMethod, SingleShooting, MultipleShooting, TrapezoidalCollocation
export TranscriptionMethod, SingleShooting, MultipleShooting
export TrapezoidalCollocation, OrthogonalCollocation
export SimResult, getinfo, sim!

include("general.jl")
Expand Down
99 changes: 99 additions & 0 deletions src/controller/construct.jl
Original file line number Diff line number Diff line change
Expand Up @@ -692,6 +692,105 @@ function validate_args(mpc::PredictiveController, ry, d, lastu, D̂, R̂y, R̂u)
size(R̂u) ≠ (nu*Hp,) && throw(DimensionMismatch("R̂u size $(size(R̂u)) ≠ manip. input size × Hp ($(nu*Hp),)"))
end

@doc raw"""
init_ZtoΔU(estim::StateEstimator, transcription::TranscriptionMethod, Hp, Hc) -> PΔu

Init decision variables to input increments over ``H_c`` conversion matrix `PΔu`.

The conversion from the decision variables ``\mathbf{Z}`` to ``\mathbf{ΔU}``, the input
increments over ``H_c``, is computed by:
```math
\mathbf{ΔU} = \mathbf{P_{Δu}} \mathbf{Z}
```

in which ``\mathbf{P_{Δu}}`` is defined in the Extended Help section.

# Extended Help
!!! details "Extended Help"
Following the decision variable definition of the [`TranscriptionMethod`](@ref), the
conversion matrix ``\mathbf{P_{Δu}}``, we have:
- ``\mathbf{P_{Δu}} = \mathbf{I}`` if `transcription` is a [`SingleShooting`](@ref)
- ``\mathbf{P_{Δu}} = [\begin{smallmatrix}\mathbf{I} & \mathbf{0} \end{smallmatrix}]`` otherwise.
The matrix is store as as `SparseMatrixCSC` to support both cases efficiently.
"""
function init_ZtoΔU(
estim::StateEstimator{NT}, transcription::TranscriptionMethod, Hp, Hc
) where {NT<:Real}
I_nu_Hc = sparse(Matrix{NT}(I, estim.model.nu*Hc, estim.model.nu*Hc))
nZ = get_nZ(estim, transcription, Hp, Hc)
nΔU = estim.model.nu*Hc
PΔu = [I_nu_Hc spzeros(NT, nΔU, nZ - nΔU)]
return PΔu
end

@doc raw"""
init_ZtoU(estim, transcription, Hp, Hc, nb) -> Pu, Tu

Init decision variables to inputs over ``H_p`` conversion matrices.

The conversion from the decision variables ``\mathbf{Z}`` to ``\mathbf{U}``, the manipulated
inputs over ``H_p``, is computed by:
```math
\mathbf{U} = \mathbf{P_u} \mathbf{Z} + \mathbf{T_u} \mathbf{u}(k-1)
```
The ``\mathbf{P_u}`` and ``\mathbf{T_u}`` matrices are defined in the Extended Help section.

# Extended Help
!!! details "Extended Help"
With ``n_i``, the ``i``th element of the ``\mathbf{n_b}`` vector defined in [`move_blocking`](@ref)
documentation, we introduce the ``\mathbf{Q}(n_i)`` matrix of size `(nu*ni, nu)`:
```math
\mathbf{Q}(n_i) = \begin{bmatrix}
\mathbf{I} \\
\mathbf{I} \\
\vdots \\
\mathbf{I} \end{bmatrix}
```
The ``\mathbf{U}`` vector and the conversion matrices are defined as:
```math
\mathbf{U} = \begin{bmatrix}
\mathbf{u}(k + 0) \\
\mathbf{u}(k + 1) \\
\vdots \\
\mathbf{u}(k + H_p - 1) \end{bmatrix} , \quad
\mathbf{P_u^†} = \begin{bmatrix}
\mathbf{Q}(n_1) & \mathbf{0} & \cdots & \mathbf{0} \\
\mathbf{Q}(n_2) & \mathbf{Q}(n_2) & \cdots & \mathbf{0} \\
\vdots & \vdots & \ddots & \vdots \\
\mathbf{Q}(n_{H_c}) & \mathbf{Q}(n_{H_c}) & \cdots & \mathbf{Q}(n_{H_c}) \end{bmatrix} , \quad
\mathbf{T_u} = \begin{bmatrix}
\mathbf{I} \\
\mathbf{I} \\
\vdots \\
\mathbf{I} \end{bmatrix}
```
and, depending on the transcription method, we have:
- ``\mathbf{P_u} = \mathbf{P_u^†}`` if `transcription` is a [`SingleShooting`](@ref)
- ``\mathbf{P_u} = [\begin{smallmatrix}\mathbf{P_u^†} & \mathbf{0} \end{smallmatrix}]``
if `transcription` is a [`MultipleShooting`](@ref)
The conversion matrices are stored as `SparseMatrixCSC` since it was benchmarked that it
is generally more performant than normal dense matrices, even for small `nu`, `Hp` and
`Hc` values. Using `Bool` element type and `BitMatrix` is also slower.
"""
function init_ZtoU(
estim::StateEstimator{NT}, transcription::TranscriptionMethod, Hp, Hc, nb
) where {NT<:Real}
nu = estim.model.nu
I_nu = sparse(Matrix{NT}(I, nu, nu))
PuDagger = Matrix{NT}(undef, nu*Hp, nu*Hc)
for i=1:Hc
ni = nb[i]
Q_ni = repeat(I_nu, ni, 1)
iRows = (1:nu*ni) .+ @views nu*sum(nb[1:i-1])
PuDagger[iRows, :] = [repeat(Q_ni, 1, i) spzeros(nu*ni, nu*(Hc-i))]
end
PuDagger = sparse(PuDagger)
nZ = get_nZ(estim, transcription, Hp, Hc)
Pu = [PuDagger spzeros(NT, nu*Hp, nZ - nu*Hc)]
Tu = repeat(I_nu, Hp)
return Pu, Tu
end

@doc raw"""
init_quadprog(
model::LinModel, transcriptions::TranscriptionMethod, weights::ControllerWeights,
Expand Down
4 changes: 2 additions & 2 deletions src/controller/execute.jl
Original file line number Diff line number Diff line change
Expand Up @@ -142,15 +142,15 @@ function getinfo(mpc::PredictiveController{NT}) where NT<:Real
info = Dict{Symbol, Any}()
ΔŨ = Vector{NT}(undef, nΔŨ)
x̂0end = similar(mpc.estim.x̂0)
K0 = Vector{NT}(undef, nK)
K = Vector{NT}(undef, nK)
Ue, Ŷe = Vector{NT}(undef, nUe), Vector{NT}(undef, nŶe)
U0, Ŷ0 = similar(mpc.Uop), similar(mpc.Yop)
Û0, X̂0 = Vector{NT}(undef, nÛ0), Vector{NT}(undef, nX̂0)
U, Ŷ = buffer.U, buffer.Ŷ
D̂ = buffer.D̂
U0 = getU0!(U0, mpc, Z̃)
ΔŨ = getΔŨ!(ΔŨ, mpc, transcription, Z̃)
Ŷ0, x̂0end = predict!(Ŷ0, x̂0end, X̂0, Û0, K0, mpc, model, transcription, U0, Z̃)
Ŷ0, x̂0end = predict!(Ŷ0, x̂0end, X̂0, Û0, K, mpc, model, transcription, U0, Z̃)
Ue, Ŷe = extended_vectors!(Ue, Ŷe, mpc, U0, Ŷ0)
U .= U0 .+ mpc.Uop
Ŷ .= Ŷ0 .+ mpc.Yop
Expand Down
20 changes: 10 additions & 10 deletions src/controller/legacy.jl
Original file line number Diff line number Diff line change
Expand Up @@ -23,21 +23,21 @@ function get_optim_functions(mpc::NonLinMPC, ::JuMP.GenericModel{JNT}) where JNT
J::Vector{JNT} = zeros(JNT, 1)
ΔŨ::Vector{JNT} = zeros(JNT, nΔŨ)
x̂0end::Vector{JNT} = zeros(JNT, nx̂)
K0::Vector{JNT} = zeros(JNT, nK)
K::Vector{JNT} = zeros(JNT, nK)
Ue::Vector{JNT}, Ŷe::Vector{JNT} = zeros(JNT, nUe), zeros(JNT, nŶe)
U0::Vector{JNT}, Ŷ0::Vector{JNT} = zeros(JNT, nU), zeros(JNT, nŶ)
Û0::Vector{JNT}, X̂0::Vector{JNT} = zeros(JNT, nU), zeros(JNT, nX̂)
gc::Vector{JNT}, g::Vector{JNT} = zeros(JNT, nc), zeros(JNT, ng)
geq::Vector{JNT} = zeros(JNT, neq)
# ---------------------- objective function -------------------------------------------
function Jfunc!(Z̃, ΔŨ, x̂0end, Ue, Ŷe, U0, Ŷ0, Û0, K0, X̂0, gc, g, geq)
update_predictions!(ΔŨ, x̂0end, Ue, Ŷe, U0, Ŷ0, Û0, K0, X̂0, gc, g, geq, mpc, Z̃)
function Jfunc!(Z̃, ΔŨ, x̂0end, Ue, Ŷe, U0, Ŷ0, Û0, K, X̂0, gc, g, geq)
update_predictions!(ΔŨ, x̂0end, Ue, Ŷe, U0, Ŷ0, Û0, K, X̂0, gc, g, geq, mpc, Z̃)
return obj_nonlinprog!(Ŷ0, U0, mpc, model, Ue, Ŷe, ΔŨ)
end
Z̃_∇J = fill(myNaN, nZ̃) # NaN to force update_predictions! at first call
∇J_context = (
Cache(ΔŨ), Cache(x̂0end), Cache(Ue), Cache(Ŷe), Cache(U0), Cache(Ŷ0),
Cache(Û0), Cache(K0), Cache(X̂0),
Cache(Û0), Cache(K), Cache(X̂0),
Cache(gc), Cache(g), Cache(geq),
)
∇J_prep = prepare_gradient(Jfunc!, grad, Z̃_∇J, ∇J_context...; strict)
Expand All @@ -64,14 +64,14 @@ function get_optim_functions(mpc::NonLinMPC, ::JuMP.GenericModel{JNT}) where JNT
end
end
# --------------------- inequality constraint functions -------------------------------
function gfunc!(g, Z̃, ΔŨ, x̂0end, Ue, Ŷe, U0, Ŷ0, Û0, K0, X̂0, gc, geq)
update_predictions!(ΔŨ, x̂0end, Ue, Ŷe, U0, Ŷ0, Û0, K0, X̂0, gc, g, geq, mpc, Z̃)
function gfunc!(g, Z̃, ΔŨ, x̂0end, Ue, Ŷe, U0, Ŷ0, Û0, K, X̂0, gc, geq)
update_predictions!(ΔŨ, x̂0end, Ue, Ŷe, U0, Ŷ0, Û0, K, X̂0, gc, g, geq, mpc, Z̃)
return g
end
Z̃_∇g = fill(myNaN, nZ̃) # NaN to force update_predictions! at first call
∇g_context = (
Cache(ΔŨ), Cache(x̂0end), Cache(Ue), Cache(Ŷe), Cache(U0), Cache(Ŷ0),
Cache(Û0), Cache(K0), Cache(X̂0),
Cache(Û0), Cache(K), Cache(X̂0),
Cache(gc), Cache(geq),
)
# temporarily enable all the inequality constraints for sparsity detection:
Expand Down Expand Up @@ -109,14 +109,14 @@ function get_optim_functions(mpc::NonLinMPC, ::JuMP.GenericModel{JNT}) where JNT
∇g_funcs![i] = ∇gfuncs_i!
end
# --------------------- equality constraint functions ---------------------------------
function geqfunc!(geq, Z̃, ΔŨ, x̂0end, Ue, Ŷe, U0, Ŷ0, Û0, K0, X̂0, gc, g)
update_predictions!(ΔŨ, x̂0end, Ue, Ŷe, U0, Ŷ0, Û0, K0, X̂0, gc, g, geq, mpc, Z̃)
function geqfunc!(geq, Z̃, ΔŨ, x̂0end, Ue, Ŷe, U0, Ŷ0, Û0, K, X̂0, gc, g)
update_predictions!(ΔŨ, x̂0end, Ue, Ŷe, U0, Ŷ0, Û0, K, X̂0, gc, g, geq, mpc, Z̃)
return geq
end
Z̃_∇geq = fill(myNaN, nZ̃) # NaN to force update_predictions! at first call
∇geq_context = (
Cache(ΔŨ), Cache(x̂0end), Cache(Ue), Cache(Ŷe), Cache(U0), Cache(Ŷ0),
Cache(Û0), Cache(K0), Cache(X̂0),
Cache(Û0), Cache(K), Cache(X̂0),
Cache(gc), Cache(g)
)
∇geq_prep = prepare_jacobian(geqfunc!, geq, jac, Z̃_∇geq, ∇geq_context...; strict)
Expand Down
Loading