Skip to content

Commit 90369ca

Browse files
authored
Merge pull request #38 from JuliaControl/continuous_nonlinmodel
starting support of continuous `NonLinModel` (RK4 only for now)
2 parents a07fd8a + 6f068ac commit 90369ca

18 files changed

+311
-142
lines changed

Project.toml

+1-1
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.18.1"
4+
version = "0.19.0"
55

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

docs/src/manual/nonlinmpc.md

+10-12
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,8 @@ The plant model is nonlinear:
3535

3636
in which ``g`` is the gravitational acceleration in m/s², ``L``, the pendulum length in m,
3737
``K``, the friction coefficient at the pivot point in kg/s, and ``m``, the mass attached at
38-
the end of the pendulum in kg. Here, the explicit Euler method discretizes the system to
39-
construct a [`NonLinModel`](@ref):
38+
the end of the pendulum in kg. Here, a [fourth order Runge-Kutta method](https://en.wikipedia.org/wiki/Runge%E2%80%93Kutta_methods)
39+
solves the differential equations to construct a [`NonLinModel`](@ref):
4040

4141
```@example 1
4242
using ModelPredictiveControl
@@ -49,17 +49,17 @@ function pendulum(par, x, u)
4949
return [dθ, dω]
5050
end
5151
# declared constants, to avoid type-instability in the f function, for speed:
52-
const par, Ts = (9.8, 0.4, 1.2, 0.3), 0.1
53-
f(x, u, _ ) = x + Ts*pendulum(par, x, u) # Euler method
52+
const par = (9.8, 0.4, 1.2, 0.3)
53+
f(x, u, _ ) = pendulum(par, x, u)
5454
h(x, _ ) = [180/π*x[1]] # [°]
55-
nu, nx, ny = 1, 2, 1
55+
Ts, nu, nx, ny = 0.1, 1, 2, 1
5656
model = NonLinModel(f, h, Ts, nu, nx, ny)
5757
```
5858

5959
The output function ``\mathbf{h}`` converts the ``θ`` angle to degrees. Note that special
6060
characters like ``θ`` can be typed in the Julia REPL or VS Code by typing `\theta` and
61-
pressing the `<TAB>` key. The tuple `par` and `Ts` are declared as constants here to improve
62-
the [performance](https://docs.julialang.org/en/v1/manual/performance-tips/#Avoid-untyped-global-variables).
61+
pressing the `<TAB>` key. The tuple `par` is constant here to improve the [performance](https://docs.julialang.org/en/v1/manual/performance-tips/#Avoid-untyped-global-variables).
62+
Note that a 4th order [`RungeKutta`](@ref) differential equation solver is used by default.
6363
It is good practice to first simulate `model` using [`sim!`](@ref) as a quick sanity check:
6464

6565
```@example 1
@@ -91,7 +91,7 @@ estimator tuning is tested on a plant with a 25 % larger friction coefficient ``
9191

9292
```@example 1
9393
const par_plant = (par[1], par[2], 1.25*par[3], par[4])
94-
f_plant(x, u, _) = x + Ts*pendulum(par_plant, x, u)
94+
f_plant(x, u, _ ) = pendulum(par_plant, x, u)
9595
plant = NonLinModel(f_plant, h, Ts, nu, nx, ny)
9696
res = sim!(estim, N, [0.5], plant=plant, y_noise=[0.5])
9797
plot(res, plotu=false, plotxwithx̂=true)
@@ -184,7 +184,7 @@ function JE(UE, ŶE, _ )
184184
τ, ω = UE[1:end-1], ŶE[2:2:end-1]
185185
return Ts*sum(τ.*ω)
186186
end
187-
empc = NonLinMPC(estim2, Hp=20, Hc=2, Mwt=[0.5, 0], Nwt=[2.5], Ewt=4.5e3, JE=JE)
187+
empc = NonLinMPC(estim2, Hp=20, Hc=2, Mwt=[0.5, 0], Nwt=[2.5], Ewt=3.5e3, JE=JE)
188188
empc = setconstraint!(empc, umin=[-1.5], umax=[+1.5])
189189
```
190190

@@ -245,9 +245,7 @@ We first linearize `model` at the point ``θ = π`` rad and ``ω = τ = 0`` (inv
245245
linmodel = linearize(model, x=[π, 0], u=[0])
246246
```
247247

248-
It is worth mentioning that the Euler method in `model` object is not the best choice for
249-
linearization since its accuracy is low (approximation of a poor approximation). A
250-
[`SteadyKalmanFilter`](@ref) and a [`LinMPC`](@ref) are designed from `linmodel`:
248+
A [`SteadyKalmanFilter`](@ref) and a [`LinMPC`](@ref) are designed from `linmodel`:
251249

252250
```@example 1
253251
kf = SteadyKalmanFilter(linmodel; σQ, σR, nint_u, σQint_u)

docs/src/public/sim_model.md

+14
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,20 @@ LinModel
2929
NonLinModel
3030
```
3131

32+
## Differential Equation Solvers
33+
34+
### DiffSolver
35+
36+
```@docs
37+
ModelPredictiveControl.DiffSolver
38+
```
39+
40+
### RungeKutta
41+
42+
```@docs
43+
RungeKutta
44+
```
45+
3246
## Set Operating Points
3347

3448
```@docs

src/ModelPredictiveControl.jl

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import OSQP, Ipopt
1212

1313

1414
export SimModel, LinModel, NonLinModel
15+
export DiffSolver, RungeKutta
1516
export setop!, setstate!, updatestate!, evaloutput, linearize
1617
export StateEstimator, InternalModel, Luenberger
1718
export SteadyKalmanFilter, KalmanFilter, UnscentedKalmanFilter, ExtendedKalmanFilter

src/controller/nonlinmpc.jl

+4-4
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ This method uses the default state estimator :
145145
146146
# Examples
147147
```jldoctest
148-
julia> model = NonLinModel((x,u,_)->0.5x+u, (x,_)->2x, 10.0, 1, 1, 1);
148+
julia> model = NonLinModel((x,u,_)->0.5x+u, (x,_)->2x, 10.0, 1, 1, 1, solver=nothing);
149149
150150
julia> mpc = NonLinMPC(model, Hp=20, Hc=1, Cwt=1e6)
151151
NonLinMPC controller with a sample time Ts = 10.0 s, Ipopt optimizer, UnscentedKalmanFilter estimator and:
@@ -166,8 +166,8 @@ NonLinMPC controller with a sample time Ts = 10.0 s, Ipopt optimizer, UnscentedK
166166
167167
The optimization relies on [`JuMP`](https://github.com/jump-dev/JuMP.jl) automatic
168168
differentiation (AD) to compute the objective and constraint derivatives. Optimizers
169-
generally benefit from exact derivatives like AD. However, the [`NonLinModel`](@ref) `f`
170-
and `h` functions must be compatible with this feature. See [Automatic differentiation](https://jump.dev/JuMP.jl/stable/manual/nlp/#Automatic-differentiation)
169+
generally benefit from exact derivatives like AD. However, the [`NonLinModel`](@ref)
170+
state-space functions must be compatible with this feature. See [Automatic differentiation](https://jump.dev/JuMP.jl/stable/manual/nlp/#Automatic-differentiation)
171171
for common mistakes when writing these functions.
172172
173173
Note that if `Cwt≠Inf`, the attribute `nlp_scaling_max_gradient` of `Ipopt` is set to
@@ -221,7 +221,7 @@ Use custom state estimator `estim` to construct `NonLinMPC`.
221221
222222
# Examples
223223
```jldoctest
224-
julia> model = NonLinModel((x,u,_)->0.5x+u, (x,_)->2x, 10.0, 1, 1, 1);
224+
julia> model = NonLinModel((x,u,_)->0.5x+u, (x,_)->2x, 10.0, 1, 1, 1, solver=nothing);
225225
226226
julia> estim = UnscentedKalmanFilter(model, σQint_ym=[0.05]);
227227

src/estimator/kalman.jl

+2-2
Original file line numberDiff line numberDiff line change
@@ -434,7 +434,7 @@ represents the measured outputs of ``\mathbf{ĥ}`` function (and unmeasured one
434434
435435
# Examples
436436
```jldoctest
437-
julia> model = NonLinModel((x,u,_)->0.1x+u, (x,_)->2x, 10.0, 1, 1, 1);
437+
julia> model = NonLinModel((x,u,_)->0.1x+u, (x,_)->2x, 10.0, 1, 1, 1, solver=nothing);
438438
439439
julia> estim = UnscentedKalmanFilter(model, σR=[1], nint_ym=[2], σP0int_ym=[1, 1])
440440
UnscentedKalmanFilter estimator with a sample time Ts = 10.0 s, NonLinModel and:
@@ -685,7 +685,7 @@ automatic differentiation.
685685
686686
# Examples
687687
```jldoctest
688-
julia> model = NonLinModel((x,u,_)->0.2x+u, (x,_)->-3x, 5.0, 1, 1, 1);
688+
julia> model = NonLinModel((x,u,_)->0.2x+u, (x,_)->-3x, 5.0, 1, 1, 1, solver=nothing);
689689
690690
julia> estim = ExtendedKalmanFilter(model, σQ=[2], σQint_ym=[2], σP0=[0.1], σP0int_ym=[0.1])
691691
ExtendedKalmanFilter estimator with a sample time Ts = 5.0 s, NonLinModel and:

src/estimator/mhe/construct.jl

+1-1
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ matrix ``\mathbf{P̂}_{k-N_k}(k-N_k+1)`` is estimated with an [`ExtendedKalmanFi
206206
207207
# Examples
208208
```jldoctest
209-
julia> model = NonLinModel((x,u,_)->0.1x+u, (x,_)->2x, 10.0, 1, 1, 1);
209+
julia> model = NonLinModel((x,u,_)->0.1x+u, (x,_)->2x, 10.0, 1, 1, 1, solver=nothing);
210210
211211
julia> estim = MovingHorizonEstimator(model, He=5, σR=[1], σP0=[0.01])
212212
MovingHorizonEstimator estimator with a sample time Ts = 10.0 s, Ipopt optimizer, NonLinModel and:

src/model/linearization.jl

+3-2
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ Jacobians of ``\mathbf{f}`` and ``\mathbf{h}`` functions are automatically compu
2222
2323
# Examples
2424
```jldoctest
25-
julia> model = NonLinModel((x,u,_)->x.^3 + u, (x,_)->x, 0.1, 1, 1, 1);
25+
julia> model = NonLinModel((x,u,_)->x.^3 + u, (x,_)->x, 0.1, 1, 1, 1, solver=nothing);
2626
2727
julia> linmodel = linearize(model, x=[10.0], u=[0.0]);
2828
@@ -40,7 +40,8 @@ julia> linmodel.A
4040
function linearize(model::NonLinModel; x=model.x, u=model.uop, d=model.dop)
4141
u0, d0 = u - model.uop, d - model.dop
4242
xnext, y = similar(x), similar(model.yop)
43-
y = model.h!(y, x, d0) .+ model.yop
43+
model.h!(y, x, d0)
44+
y .+= model.yop
4445
A = ForwardDiff.jacobian((xnext, x) -> model.f!(xnext, x, u0, d0), xnext, x)
4546
Bu = ForwardDiff.jacobian((xnext, u0) -> model.f!(xnext, x, u0, d0), xnext, u0)
4647
Bd = ForwardDiff.jacobian((xnext, d0) -> model.f!(xnext, x, u0, d0), xnext, d0)

src/model/linmodel.jl

+31-21
Original file line numberDiff line numberDiff line change
@@ -35,26 +35,35 @@ end
3535
3636
Construct a linear model from state-space model `sys` with sampling time `Ts` in second.
3737
38-
`Ts` can be omitted when `sys` is discrete-time. Its state-space matrices are:
38+
The system `sys` can be continuous or discrete-time (`Ts` can be omitted for the latter).
39+
For continuous dynamics, its state-space equations are (discrete case in Extended Help):
3940
```math
4041
\begin{aligned}
41-
\mathbf{x}(k+1) &= \mathbf{A x}(k) + \mathbf{B z}(k) \\
42-
\mathbf{y}(k) &= \mathbf{C x}(k) + \mathbf{D z}(k)
42+
\mathbf{ẋ}(t) &= \mathbf{A x}(t) + \mathbf{B z}(t) \\
43+
\mathbf{y}(t) &= \mathbf{C x}(t) + \mathbf{D z}(t)
4344
\end{aligned}
4445
```
4546
with the state ``\mathbf{x}`` and output ``\mathbf{y}`` vectors. The ``\mathbf{z}`` vector
4647
comprises the manipulated inputs ``\mathbf{u}`` and measured disturbances ``\mathbf{d}``,
4748
in any order. `i_u` provides the indices of ``\mathbf{z}`` that are manipulated, and `i_d`,
48-
the measured disturbances. See Extended Help if `sys` is continuous-time, or discrete-time
49-
with `Ts ≠ sys.Ts`.
49+
the measured disturbances. The constructor automatically discretizes continuous systems,
50+
resamples discrete ones if `Ts ≠ sys.Ts`, compute a new realization if not minimal, and
51+
separate the ``\mathbf{z}`` terms in two parts (details in Extended Help). The rest of the
52+
documentation assumes discrete dynamics since all systems end up in this form.
5053
51-
See also [`ss`](https://juliacontrol.github.io/ControlSystems.jl/stable/lib/constructors/#ControlSystemsBase.ss),
52-
[`tf`](https://juliacontrol.github.io/ControlSystems.jl/stable/lib/constructors/#ControlSystemsBase.tf).
54+
See also [`ss`](https://juliacontrol.github.io/ControlSystems.jl/stable/lib/constructors/#ControlSystemsBase.ss)
5355
5456
# Examples
5557
```jldoctest
56-
julia> model = LinModel(ss(0.4, 0.2, 0.3, 0, 0.1))
57-
Discrete-time linear model with a sample time Ts = 0.1 s and:
58+
julia> model1 = LinModel(ss(-0.1, 1.0, 1.0, 0), 2.0) # continuous-time StateSpace
59+
LinModel with a sample time Ts = 2.0 s and:
60+
1 manipulated inputs u
61+
1 states x
62+
1 outputs y
63+
0 measured disturbances d
64+
65+
julia> model2 = LinModel(ss(0.4, 0.2, 0.3, 0, 0.1)) # discrete-time StateSpace
66+
LinModel with a sample time Ts = 0.1 s and:
5867
1 manipulated inputs u
5968
1 states x
6069
1 outputs y
@@ -63,9 +72,9 @@ Discrete-time linear model with a sample time Ts = 0.1 s and:
6372
6473
# Extended Help
6574
!!! details "Extended Help"
66-
State-space matrices are similar if `sys` is continuous (replace ``\mathbf{x}(k+1)``
67-
with ``\mathbf{ẋ}(t)`` and ``k`` with ``t`` on the LHS). In such a case, it's
68-
discretized with [`c2d`](https://juliacontrol.github.io/ControlSystems.jl/stable/lib/constructors/#ControlSystemsBase.c2d)
75+
State-space equations are similar if `sys` is discrete-time (replace ``\mathbf{ẋ}(t)``
76+
with ``\mathbf{x}(k+1)`` and ``k`` with ``t`` on the LHS). Continuous dynamics are
77+
internally discretized using [`c2d`](https://juliacontrol.github.io/ControlSystems.jl/stable/lib/constructors/#ControlSystemsBase.c2d)
6978
and `:zoh` for manipulated inputs, and `:tustin`, for measured disturbances. Lastly, if
7079
`sys` is discrete and the provided argument `Ts ≠ sys.Ts`, the system is resampled by
7180
using the aforementioned discretization methods.
@@ -124,8 +133,8 @@ function LinModel(
124133
sysd_dis = sysd
125134
end
126135
end
127-
# minreal to merge common poles if possible and ensure observability
128-
sys_dis = minreal([sysu_dis sysd_dis])
136+
# minreal to merge common poles if possible and ensure controllability/observability:
137+
sys_dis = minreal([sysu_dis sysd_dis]) # same realization if already minimal
129138
nu = length(i_u)
130139
A = sys_dis.A
131140
Bu = sys_dis.B[:,1:nu]
@@ -144,10 +153,12 @@ Convert to minimal realization state-space when `sys` is a transfer function.
144153
`sys` is equal to ``\frac{\mathbf{y}(s)}{\mathbf{z}(s)}`` for continuous-time, and
145154
``\frac{\mathbf{y}(z)}{\mathbf{z}(z)}``, for discrete-time.
146155
156+
See also [`tf`](https://juliacontrol.github.io/ControlSystems.jl/stable/lib/constructors/#ControlSystemsBase.tf)
157+
147158
# Examples
148159
```jldoctest
149160
julia> model = LinModel([tf(3, [30, 1]) tf(-2, [5, 1])], 0.5, i_d=[2])
150-
Discrete-time linear model with a sample time Ts = 0.5 s and:
161+
LinModel with a sample time Ts = 0.5 s and:
151162
1 manipulated inputs u
152163
2 states x
153164
1 outputs y
@@ -177,9 +188,10 @@ end
177188
178189
Construct the model from the discrete state-space matrices `A, Bu, C, Bd, Dd` directly.
179190
180-
This syntax do not modify the state-space representation provided in argument (`minreal`
181-
is not called). Care must be taken to ensure that the model is controllable and observable.
182-
The optional parameter `NT` explicitly specifies the number type of the matrices.
191+
See [`LinModel(::StateSpace)`](@ref) Extended Help for the meaning of the matrices. This
192+
syntax do not modify the state-space representation provided in argument (`minreal` is not
193+
called). Care must be taken to ensure that the model is controllable and observable. The
194+
optional parameter `NT` explicitly specifies the number type of the matrices.
183195
"""
184196
LinModel{NT}(A, Bu, C, Bd, Dd, Ts) where NT<:Real
185197

@@ -245,6 +257,4 @@ function h!(y, model::LinModel, x, d)
245257
mul!(y, model.C, x, 1, 1)
246258
mul!(y, model.Dd, d, 1, 1)
247259
return nothing
248-
end
249-
250-
typestr(model::LinModel) = "linear"
260+
end

0 commit comments

Comments
 (0)