Skip to content

Commit 36a48aa

Browse files
committed
Merge branch 'master' into myb/obsfix
2 parents 56329e1 + 422c653 commit 36a48aa

25 files changed

+998
-442
lines changed

Project.toml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name = "ModelingToolkit"
22
uuid = "961ee093-0014-501f-94e3-6117800e7a78"
33
authors = ["Yingbo Ma <[email protected]>", "Chris Rackauckas <[email protected]> and contributors"]
4-
version = "8.23.0"
4+
version = "8.25.0"
55

66
[deps]
77
AbstractTrees = "1520ce14-60c1-5f80-bbc7-55ef81b5835c"
@@ -36,6 +36,7 @@ RuntimeGeneratedFunctions = "7e49a35a-f44a-4d26-94aa-eba1b4ca6b47"
3636
SciMLBase = "0bca4576-84f4-4d90-8ffe-ffa030f20462"
3737
Serialization = "9e88b42a-f829-5b0c-bbe9-9e923198166b"
3838
Setfield = "efcf1570-3423-57d1-acb7-fd33fddbac46"
39+
SimpleWeightedGraphs = "47aef6b3-ad0c-573a-a1e2-d07658019622"
3940
SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf"
4041
SpecialFunctions = "276daf66-3868-5448-9aa4-cd146d93841b"
4142
StaticArrays = "90137ffa-7385-5640-81b9-e52037218182"
@@ -72,6 +73,7 @@ Reexport = "0.2, 1"
7273
RuntimeGeneratedFunctions = "0.4.3, 0.5"
7374
SciMLBase = "1.58.0"
7475
Setfield = "0.7, 0.8, 1"
76+
SimpleWeightedGraphs = "1"
7577
SpecialFunctions = "0.7, 0.8, 0.9, 0.10, 1.0, 2"
7678
StaticArrays = "0.10, 0.11, 0.12, 1.0"
7779
SymbolicUtils = "0.19"
@@ -93,9 +95,9 @@ OptimizationOptimJL = "36348300-93cb-4f02-beb5-3c3902f8871e"
9395
OrdinaryDiffEq = "1dea7af3-3e70-54e6-95c3-0bf5283fa5ed"
9496
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
9597
ReferenceTests = "324d217c-45ce-50fc-942e-d289b448e8cf"
98+
SafeTestsets = "1bc83da4-3b8d-516f-aca4-4fe02f6d838f"
9699
StableRNGs = "860ef19b-820b-49d6-a774-d7a799459cd3"
97100
Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2"
98-
SafeTestsets = "1bc83da4-3b8d-516f-aca4-4fe02f6d838f"
99101
SteadyStateDiffEq = "9672c7b4-1e72-59bd-8a11-6ac3964bc41f"
100102
StochasticDiffEq = "789caeaf-c7a9-5a7d-9973-96adeb23e2a0"
101103
Sundials = "c3572dad-4567-51f8-b174-8c6c989267f4"

docs/pages.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ pages = [
1717
"basics/Variable_metadata.md",
1818
"basics/Composition.md",
1919
"basics/Events.md",
20+
"basics/Linearization.md",
2021
"basics/Validation.md",
2122
"basics/DependencyGraphs.md",
2223
"basics/FAQ.md"],

docs/src/basics/Linearization.md

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# [Linearization](@id linearization)
2+
A nonlinear dynamical system with state (differential and algebraic) ``x`` and input signals ``u``
3+
```math
4+
M \dot x = f(x, u)
5+
```
6+
can be linearized using the function [`linearize`](@ref) to produce a linear statespace system on the form
7+
```math
8+
\begin{aligned}
9+
\dot x &= Ax + Bu\\
10+
y &= Cx + Du
11+
\end{aligned}
12+
```
13+
14+
The `linearize` function expects the user to specify the inputs ``u`` and the outputs ``u`` using the syntax shown in the example below:
15+
16+
## Example
17+
```@example LINEARIZE
18+
using ModelingToolkit
19+
@variables t x(t)=0 y(t)=0 u(t)=0 r(t)=0
20+
@parameters kp = 1
21+
D = Differential(t)
22+
23+
eqs = [u ~ kp * (r - y) # P controller
24+
D(x) ~ -x + u # First-order plant
25+
y ~ x] # Output equation
26+
27+
@named sys = ODESystem(eqs, t)
28+
matrices, simplified_sys = linearize(sys, [r], [y]) # Linearize from r to y
29+
matrices
30+
```
31+
The named tuple `matrices` contains the matrices of the linear statespace representation, while `simplified_sys` is an `ODESystem` that, amongst other things, indicates the state order in the linear system through
32+
```@example LINEARIZE
33+
using ModelingToolkit: inputs, outputs
34+
[states(simplified_sys); inputs(simplified_sys); outputs(simplified_sys)]
35+
```
36+
37+
## Operating point
38+
The operating point to linearize around can be specified with the keyword argument `op` like this: `op = Dict(x => 1, r => 2)`.
39+
40+
## Batch linearization and algebraic variables
41+
If linearization is to be performed around multiple operating points, the simplification of the system has to be carried out a single time only. To facilitate this, the lower-level function [`ModelingToolkit.linearization_function`](@ref) is available. This function further allows you to obtain separate Jacobians for the differential and algebraic parts of the model. For ODE models without algebraic equations, the statespace representation above is available from the output of `linearization_function` as `A, B, C, D = f_x, f_u, h_x, h_u`.
42+
43+
44+
## Input derivatives
45+
Physical systems are always *proper*, i.e., they do not differentiate causal inputs. However, ModelingToolkit allows you to model non-proper systems, such as inverse models, and may sometimes fail to find a realization of a proper system on proper form. In these situations, `linearize` may throw an error mentioning
46+
```
47+
Input derivatives appeared in expressions (-g_z\g_u != 0)
48+
```
49+
This means that to simulate this system, some order of derivatives of the input is required. To allow `linearize` to proceed in this situation, one may pass the keyword argument `allow_input_derivatives = true`, in which case the resulting model will have twice as many inputs, ``2n_u``, where the last ``n_u`` inputs correspond to ``\dot u``.
50+
51+
If the modeled system is actually proper (but MTK failed to find a proper realization), further numerical simplification can be applied to the resulting statespace system to obtain a proper form. Such simplification is currently available in the experimental package [ControlSystemsMTK](https://github.com/baggepinnen/ControlSystemsMTK.jl#internals-transformation-of-non-proper-models-to-proper-statespace-form).
52+
53+
54+
## Tools for linear analysis
55+
[ModelingToolkitStandardLibrary](http://mtkstdlib.sciml.ai/dev/API/linear_analysis/) contains a set of [tools for more advanced linear analysis](http://mtkstdlib.sciml.ai/dev/API/linear_analysis/). These can be used to make it easier to work with and analyze causal models, such as control and signal-processing systems.
56+
57+
```@index
58+
Pages = ["Linearization.md"]
59+
```
60+
61+
```@docs
62+
linearize
63+
ModelingToolkit.linearization_function
64+
```

src/ModelingToolkit.jl

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ include("systems/dependency_graphs.jl")
147147
include("systems/systemstructure.jl")
148148
using .SystemStructures
149149

150+
include("debugging.jl")
150151
include("systems/alias_elimination.jl")
151152
include("structural_transformation/StructuralTransformations.jl")
152153

@@ -176,7 +177,8 @@ export NonlinearSystem, OptimizationSystem
176177
export alias_elimination, flatten
177178
export connect, @connector, Connection, Flow, Stream, instream
178179
export isinput, isoutput, getbounds, hasbounds, isdisturbance, istunable, getdist, hasdist,
179-
tunable_parameters, isirreducible, getdescription, hasdescription
180+
tunable_parameters, isirreducible, getdescription, hasdescription, isbinaryvar,
181+
isintegervar
180182
export ode_order_lowering, dae_order_lowering, liouville_transform
181183
export PDESystem
182184
export Differential, expand_derivatives, @derivatives
@@ -185,7 +187,7 @@ export Term, Sym
185187
export SymScope, LocalScope, ParentScope, GlobalScope
186188
export independent_variables, independent_variable, states, parameters, equations, controls,
187189
observed, structure, full_equations
188-
export structural_simplify, expand_connections, linearize, linear_statespace
190+
export structural_simplify, expand_connections, linearize, linearization_function
189191
export DiscreteSystem, DiscreteProblem
190192

191193
export calculate_jacobian, generate_jacobian, generate_function
@@ -209,5 +211,6 @@ export build_function
209211
export modelingtoolkitize
210212
export @variables, @parameters
211213
export @named, @nonamespace, @namespace, extend, compose
214+
export debug_system
212215

213216
end # module

src/bipartite_graph.jl

Lines changed: 54 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,17 @@ end
6363
function Base.setindex!(m::Matching{U}, v::Union{Integer, U}, i::Integer) where {U}
6464
if m.inv_match !== nothing
6565
oldv = m.match[i]
66-
isa(oldv, Int) && (m.inv_match[oldv] = unassigned)
66+
# TODO: maybe default Matching to always have an `inv_match`?
67+
68+
# To maintain the invariant that `m.inv_match[m.match[i]] == i`, we need
69+
# to unassign the matching at `m.inv_match[v]` if it exists.
70+
if v isa Int && (iv = m.inv_match[v]) isa Int
71+
m.match[iv] = unassigned
72+
end
73+
if isa(oldv, Int)
74+
@assert m.inv_match[oldv] == i
75+
m.inv_match[oldv] = unassigned
76+
end
6777
isa(v, Int) && (m.inv_match[v] = i)
6878
end
6979
return m.match[i] = v
@@ -191,14 +201,49 @@ end
191201
# Matrix whose only purpose is to pretty-print the bipartite graph
192202
struct BipartiteAdjacencyList
193203
u::Union{Vector{Int}, Nothing}
204+
highligh_u::Union{Set{Int}, Nothing}
205+
match::Union{Int, Unassigned}
206+
end
207+
function BipartiteAdjacencyList(u::Union{Vector{Int}, Nothing})
208+
BipartiteAdjacencyList(u, nothing, unassigned)
209+
end
210+
211+
struct HighlightInt
212+
i::Int
213+
highlight::Union{Symbol, Nothing}
214+
end
215+
Base.typeinfo_implicit(::Type{HighlightInt}) = true
216+
217+
function Base.show(io::IO, hi::HighlightInt)
218+
if hi.highlight !== nothing
219+
printstyled(io, hi.i, color = hi.highlight)
220+
else
221+
print(io, hi.i)
222+
end
194223
end
224+
195225
function Base.show(io::IO, l::BipartiteAdjacencyList)
196226
if l.u === nothing
197227
printstyled(io, '', color = :light_black)
198228
elseif isempty(l.u)
199229
printstyled(io, '', color = :light_black)
200-
else
230+
elseif l.highligh_u === nothing
201231
print(io, l.u)
232+
else
233+
function choose_color(i)
234+
i in l.highligh_u ? (i == l.match ? :light_yellow : :green) :
235+
(i == l.match ? :yellow : nothing)
236+
end
237+
if !isempty(setdiff(l.highligh_u, l.u))
238+
# Only for debugging, shouldn't happen in practice
239+
print(io, map(union(l.u, l.highligh_u)) do i
240+
HighlightInt(i, !(i in l.u) ? :light_red : choose_color(i))
241+
end)
242+
else
243+
print(io, map(l.u) do i
244+
HighlightInt(i, choose_color(i))
245+
end)
246+
end
202247
end
203248
end
204249

@@ -435,19 +480,23 @@ function set_neighbors!(g::BipartiteGraph, i::Integer, new_neighbors)
435480
for n in old_neighbors
436481
@inbounds list = g.badjlist[n]
437482
index = searchsortedfirst(list, i)
438-
deleteat!(list, index)
483+
if 1 <= index <= length(list) && list[index] == i
484+
deleteat!(list, index)
485+
end
439486
end
440487
for n in new_neighbors
441488
@inbounds list = g.badjlist[n]
442489
index = searchsortedfirst(list, i)
443-
insert!(list, index, i)
490+
if !(1 <= index <= length(list) && list[index] == i)
491+
insert!(list, index, i)
492+
end
444493
end
445494
end
446495
if iszero(new_nneighbors) # this handles Tuple as well
447496
# Warning: Aliases old_neighbors
448497
empty!(g.fadjlist[i])
449498
else
450-
g.fadjlist[i] = copy(new_neighbors)
499+
g.fadjlist[i] = unique!(sort(new_neighbors))
451500
end
452501
end
453502

src/debugging.jl

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
const LOGGED_FUN = Set([log, sqrt, (^), /, inv])
2+
is_legal(::typeof(/), a, b) = is_legal(inv, b)
3+
is_legal(::typeof(inv), a) = !iszero(a)
4+
is_legal(::Union{typeof(log), typeof(sqrt)}, a) = a isa Complex || a >= zero(a)
5+
is_legal(::typeof(^), a, b) = a isa Complex || b isa Complex || isinteger(b) || a >= zero(a)
6+
7+
struct LoggedFun{F}
8+
f::F
9+
args::Any
10+
end
11+
Base.nameof(lf::LoggedFun) = nameof(lf.f)
12+
SymbolicUtils.promote_symtype(::LoggedFun, Ts...) = Real
13+
function (lf::LoggedFun)(args...)
14+
f = lf.f
15+
symbolic_args = lf.args
16+
if is_legal(f, args...)
17+
f(args...)
18+
else
19+
args_str = join(string.(symbolic_args .=> args), ", ", ", and ")
20+
throw(DomainError(args, "$(lf.f) errors with input(s): $args_str"))
21+
end
22+
end
23+
24+
function logged_fun(f, args...)
25+
# Currently we don't really support complex numbers
26+
term(LoggedFun(f, args), args..., type = Real)
27+
end
28+
29+
debug_sub(eq::Equation) = debug_sub(eq.lhs) ~ debug_sub(eq.rhs)
30+
function debug_sub(ex)
31+
istree(ex) || return ex
32+
f = operation(ex)
33+
args = map(debug_sub, arguments(ex))
34+
f in LOGGED_FUN ? logged_fun(f, args...) :
35+
similarterm(ex, f, args, metadata = metadata(ex))
36+
end

src/inputoutput.jl

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -160,9 +160,9 @@ 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, dvs = states(sys), ps = parameters(sys); implicit_dae = false, ddvs = if implicit_dae
163+
(f_oop, f_ip), dvs, p = generate_control_function(sys::AbstractODESystem, inputs = unbound_inputs(sys); implicit_dae = false, ddvs = if implicit_dae
164164
165-
For a system `sys` that has unbound inputs (as determined by [`unbound_inputs`](@ref)), generate a function with additional input argument `in`
165+
For a system `sys` with inputs (as determined by [`unbound_inputs`](@ref) or user specified), generate a function with additional input argument `in`
166166
```
167167
f_oop : (u,in,p,t) -> rhs
168168
f_ip : (uout,u,in,p,t) -> nothing
@@ -187,7 +187,7 @@ function generate_control_function(sys::AbstractODESystem, inputs = unbound_inpu
187187
error("No unbound inputs were found in system.")
188188
end
189189

190-
sys, diff_idxs, alge_idxs = io_preprocessing(sys, inputs, []; simplify, kwargs...)
190+
sys, _ = io_preprocessing(sys, inputs, []; simplify, kwargs...)
191191

192192
dvs = states(sys)
193193
ps = parameters(sys)
@@ -269,8 +269,8 @@ function inputs_to_parameters!(state::TransformationState, io)
269269
@assert new_v > 0
270270
new_var_to_diff[new_i] = new_v
271271
end
272-
@set! structure.var_to_diff = new_var_to_diff
273-
@set! structure.graph = new_graph
272+
@set! structure.var_to_diff = complete(new_var_to_diff)
273+
@set! structure.graph = complete(new_graph)
274274

275275
@set! sys.eqs = map(Base.Fix2(substitute, input_to_parameters), equations(sys))
276276
@set! sys.states = setdiff(states(sys), keys(input_to_parameters))

src/structural_transformation/StructuralTransformations.jl

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ using ModelingToolkit: ODESystem, AbstractSystem, var_from_nested_derivative, Di
2121
ExtraVariablesSystemException,
2222
get_postprocess_fbody, vars!,
2323
IncrementalCycleTracker, add_edge_checked!, topological_sort,
24-
invalidate_cache!, Substitutions, get_or_construct_tearing_state
24+
invalidate_cache!, Substitutions, get_or_construct_tearing_state,
25+
AliasGraph
2526

2627
using ModelingToolkit.BipartiteGraphs
2728
import .BipartiteGraphs: invview

src/structural_transformation/pantelides.jl

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -74,16 +74,47 @@ end
7474
7575
Perform Pantelides algorithm.
7676
"""
77-
function pantelides!(state::TransformationState; maxiters = 8000)
77+
function pantelides!(state::TransformationState, ag::Union{AliasGraph, Nothing} = nothing;
78+
maxiters = 8000)
7879
@unpack graph, solvable_graph, var_to_diff, eq_to_diff = state.structure
7980
neqs = nsrcs(graph)
8081
nvars = nv(var_to_diff)
8182
vcolor = falses(nvars)
8283
ecolor = falses(neqs)
8384
var_eq_matching = Matching(nvars)
8485
neqs′ = neqs
86+
nnonemptyeqs = count(eq -> !isempty(𝑠neighbors(graph, eq)) && eq_to_diff[eq] === nothing,
87+
1:neqs′)
88+
89+
# Allow matching for the highest differentiated variable that
90+
# currently appears in an equation (or implicit equation in a side ag)
91+
varwhitelist = falses(nvars)
92+
for var in 1:nvars
93+
if var_to_diff[var] === nothing && !varwhitelist[var]
94+
while isempty(𝑑neighbors(graph, var)) && (ag === nothing || !haskey(ag, var))
95+
var′ = invview(var_to_diff)[var]
96+
var′ === nothing && break
97+
var = var′
98+
end
99+
if !isempty(𝑑neighbors(graph, var))
100+
if ag !== nothing && haskey(ag, var)
101+
# TODO: remove lower diff vars from whitelist
102+
c, a = ag[var]
103+
iszero(c) || (varwhitelist[a] = true)
104+
else
105+
varwhitelist[var] = true
106+
end
107+
end
108+
end
109+
end
110+
111+
if nnonemptyeqs > count(varwhitelist)
112+
throw(InvalidSystemException("System is structurally singular"))
113+
end
114+
85115
for k in 1:neqs′
86116
eq′ = k
117+
eq_to_diff[eq′] === nothing || continue
87118
isempty(𝑠neighbors(graph, eq′)) && continue
88119
pathfound = false
89120
# In practice, `maxiters=8000` should never be reached, otherwise, the
@@ -93,7 +124,6 @@ function pantelides!(state::TransformationState; maxiters = 8000)
93124
#
94125
# the derivatives and algebraic variables are zeros in the variable
95126
# association list
96-
varwhitelist = var_to_diff .== nothing
97127
resize!(vcolor, nvars)
98128
fill!(vcolor, false)
99129
resize!(ecolor, neqs)
@@ -103,11 +133,16 @@ function pantelides!(state::TransformationState; maxiters = 8000)
103133
pathfound && break # terminating condition
104134
for var in eachindex(vcolor)
105135
vcolor[var] || continue
106-
# introduce a new variable
107-
nvars += 1
108-
var_diff = var_derivative!(state, var)
109-
push!(var_eq_matching, unassigned)
110-
@assert length(var_eq_matching) == var_diff
136+
if var_to_diff[var] === nothing
137+
# introduce a new variable
138+
nvars += 1
139+
var_diff = var_derivative!(state, var)
140+
push!(var_eq_matching, unassigned)
141+
push!(varwhitelist, false)
142+
@assert length(var_eq_matching) == var_diff
143+
end
144+
varwhitelist[var] = false
145+
varwhitelist[var_to_diff[var]] = true
111146
end
112147

113148
for eq in eachindex(ecolor)

0 commit comments

Comments
 (0)