Skip to content

Commit f729c47

Browse files
authored
Merge pull request #15 from franckgaga/nint_u
Added support for unmeasured disturbances at model manipulated inputs.
2 parents 4732d8c + d458e6e commit f729c47

File tree

10 files changed

+235
-194
lines changed

10 files changed

+235
-194
lines changed

Project.toml

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

66
[deps]
77
ControlSystemsBase = "aaaaaaaa-a6ca-5380-bf3e-84a91bcd477e"

README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ for more detailed examples.
9393
- [x] supported feedback strategy:
9494
- [x] state estimator (see State Estimation features)
9595
- [x] internal model structure with a custom stochastic model
96-
- [x] offset-free tracking with a single or multiple integrators on measured outputs
96+
- [x] automatic model augmentation with integrating states for offset-free tracking
9797
- [x] support for unmeasured model outputs
9898
- [x] feedforward action with measured disturbances that supports direct transmission
9999
- [x] custom predictions for:
@@ -115,7 +115,9 @@ for more detailed examples.
115115
- [x] extended Kalman filter
116116
- [x] unscented Kalman filter
117117
- [ ] moving horizon estimator
118-
- [x] automatic model augmentation for offset-free tracking
118+
- [x] easily estimate unmeasured disturbances by adding one or more integrators at the:
119+
- [x] manipulated inputs
120+
- [x] measured outputs
119121
- [x] observers in predictor form to ease control applications
120122
- [ ] moving horizon estimator that supports:
121123
- [ ] inequality state constraints

docs/src/internals/state_estim.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
## Estimator Initialization
44

55
```@docs
6+
ModelPredictiveControl.init_estimstoch
67
ModelPredictiveControl.init_integrators
78
ModelPredictiveControl.augment_model
89
ModelPredictiveControl.init_ukf

docs/src/manual/nonlinmpc.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ plot(sim!(model, 60, u), plotu=false)
5959
An [`UnscentedKalmanFilter`](@ref) estimates the plant state :
6060

6161
```@example 1
62-
estim = UnscentedKalmanFilter(model, σQ=[0.1, 0.5], σR=[0.5], nint_ym=[1], σQ_int=[5.0])
62+
estim = UnscentedKalmanFilter(model, σQ=[0.1, 0.5], σR=[0.5], nint_ym=[1], σQint_ym=[5.0])
6363
```
6464

6565
The standard deviation of the angular velocity ``ω`` is higher here (`σQ` second value)
@@ -75,7 +75,7 @@ plot(res, plotu=false, plotxwithx̂=true)
7575
```
7676

7777
The estimate ``x̂_3`` is the integrator state that compensates for static errors (`nint_ym`
78-
and `σQ_int` parameters of [`UnscentedKalmanFilter`](@ref)). The Kalman filter performance
78+
and `σQint_ym` parameters of [`UnscentedKalmanFilter`](@ref)). The Kalman filter performance
7979
seems sufficient for control. As the motor torque is limited to -1.5 to 1.5 N m, we
8080
incorporate the input constraints in a [`NonLinMPC`](@ref):
8181

src/controller/nonlinmpc.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ Use custom state estimator `estim` to construct `NonLinMPC`.
168168
```jldoctest
169169
julia> model = NonLinModel((x,u,_)->0.5x+u, (x,_)->2x, 10.0, 1, 1, 1);
170170
171-
julia> estim = UnscentedKalmanFilter(model, σQ_int=[0.05]);
171+
julia> estim = UnscentedKalmanFilter(model, σQint_ym=[0.05]);
172172
173173
julia> mpc = NonLinMPC(estim, Hp=20, Hc=1, Cwt=1e6)
174174
NonLinMPC controller with a sample time Ts = 10.0 s, Ipopt optimizer, UnscentedKalmanFilter estimator and:

src/estimator/kalman.jl

Lines changed: 127 additions & 86 deletions
Large diffs are not rendered by default.

src/estimator/luenberger.jl

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@ struct Luenberger <: StateEstimator
77
nym::Int
88
nyu::Int
99
nxs::Int
10-
As::Matrix{Float64}
11-
Cs::Matrix{Float64}
10+
As ::Matrix{Float64}
11+
Cs_u::Matrix{Float64}
12+
Cs_y::Matrix{Float64}
13+
nint_u ::Vector{Int}
1214
nint_ym::Vector{Int}
1315
::Matrix{Float64}
1416
B̂u ::Matrix{Float64}
@@ -18,9 +20,11 @@ struct Luenberger <: StateEstimator
1820
Ĉm ::Matrix{Float64}
1921
D̂dm ::Matrix{Float64}
2022
K::Matrix{Float64}
21-
function Luenberger(model, i_ym, nint_ym, p̂)
22-
nym, nyu, nxs, nx̂, As, Cs, nint_ym = init_estimstoch(model, i_ym, nint_ym)
23-
Â, B̂u, Ĉ, B̂d, D̂d = augment_model(model, As, Cs)
23+
function Luenberger(model, i_ym, nint_u, nint_ym, p̂)
24+
nym, nyu = validate_ym(model, i_ym)
25+
As, Cs_u, Cs_y, nxs, nint_u, nint_ym = init_estimstoch(model, i_ym, nint_u, nint_ym)
26+
nx̂ = model.nx + nxs
27+
Â, B̂u, Ĉ, B̂d, D̂d = augment_model(model, As, Cs_u, Cs_y)
2428
K = try
2529
place(Â, Ĉ, p̂, :o)[:, i_ym]
2630
catch
@@ -33,7 +37,7 @@ struct Luenberger <: StateEstimator
3337
model,
3438
lastu0, x̂,
3539
i_ym, nx̂, nym, nyu, nxs,
36-
As, Cs, nint_ym,
40+
As, Cs_u, Cs_y, nint_u, nint_ym,
3741
Â, B̂u, B̂d, Ĉ, D̂d,
3842
Ĉm, D̂dm,
3943
K
@@ -45,6 +49,7 @@ end
4549
Luenberger(
4650
model::LinModel;
4751
i_ym = 1:model.ny,
52+
nint_u = 0,
4853
nint_ym = default_nint(model, i_ym),
4954
p̂ = 1e-3*(0:(model.nx + sum(nint_ym)-1)) .+ 0.5)
5055
)
@@ -53,7 +58,7 @@ Construct a Luenberger observer with the [`LinModel`](@ref) `model`.
5358
5459
`i_ym` provides the `model` output indices that are measured ``\mathbf{y^m}``, the rest are
5560
unmeasured ``\mathbf{y^u}``. `model` matrices are augmented with the stochastic model, which
56-
is specified by the numbers of output integrator `nint_ym` (see [`SteadyKalmanFilter`](@ref)
61+
is specified by the numbers of integrator `nint_u` and `nint_ym` (see [`SteadyKalmanFilter`](@ref)
5762
Extended Help). The argument `p̂` is a vector of `model.nx + sum(nint_ym)` elements
5863
specifying the observer poles/eigenvalues (near ``z=0.5`` by default). The method computes
5964
the observer gain ``\mathbf{K}`` with [`place`](https://juliacontrol.github.io/ControlSystems.jl/stable/lib/synthesis/#ControlSystemsBase.place).
@@ -74,15 +79,16 @@ Luenberger estimator with a sample time Ts = 0.5 s, LinModel and:
7479
function Luenberger(
7580
model::LinModel;
7681
i_ym::IntRangeOrVector = 1:model.ny,
77-
nint_ym::IntVectorOrInt = default_nint(model, i_ym),
82+
nint_u ::IntVectorOrInt = 0,
83+
nint_ym::IntVectorOrInt = default_nint(model, i_ym, nint_u),
7884
= 1e-3*(0:(model.nx + sum(nint_ym)-1)) .+ 0.5
7985
)
8086
nx = model.nx
8187
if length(p̂) model.nx + sum(nint_ym)
8288
error("p̂ length ($(length(p̂))) ≠ nx ($nx) + integrator quantity ($(sum(nint_ym)))")
8389
end
8490
any(abs.(p̂) .≥ 1) && error("Observer poles p̂ should be inside the unit circles.")
85-
return Luenberger(model, i_ym, nint_ym, p̂)
91+
return Luenberger(model, i_ym, nint_u, nint_ym, p̂)
8692
end
8793

8894

src/predictive_control.jl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -913,14 +913,14 @@ with [`getestimates!`](@ref). The method also returns an empty ``\mathbf{P_s}``
913913
it is useless except for [`InternalModel`](@ref) estimators.
914914
"""
915915
function init_stochpred(estim::StateEstimator, Hp)
916-
As, Cs = estim.As, estim.Cs
916+
As, Cs_y = estim.As, estim.Cs_y
917917
nxs = estim.nxs
918918
Ms = Matrix{Float64}(undef, Hp*nxs, nxs)
919919
for i = 1:Hp
920920
iRow = (1:nxs) .+ nxs*(i-1)
921921
Ms[iRow, :] = As^i
922922
end
923-
Js = repeatdiag(Cs, Hp)
923+
Js = repeatdiag(Cs_y, Hp)
924924
Ks = Js*Ms
925925
Ps = zeros(estim.model.ny*Hp, 0)
926926
return Ks, Ps

src/state_estim.jl

Lines changed: 73 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -70,26 +70,36 @@ function remove_op!(estim::StateEstimator, u, d, ym)
7070
end
7171

7272
"""
73-
init_estimstoch(model, i_ym, nint_ym)
73+
init_estimstoch(model, i_ym, nint_u, nint_ym) -> As, Cs_u, Cs_y, nxs, nint_u, nint_ym
7474
7575
Init stochastic model matrices from integrator specifications for state estimation.
76+
77+
TBW.
78+
79+
The function [`init_integrators`](@ref) builds the state-space matrice of the unmeasured
80+
disturbance models.
7681
"""
77-
function init_estimstoch(model, i_ym, nint_ym)
78-
validate_ym(model, i_ym)
79-
nx, ny = model.nx, model.ny
80-
nym, nyu = length(i_ym), ny - length(i_ym)
81-
Asm, Csm, nint_ym = init_integrators(i_ym, nint_ym)
82-
nxs = size(Asm,1)
83-
nx̂ = nx + nxs
84-
As, _ , Cs = stoch_ym2y(model, i_ym, Asm, [], Csm, [])
85-
return nym, nyu, nxs, nx̂, As, Cs, nint_ym
82+
function init_estimstoch(model, i_ym, nint_u::IntVectorOrInt, nint_ym::IntVectorOrInt)
83+
nu, ny, nym = model.nu, model.ny, length(i_ym)
84+
As_u , Cs_u , nint_u = init_integrators(nint_u , nu , "u")
85+
As_ym, Cs_ym, nint_ym = init_integrators(nint_ym, nym, "ym")
86+
As_y, _ , Cs_y = stoch_ym2y(model, i_ym, As_ym, [], Cs_ym, [])
87+
nxs_u, nxs_y = size(As_u, 1), size(As_y, 1)
88+
# combines input and output stochastic models:
89+
As = [As_u zeros(nxs_u, nxs_y); zeros(nxs_y, nxs_u) As_y]
90+
Cs_u = [Cs_u zeros(nu, nxs_y)]
91+
Cs_y = [zeros(ny, nxs_u) Cs_y]
92+
nxs = nxs_u + nxs_y
93+
return As, Cs_u, Cs_y, nxs, nint_u, nint_ym
8694
end
8795

8896
"Validate the specified measured output indices `i_ym`."
8997
function validate_ym(model::SimModel, i_ym)
9098
if length(unique(i_ym)) length(i_ym) || maximum(i_ym) > model.ny
9199
error("Measured output indices i_ym should contains valid and unique indices")
92100
end
101+
nym, nyu = length(i_ym), model.ny - length(i_ym)
102+
return nym, nyu
93103
end
94104

95105
"Convert the measured outputs stochastic model `stoch_ym` to all outputs `stoch_y`."
@@ -108,74 +118,49 @@ function stoch_ym2y(model::SimModel, i_ym, Asm, Bsm, Csm, Dsm)
108118
end
109119

110120
@doc raw"""
111-
init_integrators(i_ym, nint_ym::Vector{Int}) -> Asm, Csm, nint_ym
121+
init_integrators(nint, nys, varname::String) -> As, Cs, nint
112122
113-
Calc stochastic model matrices from output integrators specifications for state estimation.
123+
Calc state-space matrices `As, Cs` (stochastic part) from integrator specifications `nint`.
114124
115-
For closed-loop state estimators. `nint_ym` is a vector providing how many integrator should
116-
be added for each measured output ``\mathbf{y^m}``. The argument generates the `Asm` and
117-
`Csm` matrices:
125+
This function is used to initialize the stochastic part of the augmented model for the
126+
design of state estimators. The vector `nint` provides how many integrators (in series)
127+
should be incorporated for each stochastic output ``\mathbf{y_s}``:
118128
```math
119129
\begin{aligned}
120-
\mathbf{x_s}(k+1) &= \mathbf{A_s^m x_s}(k) + \mathbf{B_s^m e}(k) \\
121-
\mathbf{y_s^m}(k) &= \mathbf{C_s^m x_s}(k)
130+
\mathbf{x_s}(k+1) &= \mathbf{A_s x_s}(k) + \mathbf{B_s e}(k) \\
131+
\mathbf{y_s}(k) &= \mathbf{C_s x_s}(k)
122132
\end{aligned}
123133
```
124-
where ``\mathbf{e}(k)`` is a conceptual and unknown zero mean white noise.
125-
``\mathbf{B_s^m}`` is not used for closed-loop state estimators thus ignored.
126-
"""
127-
function init_integrators(i_ym, nint_ym)
128-
if nint_ym == 0 # alias for no output integrator at all
129-
nint_ym = fill(0, length(i_ym))
130-
end
131-
nym = length(i_ym);
132-
if length(nint_ym) nym
133-
error("nint_ym size ($(length(nint_ym))) ≠ measured output quantity nym ($nym)")
134-
end
135-
any(nint_ym .< 0) && error("nint_ym values should be ≥ 0")
136-
nxs = sum(nint_ym)
137-
Asm, Csm = zeros(nxs, nxs), zeros(nym, nxs)
138-
if nxs 0 # construct stochastic model state-space matrices (integrators) :
139-
i_Asm, i_Csm = 1, 1
140-
for iym = 1:nym
141-
nint = nint_ym[iym]
142-
if nint 0
143-
rows_Asm = (i_Asm):(i_Asm + nint - 1)
144-
Asm[rows_Asm, rows_Asm] = Bidiagonal(ones(nint), ones(nint-1), :L)
145-
Csm[iym, i_Csm+nint-1] = 1
146-
i_Asm += nint
147-
i_Csm += nint
148-
end
149-
end
150-
end
151-
return Asm, Csm, nint_ym
152-
end
134+
where ``\mathbf{e}(k)`` is an unknown zero mean white noise. The estimations does not use
135+
``\mathbf{B_s}``, it is thus ignored. Note that this function is called twice :
153136
154-
function init_integrators_u(nu, nint_u)
155-
if nint_u == 0 # alias for no output integrator at all
156-
nint_u = fill(0, length(nu))
137+
1. for the unmodeled disturbances at measured outputs ``\mathbf{y^m}``
138+
2. for the unmodeled disturbances at manipulated inputs ``\mathbf{u}``
139+
"""
140+
function init_integrators(nint::IntVectorOrInt, nys, varname::String)
141+
if nint == 0 # alias for no integrator at all
142+
nint = fill(0, nys)
157143
end
158-
#nym = length(i_ym);
159-
if length(nint_u) nu
160-
error("nint_u size ($(length(nint_u))) ≠ manipulated input quantity nu ($nu)")
144+
if length(nint) nys
145+
error("nint_$(varname) size ($(length(nint))) ≠ n$(varname) ($nys)")
161146
end
162-
any(nint_u .< 0) && error("nint_u values should be ≥ 0")
163-
nxs = sum(nint_u)
164-
Asm, Csm = zeros(nxs, nxs), zeros(nym, nxs)
147+
any(nint .< 0) && error("nint_$(varname) values should be ≥ 0")
148+
nxs = sum(nint)
149+
As, Cs = zeros(nxs, nxs), zeros(nys, nxs)
165150
if nxs 0 # construct stochastic model state-space matrices (integrators) :
166-
i_Asm, i_Csm = 1, 1
167-
for iym = 1:nym
168-
nint = nint_u[iym]
169-
if nint 0
170-
rows_Asm = (i_Asm):(i_Asm + nint - 1)
171-
Asm[rows_Asm, rows_Asm] = Bidiagonal(ones(nint), ones(nint-1), :L)
172-
Csm[iym, i_Csm+nint-1] = 1
173-
i_Asm += nint
174-
i_Csm += nint
151+
i_As, i_Cs = 1, 1
152+
for i = 1:nys
153+
nint_i = nint[i]
154+
if nint_i 0
155+
rows_As = (i_As):(i_As + nint_i - 1)
156+
As[rows_As, rows_As] = Bidiagonal(ones(nint_i), ones(nint_i-1), :L)
157+
Cs[i, i_Cs+nint_i-1] = 1
158+
i_As += nint_i
159+
i_Cs += nint_i
175160
end
176161
end
177162
end
178-
return Asm, Csm, nint_u
163+
return As, Cs, nint
179164
end
180165

181166
@doc raw"""
@@ -195,26 +180,27 @@ returns the augmented matrices `Â`, `B̂u`, `Ĉ`, `B̂d` and `D̂d`:
195180
```
196181
An error is thrown if the augmented model is not observable and `verify_obsv == true`.
197182
"""
198-
function augment_model(model::LinModel, As, Cs; verify_obsv=true)
183+
function augment_model(model::LinModel, As, Cs_u, Cs_y; verify_obsv=true)
199184
nu, nx, nd = model.nu, model.nx, model.nd
200185
nxs = size(As, 1)
201-
= [model.A zeros(nx,nxs); zeros(nxs,nx) As]
202-
B̂u = [model.Bu; zeros(nxs,nu)]
203-
= [model.C Cs]
204-
B̂d = [model.Bd; zeros(nxs,nd)]
186+
= [model.A model.Bu*Cs_u; zeros(nxs,nx) As]
187+
B̂u = [model.Bu; zeros(nxs, nu)]
188+
= [model.C Cs_y]
189+
B̂d = [model.Bd; zeros(nxs, nd)]
205190
D̂d = model.Dd
206191
# observability on Ĉ instead of Ĉm, since it would always return false when nym ≠ ny:
207192
if verify_obsv && !observability(Â, Ĉ)[:isobservable]
208-
error("The augmented model is unobservable. You may try to use 0 "*
209-
"integrator on model integrating outputs with nint_ym parameter.")
193+
error("The augmented model is unobservable. You may try to use 0 integrator on "*
194+
"model integrating outputs with nint_ym parameter. Adding integrators at both "*
195+
"inputs (nint_u) and outputs (nint_ym) can also violate observability.")
210196
end
211197
return Â, B̂u, Ĉ, B̂d, D̂d
212198
end
213199
"No need to augment the model if `model` is not a [`LinModel`](@ref)."
214-
augment_model(::SimModel, _ , _ ) = nothing
200+
augment_model(::SimModel, _ , _ , _ ) = nothing
215201

216202
@doc raw"""
217-
default_nint(model::LinModel, i_ym=1:model.ny)
203+
default_nint(model::LinModel, i_ym=1:model.ny, nint_u)
218204
219205
Get default integrator quantity per measured outputs `nint_ym` for [`LinModel`](@ref).
220206
@@ -234,24 +220,27 @@ julia> nint_ym = default_nint(model)
234220
1
235221
```
236222
"""
237-
function default_nint(model::LinModel, i_ym::IntRangeOrVector = 1:model.ny)
223+
function default_nint(model::LinModel, i_ym=1:model.ny, nint_u=0)
224+
validate_ym(model, i_ym)
238225
nint_ym = fill(0, length(i_ym))
239226
for i in eachindex(i_ym)
240227
nint_ym[i] = 1
241-
Asm, Csm = init_integrators(i_ym, nint_ym)
242-
As , _ , Cs = stoch_ym2y(model, i_ym, Asm, [], Csm, [])
243-
 , _ , Ĉ = augment_model(model, As, Cs, verify_obsv=false)
228+
As, Cs_u, Cs_y = init_estimstoch(model, i_ym, nint_u, nint_ym)
229+
Â, _ , Ĉ = augment_model(model, As, Cs_u, Cs_y, verify_obsv=false)
244230
# observability on Ĉ instead of Ĉm, since it would always return false when nym ≠ ny
245231
observability(Â, Ĉ)[:isobservable] || (nint_ym[i] = 0)
246232
end
247233
return nint_ym
248234
end
249235
"""
250-
default_nint(model::SimModel, i_ym=1:model.ny)
236+
default_nint(model::SimModel, i_ym=1:model.ny, nint_u=0)
251237
252238
One integrator on each measured output by default if `model` is not a [`LinModel`](@ref).
253239
"""
254-
default_nint(::SimModel, i_ym::IntRangeOrVector = 1:model.ny) = fill(1, length(i_ym))
240+
function default_nint(model::SimModel, i_ym=1:model.ny, nint_u=0)
241+
validate_ym(model, i_ym)
242+
return fill(1, length(i_ym))
243+
end
255244

256245
@doc raw"""
257246
f̂(estim::StateEstimator, x̂, u, d)
@@ -269,8 +258,8 @@ function returns the next state of the augmented model, defined as:
269258
"""
270259
function (estim::StateEstimator, x̂, u, d)
271260
# `@views` macro avoid copies with matrix slice operator e.g. [a:b]
272-
nx = estim.model.nx
273-
@views return [f(estim.model, x̂[1:nx], u, d); estim.As*x̂[nx+1:end]]
261+
@views x̂d, x̂s = x̂[1:estim.model.nx], x̂[estim.model.nx+1:end]
262+
return [f(estim.model, x̂d, u + estim.Cs_u*x̂s, d); estim.As*x̂s]
274263
end
275264

276265
@doc raw"""
@@ -280,8 +269,8 @@ Output function ``\mathbf{ĥ}`` of the augmented model, see [`f̂`](@ref) for d
280269
"""
281270
function (estim::StateEstimator, x̂, d)
282271
# `@views` macro avoid copies with matrix slice operator e.g. [a:b]
283-
nx = estim.model.nx
284-
@views return h(estim.model, x̂[1:nx], d) + estim.Cs*x̂[nx+1:end]
272+
@views x̂d, x̂s = x̂[1:estim.model.nx], x̂[estim.model.nx+1:end]
273+
return h(estim.model, x̂d, d) + estim.Cs_y*x̂s
285274
end
286275

287276

0 commit comments

Comments
 (0)