|
| 1 | +@doc raw""" |
| 2 | + initstate!(mpc::PredictiveController, u, ym, d=[]) -> x̂ |
| 3 | +
|
| 4 | +Init the states of `mpc.estim` [`StateEstimator`](@ref) and warm start `mpc.ΔŨ` at zero. |
| 5 | +""" |
| 6 | +function initstate!(mpc::PredictiveController, u, ym, d=empty(mpc.estim.x̂)) |
| 7 | + mpc.ΔŨ .= 0 |
| 8 | + return initstate!(mpc.estim, u, ym, d) |
| 9 | +end |
| 10 | + |
| 11 | +@doc raw""" |
| 12 | + moveinput!(mpc::PredictiveController, ry=mpc.estim.model.yop, d=[]; <keyword args>) -> u |
| 13 | +
|
| 14 | +Compute the optimal manipulated input value `u` for the current control period. |
| 15 | +
|
| 16 | +Solve the optimization problem of `mpc` [`PredictiveController`](@ref) and return the |
| 17 | +results ``\mathbf{u}(k)``. Following the receding horizon principle, the algorithm discards |
| 18 | +the optimal future manipulated inputs ``\mathbf{u}(k+1), \mathbf{u}(k+2), ...`` Note that |
| 19 | +the method mutates `mpc` internal data but it does not modifies `mpc.estim` states. Call |
| 20 | +[`updatestate!(mpc, u, ym, d)`](@ref) to update `mpc` state estimates. |
| 21 | +
|
| 22 | +Calling a [`PredictiveController`](@ref) object calls this method. |
| 23 | +
|
| 24 | +See also [`LinMPC`](@ref), [`ExplicitMPC`](@ref), [`NonLinMPC`](@ref). |
| 25 | +
|
| 26 | +# Arguments |
| 27 | +- `mpc::PredictiveController` : solve optimization problem of `mpc`. |
| 28 | +- `ry=mpc.estim.model.yop` : current output setpoints ``\mathbf{r_y}(k)``. |
| 29 | +- `d=[]` : current measured disturbances ``\mathbf{d}(k)``. |
| 30 | +- `D̂=repeat(d, mpc.Hp)` : predicted measured disturbances ``\mathbf{D̂}``, constant in the |
| 31 | + future by default or ``\mathbf{d̂}(k+j)=\mathbf{d}(k)`` for ``j=1`` to ``H_p``. |
| 32 | +- `R̂y=repeat(ry, mpc.Hp)` : predicted output setpoints ``\mathbf{R̂_y}``, constant in the |
| 33 | + future by default or ``\mathbf{r̂_y}(k+j)=\mathbf{r_y}(k)`` for ``j=1`` to ``H_p``. |
| 34 | +- `R̂u=repeat(mpc.estim.model.uop, mpc.Hp)` : predicted manipulated input setpoints, constant |
| 35 | + in the future by default or ``\mathbf{r̂_u}(k+j)=\mathbf{u_{op}}`` for ``j=0`` to ``H_p-1``. |
| 36 | +- `ym=nothing` : current measured outputs ``\mathbf{y^m}(k)``, only required if `mpc.estim` |
| 37 | + is an [`InternalModel`](@ref). |
| 38 | +
|
| 39 | +# Examples |
| 40 | +```jldoctest |
| 41 | +julia> mpc = LinMPC(LinModel(tf(5, [2, 1]), 3), Nwt=[0], Hp=1000, Hc=1); |
| 42 | +
|
| 43 | +julia> ry = [5]; u = moveinput!(mpc, ry); round.(u, digits=3) |
| 44 | +1-element Vector{Float64}: |
| 45 | + 1.0 |
| 46 | +``` |
| 47 | +""" |
| 48 | +function moveinput!( |
| 49 | + mpc::PredictiveController, |
| 50 | + ry::Vector = mpc.estim.model.yop, |
| 51 | + d ::Vector = empty(mpc.estim.x̂); |
| 52 | + D̂ ::Vector = repeat(d, mpc.Hp), |
| 53 | + R̂y::Vector = repeat(ry, mpc.Hp), |
| 54 | + R̂u::Vector = mpc.noR̂u ? empty(mpc.estim.x̂) : repeat(mpc.estim.model.uop, mpc.Hp), |
| 55 | + ym::Union{Vector, Nothing} = nothing |
| 56 | +) |
| 57 | + validate_args(mpc, ry, d, D̂, R̂y, R̂u) |
| 58 | + initpred!(mpc, mpc.estim.model, d, ym, D̂, R̂y, R̂u) |
| 59 | + linconstraint!(mpc, mpc.estim.model) |
| 60 | + ΔŨ = optim_objective!(mpc) |
| 61 | + Δu = ΔŨ[1:mpc.estim.model.nu] # receding horizon principle: only Δu(k) is used (1st one) |
| 62 | + u = mpc.estim.lastu0 + mpc.estim.model.uop + Δu |
| 63 | + return u |
| 64 | +end |
| 65 | + |
| 66 | + |
| 67 | + |
| 68 | +@doc raw""" |
| 69 | + getinfo(mpc::PredictiveController) -> info |
| 70 | +
|
| 71 | +Get additional information about `mpc` controller optimum to ease troubleshooting. |
| 72 | +
|
| 73 | +The function should be called after calling [`moveinput!`](@ref). It returns the dictionary |
| 74 | +`info` with the following fields: |
| 75 | +
|
| 76 | +- `:ΔU` : optimal manipulated input increments over `Hc`, ``\mathbf{ΔU}``. |
| 77 | +- `:ϵ` : optimal slack variable, ``ϵ``. |
| 78 | +- `:J` : objective value optimum, ``J``. |
| 79 | +- `:U` : optimal manipulated inputs over `Hp`, ``\mathbf{U}``. |
| 80 | +- `:u` : current optimal manipulated input, ``\mathbf{u}(k)``. |
| 81 | +- `:d` : current measured disturbance, ``\mathbf{d}(k)``. |
| 82 | +- `:D̂` : predicted measured disturbances over `Hp`, ``\mathbf{D̂}``. |
| 83 | +- `:ŷ` : current estimated output, ``\mathbf{ŷ}(k)``. |
| 84 | +- `:Ŷ` : optimal predicted outputs over `Hp`, ``\mathbf{Ŷ}``. |
| 85 | +- `:x̂end`: optimal terminal states, ``\mathbf{x̂}_{k-1}(k+H_p)``. |
| 86 | +- `:Ŷs` : predicted stochastic output over `Hp` of [`InternalModel`](@ref), ``\mathbf{Ŷ_s}``. |
| 87 | +- `:R̂y` : predicted output setpoint over `Hp`, ``\mathbf{R̂_y}``. |
| 88 | +- `:R̂u` : predicted manipulated input setpoint over `Hp`, ``\mathbf{R̂_u}``. |
| 89 | +
|
| 90 | +For [`LinMPC`](@ref) and [`NonLinMPC`](@ref), the field `:sol` also contains the optimizer |
| 91 | +solution summary that can be printed. Lastly, the optimal economic cost `:JE` is also |
| 92 | +available for [`NonLinMPC`](@ref). |
| 93 | +
|
| 94 | +# Examples |
| 95 | +```jldoctest |
| 96 | +julia> mpc = LinMPC(LinModel(tf(5, [2, 1]), 3), Nwt=[0], Hp=1, Hc=1); |
| 97 | +
|
| 98 | +julia> u = moveinput!(mpc, [10]); |
| 99 | +
|
| 100 | +julia> round.(getinfo(mpc)[:Ŷ], digits=3) |
| 101 | +1-element Vector{Float64}: |
| 102 | + 10.0 |
| 103 | +``` |
| 104 | +""" |
| 105 | +function getinfo(mpc::PredictiveController{NT}) where NT<:Real |
| 106 | + info = Dict{Symbol, Union{JuMP._SolutionSummary, Vector{NT}, NT}}() |
| 107 | + Ŷ, x̂ = similar(mpc.Ŷop), similar(mpc.estim.x̂) |
| 108 | + Ŷ, x̂end = predict!(Ŷ, x̂, mpc, mpc.estim.model, mpc.ΔŨ) |
| 109 | + info[:ΔU] = mpc.ΔŨ[1:mpc.Hc*mpc.estim.model.nu] |
| 110 | + info[:ϵ] = isinf(mpc.C) ? NaN : mpc.ΔŨ[end] |
| 111 | + info[:J] = obj_nonlinprog(mpc, mpc.estim.model, Ŷ, mpc.ΔŨ) |
| 112 | + info[:U] = mpc.S̃*mpc.ΔŨ + mpc.T*(mpc.estim.lastu0 + mpc.estim.model.uop) |
| 113 | + info[:u] = info[:U][1:mpc.estim.model.nu] |
| 114 | + info[:d] = mpc.d0 + mpc.estim.model.dop |
| 115 | + info[:D̂] = mpc.D̂0 + mpc.Dop |
| 116 | + info[:ŷ] = mpc.ŷ |
| 117 | + info[:Ŷ] = Ŷ |
| 118 | + info[:x̂end] = x̂end |
| 119 | + info[:Ŷs] = mpc.Ŷop - repeat(mpc.estim.model.yop, mpc.Hp) # Ŷop = Ŷs + Yop |
| 120 | + info[:R̂y] = mpc.R̂y |
| 121 | + info[:R̂u] = mpc.R̂u |
| 122 | + info = addinfo!(info, mpc) |
| 123 | + return info |
| 124 | +end |
| 125 | + |
| 126 | +""" |
| 127 | + addinfo!(info, mpc::PredictiveController) -> info |
| 128 | +
|
| 129 | +By default, add the solution summary `:sol` that can be printed to `info`. |
| 130 | +""" |
| 131 | +function addinfo!(info, mpc::PredictiveController) |
| 132 | + info[:sol] = solution_summary(mpc.optim, verbose=true) |
| 133 | + return info |
| 134 | +end |
| 135 | + |
| 136 | + |
| 137 | +@doc raw""" |
| 138 | + initpred!(mpc, model::LinModel, d, ym, D̂, R̂y, R̂u) |
| 139 | +
|
| 140 | +Init linear model prediction matrices `F, q̃, p` and current estimated output `ŷ`. |
| 141 | +
|
| 142 | +See [`init_predmat`](@ref) and [`init_quadprog`](@ref) for the definition of the matrices. |
| 143 | +""" |
| 144 | +function initpred!(mpc::PredictiveController, model::LinModel, d, ym, D̂, R̂y, R̂u) |
| 145 | + mpc.ŷ[:] = evalŷ(mpc.estim, ym, d) |
| 146 | + predictstoch!(mpc, mpc.estim, d, ym) # init mpc.Ŷop for InternalModel |
| 147 | + mpc.F[:] = mpc.K * mpc.estim.x̂ + mpc.V * mpc.estim.lastu0 + mpc.Ŷop |
| 148 | + if model.nd ≠ 0 |
| 149 | + mpc.d0[:], mpc.D̂0[:] = d - model.dop, D̂ - mpc.Dop |
| 150 | + mpc.D̂E[:] = [d; D̂] |
| 151 | + mpc.F[:] = mpc.F + mpc.G * mpc.d0 + mpc.J * mpc.D̂0 |
| 152 | + end |
| 153 | + mpc.R̂y[:] = R̂y |
| 154 | + Ẑ = mpc.F - mpc.R̂y |
| 155 | + mpc.q̃[:] = 2(mpc.M_Hp*mpc.Ẽ)'*Ẑ |
| 156 | + mpc.p[] = dot(Ẑ, mpc.M_Hp, Ẑ) |
| 157 | + if ~mpc.noR̂u |
| 158 | + mpc.R̂u[:] = R̂u |
| 159 | + lastu = mpc.estim.lastu0 + model.uop |
| 160 | + Ẑ = mpc.T*lastu - mpc.R̂u |
| 161 | + mpc.q̃[:] = mpc.q̃ + 2(mpc.L_Hp*mpc.S̃)'*Ẑ |
| 162 | + mpc.p[] = mpc.p[] + dot(Ẑ, mpc.L_Hp, Ẑ) |
| 163 | + end |
| 164 | + return nothing |
| 165 | +end |
| 166 | + |
| 167 | +@doc raw""" |
| 168 | + initpred!(mpc::PredictiveController, model::SimModel, d, ym, D̂, R̂y, R̂u) |
| 169 | +
|
| 170 | +Init `ŷ, Ŷop, d0, D̂0` matrices when model is not a [`LinModel`](@ref). |
| 171 | +
|
| 172 | +`d0` and `D̂0` are the measured disturbances and its predictions without the operating points |
| 173 | +``\mathbf{d_{op}}``. The vector `Ŷop` is kept unchanged if `mpc.estim` is not an |
| 174 | +[`InternalModel`](@ref). |
| 175 | +""" |
| 176 | +function initpred!(mpc::PredictiveController, model::SimModel, d, ym, D̂, R̂y, R̂u) |
| 177 | + mpc.ŷ[:] = evalŷ(mpc.estim, ym, d) |
| 178 | + predictstoch!(mpc, mpc.estim, d, ym) # init mpc.Ŷop for InternalModel |
| 179 | + if model.nd ≠ 0 |
| 180 | + mpc.d0[:], mpc.D̂0[:] = d - model.dop, D̂ - mpc.Dop |
| 181 | + mpc.D̂E[:] = [d; D̂] |
| 182 | + end |
| 183 | + mpc.R̂y[:] = R̂y |
| 184 | + if ~mpc.noR̂u |
| 185 | + mpc.R̂u[:] = R̂u |
| 186 | + end |
| 187 | + return nothing |
| 188 | +end |
| 189 | + |
| 190 | +@doc raw""" |
| 191 | + predictstoch!(mpc::PredictiveController, estim::InternalModel, x̂s, d, ym) |
| 192 | +
|
| 193 | +Init `Ŷop` vector when if `estim` is an [`InternalModel`](@ref). |
| 194 | +
|
| 195 | +The vector combines the output operating points and the stochastic predictions: |
| 196 | +``\mathbf{Ŷ_{op} = Ŷ_{s} + Y_{op}}`` (both values are constant between the nonlinear |
| 197 | +programming iterations). |
| 198 | +""" |
| 199 | +function predictstoch!( |
| 200 | + mpc::PredictiveController{NT}, estim::InternalModel, d, ym |
| 201 | +) where {NT<:Real} |
| 202 | + isnothing(ym) && error("Predictive controllers with InternalModel need the measured "* |
| 203 | + "outputs ym in keyword argument to compute control actions u") |
| 204 | + ŷd = h(estim.model, estim.x̂d, d - estim.model.dop) + estim.model.yop |
| 205 | + ŷs = zeros(NT, estim.model.ny) |
| 206 | + ŷs[estim.i_ym] = ym - ŷd[estim.i_ym] # ŷs=0 for unmeasured outputs |
| 207 | + Ŷs = mpc.Ks*mpc.estim.x̂s + mpc.Ps*ŷs |
| 208 | + mpc.Ŷop[:] = Ŷs + repeat(estim.model.yop, mpc.Hp) |
| 209 | + return nothing |
| 210 | +end |
| 211 | +"Separate stochastic predictions are not needed if `estim` is not [`InternalModel`](@ref)." |
| 212 | +predictstoch!(::PredictiveController, ::StateEstimator, _ , _ ) = nothing |
| 213 | + |
| 214 | +@doc raw""" |
| 215 | + predict!(Ŷ, x̂, mpc::PredictiveController, model::LinModel, ΔŨ) -> Ŷ, x̂end |
| 216 | +
|
| 217 | +Compute the predictions `Ŷ` and terminal states `x̂end` if model is a [`LinModel`](@ref). |
| 218 | +
|
| 219 | +The method mutates `Ŷ` and `x̂` vector arguments. The `x̂end` vector is used for |
| 220 | +the terminal constraints applied on ``\mathbf{x̂}_{k-1}(k+H_p)``. |
| 221 | +""" |
| 222 | +function predict!(Ŷ, x̂, mpc::PredictiveController, ::LinModel, ΔŨ::Vector{NT}) where {NT<:Real} |
| 223 | + # in-place operations to reduce allocations : |
| 224 | + Ŷ[:] = mul!(Ŷ, mpc.Ẽ, ΔŨ) + mpc.F |
| 225 | + x̂[:] = mul!(x̂, mpc.con.ẽx̂, ΔŨ) + mpc.con.fx̂ |
| 226 | + x̂end = x̂ |
| 227 | + return Ŷ, x̂end |
| 228 | +end |
| 229 | + |
| 230 | +@doc raw""" |
| 231 | + predict!(Ŷ, x̂, mpc::PredictiveController, model::SimModel, ΔŨ) -> Ŷ, x̂end |
| 232 | +
|
| 233 | +Compute both vectors if `model` is not a [`LinModel`](@ref). |
| 234 | +""" |
| 235 | +function predict!(Ŷ, x̂, mpc::PredictiveController, model::SimModel, ΔŨ::Vector{NT}) where {NT<:Real} |
| 236 | + nu, ny, nd, Hp, Hc = model.nu, model.ny, model.nd, mpc.Hp, mpc.Hc |
| 237 | + x̂[:] = mpc.estim.x̂ |
| 238 | + u0::Vector{NT} = copy(mpc.estim.lastu0) |
| 239 | + d0 = @views mpc.d0[1:end] |
| 240 | + for j=1:Hp |
| 241 | + if j ≤ Hc |
| 242 | + u0[:] = @views u0 + ΔŨ[(1 + nu*(j-1)):(nu*j)] |
| 243 | + end |
| 244 | + x̂[:] = f̂(mpc.estim, model, x̂, u0, d0) |
| 245 | + d0 = @views mpc.D̂0[(1 + nd*(j-1)):(nd*j)] |
| 246 | + Ŷ[(1 + ny*(j-1)):(ny*j)] = ĥ(mpc.estim, model, x̂, d0) |
| 247 | + end |
| 248 | + Ŷ[:] = Ŷ + mpc.Ŷop # Ŷop = Ŷs + Yop, and Ŷs=0 if mpc.estim is not an InternalModel |
| 249 | + x̂end = x̂ |
| 250 | + return Ŷ, x̂end |
| 251 | +end |
| 252 | + |
| 253 | +@doc raw""" |
| 254 | + linconstraint!(mpc::PredictiveController, model::LinModel) |
| 255 | +
|
| 256 | +Set `b` vector for the linear model inequality constraints (``\mathbf{A ΔŨ ≤ b}``). |
| 257 | +
|
| 258 | +Also init ``\mathbf{f_x̂}`` vector for the terminal constraints, see [`init_predmat`](@ref). |
| 259 | +""" |
| 260 | +function linconstraint!(mpc::PredictiveController, model::LinModel) |
| 261 | + mpc.con.fx̂[:] = mpc.con.kx̂ * mpc.estim.x̂ + mpc.con.vx̂ * mpc.estim.lastu0 |
| 262 | + if model.nd ≠ 0 |
| 263 | + mpc.con.fx̂[:] = mpc.con.fx̂ + mpc.con.gx̂ * mpc.d0 + mpc.con.jx̂ * mpc.D̂0 |
| 264 | + end |
| 265 | + lastu = mpc.estim.lastu0 + model.uop |
| 266 | + mpc.con.b[:] = [ |
| 267 | + -mpc.con.Umin + mpc.T*lastu |
| 268 | + +mpc.con.Umax - mpc.T*lastu |
| 269 | + -mpc.con.ΔŨmin |
| 270 | + +mpc.con.ΔŨmax |
| 271 | + -mpc.con.Ymin + mpc.F |
| 272 | + +mpc.con.Ymax - mpc.F |
| 273 | + -mpc.con.x̂min + mpc.con.fx̂ |
| 274 | + +mpc.con.x̂max - mpc.con.fx̂ |
| 275 | + ] |
| 276 | + lincon = mpc.optim[:linconstraint] |
| 277 | + set_normalized_rhs.(lincon, mpc.con.b[mpc.con.i_b]) |
| 278 | +end |
| 279 | + |
| 280 | +"Set `b` excluding predicted output constraints when `model` is not a [`LinModel`](@ref)." |
| 281 | +function linconstraint!(mpc::PredictiveController, model::SimModel) |
| 282 | + lastu = mpc.estim.lastu0 + model.uop |
| 283 | + mpc.con.b[:] = [ |
| 284 | + -mpc.con.Umin + mpc.T*lastu |
| 285 | + +mpc.con.Umax - mpc.T*lastu |
| 286 | + -mpc.con.ΔŨmin |
| 287 | + +mpc.con.ΔŨmax |
| 288 | + ] |
| 289 | + lincon = mpc.optim[:linconstraint] |
| 290 | + set_normalized_rhs.(lincon, mpc.con.b[mpc.con.i_b]) |
| 291 | +end |
| 292 | + |
| 293 | +""" |
| 294 | + optim_objective!(mpc::PredictiveController) |
| 295 | +
|
| 296 | +Optimize the objective function ``J`` of `mpc` controller and return the solution `ΔŨ`. |
| 297 | +""" |
| 298 | +function optim_objective!(mpc::PredictiveController{NT}) where {NT<:Real} |
| 299 | + optim = mpc.optim |
| 300 | + model = mpc.estim.model |
| 301 | + ΔŨvar::Vector{VariableRef} = optim[:ΔŨvar] |
| 302 | + lastΔŨ = mpc.ΔŨ |
| 303 | + # initial ΔŨ (warm-start): [Δu_{k-1}(k); Δu_{k-1}(k+1); ... ; 0_{nu × 1}; ϵ_{k-1}] |
| 304 | + ϵ0 = !isinf(mpc.C) ? [lastΔŨ[end]] : empty(mpc.ΔŨ) |
| 305 | + ΔŨ0 = [lastΔŨ[(model.nu+1):(mpc.Hc*model.nu)]; zeros(NT, model.nu); ϵ0] |
| 306 | + set_start_value.(ΔŨvar, ΔŨ0) |
| 307 | + set_objective_linear_coef!(mpc, ΔŨvar) |
| 308 | + try |
| 309 | + optimize!(optim) |
| 310 | + catch err |
| 311 | + if isa(err, MOI.UnsupportedAttribute{MOI.VariablePrimalStart}) |
| 312 | + # reset_optimizer to unset warm-start, set_start_value.(nothing) seems buggy |
| 313 | + MOIU.reset_optimizer(optim) |
| 314 | + optimize!(optim) |
| 315 | + else |
| 316 | + rethrow(err) |
| 317 | + end |
| 318 | + end |
| 319 | + status = termination_status(optim) |
| 320 | + ΔŨcurr, ΔŨlast = value.(ΔŨvar), ΔŨ0 |
| 321 | + if !(status == OPTIMAL || status == LOCALLY_SOLVED) |
| 322 | + if isfatal(status) |
| 323 | + @error("MPC terminated without solution: returning last solution shifted", |
| 324 | + status, ΔŨcurr, ΔŨlast) |
| 325 | + else |
| 326 | + @warn("MPC termination status not OPTIMAL or LOCALLY_SOLVED: keeping "* |
| 327 | + "solution anyway", status, ΔŨcurr, ΔŨlast) |
| 328 | + end |
| 329 | + @debug solution_summary(optim, verbose=true) |
| 330 | + end |
| 331 | + mpc.ΔŨ[:] = isfatal(status) ? ΔŨlast : ΔŨcurr |
| 332 | + return mpc.ΔŨ |
| 333 | +end |
| 334 | + |
| 335 | +"By default, no need to modify the objective function." |
| 336 | +set_objective_linear_coef!(::PredictiveController, _ ) = nothing |
| 337 | + |
| 338 | +""" |
| 339 | + updatestate!(mpc::PredictiveController, u, ym, d=[]) -> x̂ |
| 340 | +
|
| 341 | +Call [`updatestate!`](@ref) on `mpc.estim` [`StateEstimator`](@ref). |
| 342 | +""" |
| 343 | +updatestate!(mpc::PredictiveController, u, ym, d=empty(mpc.estim.x̂)) = updatestate!(mpc.estim,u,ym,d) |
| 344 | +updatestate!(::PredictiveController, _ ) = throw(ArgumentError("missing measured outputs ym")) |
0 commit comments