Skip to content

Commit 179361b

Browse files
authored
Merge pull request #179 from JuliaControl/diff_interface_linearize
added: `linearize!` now uses `DifferentiationInterface.jl`
2 parents bb68996 + 9ff9fc6 commit 179361b

7 files changed

+149
-156
lines changed

src/ModelPredictiveControl.jl

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
module ModelPredictiveControl
22

3-
using PrecompileTools # TODO: remove this dep if possible (with Cache of DI.jl)
3+
using PrecompileTools
44
using LinearAlgebra
55
using Random: randn
66

@@ -13,7 +13,7 @@ using DifferentiationInterface: Constant, Cache
1313
using SparseConnectivityTracer: TracerSparsityDetector
1414
using SparseMatrixColorings: GreedyColoringAlgorithm, sparsity_pattern
1515

16-
import ForwardDiff #TODO: delete this after `linearize!` and `ExtendedKalmanFilter` are updated
16+
import ForwardDiff
1717

1818
import ControlSystemsBase
1919
import ControlSystemsBase: ss, tf, delay
@@ -25,7 +25,7 @@ import JuMP
2525
import JuMP: MOIU, MOI, GenericModel, Model, optimizer_with_attributes, register
2626
import JuMP: @variable, @operator, @constraint, @objective
2727

28-
import PreallocationTools: DiffCache, get_tmp
28+
import PreallocationTools: DiffCache, get_tmp # TODO: remove this dep if possible (with Cache of DI.jl)
2929

3030
import OSQP, Ipopt
3131

src/controller/linmpc.jl

+1-1
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ arguments. This controller allocates memory at each time step for the optimizati
135135
136136
# Arguments
137137
- `model::LinModel` : model used for controller predictions and state estimations.
138-
- `Hp=10+nk`: prediction horizon ``H_p``, `nk` is the number of delays in `model`.
138+
- `Hp=10+nk` : prediction horizon ``H_p``, `nk` is the number of delays in `model`.
139139
- `Hc=2` : control horizon ``H_c``.
140140
- `Mwt=fill(1.0,model.ny)` : main diagonal of ``\mathbf{M}`` weight matrix (vector).
141141
- `Nwt=fill(0.1,model.nu)` : main diagonal of ``\mathbf{N}`` weight matrix (vector).

src/general.jl

+4-25
Original file line numberDiff line numberDiff line change
@@ -6,31 +6,6 @@ const DEFAULT_LWT = 0.0
66
const DEFAULT_CWT = 1e5
77
const DEFAULT_EWT = 0.0
88

9-
"Abstract type for all differentiation buffers."
10-
abstract type DifferentiationBuffer end
11-
12-
function Base.show(io::IO, buffer::DifferentiationBuffer)
13-
return print(io, "DifferentiationBuffer with a $(typeof(buffer.config).name.name)")
14-
end
15-
16-
"Struct with both function and configuration for ForwardDiff Jacobian."
17-
struct JacobianBuffer{FT<:Function, CT<:ForwardDiff.JacobianConfig} <: DifferentiationBuffer
18-
f!::FT
19-
config::CT
20-
end
21-
22-
"Create a JacobianBuffer with in-place function `f!`, output `y` and input `x`."
23-
JacobianBuffer(f!, y, x) = JacobianBuffer(f!, ForwardDiff.JacobianConfig(f!, y, x))
24-
25-
"Compute in-place and return the Jacobian matrix of `buffer.f!` at `x`."
26-
function get_jacobian!(A, buffer::JacobianBuffer, y, x)
27-
return ForwardDiff.jacobian!(A, buffer.f!, y, x, buffer.config)
28-
end
29-
30-
"Init a differentiation result matrix as dense or sparse matrix, as required by `backend`."
31-
init_diffmat(T, backend::AbstractADType, _ , nx , ny) = Matrix{T}(undef, ny, nx)
32-
init_diffmat(T, backend::AutoSparse ,prep , _ , _ ) = similar(sparsity_pattern(prep), T)
33-
349
"Termination status that means 'no solution available'."
3510
const ERROR_STATUSES = (
3611
JuMP.INFEASIBLE, JuMP.DUAL_INFEASIBLE, JuMP.LOCALLY_INFEASIBLE,
@@ -81,6 +56,10 @@ function limit_solve_time(optim::GenericModel, Ts)
8156
end
8257
end
8358

59+
"Init a differentiation result matrix as dense or sparse matrix, as required by `backend`."
60+
init_diffmat(T, backend::AbstractADType, _ , nx , ny) = Matrix{T}(undef, ny, nx)
61+
init_diffmat(T, backend::AutoSparse ,prep , _ , _ ) = similar(sparsity_pattern(prep), T)
62+
8463
"Verify that x and y elements are different using `!==`."
8564
isdifferent(x, y) = any(xi !== yi for (xi, yi) in zip(x, y))
8665

src/model/linearization.jl

+56-81
Original file line numberDiff line numberDiff line change
@@ -1,81 +1,59 @@
1-
"A linearization buffer for the [`linearize`](@ref) function."
2-
struct LinearizationBuffer{
3-
NT <:Real,
4-
JB_FUD <:JacobianBuffer,
5-
JB_FXD <:JacobianBuffer,
6-
JB_FXU <:JacobianBuffer,
7-
JB_HD <:JacobianBuffer,
8-
JB_HX <:JacobianBuffer
9-
}
10-
x::Vector{NT}
11-
u::Vector{NT}
12-
d::Vector{NT}
13-
buffer_f_at_u_d::JB_FUD
14-
buffer_f_at_x_d::JB_FXD
15-
buffer_f_at_x_u::JB_FXU
16-
buffer_h_at_d ::JB_HD
17-
buffer_h_at_x ::JB_HX
18-
function LinearizationBuffer(
19-
x::Vector{NT},
20-
u::Vector{NT},
21-
d::Vector{NT},
22-
buffer_f_at_u_d::JB_FUD,
23-
buffer_f_at_x_d::JB_FXD,
24-
buffer_f_at_x_u::JB_FXU,
25-
buffer_h_at_d::JB_HD,
26-
buffer_h_at_x::JB_HX
27-
) where {NT<:Real, JB_FUD, JB_FXD, JB_FXU, JB_HD, JB_HX}
28-
return new{NT, JB_FUD, JB_FXD, JB_FXU, JB_HD, JB_HX}(
29-
x, u, d,
30-
buffer_f_at_u_d,
31-
buffer_f_at_x_d,
32-
buffer_f_at_x_u,
33-
buffer_h_at_d,
34-
buffer_h_at_x
35-
)
36-
end
37-
38-
end
1+
"""
2+
get_linearization_func(NT, f!, h!, nu, nx, ny, nd, p, backend) -> linfunc!
393
40-
Base.show(io::IO, buffer::LinearizationBuffer) = print(io, "LinearizationBuffer object")
41-
42-
function LinearizationBuffer(NT, f!, h!, nu, nx, ny, nd, p)
43-
x, u, d, f_at_u_d!, f_at_x_d!, f_at_x_u!, h_at_d!, h_at_x! = get_linearize_funcs(
44-
NT, f!, h!, nu, nx, ny, nd, p
45-
)
46-
xnext, y = Vector{NT}(undef, nx), Vector{NT}(undef, ny) # TODO: to replace ?
47-
return LinearizationBuffer(
48-
x, u, d,
49-
JacobianBuffer(f_at_u_d!, xnext, x),
50-
JacobianBuffer(f_at_x_d!, xnext, u),
51-
JacobianBuffer(f_at_x_u!, xnext, d),
52-
JacobianBuffer(h_at_d!, y, x),
53-
JacobianBuffer(h_at_x!, y, d)
54-
)
55-
end
4+
Return the `linfunc!` function that computes the Jacobians of `f!` and `h!` functions.
565
57-
"Get the linearization functions for [`NonLinModel`](@ref) `f!` and `h!` functions."
58-
function get_linearize_funcs(NT, f!, h!, nu, nx, ny, nd, p)
59-
x = Vector{NT}(undef, nx)
60-
u = Vector{NT}(undef, nu)
61-
d = Vector{NT}(undef, nd)
62-
f_at_u_d!(xnext, x) = f!(xnext, x, u, d, p)
63-
f_at_x_d!(xnext, u) = f!(xnext, x, u, d, p)
64-
f_at_x_u!(xnext, d) = f!(xnext, x, u, d, p)
65-
h_at_d!(y, x) = h!(y, x, d, p)
66-
h_at_x!(y, d) = h!(y, x, d, p)
67-
return x, u, d, f_at_u_d!, f_at_x_d!, f_at_x_u!, h_at_d!, h_at_x!
6+
The function has the following signature:
7+
```
8+
linfunc!(xnext, y, A, Bu, C, Bd, Dd, backend, x, u, d, cst_x, cst_u, cst_d) -> nothing
9+
```
10+
and it should modifies in-place all the arguments before `backend`. The `backend` argument
11+
is an `AbstractADType` backend from `DifferentiationInterface`. The `cst_x`, `cst_u` and
12+
`cst_d` are `DifferentiationInterface.Constant` objects with the linearization points.
13+
"""
14+
function get_linearization_func(NT, f!, h!, nu, nx, ny, nd, p, backend)
15+
f_x!(xnext, x, u, d) = f!(xnext, x, u, d, p)
16+
f_u!(xnext, u, x, d) = f!(xnext, x, u, d, p)
17+
f_d!(xnext, d, x, u) = f!(xnext, x, u, d, p)
18+
h_x!(y, x, d) = h!(y, x, d, p)
19+
h_d!(y, d, x) = h!(y, x, d, p)
20+
strict = Val(true)
21+
xnext = zeros(NT, nx)
22+
y = zeros(NT, ny)
23+
x = zeros(NT, nx)
24+
u = zeros(NT, nu)
25+
d = zeros(NT, nd)
26+
cst_x = Constant(x)
27+
cst_u = Constant(u)
28+
cst_d = Constant(d)
29+
A_prep = prepare_jacobian(f_x!, xnext, backend, x, cst_u, cst_d; strict)
30+
Bu_prep = prepare_jacobian(f_u!, xnext, backend, u, cst_x, cst_d; strict)
31+
Bd_prep = prepare_jacobian(f_d!, xnext, backend, d, cst_x, cst_u; strict)
32+
C_prep = prepare_jacobian(h_x!, y, backend, x, cst_d ; strict)
33+
Dd_prep = prepare_jacobian(h_d!, y, backend, d, cst_x ; strict)
34+
function linfunc!(xnext, y, A, Bu, C, Bd, Dd, backend, x, u, d, cst_x, cst_u, cst_d)
35+
# all the arguments before `x` are mutated in this function
36+
jacobian!(f_x!, xnext, A, A_prep, backend, x, cst_u, cst_d)
37+
jacobian!(f_u!, xnext, Bu, Bu_prep, backend, u, cst_x, cst_d)
38+
jacobian!(f_d!, xnext, Bd, Bd_prep, backend, d, cst_x, cst_u)
39+
jacobian!(h_x!, y, C, C_prep, backend, x, cst_d)
40+
jacobian!(h_d!, y, Dd, Dd_prep, backend, d, cst_x)
41+
return nothing
42+
end
43+
return linfunc!
6844
end
6945

46+
7047
@doc raw"""
7148
linearize(model::SimModel; x=model.x0+model.xop, u=model.uop, d=model.dop) -> linmodel
7249
7350
Linearize `model` at the operating points `x`, `u`, `d` and return the [`LinModel`](@ref).
7451
7552
The arguments `x`, `u` and `d` are the linearization points for the state ``\mathbf{x}``,
7653
manipulated input ``\mathbf{u}`` and measured disturbance ``\mathbf{d}``, respectively (not
77-
necessarily an equilibrium, details in Extended Help). The Jacobians of ``\mathbf{f}`` and
78-
``\mathbf{h}`` functions are automatically computed with [`ForwardDiff`](@extref ForwardDiff).
54+
necessarily an equilibrium, details in Extended Help). By default, [`ForwardDiff`](@extref ForwardDiff)
55+
automatically computes the Jacobians of ``\mathbf{f}`` and ``\mathbf{h}`` functions. Modify
56+
the `jacobian` keyword argument at the construction of `model` to swap the backend.
7957
8058
!!! warning
8159
See Extended Help if you get an error like:
@@ -181,8 +159,7 @@ function linearize!(
181159
u0 .= u .- nonlinmodel.uop
182160
d0 .= d .- nonlinmodel.dop
183161
# --- compute the Jacobians at linearization points ---
184-
xnext0::Vector{NT}, y0::Vector{NT} = linmodel.buffer.x, linmodel.buffer.y
185-
get_jacobians!(linmodel, xnext0, y0, nonlinmodel, x0, u0, d0)
162+
linearize_core!(linmodel, nonlinmodel, x0, u0, d0)
186163
# --- compute the nonlinear model output at operating points ---
187164
xnext0, y0 = linmodel.buffer.x, linmodel.buffer.y
188165
h!(y0, nonlinmodel, x0, d0, model.p)
@@ -203,22 +180,20 @@ function linearize!(
203180
return linmodel
204181
end
205182

206-
"Compute the 5 Jacobians of `model` at the linearization point and write them in `linmodel`."
207-
function get_jacobians!(linmodel::LinModel, xnext0, y0, model::SimModel, x0, u0, d0)
208-
linbuffer = model.linbuffer # a LinearizationBuffer object
209-
linbuffer.x .= x0
210-
linbuffer.u .= u0
211-
linbuffer.d .= d0
212-
get_jacobian!(linmodel.A, linbuffer.buffer_f_at_u_d, xnext0, x0)
213-
get_jacobian!(linmodel.Bu, linbuffer.buffer_f_at_x_d, xnext0, u0)
214-
get_jacobian!(linmodel.Bd, linbuffer.buffer_f_at_x_u, xnext0, d0)
215-
get_jacobian!(linmodel.C, linbuffer.buffer_h_at_d, y0, x0)
216-
get_jacobian!(linmodel.Dd, linbuffer.buffer_h_at_x, y0, d0)
183+
"Call `linfunc!` function to compute the Jacobians of `model` at the linearization point."
184+
function linearize_core!(linmodel::LinModel, model::SimModel, x0, u0, d0)
185+
xnext0, y0 = linmodel.buffer.x, linmodel.buffer.y
186+
A, Bu, C, Bd, Dd = linmodel.A, linmodel.Bu, linmodel.C, linmodel.Bd, linmodel.Dd
187+
cst_x = Constant(x0)
188+
cst_u = Constant(u0)
189+
cst_d = Constant(d0)
190+
backend = model.jacobian
191+
model.linfunc!(xnext0, y0, A, Bu, C, Bd, Dd, backend, x0, u0, d0, cst_x, cst_u, cst_d)
217192
return nothing
218193
end
219194

220195
"Copy the state-space matrices of `model` to `linmodel` if `model` is already linear."
221-
function get_jacobians!(linmodel::LinModel, _ , _ , model::LinModel, _ , _ , _)
196+
function linearize_core!(linmodel::LinModel, model::LinModel, _ , _ , _ )
222197
linmodel.A .= model.A
223198
linmodel.Bu .= model.Bu
224199
linmodel.C .= model.C

src/model/nonlinmodel.jl

+46-21
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
struct NonLinModel{
2-
NT<:Real, F<:Function, H<:Function, PT<:Any, DS<:DiffSolver, LB<:LinearizationBuffer
2+
NT<:Real,
3+
F<:Function,
4+
H<:Function,
5+
PT<:Any,
6+
DS<:DiffSolver,
7+
JB<:AbstractADType,
8+
LF<:Function
39
} <: SimModel{NT}
410
x0::Vector{NT}
511
f!::F
@@ -21,11 +27,20 @@ struct NonLinModel{
2127
yname::Vector{String}
2228
dname::Vector{String}
2329
xname::Vector{String}
24-
linbuffer::LB
30+
jacobian::JB
31+
linfunc!::LF
2532
buffer::SimModelBuffer{NT}
2633
function NonLinModel{NT}(
27-
f!::F, h!::H, Ts, nu, nx, ny, nd, p::PT, solver::DS, linbuffer::LB
28-
) where {NT<:Real, F<:Function, H<:Function, PT<:Any, DS<:DiffSolver, LB<:LinearizationBuffer}
34+
f!::F, h!::H, Ts, nu, nx, ny, nd, p::PT, solver::DS, jacobian::JB, linfunc!::LF
35+
) where {
36+
NT<:Real,
37+
F<:Function,
38+
H<:Function,
39+
PT<:Any,
40+
DS<:DiffSolver,
41+
JB<:AbstractADType,
42+
LF<:Function
43+
}
2944
Ts > 0 || error("Sampling time Ts must be positive")
3045
uop = zeros(NT, nu)
3146
yop = zeros(NT, ny)
@@ -39,7 +54,7 @@ struct NonLinModel{
3954
x0 = zeros(NT, nx)
4055
t = zeros(NT, 1)
4156
buffer = SimModelBuffer{NT}(nu, nx, ny, nd)
42-
return new{NT, F, H, PT, DS, LB}(
57+
return new{NT, F, H, PT, DS, JB, LF}(
4358
x0,
4459
f!, h!,
4560
p,
@@ -48,15 +63,15 @@ struct NonLinModel{
4863
nu, nx, ny, nd,
4964
uop, yop, dop, xop, fop,
5065
uname, yname, dname, xname,
51-
linbuffer,
66+
jacobian, linfunc!,
5267
buffer
5368
)
5469
end
5570
end
5671

5772
@doc raw"""
58-
NonLinModel{NT}(f::Function, h::Function, Ts, nu, nx, ny, nd=0; p=[], solver=RungeKutta(4))
59-
NonLinModel{NT}(f!::Function, h!::Function, Ts, nu, nx, ny, nd=0; p=[], solver=RungeKutta(4))
73+
NonLinModel{NT}(f::Function, h::Function, Ts, nu, nx, ny, nd=0; <keyword arguments>)
74+
NonLinModel{NT}(f!::Function, h!::Function, Ts, nu, nx, ny, nd=0; <keyword arguments>)
6075
6176
Construct a nonlinear model from state-space functions `f`/`f!` and `h`/`h!`.
6277
@@ -71,24 +86,20 @@ functions are defined as:
7186
```
7287
where ``\mathbf{x}``, ``\mathbf{y}``, ``\mathbf{u}``, ``\mathbf{d}`` and ``\mathbf{p}`` are
7388
respectively the state, output, manipulated input, measured disturbance and parameter
74-
vectors. If the dynamics is a function of the time, simply add a measured disturbance
75-
defined as ``d(t) = t``. The functions can be implemented in two possible ways:
89+
vectors. As a mather of fact, the parameter argument `p` can be any Julia objects but use a
90+
mutable type if you want to change them later e.g.: a vector. If the dynamics is a function
91+
of the time, simply add a measured disturbance defined as ``d(t) = t``. The functions can be
92+
implemented in two possible ways:
7693
7794
1. **Non-mutating functions** (out-of-place): define them as `f(x, u, d, p) -> ẋ` and
7895
`h(x, d, p) -> y`. This syntax is simple and intuitive but it allocates more memory.
7996
2. **Mutating functions** (in-place): define them as `f!(ẋ, x, u, d, p) -> nothing` and
8097
`h!(y, x, d, p) -> nothing`. This syntax reduces the allocations and potentially the
8198
computational burden as well.
8299
83-
`Ts` is the sampling time in second. `nu`, `nx`, `ny` and `nd` are the respective number of
84-
manipulated inputs, states, outputs and measured disturbances. The keyword argument `p`
85-
is the parameters of the model passed to the two functions. It can be any Julia objects but
86-
use a mutable type if you want to change them later e.g.: a vector.
87-
88100
!!! tip
89101
Replace the `d` or `p` argument with `_` in your functions if not needed (see Examples below).
90102
91-
A 4th order [`RungeKutta`](@ref) solver discretizes the differential equations by default.
92103
The rest of the documentation assumes discrete dynamics since all models end up in this
93104
form. The optional parameter `NT` explicitly set the number type of vectors (default to
94105
`Float64`).
@@ -100,6 +111,20 @@ form. The optional parameter `NT` explicitly set the number type of vectors (def
100111
101112
See also [`LinModel`](@ref).
102113
114+
# Arguments
115+
- `f::Function` or `f!`: state function.
116+
- `h::Function` or `h!`: output function.
117+
- `Ts`: sampling time of in second.
118+
- `nu`: number of manipulated inputs.
119+
- `nx`: number of states.
120+
- `ny`: number of outputs.
121+
- `nd=0`: number of measured disturbances.
122+
- `p=[]`: parameters of the model (any type).
123+
- `solver=RungeKutta(4)`: a [`DiffSolver`](@ref) object for the discretization of continuous
124+
dynamics, use `nothing` for discrete-time models (default to 4th order [`RungeKutta`](@ref)).
125+
- `jacobian=AutoForwardDiff()`: an `AbstractADType` backend when [`linearize`](@ref) is
126+
called, see [`DifferentiationInterface` doc](@extref DifferentiationInterface List).
127+
103128
# Examples
104129
```jldoctest
105130
julia> f!(ẋ, x, u, _ , p) = (ẋ .= p*x .+ u; nothing);
@@ -143,20 +168,20 @@ NonLinModel with a sample time Ts = 2.0 s, empty solver and:
143168
"""
144169
function NonLinModel{NT}(
145170
f::Function, h::Function, Ts::Real, nu::Int, nx::Int, ny::Int, nd::Int=0;
146-
p=NT[], solver=RungeKutta(4)
171+
p=NT[], solver=RungeKutta(4), jacobian=AutoForwardDiff()
147172
) where {NT<:Real}
148173
isnothing(solver) && (solver=EmptySolver())
149174
f!, h! = get_mutating_functions(NT, f, h)
150175
f!, h! = get_solver_functions(NT, solver, f!, h!, Ts, nu, nx, ny, nd)
151-
linbuffer = LinearizationBuffer(NT, f!, h!, nu, nx, ny, nd, p)
152-
return NonLinModel{NT}(f!, h!, Ts, nu, nx, ny, nd, p, solver, linbuffer)
176+
linfunc! = get_linearization_func(NT, f!, h!, nu, nx, ny, nd, p, jacobian)
177+
return NonLinModel{NT}(f!, h!, Ts, nu, nx, ny, nd, p, solver, jacobian, linfunc!)
153178
end
154179

155180
function NonLinModel(
156181
f::Function, h::Function, Ts::Real, nu::Int, nx::Int, ny::Int, nd::Int=0;
157-
p=Float64[], solver=RungeKutta(4)
182+
p=Float64[], solver=RungeKutta(4), jacobian=AutoForwardDiff()
158183
)
159-
return NonLinModel{Float64}(f, h, Ts, nu, nx, ny, nd; p, solver)
184+
return NonLinModel{Float64}(f, h, Ts, nu, nx, ny, nd; p, solver, jacobian)
160185
end
161186

162187
"Get the mutating functions `f!` and `h!` from the provided functions in argument."

0 commit comments

Comments
 (0)