Skip to content

Commit 10edb8b

Browse files
authored
Merge pull request #1853 from SciML/fb/disturbance_models
add disturbance models
2 parents 8274396 + 9f7997c commit 10edb8b

File tree

3 files changed

+190
-8
lines changed

3 files changed

+190
-8
lines changed

src/inputoutput.jl

Lines changed: 112 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -160,15 +160,24 @@ has_var(ex, x) = x ∈ Set(get_variables(ex))
160160
# Build control function
161161

162162
"""
163-
(f_oop, f_ip), dvs, p = generate_control_function(sys::AbstractODESystem, inputs = unbound_inputs(sys); implicit_dae = false, ddvs = if implicit_dae
163+
(f_oop, f_ip), dvs, p = generate_control_function(
164+
sys::AbstractODESystem,
165+
inputs = unbound_inputs(sys),
166+
disturbance_inputs = nothing;
167+
implicit_dae = false,
168+
simplify = false,
169+
)
164170
165171
For a system `sys` with inputs (as determined by [`unbound_inputs`](@ref) or user specified), generate a function with additional input argument `in`
166172
```
167-
f_oop : (u,in,p,t) -> rhs
168-
f_ip : (uout,u,in,p,t) -> nothing
173+
f_oop : (x,u,p,t) -> rhs
174+
f_ip : (xout,x,u,p,t) -> nothing
169175
```
170176
The return values also include the remaining states and parameters, in the order they appear as arguments to `f`.
171177
178+
If `disturbance_inputs` is an array of variables, the generated dynamics function will preserve any state and dynamics associated with distrubance inputs, but the distrubance inputs themselves will not be included as inputs to the generated function. The use case for this is to generate dynamics for state observers that estimate the influence of unmeasured disturbances, and thus require state variables for the disturbance model, but without disturbance inputs since the disturbances are not available for measurement.
179+
See [`add_input_disturbance`](@ref) for a higher-level interface to this functionality.
180+
172181
# Example
173182
```
174183
using ModelingToolkit: generate_control_function, varmap_to_vars, defaults
@@ -179,22 +188,38 @@ t = 0
179188
f[1](x, inputs, p, t)
180189
```
181190
"""
182-
function generate_control_function(sys::AbstractODESystem, inputs = unbound_inputs(sys);
191+
function generate_control_function(sys::AbstractODESystem, inputs = unbound_inputs(sys),
192+
disturbance_inputs = disturbances(sys);
183193
implicit_dae = false,
184194
simplify = false,
185195
kwargs...)
186196
if isempty(inputs)
187197
error("No unbound inputs were found in system.")
188198
end
189199

200+
if disturbance_inputs !== nothing
201+
# add to inputs for the purposes of io processing
202+
inputs = [inputs; disturbance_inputs]
203+
end
204+
190205
sys, _ = io_preprocessing(sys, inputs, []; simplify, kwargs...)
191206

192207
dvs = states(sys)
193208
ps = parameters(sys)
194209
ps = setdiff(ps, inputs)
210+
if disturbance_inputs !== nothing
211+
# remove from inputs since we do not want them as actual inputs to the dynamics
212+
inputs = setdiff(inputs, disturbance_inputs)
213+
# ps = [ps; disturbance_inputs]
214+
end
195215
inputs = map(x -> time_varying_as_func(value(x), sys), inputs)
196216

197217
eqs = [eq for eq in equations(sys) if !isdifferenceeq(eq)]
218+
if disturbance_inputs !== nothing
219+
# Set all disturbance *inputs* to zero (we just want to keep the disturbance state)
220+
subs = Dict(disturbance_inputs .=> 0)
221+
eqs = [eq.lhs ~ substitute(eq.rhs, subs) for eq in eqs]
222+
end
198223
check_operator_variables(eqs, Differential)
199224
# substitute x(t) by just x
200225
rhss = implicit_dae ? [_iszero(eq.lhs) ? eq.rhs : eq.rhs - eq.lhs for eq in eqs] :
@@ -294,3 +319,86 @@ function inputs_to_parameters!(state::TransformationState, io)
294319
base_params = length(ps)
295320
return state, (base_params + 1):(base_params + length(new_parameters)) # (1:length(new_parameters)) .+ base_params
296321
end
322+
323+
"""
324+
DisturbanceModel{M}
325+
326+
The structure represents a model of a disturbance, along with the input variable that is affected by the disturbance. See [`add_input_disturbance`](@ref) for additional details and an example.
327+
328+
# Fields:
329+
- `input`: The variable affected by the disturbance.
330+
- `model::M`: A model of the disturbance. This is typically an `ODESystem`, but type that implements [`ModelingToolkit.get_disturbance_system`](@ref)`(dist::DisturbanceModel) -> ::ODESystem` is supported.
331+
"""
332+
struct DisturbanceModel{M}
333+
input::Any
334+
model::M
335+
end
336+
337+
# Point of overloading for libraries, e.g., to be able to support disturbance models from ControlSystemsBase
338+
function get_disturbance_system(dist::DisturbanceModel{<:ODESystem})
339+
dist.model
340+
end
341+
342+
"""
343+
(f_oop, f_ip), augmented_sys, dvs, p = add_input_disturbance(sys, dist::DisturbanceModel)
344+
345+
Add a model of an unmeasured disturbance to `sys`. The disturbance model is an instance of [`DisturbanceModel`](@ref).
346+
347+
The generated dynamics functions `(f_oop, f_ip)` will preserve any state and dynamics associated with disturbance inputs, but the disturbance inputs themselves will not be included as inputs to the generated function. The use case for this is to generate dynamics for state observers that estimate the influence of unmeasured disturbances, and thus require state variables for the disturbance model, but without disturbance inputs since the disturbances are not available for measurement.
348+
349+
`dvs` will be the states of the simplified augmented system, consisting of the states of `sys` as well as the states of the disturbance model.
350+
351+
# Example
352+
The example below builds a double-mass model and adds an integrating disturbance to the input
353+
```julia
354+
using ModelingToolkit
355+
using ModelingToolkitStandardLibrary.Mechanical.Rotational
356+
using ModelingToolkitStandardLibrary.Blocks
357+
using ModelingToolkitStandardLibrary.Blocks: t
358+
359+
# Parameters
360+
m1 = 1
361+
m2 = 1
362+
k = 1000 # Spring stiffness
363+
c = 10 # Damping coefficient
364+
365+
@named inertia1 = Inertia(; J = m1)
366+
@named inertia2 = Inertia(; J = m2)
367+
@named spring = Spring(; c = k)
368+
@named damper = Damper(; d = c)
369+
@named torque = Torque()
370+
371+
eqs = [
372+
connect(torque.flange, inertia1.flange_a)
373+
connect(inertia1.flange_b, spring.flange_a, damper.flange_a)
374+
connect(inertia2.flange_a, spring.flange_b, damper.flange_b)
375+
]
376+
if u !== nothing
377+
push!(eqs, connect(torque.tau, u.output))
378+
return @named model = ODESystem(eqs, t; systems = [torque, inertia1, inertia2, spring, damper, u])
379+
end
380+
model = ODESystem(eqs, t; systems = [torque, inertia1, inertia2, spring, damper], name)
381+
model_outputs = [model.inertia1.w, model.inertia2.w, model.inertia1.phi, model.inertia2.phi]
382+
383+
# Disturbance model
384+
@named dmodel = Blocks.StateSpace([0.0], [1.0], [1.0], [0.0]) # An integrating disturbance
385+
dist = ModelingToolkit.DisturbanceModel(model.torque.tau.u, dmodel)
386+
(f_oop, f_ip), augmented_sys, dvs, p = ModelingToolkit.add_input_disturbance(model, dist)
387+
```
388+
`f_oop` will have an extra state corresponding to the integrator in the disturbance model. This state will not be affected by any input, but will affect the dynamics from where it enters, in this case it will affect additively from `model.torque.tau.u`.
389+
"""
390+
function add_input_disturbance(sys, dist::DisturbanceModel)
391+
t = get_iv(sys)
392+
@variables d(t)=0 [disturbance = true]
393+
@variables u(t)=0 [input = true]
394+
dsys = get_disturbance_system(dist)
395+
396+
eqs = [dsys.input.u[1] ~ d
397+
dist.input ~ u + dsys.output.u[1]]
398+
399+
augmented_sys = ODESystem(eqs, t, systems = [sys, dsys], name = gensym(:outer))
400+
401+
(f_oop, f_ip), dvs, p = generate_control_function(augmented_sys, [u],
402+
[d])
403+
(f_oop, f_ip), augmented_sys, dvs, p
404+
end

src/variables.jl

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,10 @@ function isdisturbance(x)
158158
Symbolics.getmetadata(x, VariableDisturbance, false)
159159
end
160160

161+
function disturbances(sys)
162+
[filter(isdisturbance, states(sys)); filter(isdisturbance, parameters(sys))]
163+
end
164+
161165
## Tunable =====================================================================
162166
struct VariableTunable end
163167
Symbolics.option_to_metadata_type(::Val{:tunable}) = VariableTunable

test/input_output_handling.jl

Lines changed: 74 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -140,21 +140,21 @@ function Mass(; name, m = 1.0, p = 0, v = 0)
140140
ODESystem(eqs, t, [pos, vel, y], ps; name)
141141
end
142142

143-
function Spring(; name, k = 1e4)
143+
function MySpring(; name, k = 1e4)
144144
ps = @parameters k = k
145145
@variables x(t) = 0 # Spring deflection
146146
ODESystem(Equation[], t, [x], ps; name)
147147
end
148148

149-
function Damper(; name, c = 10)
149+
function MyDamper(; name, c = 10)
150150
ps = @parameters c = c
151151
@variables vel(t) = 0
152152
ODESystem(Equation[], t, [vel], ps; name)
153153
end
154154

155155
function SpringDamper(; name, k = false, c = false)
156-
spring = Spring(; name = :spring, k)
157-
damper = Damper(; name = :damper, c)
156+
spring = MySpring(; name = :spring, k)
157+
damper = MyDamper(; name = :damper, c)
158158
compose(ODESystem(Equation[], t; name),
159159
spring, damper)
160160
end
@@ -195,3 +195,73 @@ i = findfirst(isequal(u[1]), out)
195195
eqs = [Differential(t)(x) ~ u]
196196
@named sys = ODESystem(eqs, t)
197197
@test_nowarn structural_simplify(sys)
198+
199+
#=
200+
## Disturbance input handling
201+
We test that the generated disturbance dynamics is correct by calling the dynamics in two different points that differ in the disturbance state, and check that we get the same result as when we call the linearized dynamics in the same two points. The true system is linear so the linearized dynamics are exact.
202+
203+
The test below builds a double-mass model and adds an integrating disturbance to the input
204+
=#
205+
206+
using ModelingToolkit
207+
using ModelingToolkitStandardLibrary.Mechanical.Rotational
208+
using ModelingToolkitStandardLibrary.Blocks
209+
@parameters t
210+
211+
# Parameters
212+
m1 = 1
213+
m2 = 1
214+
k = 1000 # Spring stiffness
215+
c = 10 # Damping coefficient
216+
217+
@named inertia1 = Rotational.Inertia(; J = m1)
218+
@named inertia2 = Rotational.Inertia(; J = m2)
219+
@named spring = Rotational.Spring(; c = k)
220+
@named damper = Rotational.Damper(; d = c)
221+
@named torque = Rotational.Torque()
222+
223+
function SystemModel(u = nothing; name = :model)
224+
eqs = [connect(torque.flange, inertia1.flange_a)
225+
connect(inertia1.flange_b, spring.flange_a, damper.flange_a)
226+
connect(inertia2.flange_a, spring.flange_b, damper.flange_b)]
227+
if u !== nothing
228+
push!(eqs, connect(torque.tau, u.output))
229+
return @named model = ODESystem(eqs, t;
230+
systems = [
231+
torque,
232+
inertia1,
233+
inertia2,
234+
spring,
235+
damper,
236+
u,
237+
])
238+
end
239+
ODESystem(eqs, t; systems = [torque, inertia1, inertia2, spring, damper], name)
240+
end
241+
242+
model = SystemModel() # Model with load disturbance
243+
model_outputs = [model.inertia1.w, model.inertia2.w, model.inertia1.phi, model.inertia2.phi]
244+
245+
@named dmodel = Blocks.StateSpace([0.0], [1.0], [1.0], [0.0]) # An integrating disturbance
246+
247+
dist = ModelingToolkit.DisturbanceModel(model.torque.tau.u, dmodel)
248+
(f_oop, f_ip), outersys, dvs, p = ModelingToolkit.add_input_disturbance(model, dist)
249+
250+
@unpack u, d = outersys
251+
matrices, ssys = linearize(outersys, [u, d], model_outputs)
252+
253+
def = ModelingToolkit.defaults(outersys)
254+
255+
# Create a perturbation in the disturbance state
256+
dstate = setdiff(dvs, model_outputs)[]
257+
x_add = ModelingToolkit.varmap_to_vars(merge(Dict(dvs .=> 0), Dict(dstate => 1)), dvs)
258+
259+
x0 = randn(5)
260+
x1 = copy(x0) + x_add # add disturbance state perturbation
261+
u = randn(1)
262+
pn = ModelingToolkit.varmap_to_vars(def, p)
263+
xp0 = f_oop(x0, u, pn, 0)
264+
xp1 = f_oop(x1, u, pn, 0)
265+
266+
@test xp0 matrices.A * x0 + matrices.B * [u; 0]
267+
@test xp1 matrices.A * x1 + matrices.B * [u; 0]

0 commit comments

Comments
 (0)