Skip to content

Commit 1313f07

Browse files
authored
Merge pull request #374 from JuliaControl/covestim_mhe_const
added: `covestim` arg. in both `MovingHorizonEstimator` constructors + honor `P̂_0`
2 parents f8de776 + d43ab01 commit 1313f07

3 files changed

Lines changed: 74 additions & 40 deletions

File tree

Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name = "ModelPredictiveControl"
22
uuid = "61f9bdb8-6ae4-484a-811f-bbf86720c31c"
3-
version = "2.4.2"
3+
version = "2.4.3"
44
authors = ["Francis Gagnon"]
55

66
[deps]

src/estimator/mhe/construct.jl

Lines changed: 58 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -278,25 +278,25 @@ transcription for now.
278278
- `model::SimModel` : (deterministic) model for the estimations.
279279
- `He=nothing` : estimation horizon ``H_e``, must be specified.
280280
- `i_ym=1:model.ny` : `model` output indices that are measured ``\mathbf{y^m}``, the rest
281-
are unmeasured ``\mathbf{y^u}``.
281+
are unmeasured ``\mathbf{y^u}``.
282282
- `σP_0=fill(1/model.nx,model.nx)` or *`sigmaP_0`* : main diagonal of the initial estimate
283-
covariance ``\mathbf{P}(0)``, specified as a standard deviation vector.
283+
covariance ``\mathbf{P}(0)``, specified as a standard deviation vector.
284284
- `σQ=fill(1/model.nx,model.nx)` or *`sigmaQ`* : main diagonal of the process noise
285-
covariance ``\mathbf{Q}`` of `model`, specified as a standard deviation vector.
285+
covariance ``\mathbf{Q}`` of `model`, specified as a standard deviation vector.
286286
- `σR=fill(1,length(i_ym))` or *`sigmaR`* : main diagonal of the sensor noise covariance
287-
``\mathbf{R}`` of `model` measured outputs, specified as a standard deviation vector.
287+
``\mathbf{R}`` of `model` measured outputs, specified as a standard deviation vector.
288288
- `nint_u=0`: integrator quantity for the stochastic model of the unmeasured disturbances at
289-
the manipulated inputs (vector), use `nint_u=0` for no integrator (see Extended Help).
289+
the manipulated inputs (vector), use `nint_u=0` for no integrator (see Extended Help).
290290
- `nint_ym=default_nint(model,i_ym,nint_u)` : same than `nint_u` but for the unmeasured
291-
disturbances at the measured outputs, use `nint_ym=0` for no integrator (see Extended Help).
291+
disturbances at the measured outputs, use `nint_ym=0` for no integrator (see Extended Help).
292292
- `σQint_u=fill(1,sum(nint_u))` or *`sigmaQint_u`* : same than `σQ` but for the unmeasured
293-
disturbances at manipulated inputs ``\mathbf{Q_{int_u}}`` (composed of integrators).
293+
disturbances at manipulated inputs ``\mathbf{Q_{int_u}}`` (composed of integrators).
294294
- `σPint_u_0=fill(1,sum(nint_u))` or *`sigmaPint_u_0`* : same than `σP_0` but for the unmeasured
295-
disturbances at manipulated inputs ``\mathbf{P_{int_u}}(0)`` (composed of integrators).
295+
disturbances at manipulated inputs ``\mathbf{P_{int_u}}(0)`` (composed of integrators).
296296
- `σQint_ym=fill(1,sum(nint_ym))` or *`sigmaQint_u`* : same than `σQ` for the unmeasured
297-
disturbances at measured outputs ``\mathbf{Q_{int_{ym}}}`` (composed of integrators).
297+
disturbances at measured outputs ``\mathbf{Q_{int_{ym}}}`` (composed of integrators).
298298
- `σPint_ym_0=fill(1,sum(nint_ym))` or *`sigmaPint_ym_0`* : same than `σP_0` but for the unmeasured
299-
disturbances at measured outputs ``\mathbf{P_{int_{ym}}}(0)`` (composed of integrators).
299+
disturbances at measured outputs ``\mathbf{P_{int_{ym}}}(0)`` (composed of integrators).
300300
- `Cwt=Inf` : slack variable weight ``C``, default to `Inf` meaning hard constraints only.
301301
- `gc=(_,_,_,_,_,_,_,_,_,_,_)->nothing` or `gc!` : custom nonlinear inequality constraint function
302302
``\mathbf{g_c}(\mathbf{X̂_e, V̂_e, Ŵ_e, U_e, Y_e^m, D_e, P̄, x̄, p}, ε)``, mutating or not
@@ -313,6 +313,8 @@ transcription for now.
313313
- `hessian=false` : an `AbstractADType` backend for the Hessian of the Lagrangian, see
314314
`gradient` above for the options. The default `false` skip it and use the quasi-Newton
315315
method of `optim` (see Extended Help).
316+
- `covestim=nothing`: a [`StateEstimator`](@ref) object for the arrival covariance estimation
317+
``\mathbf{P̂}_{k-N_k}(k-N_k+p)``, `nothing` means the default choice (see Extended Help).
316318
- `direct=true`: construct with a direct transmission from ``\mathbf{y^m}`` (a.k.a. current
317319
estimator, in opposition to the delayed/predictor form).
318320
@@ -449,7 +451,7 @@ MovingHorizonEstimator estimator with a sample time Ts = 10.0 s:
449451
[`JuMP` documentation](@extref JuMP Common-mistakes-when-writing-a-user-defined-operator)
450452
for common mistakes when writing these functions. Also, an [`UnscentedKalmanFilter`](@ref)
451453
estimates the arrival covariance by default.
452-
454+
453455
One exception about AD: the selected backend for the Hessian of the Lagrangian function
454456
with `hessian=true` options is sparse:
455457
```julia
@@ -469,7 +471,15 @@ MovingHorizonEstimator estimator with a sample time Ts = 10.0 s:
469471
)
470472
)
471473
```
472-
that is, it will test many coloring orders at preparation and keep the best.
474+
that is, it will test many coloring orders at preparation and keep the best. The
475+
argument `covestim` customizes the arrival covariance estimator. The supported types are
476+
[`SteadyKalmanFilter`](@ref), [`KalmanFilter`](@ref), [`UnscentedKalmanFilter`](@ref)
477+
and [`ExtendedKalmanFilter`](@ref). A constant arrival covariance is supported using
478+
the [`SteadyKalmanFilter`](@ref). The constant is fixed by `σP_0`, `σPint_ym_0` and
479+
`σPint_u_0` arguments. If `isnothing(σP_0)`, it is fixed at `covestim.cov.P̂`, which is
480+
the steady-state value ``\mathbf{P̂}(∞)`` for the [`SteadyKalmanFilter`](@ref). For
481+
[`NonLinModel`](@ref), construct a [`SteadyKalmanFilter`](@ref) with an arbitrary
482+
[`LinModel`](@ref), as long as the number of estimated states `nx̂` matches the MHE.
473483
474484
Note that if `Cwt≠Inf`, the attribute `nlp_scaling_max_gradient` of `Ipopt` is set to
475485
`10/Cwt` (if not already set), to scale the small values of ``ε``. Use the second
@@ -497,6 +507,7 @@ function MovingHorizonEstimator(
497507
gradient::AbstractADType = DEFAULT_NONLINMHE_GRADIENT,
498508
jacobian::AbstractADType = DEFAULT_NONLINMHE_JACOBIAN,
499509
hessian::Union{AbstractADType, Bool, Nothing} = false,
510+
covestim::Union{StateEstimator, Nothing} = nothing,
500511
direct = true,
501512
σP_0 = sigmaP_0,
502513
σQ = sigmaQ,
@@ -507,25 +518,16 @@ function MovingHorizonEstimator(
507518
σQint_ym = sigmaQint_ym,
508519
) where {NT<:Real, SM<:SimModel{NT}, JM<:JuMP.GenericModel}
509520
# estimated covariances matrices (variance = σ²) :
510-
P̂_0 = Diagonal([σP_0; σPint_u_0; σPint_ym_0].^2)
521+
P̂_0 = isnothing(σP_0) ? nothing : Diagonal([σP_0; σPint_u_0; σPint_ym_0].^2)
511522
= Diagonal([σQ; σQint_u; σQint_ym ].^2)
512523
= Diagonal([σR;].^2)
513524
isnothing(He) && throw(ArgumentError("Estimation horizon He must be explicitly specified"))
514525
return MovingHorizonEstimator(
515526
model, He, i_ym, nint_u, nint_ym, P̂_0, Q̂, R̂, Cwt;
516-
gc, gc!, nc, p, direct, optim, gradient, jacobian, hessian
527+
gc, gc!, nc, p, optim, gradient, jacobian, hessian, covestim, direct
517528
)
518529
end
519530

520-
"Default optimizer for MHE, depending on the model and the number of custom NL constraints."
521-
function default_optim_mhe(model::SimModel, nc)
522-
if model isa LinModel && iszero(nc)
523-
return JuMP.Model(DEFAULT_LINMHE_OPTIMIZER, add_bridges=false)
524-
else
525-
return JuMP.Model(DEFAULT_NONLINMHE_OPTIMIZER, add_bridges=false)
526-
end
527-
end
528-
529531
@doc raw"""
530532
MovingHorizonEstimator(
531533
model, He, i_ym, nint_u, nint_ym, P̂_0, Q̂, R̂, Cwt=Inf;
@@ -536,20 +538,15 @@ end
536538
gradient=AutoForwardDiff(),
537539
jacobian=AutoForwardDiff(),
538540
hessian=false,
541+
covestim=nothing,
539542
direct=true,
540-
covestim=default_covestim_mhe(model, i_ym, nint_u, nint_ym, P̂_0, Q̂, R̂; direct)
541543
)
542544
543545
Construct the estimator from the augmented covariance matrices `P̂_0`, `Q̂` and `R̂`.
544546
545547
This syntax allows nonzero off-diagonal elements in ``\mathbf{P̂_i}, \mathbf{Q̂, R̂}``,
546-
where ``\mathbf{P̂_i}`` is the initial estimation covariance, provided by `P̂_0` argument. The
547-
keyword argument `covestim` also allows specifying a custom [`StateEstimator`](@ref) object
548-
for the estimation of covariance at the arrival ``\mathbf{P̂}_{k-N_k}(k-N_k+p)``. The
549-
supported types are [`SteadyKalmanFilter`](@ref), [`KalmanFilter`](@ref),
550-
[`UnscentedKalmanFilter`](@ref) and [`ExtendedKalmanFilter`](@ref). A constant arrival
551-
covariance is supported with [`SteadyKalmanFilter`](@ref), and by setting the `P̂` argument
552-
of [`setstate!`](@ref) at the desired value.
548+
where ``\mathbf{P̂_i}`` is the initial estimation covariance. Its value is provided by `P̂_0`
549+
argument. If `isnothing(P̂_0)`, its value will be fetch in `covestim.cov.P̂`.
553550
"""
554551
function MovingHorizonEstimator(
555552
model::SM, He, i_ym, nint_u, nint_ym, P̂_0, Q̂, R̂, Cwt=Inf;
@@ -561,14 +558,24 @@ function MovingHorizonEstimator(
561558
gradient::AbstractADType = DEFAULT_NONLINMHE_GRADIENT,
562559
jacobian::AbstractADType = DEFAULT_NONLINMHE_JACOBIAN,
563560
hessian::Union{AbstractADType, Bool, Nothing} = false,
561+
covestim::Union{StateEstimator, Nothing} = nothing,
564562
direct = true,
565-
covestim::CE = default_covestim_mhe(model, i_ym, nint_u, nint_ym, P̂_0, Q̂, R̂; direct)
566-
) where {NT<:Real, SM<:SimModel{NT}, JM<:JuMP.GenericModel, CE<:StateEstimator{NT}}
563+
) where {NT<:Real, SM<:SimModel{NT}, JM<:JuMP.GenericModel}
564+
if isnothing(P̂_0)
565+
if isnothing(covestim)
566+
throw(ArgumentError("a covestim argument should be specified to fetch its covariance P̂"))
567+
end
568+
P̂_0 = covestim.cov.
569+
end
567570
P̂_0, Q̂, R̂ = to_mat(P̂_0), to_mat(Q̂), to_mat(R̂)
568571
cov = KalmanCovariances(model, i_ym, nint_u, nint_ym, Q̂, R̂, P̂_0, He)
569572
gc! = get_mutating_gc_mhe(NT, gc)
570573
hessian = validate_hessian(hessian, gradient, DEFAULT_NONLINMHE_HESSIAN)
574+
if isnothing(covestim)
575+
covestim = default_covestim_mhe(model, i_ym, nint_u, nint_ym, P̂_0, Q̂, R̂; direct)
576+
end
571577
validate_covestim(cov, covestim)
578+
setstate!(covestim, covestim.x̂0 + covestim.x̂op, P̂_0)
572579
return MovingHorizonEstimator{NT}(
573580
model,
574581
He, i_ym, nint_u, nint_ym, cov, Cwt,
@@ -578,17 +585,30 @@ function MovingHorizonEstimator(
578585
)
579586
end
580587

581-
function default_covestim_mhe(model::LinModel, i_ym, nint_u, nint_ym, P̂_0, Q̂, R̂; direct)
582-
return KalmanFilter(model, i_ym, nint_u, nint_ym, P̂_0, Q̂, R̂; direct)
588+
"Default optimizer for MHE, depending on the model and the number of custom NL constraints."
589+
function default_optim_mhe(model::SimModel, nc)
590+
if model isa LinModel && iszero(nc)
591+
return JuMP.Model(DEFAULT_LINMHE_OPTIMIZER, add_bridges=false)
592+
else
593+
return JuMP.Model(DEFAULT_NONLINMHE_OPTIMIZER, add_bridges=false)
594+
end
583595
end
596+
597+
"Default arrival covariance estimator for MHE, depending on the model type only."
584598
function default_covestim_mhe(model::SimModel, i_ym, nint_u, nint_ym, P̂_0, Q̂, R̂; direct)
585-
return UnscentedKalmanFilter(model, i_ym, nint_u, nint_ym, P̂_0, Q̂, R̂; direct)
599+
if model isa LinModel
600+
return KalmanFilter(model, i_ym, nint_u, nint_ym, P̂_0, Q̂, R̂; direct)
601+
else
602+
return UnscentedKalmanFilter(model, i_ym, nint_u, nint_ym, P̂_0, Q̂, R̂; direct)
603+
end
586604
end
587605

588606
"Validate covestim type and dimensions."
589607
function validate_covestim(cov::KalmanCovariances, covestim::KalmanEstimator)
590-
if size(cov.P̂) != size(covestim.cov.P̂)
591-
throw(ArgumentError("estimation covariance covestim.cov.P̂ size does not match the MHE"))
608+
invP̄, P̂ = cov.invP̄, covestim.cov.
609+
nx̂ = size(invP̄, 1)
610+
if size(invP̄) != size(P̂)
611+
throw(ArgumentError("P̂ covariance size $(size(P̂)) of covestim does match nx̂=$nx̂"))
592612
end
593613
return nothing
594614
end

test/2_test_state_estim.jl

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -866,7 +866,20 @@ end
866866

867867
linmodel2 = LinModel{Float32}(0.5*ones(1,1), ones(1,1), ones(1,1), zeros(1,0), zeros(1,0), 1.0)
868868
mhe13 = MovingHorizonEstimator(linmodel2, He=5)
869-
@test isa(mhe13, MovingHorizonEstimator{Float32})
869+
@test isa(mhe13, MovingHorizonEstimator{Float32})
870+
871+
covestim = SteadyKalmanFilter(linmodel)
872+
σP_0 = 1:4
873+
σPint_ym_0 = 5:6
874+
mhe14 = MovingHorizonEstimator(linmodel; He=1, σP_0, σPint_ym_0, covestim)
875+
@test mhe14.cov.invP̄ inv(diagm((1:6).^2))
876+
preparestate!(mhe14, linmodel.yop, linmodel.dop)
877+
@test mhe14.cov.invP̄ inv(diagm((1:6).^2))
878+
879+
covestim = SteadyKalmanFilter(linmodel)
880+
σP_0 = nothing
881+
mhe15 = MovingHorizonEstimator(linmodel; He=1, σP_0, covestim)
882+
@test mhe15.cov.invP̄ inv(covestim.cov.P̂)
870883

871884
function gcl(X̂e, _ , _ , _ , _ , _ , _ , _ , nx, ε)
872885
gc = X̂e .- 100 .- ε
@@ -883,6 +896,7 @@ end
883896
@test_throws ArgumentError MovingHorizonEstimator(linmodel)
884897
@test_throws ArgumentError MovingHorizonEstimator(linmodel, He=0)
885898
@test_throws ArgumentError MovingHorizonEstimator(linmodel, Cwt=-1)
899+
@test_throws ArgumentError MovingHorizonEstimator(linmodel, He=1, σP_0=nothing)
886900
end
887901

888902
@testitem "MHE construction (NonLinModel)" setup=[SetupMPCtests] begin

0 commit comments

Comments
 (0)