Skip to content

Commit 87872dd

Browse files
Merge pull request #3462 from TorkelE/add_toplevel_getter_kwarg
add toplevel arguments to various getters
2 parents 9c415dc + 9a19c1d commit 87872dd

File tree

5 files changed

+258
-14
lines changed

5 files changed

+258
-14
lines changed

docs/src/basics/AbstractSystem.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@ Optionally, a system could have:
6969
Note that if you know a system is an `AbstractTimeDependentSystem` you could use `get_iv` to get the
7070
unique independent variable directly, rather than using `independent_variables(sys)[1]`, which is clunky and may cause problems if `sys` is an `AbstractMultivariateSystem` because there may be more than one independent variable. `AbstractTimeIndependentSystem`s do not have a method `get_iv`, and `independent_variables(sys)` will return a size-zero result for such. For an `AbstractMultivariateSystem`, `get_ivs` is equivalent.
7171

72+
For the `parameters`, `unknowns`, `continuous_events`, and `discrete_events` accessors there are corresponding `parameters_toplevel`, `unknowns_toplevel`, `continuous_events_toplevel`, and `discrete_events_toplevel` accessors which work similarly, but ignore the content of subsystems. Furthermore, a `equations_toplevel` version of `equations` exists as well, however, it can only be applied to non-complete systems.
73+
7274
A system could also have caches:
7375

7476
- `get_jac(sys)`: The Jacobian of a system.

src/systems/abstractsystem.jl

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1315,6 +1315,18 @@ function unknowns(sys::AbstractSystem)
13151315
unique(nonunique_unknowns)
13161316
end
13171317

1318+
"""
1319+
unknowns_toplevel(sys::AbstractSystem)
1320+
1321+
Replicates the behaviour of `unknowns`, but ignores unknowns of subsystems.
1322+
"""
1323+
function unknowns_toplevel(sys::AbstractSystem)
1324+
if has_parent(sys) && (parent = get_parent(sys)) !== nothing
1325+
return unknowns_toplevel(parent)
1326+
end
1327+
return get_unknowns(sys)
1328+
end
1329+
13181330
"""
13191331
$(TYPEDSIGNATURES)
13201332
@@ -1355,6 +1367,18 @@ function dependent_parameters(sys::AbstractSystem)
13551367
return map(eq -> eq.lhs, parameter_dependencies(sys))
13561368
end
13571369

1370+
"""
1371+
parameters_toplevel(sys::AbstractSystem)
1372+
1373+
Replicates the behaviour of `parameters`, but ignores parameters of subsystems.
1374+
"""
1375+
function parameters_toplevel(sys::AbstractSystem)
1376+
if has_parent(sys) && (parent = get_parent(sys)) !== nothing
1377+
return parameters_toplevel(parent)
1378+
end
1379+
return get_ps(sys)
1380+
end
1381+
13581382
"""
13591383
$(TYPEDSIGNATURES)
13601384
Get the parameter dependencies of the system `sys` and its subsystems.
@@ -1565,6 +1589,22 @@ function equations(sys::AbstractSystem)
15651589
end
15661590
end
15671591

1592+
"""
1593+
equations_toplevel(sys::AbstractSystem)
1594+
1595+
Replicates the behaviour of `equations`, but ignores equations of subsystems.
1596+
1597+
Notes:
1598+
- Cannot be applied to non-complete systems.
1599+
"""
1600+
function equations_toplevel(sys::AbstractSystem)
1601+
iscomplete(sys) && error("Cannot apply `equations_toplevel` to complete systems.")
1602+
if has_parent(sys) && (parent = get_parent(sys)) !== nothing
1603+
return equations_toplevel(parent)
1604+
end
1605+
return get_eqs(sys)
1606+
end
1607+
15681608
"""
15691609
$(TYPEDSIGNATURES)
15701610

src/systems/callbacks.jl

Lines changed: 44 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -94,17 +94,17 @@ A [`ContinuousCallback`](@ref SciMLBase.ContinuousCallback) specified symbolical
9494
as well as the positive-edge `affect` and negative-edge `affect_neg` that apply when *any* of `eq` are satisfied.
9595
By default `affect_neg = affect`; to only get rising edges specify `affect_neg = nothing`.
9696
97-
Assume without loss of generality that the equation is of the form `c(u,p,t) ~ 0`; we denote the integrator state as `i.u`.
97+
Assume without loss of generality that the equation is of the form `c(u,p,t) ~ 0`; we denote the integrator state as `i.u`.
9898
For compactness, we define `prev_sign = sign(c(u[t-1], p[t-1], t-1))` and `cur_sign = sign(c(u[t], p[t], t))`.
99-
A condition edge will be detected and the callback will be invoked iff `prev_sign * cur_sign <= 0`.
99+
A condition edge will be detected and the callback will be invoked iff `prev_sign * cur_sign <= 0`.
100100
The positive edge `affect` will be triggered iff an edge is detected and if `prev_sign < 0`; similarly, `affect_neg` will be
101-
triggered iff an edge is detected and `prev_sign > 0`.
101+
triggered iff an edge is detected and `prev_sign > 0`.
102102
103-
Inter-sample condition activation is not guaranteed; for example if we use the dirac delta function as `c` to insert a
103+
Inter-sample condition activation is not guaranteed; for example if we use the dirac delta function as `c` to insert a
104104
sharp discontinuity between integrator steps (which in this example would not normally be identified by adaptivity) then the condition is not
105105
guaranteed to be triggered.
106106
107-
Once detected the integrator will "wind back" through a root-finding process to identify the point when the condition became active; the method used
107+
Once detected the integrator will "wind back" through a root-finding process to identify the point when the condition became active; the method used
108108
is specified by `rootfind` from [`SciMLBase.RootfindOpt`](@ref). If we denote the time when the condition becomes active as `tc`,
109109
the value in the integrator after windback will be:
110110
* `u[tc-epsilon], p[tc-epsilon], tc` if `LeftRootFind` is used,
@@ -116,7 +116,7 @@ it passed through 0.
116116
117117
Multiple callbacks in the same system with different `rootfind` operations will be grouped
118118
by their `rootfind` value into separate VectorContinuousCallbacks in the enumeration order of `SciMLBase.RootfindOpt`. This may cause some callbacks to not fire if several become
119-
active at the same instant. See the `SciMLBase` documentation for more information on the semantic rules.
119+
active at the same instant. See the `SciMLBase` documentation for more information on the semantic rules.
120120
121121
Affects (i.e. `affect` and `affect_neg`) can be specified as either:
122122
* A list of equations that should be applied when the callback is triggered (e.g. `x ~ 3, y ~ 7`) which must be of the form `unknown ~ observed value` where each `unknown` appears only once. Equations will be applied in the order that they appear in the vector; parameters and state updates will become immediately visible to following equations.
@@ -328,13 +328,13 @@ The `SymbolicContinuousCallback`s in the returned vector are structs with two fi
328328
`eqs => affect`.
329329
"""
330330
function continuous_events(sys::AbstractSystem)
331-
obs = get_continuous_events(sys)
332-
filter(!isempty, obs)
331+
cbs = get_continuous_events(sys)
332+
filter(!isempty, cbs)
333333

334334
systems = get_systems(sys)
335-
cbs = [obs;
335+
cbs = [cbs;
336336
reduce(vcat,
337-
(map(o -> namespace_callback(o, s), continuous_events(s))
337+
(map(cb -> namespace_callback(cb, s), continuous_events(s))
338338
for s in systems),
339339
init = SymbolicContinuousCallback[])]
340340
filter(!isempty, cbs)
@@ -356,6 +356,21 @@ function vars!(vars, cb::SymbolicContinuousCallback; op = Differential)
356356
return vars
357357
end
358358

359+
"""
360+
continuous_events_toplevel(sys::AbstractSystem)
361+
362+
Replicates the behaviour of `continuous_events`, but ignores events of subsystems.
363+
364+
Notes:
365+
- Cannot be applied to non-complete systems.
366+
"""
367+
function continuous_events_toplevel(sys::AbstractSystem)
368+
if has_parent(sys) && (parent = get_parent(sys)) !== nothing
369+
return continuous_events_toplevel(parent)
370+
end
371+
return get_continuous_events(sys)
372+
end
373+
359374
#################################### discrete events #####################################
360375

361376
struct SymbolicDiscreteCallback
@@ -483,11 +498,11 @@ The `SymbolicDiscreteCallback`s in the returned vector are structs with two fiel
483498
`condition => affect`.
484499
"""
485500
function discrete_events(sys::AbstractSystem)
486-
obs = get_discrete_events(sys)
501+
cbs = get_discrete_events(sys)
487502
systems = get_systems(sys)
488-
cbs = [obs;
503+
cbs = [cbs;
489504
reduce(vcat,
490-
(map(o -> namespace_callback(o, s), discrete_events(s)) for s in systems),
505+
(map(cb -> namespace_callback(cb, s), discrete_events(s)) for s in systems),
491506
init = SymbolicDiscreteCallback[])]
492507
cbs
493508
end
@@ -514,6 +529,21 @@ function vars!(vars, cb::SymbolicDiscreteCallback; op = Differential)
514529
return vars
515530
end
516531

532+
"""
533+
discrete_events_toplevel(sys::AbstractSystem)
534+
535+
Replicates the behaviour of `discrete_events`, but ignores events of subsystems.
536+
537+
Notes:
538+
- Cannot be applied to non-complete systems.
539+
"""
540+
function discrete_events_toplevel(sys::AbstractSystem)
541+
if has_parent(sys) && (parent = get_parent(sys)) !== nothing
542+
return discrete_events_toplevel(parent)
543+
end
544+
return get_discrete_events(sys)
545+
end
546+
517547
################################# compilation functions ####################################
518548

519549
# handles ensuring that affect! functions work with integrator arguments
@@ -688,7 +718,7 @@ function generate_rootfinding_callback(sys::AbstractTimeDependentSystem,
688718
generate_rootfinding_callback(cbs, sys, dvs, ps; kwargs...)
689719
end
690720
"""
691-
Generate a single rootfinding callback; this happens if there is only one equation in `cbs` passed to
721+
Generate a single rootfinding callback; this happens if there is only one equation in `cbs` passed to
692722
generate_rootfinding_callback and thus we can produce a ContinuousCallback instead of a VectorContinuousCallback.
693723
"""
694724
function generate_single_rootfinding_callback(

test/accessor_functions.jl

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
### Preparations ###
2+
3+
# Fetch packages.
4+
using ModelingToolkit, Test
5+
using ModelingToolkit: t_nounits as t, D_nounits as D
6+
import ModelingToolkit: get_ps, get_unknowns, get_observed, get_eqs, get_continuous_events,
7+
get_discrete_events, namespace_equations
8+
import ModelingToolkit: parameters_toplevel, unknowns_toplevel, equations_toplevel,
9+
continuous_events_toplevel, discrete_events_toplevel
10+
11+
# Creates helper functions.
12+
function all_sets_equal(args...)
13+
for arg in args[2:end]
14+
issetequal(args[1], arg) || return false
15+
end
16+
return true
17+
end
18+
function sym_issubset(set1, set2)
19+
for sym1 in set1
20+
any(isequal(sym1, sym2) for sym2 in set2) || return false
21+
end
22+
return true
23+
end
24+
25+
### Basic Tests ###
26+
27+
# Checks `toplevel = false` argument for various accessors (currently only for `ODESystem`s).
28+
# Compares to `` version, and `get_` functions.
29+
# Checks accessors for parameters, unknowns, equations, observables, and events.
30+
# Some tests looks funny (caused by the formatter).
31+
let
32+
# Prepares model components.
33+
@parameters p_top p_mid1 p_mid2 p_bot d
34+
@variables X_top(t) X_mid1(t) X_mid2(t) X_bot(t) Y(t) O(t)
35+
36+
# Creates the systems (individual and hierarchical).
37+
eqs_top = [
38+
D(X_top) ~ p_top - d * X_top,
39+
D(Y) ~ log(X_top) - Y^2 + 3.0,
40+
O ~ (p_top + d) * X_top + Y
41+
]
42+
eqs_mid1 = [
43+
D(X_mid1) ~ p_mid1 - d * X_mid1^2,
44+
D(Y) ~ D(X_mid1) - Y^3,
45+
O ~ (p_mid1 + d) * X_mid1 + Y
46+
]
47+
eqs_mid2 = [
48+
D(X_mid2) ~ p_mid2 - d * X_mid2,
49+
X_mid2^3 ~ log(X_mid2 + Y) - Y^2 + 3.0,
50+
O ~ (p_mid2 + d) * X_mid2 + Y
51+
]
52+
eqs_bot = [
53+
D(X_bot) ~ p_bot - d * X_bot,
54+
D(Y) ~ -Y^3,
55+
O ~ (p_bot + d) * X_bot + Y
56+
]
57+
cevs = [[t ~ 1.0] => [Y ~ Y + 2.0]]
58+
devs = [(t == 2.0) => [Y ~ Y + 2.0]]
59+
@named sys_bot = ODESystem(
60+
eqs_bot, t; systems = [], continuous_events = cevs, discrete_events = devs)
61+
@named sys_mid2 = ODESystem(
62+
eqs_mid2, t; systems = [], continuous_events = cevs, discrete_events = devs)
63+
@named sys_mid1 = ODESystem(
64+
eqs_mid1, t; systems = [sys_bot], continuous_events = cevs, discrete_events = devs)
65+
@named sys_top = ODESystem(eqs_top, t; systems = [sys_mid1, sys_mid2],
66+
continuous_events = cevs, discrete_events = devs)
67+
sys_bot_comp = complete(sys_bot)
68+
sys_mid2_comp = complete(sys_mid2)
69+
sys_mid1_comp = complete(sys_mid1)
70+
sys_top_comp = complete(sys_top)
71+
sys_bot_ss = structural_simplify(sys_bot)
72+
sys_mid2_ss = structural_simplify(sys_mid2)
73+
sys_mid1_ss = structural_simplify(sys_mid1)
74+
sys_top_ss = structural_simplify(sys_top)
75+
76+
# Checks `parameters1.
77+
@test all_sets_equal(parameters.([sys_bot, sys_bot_comp, sys_bot_ss])..., [d, p_bot])
78+
@test all_sets_equal(parameters.([sys_mid1, sys_mid1_comp, sys_mid1_ss])...,
79+
[d, p_mid1, sys_bot.d, sys_bot.p_bot])
80+
@test all_sets_equal(
81+
parameters.([sys_mid2, sys_mid2_comp, sys_mid2_ss])..., [d, p_mid2])
82+
@test all_sets_equal(parameters.([sys_top, sys_top_comp, sys_top_ss])...,
83+
[d, p_top, sys_mid1.d, sys_mid1.p_mid1, sys_mid1.sys_bot.d,
84+
sys_mid1.sys_bot.p_bot, sys_mid2.d, sys_mid2.p_mid2])
85+
86+
# Checks `parameters_toplevel`. Compares to known parameters and also checks that
87+
# these are subset of what `get_ps` returns.
88+
@test all_sets_equal(
89+
parameters_toplevel.([sys_bot, sys_bot_comp, sys_bot_ss])..., [d, p_bot])
90+
@test all_sets_equal(
91+
parameters_toplevel.([sys_mid1, sys_mid1_comp, sys_mid1_ss])...,
92+
[d, p_mid1])
93+
@test all_sets_equal(
94+
parameters_toplevel.([sys_mid2, sys_mid2_comp, sys_mid2_ss])...,
95+
[d, p_mid2])
96+
@test all_sets_equal(
97+
parameters_toplevel.([sys_top, sys_top_comp, sys_top_ss])..., [d, p_top])
98+
@test all(sym_issubset(parameters_toplevel(sys), get_ps(sys))
99+
for sys in [sys_bot, sys_mid2, sys_mid1, sys_top])
100+
101+
# Checks `unknowns`. O(t) is eliminated by `structural_simplify` and
102+
# must be considered separately.
103+
@test all_sets_equal(unknowns.([sys_bot, sys_bot_comp])..., [O, Y, X_bot])
104+
@test all_sets_equal(unknowns.([sys_bot_ss])..., [Y, X_bot])
105+
@test all_sets_equal(unknowns.([sys_mid1, sys_mid1_comp])...,
106+
[O, Y, X_mid1, sys_bot.Y, sys_bot.O, sys_bot.X_bot])
107+
@test all_sets_equal(unknowns.([sys_mid1_ss])..., [Y, X_mid1, sys_bot.Y, sys_bot.X_bot])
108+
@test all_sets_equal(unknowns.([sys_mid2, sys_mid2_comp])..., [O, Y, X_mid2])
109+
@test all_sets_equal(unknowns.([sys_mid2_ss])..., [Y, X_mid2])
110+
@test all_sets_equal(unknowns.([sys_top, sys_top_comp])...,
111+
[O, Y, X_top, sys_mid1.O, sys_mid1.Y, sys_mid1.X_mid1,
112+
sys_mid1.sys_bot.O, sys_mid1.sys_bot.Y, sys_mid1.sys_bot.X_bot,
113+
sys_mid2.O, sys_mid2.Y, sys_mid2.X_mid2])
114+
@test all_sets_equal(unknowns.([sys_top_ss])...,
115+
[Y, X_top, sys_mid1.Y, sys_mid1.X_mid1, sys_mid1.sys_bot.Y,
116+
sys_mid1.sys_bot.X_bot, sys_mid2.Y, sys_mid2.X_mid2])
117+
118+
# Checks `unknowns_toplevel`. Note that O is not eliminated here (as we go back
119+
# to original parent system). Also checks that outputs are subsets of what `get_unknowns` returns.
120+
@test all_sets_equal(
121+
unknowns_toplevel.([sys_bot, sys_bot_comp, sys_bot_ss])..., [O, Y, X_bot])
122+
@test all_sets_equal(
123+
unknowns_toplevel.([sys_mid1, sys_mid1_comp])..., [O, Y, X_mid1])
124+
@test all_sets_equal(
125+
unknowns_toplevel.([sys_mid2, sys_mid2_comp])..., [O, Y, X_mid2])
126+
@test all_sets_equal(
127+
unknowns_toplevel.([sys_top, sys_top_comp])..., [O, Y, X_top])
128+
@test all(sym_issubset(unknowns_toplevel(sys), get_unknowns(sys))
129+
for sys in [sys_bot, sys_mid1, sys_mid2, sys_top])
130+
131+
# Checks `equations`. Do not check ss equations as these might potentially
132+
# be structurally simplified to new equations.
133+
@test all_sets_equal(equations.([sys_bot, sys_bot_comp])..., eqs_bot)
134+
@test all_sets_equal(
135+
equations.([sys_mid1, sys_mid1_comp])..., [eqs_mid1; namespace_equations(sys_bot)])
136+
@test all_sets_equal(equations.([sys_mid2, sys_mid2_comp])..., eqs_mid2)
137+
@test all_sets_equal(equations.([sys_top, sys_top_comp])...,
138+
[eqs_top; namespace_equations(sys_mid1); namespace_equations(sys_mid2)])
139+
140+
# Checks `equations_toplevel`. Do not check ss equations directly as these
141+
# might potentially be structurally simplified to new equations. Do not check
142+
@test all_sets_equal(equations_toplevel.([sys_bot])..., eqs_bot)
143+
@test all_sets_equal(
144+
equations_toplevel.([sys_mid1])..., eqs_mid1)
145+
@test all_sets_equal(
146+
equations_toplevel.([sys_mid2])..., eqs_mid2)
147+
@test all_sets_equal(equations_toplevel.([sys_top])..., eqs_top)
148+
@test all(sym_issubset(equations_toplevel(sys), get_eqs(sys))
149+
for sys in [sys_bot, sys_mid2, sys_mid1, sys_top])
150+
151+
# Checks `continuous_events_toplevel` and `discrete_events_toplevel` (straightforward
152+
# as I stored the same singe event in all systems). Don't check for non-toplevel cases as
153+
# technically not needed for these tests and name spacing the events is a mess.
154+
mtk_cev = ModelingToolkit.SymbolicContinuousCallback.(cevs)[1]
155+
mtk_dev = ModelingToolkit.SymbolicDiscreteCallback.(devs)[1]
156+
@test all_sets_equal(
157+
continuous_events_toplevel.(
158+
[sys_bot, sys_bot_comp, sys_bot_ss, sys_mid1, sys_mid1_comp, sys_mid1_ss,
159+
sys_mid2, sys_mid2_comp, sys_mid2_ss, sys_top, sys_top_comp, sys_top_ss])...,
160+
[mtk_cev])
161+
@test all_sets_equal(
162+
discrete_events_toplevel.(
163+
[sys_bot, sys_bot_comp, sys_bot_ss, sys_mid1, sys_mid1_comp, sys_mid1_ss,
164+
sys_mid2, sys_mid2_comp, sys_mid2_ss, sys_top, sys_top_comp, sys_top_ss])...,
165+
[mtk_dev])
166+
@test all(sym_issubset(
167+
continuous_events_toplevel(sys), get_continuous_events(sys))
168+
for sys in [sys_bot, sys_mid2, sys_mid1, sys_top])
169+
@test all(sym_issubset(discrete_events_toplevel(sys), get_discrete_events(sys))
170+
for sys in [sys_bot, sys_mid2, sys_mid1, sys_top])
171+
end

test/runtests.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ end
6161
@safetestset "Constants Test" include("constants.jl")
6262
@safetestset "Parameter Dependency Test" include("parameter_dependencies.jl")
6363
@safetestset "Equation Type Accessors Test" include("equation_type_accessors.jl")
64+
@safetestset "System Accessor Functions Test" include("accessor_functions.jl")
6465
@safetestset "Equations with complex values" include("complex.jl")
6566
end
6667
end

0 commit comments

Comments
 (0)