Skip to content

Commit f9f6924

Browse files
committed
NonLinMHE continuation
1 parent 9797d3e commit f9f6924

File tree

8 files changed

+192
-29
lines changed

8 files changed

+192
-29
lines changed

docs/make.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ DocMeta.setdocmeta!(
1414
)
1515
makedocs(
1616
sitename = "ModelPredictiveControl.jl",
17+
#format = Documenter.LaTeX(platform = "none"),
1718
doctest = true,
1819
format = Documenter.HTML(
1920
prettyurls = get(ENV, "CI", nothing) == "true",

docs/src/manual/installation.md

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,8 @@ To install the `ModelPredictiveControl` package, run this command in the Julia R
66
using Pkg; Pkg.add("ModelPredictiveControl")
77
```
88

9-
It will also automatically install all the dependencies, including [`JuMP.jl`](https://github.com/jump-dev/JuMP.jl)
10-
and [`ControlSystemsBase.jl`](https://github.com/JuliaControl/ControlSystems.jl). Note that
11-
that the construction of linear models typically requires `ss` or `tf` functions, it is thus
12-
recommended to load the package with:
9+
Note that that the construction of linear models typically requires `ss` or `tf` functions,
10+
it is thus recommended to load the package with:
1311

1412
```julia
1513
using ModelPredictiveControl, ControlSystemsBase

example/juMPC.jl

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,11 @@ updatestate!(luenberger, [0,0], [0,0])
5757

5858
mpcluen = LinMPC(luenberger)
5959

60+
mhe = NonLinMHE(nonLinModel2)
61+
62+
updatestate!(mhe, [0, 0], [0, 0], [1])
63+
64+
#=
6065
kalmanFilter1 = KalmanFilter(linModel1)
6166
kalmanFilter2 = KalmanFilter(linModel1,nint_ym=0)
6267
@@ -207,4 +212,5 @@ display(pd)
207212
display(pu)
208213
display(py)
209214
215+
=#
210216
=#

src/ModelPredictiveControl.jl

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,13 @@ import OSQP, Ipopt
1414
export SimModel, LinModel, NonLinModel, setop!, setstate!, updatestate!, evaloutput
1515
export StateEstimator, InternalModel, Luenberger
1616
export SteadyKalmanFilter, KalmanFilter, UnscentedKalmanFilter, ExtendedKalmanFilter
17+
export NonLinMHE
1718
export initstate!
1819
export PredictiveController, LinMPC, NonLinMPC, setconstraint!, moveinput!, getinfo, sim!
1920

21+
"Generate a block diagonal matrix repeating `n` times the matrix `A`."
22+
repeatdiag(A, n::Int) = kron(I(n), A)
23+
2024
include("sim_model.jl")
2125
include("state_estim.jl")
2226
include("predictive_control.jl")

src/controller/nonlinmpc.jl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -324,7 +324,7 @@ end
324324
"""
325325
obj_nonlinprog(mpc::NonLinMPC, model::LinModel, ΔŨ::Vector{Real})
326326
327-
Objective function for [`NonLinMPC`] when `model` is a [`LinModel`](@ref).
327+
Objective function for [`NonLinMPC`](@ref) when `model` is a [`LinModel`](@ref).
328328
"""
329329
function obj_nonlinprog(mpc::NonLinMPC, model::LinModel, Ŷ, ΔŨ::Vector{T}) where {T<:Real}
330330
J = obj_quadprog(ΔŨ, mpc.P̃, mpc.q̃)
@@ -341,7 +341,7 @@ end
341341
"""
342342
obj_nonlinprog(mpc::NonLinMPC, model::SimModel, ΔŨ::Vector{Real})
343343
344-
Objective function for [`NonLinMPC`] when `model` is not a [`LinModel`](@ref).
344+
Objective function for [`NonLinMPC`](@ref) when `model` is not a [`LinModel`](@ref).
345345
"""
346346
function obj_nonlinprog(mpc::NonLinMPC, model::SimModel, Ŷ, ΔŨ::Vector{T}) where {T<:Real}
347347
# --- output setpoint tracking term ---

src/estimator/mhe.jl

Lines changed: 172 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
struct NonLinMHE{M<:SimModel} <: StateEstimator
22
model::M
33
optim::JuMP.Model
4+
::Vector{Float64}
45
lastu0::Vector{Float64}
56
::Vector{Float64}
67
::Hermitian{Float64, Matrix{Float64}}
@@ -12,36 +13,51 @@ struct NonLinMHE{M<:SimModel} <: StateEstimator
1213
As::Matrix{Float64}
1314
Cs::Matrix{Float64}
1415
nint_ym::Vector{Int}
16+
He::Int
1517
P̂0::Hermitian{Float64, Matrix{Float64}}
16-
::Hermitian{Float64, Matrix{Float64}}
17-
::Hermitian{Float64, Matrix{Float64}}
18-
He::Int
19-
X̂max::Vector{Float64}
18+
Q̂_He::Hermitian{Float64, Matrix{Float64}}
19+
R̂_He::Hermitian{Float64, Matrix{Float64}}
2020
X̂min::Vector{Float64}
21-
function NonLinMHE{M}(model::M, i_ym, nint_ym, P̂0, Q̂, R̂, He) where {M<:SimModel}
22-
nu, nx, ny = model.nu, model.nx, model.ny
21+
X̂max::Vector{Float64}
22+
::Vector{Float64}
23+
Ym::Vector{Float64}
24+
U ::Vector{Float64}
25+
D ::Vector{Float64}
26+
::Vector{Float64}
27+
x̂0_past::Vector{Float64}
28+
function NonLinMHE{M}(model::M, i_ym, nint_ym, He, P̂0, Q̂, R̂, optim) where {M<:SimModel}
29+
nu, nd, nx, ny = model.nu, model.nd, model.nx, model.ny
30+
He < 1 && error("Estimation horizon He should be ≥ 1")
2331
nym, nyu = length(i_ym), ny - length(i_ym)
2432
Asm, Csm, nint_ym = init_estimstoch(i_ym, nint_ym)
2533
nxs = size(Asm,1)
2634
nx̂ = nx + nxs
2735
validate_kfcov(nym, nx̂, Q̂, R̂, P̂0)
2836
As, _ , Cs, _ = stoch_ym2y(model, i_ym, Asm, [], Csm, [])
29-
nσ, γ, m̂, Ŝ = init_mhe(nx̂, He)
3037
i_ym = collect(i_ym)
3138
lastu0 = zeros(nu)
3239
= [zeros(model.nx); zeros(nxs)]
3340
P̂0 = Hermitian(P̂0, :L)
34-
= Hermitian(, :L)
35-
= Hermitian(, :L)
41+
Q̂_He = Hermitian(repeatdiag(Q̂, He), :L)
42+
R̂_He = Hermitian(repeatdiag(R̂, He), :L)
3643
= copy(P̂0)
37-
return new(
38-
model,
44+
X̂min, X̂max = fill(-Inf, nx̂*He), fill(+Inf, nx̂*He)
45+
nvar = nx̂*(He + 1)
46+
= zeros(nvar)
47+
X̂, Ym, U, D, Ŵ = zeros(nx̂*He), zeros(nym*He), zeros(nu*He), zeros(nd*He), zeros(nx̂*He)
48+
x̂0_past = zeros(nx̂)
49+
estim = new(
50+
model, optim, W̃,
3951
lastu0, x̂, P̂,
4052
i_ym, nx̂, nym, nyu, nxs,
4153
As, Cs, nint_ym,
42-
P̂0, Q̂, R̂,
43-
nσ, γ, m̂, Ŝ
54+
He, P̂0, Q̂_He, R̂_He,
55+
X̂min, X̂max,
56+
X̂, Ym, U, D, Ŵ,
57+
x̂0_past
4458
)
59+
init_optimization!(estim)
60+
return estim
4561
end
4662
end
4763

@@ -68,19 +84,20 @@ julia> model = NonLinModel((x,u,_)->0.1x+u, (x,_)->2x, 10.0, 1, 1, 1);
6884
function NonLinMHE(
6985
model::M;
7086
i_ym::IntRangeOrVector = 1:model.ny,
87+
He::Int = 10,
7188
σP0::Vector = fill(1/model.nx, model.nx),
7289
σQ::Vector = fill(1/model.nx, model.nx),
7390
σR::Vector = fill(1, length(i_ym)),
7491
nint_ym::IntVectorOrInt = fill(1, length(i_ym)),
7592
σP0_int::Vector = fill(1, max(sum(nint_ym), 0)),
7693
σQ_int::Vector = fill(1, max(sum(nint_ym), 0)),
77-
He::Int = 10
94+
optim::JuMP.Model = JuMP.Model(optimizer_with_attributes(Ipopt.Optimizer,"sb"=>"yes"))
7895
) where {M<:SimModel}
7996
# estimated covariances matrices (variance = σ²) :
8097
P̂0 = Diagonal{Float64}([σP0 ; σP0_int ].^2);
8198
= Diagonal{Float64}([σQ ; σQ_int ].^2);
8299
= Diagonal{Float64}(σR.^2);
83-
return NonLinMHE{M}(model, i_ym, nint_ym, P̂0, Q̂ , R̂, He)
100+
return NonLinMHE{M}(model, i_ym, nint_ym, He, P̂0, Q̂, R̂, optim)
84101
end
85102

86103
@doc raw"""
@@ -90,4 +107,143 @@ Construct the estimator from the augmented covariance matrices `P̂0`, `Q̂` and
90107
91108
This syntax allows nonzero off-diagonal elements in ``\mathbf{P̂}_{-1}(0), \mathbf{Q̂, R̂}``.
92109
"""
93-
NonLinMHE{M}(model::M, i_ym, nint_ym, P̂0, Q̂, R̂, He) where {M<:SimModel}
110+
NonLinMHE{M}(model::M, i_ym, nint_ym, P̂0, Q̂, R̂, He) where {M<:SimModel}
111+
112+
@doc raw"""
113+
init_stochpred(As, Cs, He)
114+
115+
Construct stochastic model prediction matrices for nonlinear observers.
116+
117+
It allows separate simulations of the stochastic model (integrators). Stochastic model
118+
states and outputs are simulated using :
119+
```math
120+
\begin{aligned}
121+
\mathbf{X̂_s} &= \mathbf{M_s x̂_s}(k) + \mathbf{N_s Ŵ_s}
122+
\mathbf{Ŷ_s} &= \mathbf{P_s X̂_s}
123+
\end{aligned}
124+
```
125+
where ``\mathbf{X̂_s}`` and ``\mathbf{Ŷ_s}`` are integrator states and outputs from ``k + 1``
126+
to ``k + H_e`` inclusively, and ``\mathbf{Ŵ_s}`` are integrator process noises from ``k`` to
127+
``k + H_e - 1``. Stochastic simulations are combined with ``\mathbf{f, h}`` results to
128+
simulate the augmented model:
129+
```math
130+
\begin{aligned}
131+
\mathbf{X̂} &= \mathbf{M_s x̂_s}(k) + \mathbf{N_s Ŵ_s}
132+
\mathbf{Ŷ_s} &= \mathbf{P_s X̂_s}
133+
\end{aligned}
134+
```
135+
Xhat = [XhatD; XhatS]
136+
Yhat = YhatD + YhatS
137+
where XhatD and YhatD are SimulFunc states and outputs from ``k + 1`` to ``k + H_e``.
138+
"""
139+
function init_stochpred(As, Cs, He)
140+
nxs = size(As,1);
141+
Ms = zeros(He*nxs, nxs);
142+
for i = 1:Ho
143+
iRow = (1:nxs) + nxs*(i-1);
144+
Ms[iRow, :] = As^i;
145+
end
146+
Ns = zeros(Ho*nxs, He*nxs);
147+
for i = 1:Ho
148+
iCol = (1:nxs) + nxs*(i-1);
149+
Ns[nxs*(i-1)+1:end, iCol] = [eye(nxs); Ms(1:nxs*(Ho-i),:)];
150+
end
151+
Ps = kron(eye(Ho),Cs);
152+
return (Ms, Ns, Ps)
153+
end
154+
155+
156+
157+
"""
158+
init_optimization!(estim::NonLinMHE)
159+
160+
Init the nonlinear optimization for [`NonLinMHE`](@ref).
161+
"""
162+
function init_optimization!(estim::NonLinMHE)
163+
# --- variables and linear constraints ---
164+
optim = estim.optim
165+
nvar = length(estim.W̃)
166+
set_silent(optim)
167+
@variable(optim, W̃var[1:nvar])
168+
W̃var = optim[:W̃var]
169+
# --- nonlinear optimization init ---
170+
model = estim.model
171+
nym, nx̂, He = estim.nym, estim.nx̂, estim.He
172+
# inspired from https://jump.dev/JuMP.jl/stable/tutorials/nonlinear/tips_and_tricks/#User-defined-functions-with-vector-outputs
173+
Jfunc = let estim=estim, model=model, nvar=nvar , nŶm=He*nym, nX̂=He*nx̂
174+
last_W̃tup_float, last_W̃tup_dual = nothing, nothing
175+
Ŷm_cache::DiffCacheType = DiffCache(zeros(nŶm), nvar + 3)
176+
X̂_cache ::DiffCacheType = DiffCache(zeros(nX̂) , nvar + 3)
177+
function Jfunc(W̃tup::Float64...)
178+
Ŷm, X̂ = get_tmp(Ŷm_cache, W̃tup[1]), get_tmp(X̂_cache, W̃tup[1])
179+
= collect(W̃tup)
180+
if W̃tup != last_W̃tup_float
181+
Ŷm[:], X̂[:] = predict(estim, model, W̃)
182+
last_W̃tup_float = W̃tup
183+
end
184+
return obj_nonlinprog(estim, model, Ŷm, W̃)
185+
end
186+
function Jfunc(W̃tup::Real...)
187+
Ŷm, X̂ = get_tmp(Ŷm_cache, W̃tup[1]), get_tmp(X̂_cache, W̃tup[1])
188+
= collect(W̃tup)
189+
if W̃tup != last_W̃tup_dual
190+
Ŷm[:], X̂[:] = predict(estim, model, W̃)
191+
last_W̃tup_dual = W̃tup
192+
end
193+
return obj_nonlinprog(estim, model, Ŷm, W̃)
194+
end
195+
Jfunc
196+
end
197+
register(optim, :Jfunc, nvar, Jfunc, autodiff=true)
198+
@NLobjective(optim, Min, Jfunc(W̃var...))
199+
return nothing
200+
end
201+
202+
"""
203+
obj_nonlinprog(estim::NonLinMHE, model::SimModel, ΔŨ::Vector{Real})
204+
205+
Objective function for [`NonLinMHE`] when `model` is not a [`LinModel`](@ref).
206+
"""
207+
function obj_nonlinprog(estim::NonLinMHE, ::SimModel, Ŷm, W̃::Vector{T}) where {T<:Real}
208+
# `@views` macro avoid copies with matrix slice operator e.g. [a:b]
209+
@views x̄0 = W̃[1:estim.nx̂] - estim.x̂0_past
210+
= estim.win_Ym - Ŷm
211+
@views= W̃[estim.nx̂+1:end]
212+
return x̄0'/estim.P̂0*x̄0 +'/Q̂_He*+'/R̂_He*
213+
end
214+
215+
function predict(estim::NonLinMHE, model::SimModel, W̃::Vector{T}) where {T<:Real}
216+
nu, nd, nym, nx̂, He = model.nu, model.nd, estim.nym, estim.nx̂, estim.He
217+
Ŷm::Vector{T} = Vector{T}(undef, nym*(He+1))
218+
::Vector{T} = Vector{T}(undef, nx̂*(He+1))
219+
::Vector{T} = @views W̃[nx̂+1:end]
220+
u ::Vector{T} = Vector{T}(undef, nu)
221+
d ::Vector{T} = Vector{T}(undef, nu)
222+
::Vector{T} = Vector{T}(undef, nx̂)
223+
::Vector{T} = W̃[1:nx̂]
224+
for j=1:He
225+
u[:] = @views estim.U[(1 + nu*(j-1)):(nu*j)]
226+
d[:] = @views estim.D[(1 + nd*(j-1)):(nd*j)]
227+
ŵ[:] = @views Ŵ[(1 + nx̂*(j-1)):(nx̂*j)]
228+
X̂[(1 + nx̂*(j-1)):(nx̂*j)] =
229+
Ŷm[(1 + ny*(j-1)):(ny*j)] = @views (estim, x̂, d)[estim.i_ym]
230+
x̂[:] = (estim, x̂, u, d) +
231+
end
232+
X̂[end-nx̂+1:end] =
233+
Ŷm[end-nx̂+1:end] = @views (estim, x̂, estin.D[end-nd+1:end])[estim.i_ym]
234+
return Ŷm, X̂
235+
end
236+
237+
function update_estimate!(estim::NonLinMHE, u, ym, d)
238+
model, x̂ = estim.model, estim.
239+
nx̂, nym, nu, nd, nŵ = estim.nx̂, estim.nym, model.nu, model.nd, estim.nx̂
240+
= zeros(nŵ) # ŵ(k) = 0 for warm-starting
241+
# --- adding new data in time windows ---
242+
estim.X̂[:] = @views [estim.X̂[nx̂+1:end] ; x̂]
243+
estim.Ym[:] = @views [estim.Ym[nym+1:end]; ym]
244+
estim.U[:] = @views [estim.U[nu+1:end] ; u]
245+
estim.D[:] = @views [estim.D[nd+1:end] ; d]
246+
estim.Ŵ[:] = @views [estim.Ŵ[nŵ+1:end] ; ŵ]
247+
248+
#estim.x̂0_past[:] =
249+
end

src/predictive_control.jl

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -449,19 +449,19 @@ function initpred!(mpc::PredictiveController, model::SimModel, d, D̂, R̂y)
449449
return nothing
450450
end
451451

452-
"""
452+
@doc raw"""
453453
predict(mpc::PredictiveController, model::LinModel, ΔŨ)
454454
455-
Evaluate the outputs predictions ``\\mathbf{Ŷ}`` when `model` is a [`LinModel`](@ref).
455+
Evaluate the outputs predictions ``\mathbf{Ŷ}`` when `model` is a [`LinModel`](@ref).
456456
"""
457457
function predict(mpc::PredictiveController, ::LinModel, ΔŨ::Vector{T}) where {T<:Real}
458458
return mpc.*ΔŨ + mpc.F
459459
end
460460

461-
"""
461+
@doc raw"""
462462
predict(mpc::PredictiveController, model::SimModel, ΔŨ)
463463
464-
Evaluate ``\\mathbf{Ŷ}`` when `model` is not a [`LinModel`](@ref).
464+
Evaluate ``\mathbf{Ŷ}`` when `model` is not a [`LinModel`](@ref).
465465
"""
466466
function predict(mpc::PredictiveController, model::SimModel, ΔŨ::Vector{T}) where {T<:Real}
467467
nu, ny, nd, Hp = model.nu, model.ny, model.nd, mpc.Hp
@@ -1000,9 +1000,6 @@ function validate_weights(model, Hp, Hc, Mwt, Nwt, Lwt, Cwt, ru, Ewt=nothing)
10001000
!isnothing(Ewt) && size(Ewt) () && error("Ewt should be a real scalar")
10011001
end
10021002

1003-
"Generate a block diagonal matrix repeating `n` times the matrix `A`."
1004-
repeatdiag(A, n::Int) = kron(I(n), A)
1005-
10061003
function Base.show(io::IO, mpc::PredictiveController)
10071004
Hp, Hc = mpc.Hp, mpc.Hc
10081005
nu, nd = mpc.estim.model.nu, mpc.estim.model.nd

src/state_estim.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,7 @@ end
303303
include("estimator/kalman.jl")
304304
include("estimator/luenberger.jl")
305305
include("estimator/internal_model.jl")
306+
include("estimator/mhe.jl")
306307

307308
"Get [`InternalModel`](@ref) output `ŷ` from current measured outputs `ym` and dist. `d`."
308309
evalŷ(estim::InternalModel, ym, d) = evaloutput(estim,ym, d)

0 commit comments

Comments
 (0)