Skip to content

Commit d8f9b5c

Browse files
authored
Merge pull request #19 from franckgaga/MHE2
Added: `MovingHorizonEstimator` with state constraints
2 parents f78a98b + a262420 commit d8f9b5c

File tree

14 files changed

+634
-69
lines changed

14 files changed

+634
-69
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,6 @@ for more detailed examples.
121121
- [x] measured outputs
122122
- [x] bumpless manual to automatic transfer for control with a proper intial estimate
123123
- [x] observers in predictor form to ease control applications
124-
- [ ] moving horizon estimator that supports:
125-
- [ ] inequality state constraints
124+
- [x] moving horizon estimator that supports:
125+
- [x] inequality state constraints
126126
- [ ] zero process noise equality constraint to reduce the problem size

docs/src/public/generic_func.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,12 @@ initstate!
3131
setstate!
3232
```
3333

34+
## Set Constraint
35+
36+
```@docs
37+
setconstraint!
38+
```
39+
3440
## Quick Simulation
3541

3642
### Simulate

docs/src/public/predictive_control.md

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -78,12 +78,6 @@ ExplicitMPC
7878
NonLinMPC
7979
```
8080

81-
## Set Constraint
82-
83-
```@docs
84-
setconstraint!
85-
```
86-
8781
## Move Manipulated Input u
8882

8983
```@docs

docs/src/public/state_estim.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,12 @@ UnscentedKalmanFilter
7171
ExtendedKalmanFilter
7272
```
7373

74+
## MovingHorizonEstimator
75+
76+
```@docs
77+
MovingHorizonEstimator
78+
```
79+
7480
## InternalModel
7581

7682
```@docs

src/ModelPredictiveControl.jl

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,18 @@ export SimModel, LinModel, NonLinModel
1515
export setop!, setstate!, updatestate!, evaloutput, linearize
1616
export StateEstimator, InternalModel, Luenberger
1717
export SteadyKalmanFilter, KalmanFilter, UnscentedKalmanFilter, ExtendedKalmanFilter
18+
export MovingHorizonEstimator
1819
export default_nint, initstate!
1920
export PredictiveController, ExplicitMPC, LinMPC, NonLinMPC, setconstraint!, moveinput!
2021
export SimResult, getinfo, sim!
2122

23+
"Termination status that means 'no solution available'."
24+
const FATAL_STATUSES = [
25+
INFEASIBLE, DUAL_INFEASIBLE, LOCALLY_INFEASIBLE, INFEASIBLE_OR_UNBOUNDED,
26+
NUMERICAL_ERROR, INVALID_MODEL, INVALID_OPTION, INTERRUPTED,
27+
OTHER_ERROR
28+
]
29+
2230
"Generate a block diagonal matrix repeating `n` times the matrix `A`."
2331
repeatdiag(A, n::Int) = kron(I(n), A)
2432

src/controller/explicitmpc.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ state estimator, a [`SteadyKalmanFilter`](@ref) with default arguments.
9898
- `Lwt=fill(0.0,model.nu)` : main diagonal of ``\mathbf{L}`` weight matrix (vector).
9999
- `M_Hp` / `N_Hc` / `L_Hp` : diagonal matrices ``\mathbf{M}_{H_p}, \mathbf{N}_{H_c},
100100
\mathbf{L}_{H_p}``, for time-varying weights (generated from `Mwt/Nwt/Lwt` args if omitted).
101-
- additionnal keyword arguments are passed to [`SteadyKalmanFilter`](@ref) constructor.
101+
- additional keyword arguments are passed to [`SteadyKalmanFilter`](@ref) constructor.
102102
103103
# Examples
104104
```jldoctest

src/controller/nonlinmpc.jl

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ This method uses the default state estimator :
134134
- `optim=JuMP.Model(Ipopt.Optimizer)` : nonlinear optimizer used in the predictive
135135
controller, provided as a [`JuMP.Model`](https://jump.dev/JuMP.jl/stable/api/JuMP/#JuMP.Model)
136136
(default to [`Ipopt.jl`](https://github.com/jump-dev/Ipopt.jl) optimizer).
137-
- additionnal keyword arguments are passed to [`UnscentedKalmanFilter`](@ref) constructor
137+
- additional keyword arguments are passed to [`UnscentedKalmanFilter`](@ref) constructor
138138
(or [`SteadyKalmanFilter`](@ref), for [`LinModel`](@ref)).
139139
140140
# Examples
@@ -275,7 +275,6 @@ function init_optimization!(mpc::NonLinMPC, optim::JuMP.GenericModel{JNT}) where
275275
set_silent(optim)
276276
limit_solve_time(mpc)
277277
@variable(optim, ΔŨvar[1:nvar])
278-
ΔŨvar = optim[:ΔŨvar]
279278
A = con.A[con.i_b, :]
280279
b = con.b[con.i_b]
281280
@constraint(optim, linconstraint, A*ΔŨvar .≤ b)
@@ -295,7 +294,7 @@ function init_optimization!(mpc::NonLinMPC, optim::JuMP.GenericModel{JNT}) where
295294
= get_tmp(x̂_cache, ΔŨtup[1])
296295
g = get_tmp(g_cache, ΔŨtup[1])
297296
Ŷ, x̂end = predict!(Ŷ, x̂, mpc, model, ΔŨ)
298-
con_nonlinprog!(g, mpc, model, x̂end, Ŷ, ΔŨ)
297+
g = con_nonlinprog!(g, mpc, model, x̂end, Ŷ, ΔŨ)
299298
last_ΔŨtup_float = ΔŨtup
300299
end
301300
return obj_nonlinprog(mpc, model, Ŷ, ΔŨ)
@@ -307,7 +306,7 @@ function init_optimization!(mpc::NonLinMPC, optim::JuMP.GenericModel{JNT}) where
307306
= get_tmp(x̂_cache, ΔŨtup[1])
308307
g = get_tmp(g_cache, ΔŨtup[1])
309308
Ŷ, x̂end = predict!(Ŷ, x̂, mpc, model, ΔŨ)
310-
con_nonlinprog!(g, mpc, model, x̂end, Ŷ, ΔŨ)
309+
g = con_nonlinprog!(g, mpc, model, x̂end, Ŷ, ΔŨ)
311310
last_ΔŨtup_dual = ΔŨtup
312311
end
313312
return obj_nonlinprog(mpc, model, Ŷ, ΔŨ)
@@ -366,7 +365,7 @@ end
366365
"Set the nonlinear constraints on the output predictions `Ŷ` and terminal states `x̂end`."
367366
function setnonlincon!(mpc::NonLinMPC, ::NonLinModel)
368367
optim = mpc.optim
369-
ΔŨvar = mpc.optim[:ΔŨvar]
368+
ΔŨvar = optim[:ΔŨvar]
370369
con = mpc.con
371370
map(con -> delete(optim, con), all_nonlinear_constraints(optim))
372371
for i in findall(.!isinf.(con.Ymin))

src/estimator/internal_model.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,7 @@ function update_estimate!(
229229
ŷs = zeros(NT, model.ny)
230230
ŷs[estim.i_ym] = ym - ŷd[estim.i_ym] # ŷs=0 for unmeasured outputs
231231
x̂s[:] = estim.Âs*x̂s + estim.B̂s*ŷs
232-
return x̂d
232+
return nothing
233233
end
234234

235235
@doc raw"""

src/estimator/kalman.jl

Lines changed: 49 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -148,8 +148,8 @@ function SteadyKalmanFilter(
148148
σQint_ym::Vector = fill(1, max(sum(nint_ym), 0))
149149
) where {NT<:Real, SM<:LinModel{NT}}
150150
# estimated covariances matrices (variance = σ²) :
151-
= Diagonal{NT}([σQ; σQint_u; σQint_ym].^2);
152-
= Diagonal{NT}(σR.^2);
151+
= Diagonal{NT}([σQ; σQint_u; σQint_ym].^2)
152+
= Diagonal{NT}(σR.^2)
153153
return SteadyKalmanFilter{NT, SM}(model, i_ym, nint_u, nint_ym, Q̂ , R̂)
154154
end
155155

@@ -181,7 +181,7 @@ function update_estimate!(estim::SteadyKalmanFilter, u, ym, d=empty(estim.x̂))
181181
Â, B̂u, B̂d, Ĉm, D̂dm = estim.Â, estim.B̂u, estim.B̂d, estim.Ĉm, estim.D̂dm
182182
x̂, K̂ = estim.x̂, estim.
183183
x̂[:] =*+ B̂u*u + B̂d*d +*(ym - Ĉm*- D̂dm*d)
184-
return
184+
return nothing
185185
end
186186

187187
struct KalmanFilter{NT<:Real, SM<:LinModel} <: StateEstimator{NT}
@@ -253,13 +253,13 @@ its initial value with ``\mathbf{P̂}_{-1}(0) =
253253
254254
# Arguments
255255
- `model::LinModel` : (deterministic) model for the estimations.
256-
- `σP0=fill(1/model.nx,model.nx)` : main diagonal of the initial estimate covariance
257-
``\mathbf{P}(0)``, specified as a standard deviation vector.
258-
- `σP0int_u=fill(1,sum(nint_u))` : same than `σP0` but for the unmeasured disturbances at
259-
manipulated inputs ``\mathbf{P_{int_u}}(0)`` (composed of integrators).
260-
- `σP0int_ym=fill(1,sum(nint_ym))` : same than `σP0` but for the unmeasured disturbances at
261-
measured outputs ``\mathbf{P_{int_{ym}}}(0)`` (composed of integrators).
262256
- `<keyword arguments>` of [`SteadyKalmanFilter`](@ref) constructor.
257+
- `σP0=σQ` : main diagonal of the initial estimate covariance ``\mathbf{P}(0)``, specified
258+
as a standard deviation vector.
259+
- `σP0int_u=σQint_u` : same than `σP0` but for the unmeasured disturbances at manipulated
260+
inputs ``\mathbf{P_{int_u}}(0)`` (composed of integrators).
261+
- `σP0int_ym=σQint_ym` : same than `σP0` but for the unmeasured disturbances at measured
262+
outputs ``\mathbf{P_{int_{ym}}}(0)`` (composed of integrators).
263263
264264
# Examples
265265
```jldoctest
@@ -277,20 +277,20 @@ KalmanFilter estimator with a sample time Ts = 0.5 s, LinModel and:
277277
function KalmanFilter(
278278
model::SM;
279279
i_ym::IntRangeOrVector = 1:model.ny,
280-
σP0::Vector = fill(1/model.nx, model.nx),
281-
σQ ::Vector = fill(1/model.nx, model.nx),
282-
σR ::Vector = fill(1, length(i_ym)),
280+
σQ ::Vector = fill(1/model.nx, model.nx),
281+
σR ::Vector = fill(1, length(i_ym)),
282+
σP0::Vector = σQ,
283283
nint_u ::IntVectorOrInt = 0,
284284
σQint_u ::Vector = fill(1, max(sum(nint_u), 0)),
285-
σP0int_u ::Vector = fill(1, max(sum(nint_u), 0)),
285+
σP0int_u ::Vector = σQint_u,
286286
nint_ym ::IntVectorOrInt = default_nint(model, i_ym, nint_u),
287287
σQint_ym ::Vector = fill(1, max(sum(nint_ym), 0)),
288-
σP0int_ym::Vector = fill(1, max(sum(nint_ym), 0))
288+
σP0int_ym::Vector = σQint_ym,
289289
) where {NT<:Real, SM<:LinModel{NT}}
290290
# estimated covariances matrices (variance = σ²) :
291-
P̂0 = Diagonal{NT}([σP0; σP0int_u; σP0int_ym].^2);
292-
= Diagonal{NT}([σQ; σQint_u; σQint_ym].^2);
293-
= Diagonal{NT}(σR.^2);
291+
P̂0 = Diagonal{NT}([σP0; σP0int_u; σP0int_ym].^2)
292+
= Diagonal{NT}([σQ; σQint_u; σQint_ym].^2)
293+
= Diagonal{NT}(σR.^2)
294294
return KalmanFilter{NT, SM}(model, i_ym, nint_u, nint_ym, P̂0, Q̂ , R̂)
295295
end
296296

@@ -443,23 +443,23 @@ responsibility to ensure that the augmented model is still observable.
443443
function UnscentedKalmanFilter(
444444
model::SM;
445445
i_ym::IntRangeOrVector = 1:model.ny,
446-
σP0::Vector = fill(1/model.nx, model.nx),
447-
σQ::Vector = fill(1/model.nx, model.nx),
448-
σR::Vector = fill(1, length(i_ym)),
446+
σQ ::Vector = fill(1/model.nx, model.nx),
447+
σR ::Vector = fill(1, length(i_ym)),
448+
σP0::Vector = σQ,
449449
nint_u ::IntVectorOrInt = 0,
450450
σQint_u ::Vector = fill(1, max(sum(nint_u), 0)),
451-
σP0int_u ::Vector = fill(1, max(sum(nint_u), 0)),
451+
σP0int_u ::Vector = σQint_u,
452452
nint_ym ::IntVectorOrInt = default_nint(model, i_ym, nint_u),
453453
σQint_ym ::Vector = fill(1, max(sum(nint_ym), 0)),
454-
σP0int_ym::Vector = fill(1, max(sum(nint_ym), 0)),
454+
σP0int_ym::Vector = σQint_ym,
455455
α::Real = 1e-3,
456456
β::Real = 2,
457457
κ::Real = 0
458458
) where {NT<:Real, SM<:SimModel{NT}}
459459
# estimated covariances matrices (variance = σ²) :
460-
P̂0 = Diagonal{NT}([σP0; σP0int_u; σP0int_ym].^2);
461-
= Diagonal{NT}([σQ; σQint_u; σQint_ym].^2);
462-
= Diagonal{NT}(σR.^2);
460+
P̂0 = Diagonal{NT}([σP0; σP0int_u; σP0int_ym].^2)
461+
= Diagonal{NT}([σQ; σQint_u; σQint_ym].^2)
462+
= Diagonal{NT}(σR.^2)
463463
return UnscentedKalmanFilter{NT, SM}(model, i_ym, nint_u, nint_ym, P̂0, Q̂, R̂, α, β, κ)
464464
end
465465

@@ -578,7 +578,7 @@ function update_estimate!(estim::UnscentedKalmanFilter{NT}, u, ym, d) where NT<:
578578
x̂[:] = X̂_next *
579579
X̄_next = X̂_next .-
580580
.data[:] = X̄_next ** X̄_next' +# .data is necessary for Hermitians
581-
return x̂, P̂
581+
return nothing
582582
end
583583

584584
struct ExtendedKalmanFilter{NT<:Real, SM<:SimModel} <: StateEstimator{NT}
@@ -669,20 +669,20 @@ ExtendedKalmanFilter estimator with a sample time Ts = 5.0 s, NonLinModel and:
669669
function ExtendedKalmanFilter(
670670
model::SM;
671671
i_ym::IntRangeOrVector = 1:model.ny,
672-
σP0::Vector = fill(1/model.nx, model.nx),
673-
σQ::Vector = fill(1/model.nx, model.nx),
674-
σR::Vector = fill(1, length(i_ym)),
672+
σQ ::Vector = fill(1/model.nx, model.nx),
673+
σR ::Vector = fill(1, length(i_ym)),
674+
σP0::Vector = σQ,
675675
nint_u ::IntVectorOrInt = 0,
676676
σQint_u ::Vector = fill(1, max(sum(nint_u), 0)),
677-
σP0int_u ::Vector = fill(1, max(sum(nint_u), 0)),
677+
σP0int_u ::Vector = σQint_u,
678678
nint_ym ::IntVectorOrInt = default_nint(model, i_ym, nint_u),
679679
σQint_ym ::Vector = fill(1, max(sum(nint_ym), 0)),
680-
σP0int_ym::Vector = fill(1, max(sum(nint_ym), 0))
680+
σP0int_ym::Vector = σQint_ym,
681681
) where {NT<:Real, SM<:SimModel{NT}}
682682
# estimated covariances matrices (variance = σ²) :
683-
P̂0 = Diagonal{NT}([σP0; σP0int_u; σP0int_ym].^2);
684-
= Diagonal{NT}([σQ; σQint_u; σQint_ym].^2);
685-
= Diagonal{NT}(σR.^2);
683+
P̂0 = Diagonal{NT}([σP0; σP0int_u; σP0int_ym].^2)
684+
= Diagonal{NT}([σQ; σQint_u; σQint_ym].^2)
685+
= Diagonal{NT}(σR.^2)
686686
return ExtendedKalmanFilter{NT, SM}(model, i_ym, nint_u, nint_ym, P̂0, Q̂ , R̂)
687687
end
688688

@@ -746,36 +746,42 @@ end
746746
"""
747747
validate_kfcov(nym, nx̂, Q̂, R̂, P̂0=nothing)
748748
749-
Validate sizes of process `Q̂`` and sensor `R̂` noises covariance matrices.
749+
Validate sizes and Hermitianity of process `Q̂`` and sensor `R̂` noises covariance matrices.
750750
751-
Also validate initial estimate covariance size, if provided.
751+
Also validate initial estimate covariance `P̂0`, if provided.
752752
"""
753753
function validate_kfcov(nym, nx̂, Q̂, R̂, P̂0=nothing)
754754
size(Q̂) (nx̂, nx̂) && error("Q̂ size $(size(Q̂)) ≠ nx̂, nx̂ $((nx̂, nx̂))")
755+
!ishermitian(Q̂) && error("Q̂ is not Hermitian")
755756
size(R̂) (nym, nym) && error("R̂ size $(size(R̂)) ≠ nym, nym $((nym, nym))")
757+
!ishermitian(R̂) && error("R̂ is not Hermitian")
756758
if ~isnothing(P̂0)
757759
size(P̂0) (nx̂, nx̂) && error("P̂0 size $(size(P̂0)) ≠ nx̂, nx̂ $((nx̂, nx̂))")
760+
!ishermitian(P̂0) && error("P̂0 is not Hermitian")
758761
end
759762
end
760763

761764
"""
762-
update_estimate_kf!(estim, Â, Ĉm, u, ym, d) -> x̂, P̂
765+
update_estimate_kf!(estim, Â, Ĉm, u, ym, d; updatestate=true)
763766
764767
Update time-varying/extended Kalman Filter estimates with augmented `Â` and `Ĉm` matrices.
765768
766769
Allows code reuse for [`KalmanFilter`](@ref) and [`ExtendedKalmanFilterKalmanFilter`](@ref).
767770
They update the state `x̂` and covariance `P̂` with the same equations. The extended filter
768771
substitutes the augmented model matrices with its Jacobians (`Â = F̂` and `Ĉm = Ĥm`).
769772
The implementation uses in-place operations and explicit factorization to reduce
770-
allocations. See e.g. [`KalmanFilter`](@ref) docstring for the equations.
773+
allocations. See e.g. [`KalmanFilter`](@ref) docstring for the equations. If `updatestate`
774+
is `false`, only the covariance `P̂` is updated.
771775
"""
772-
function update_estimate_kf!(estim, Â, Ĉm, u, ym, d)
776+
function update_estimate_kf!(estim, Â, Ĉm, u, ym, d; updatestate=true)
773777
x̂, P̂, Q̂, R̂, K̂, M̂ = estim.x̂, estim.P̂, estim.Q̂, estim.R̂, estim.K̂, estim.
774778
mul!(M̂, P̂, Ĉm')
775779
rdiv!(M̂, cholesky!(Hermitian(Ĉm ** Ĉm' + R̂)))
776-
mul!(K̂, Â, M̂)
777-
ŷm = @views (estim, estim.model, x̂, d)[estim.i_ym]
778-
x̂[:] = (estim, estim.model, x̂, u, d) +* (ym - ŷm)
780+
if updatestate
781+
mul!(K̂, Â, M̂)
782+
ŷm = @views (estim, estim.model, x̂, d)[estim.i_ym]
783+
x̂[:] = (estim, estim.model, x̂, u, d) +* (ym - ŷm)
784+
end
779785
.data[:] =* (P̂ -* Ĉm * P̂) *' +# .data is necessary for Hermitians
780-
return x̂, P̂
786+
return nothing
781787
end

src/estimator/luenberger.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,5 +109,5 @@ function update_estimate!(estim::Luenberger, u, ym, d=empty(estim.x̂))
109109
Â, B̂u, B̂d, Ĉm, D̂dm = estim.Â, estim.B̂u, estim.B̂d, estim.Ĉm, estim.D̂dm
110110
x̂, K̂ = estim.x̂, estim.
111111
x̂[:] =*+ B̂u*u + B̂d*d +*(ym - Ĉm*- D̂dm*d)
112-
return
112+
return nothing
113113
end

0 commit comments

Comments
 (0)