Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/src/basics/AbstractSystem.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ Optionally, a system could have:
Note that if you know a system is an `AbstractTimeDependentSystem` you could use `get_iv` to get the
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.

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 works similarly, but ignores the content of subsystems. Furthermore, a `equations_toplevel` version of `equations` exists as well, however, it can only be applied to non-complete systems.

A system could also have caches:

- `get_jac(sys)`: The Jacobian of a system.
Expand Down
41 changes: 41 additions & 0 deletions src/systems/abstractsystem.jl
Original file line number Diff line number Diff line change
Expand Up @@ -622,7 +622,7 @@
"""
Initial(x)

The `Initial` operator. Used by initializaton to store constant constraints on variables

Check warning on line 625 in src/systems/abstractsystem.jl

View workflow job for this annotation

GitHub Actions / Spell Check with Typos

"initializaton" should be "initialization".
of a system. See the documentation section on initialization for more information.
"""
struct Initial <: Symbolics.Operator end
Expand Down Expand Up @@ -1313,12 +1313,25 @@
unique(nonunique_unknowns)
end

"""
unknowns_toplevel(sys::AbstractSystem)

Replicates the behaviour of `unknowns`, but ignores unknowns of subsystems.
"""
function unknowns_toplevel(sys::AbstractSystem)
if has_parent(sys) && (parent = get_parent(sys)) !== nothing
return unknowns_toplevel(parent)
end
return get_unknowns(sys)
end

"""
$(TYPEDSIGNATURES)

Get the parameters of the system `sys` and its subsystems.

See also [`@parameters`](@ref) and [`ModelingToolkit.get_ps`](@ref).

"""
function parameters(sys::AbstractSystem; initial_parameters = false)
ps = get_ps(sys)
Expand Down Expand Up @@ -1353,6 +1366,18 @@
return map(eq -> eq.lhs, parameter_dependencies(sys))
end

"""
parameters_toplevel(sys::AbstractSystem)

Replicates the behaviour of `parameters`, but ignores parameters of subsystems.
"""
function parameters_toplevel(sys::AbstractSystem)
if has_parent(sys) && (parent = get_parent(sys)) !== nothing
return parameters_toplevel(parent)
end
return get_ps(sys)
end

"""
$(TYPEDSIGNATURES)
Get the parameter dependencies of the system `sys` and its subsystems.
Expand Down Expand Up @@ -1563,6 +1588,22 @@
end
end

"""
equations_toplevel(sys::AbstractSystem)

Replicates the behaviour of `equations`, but ignores equations of subsystems.

Notes:
- Cannot be applied to non-complete systems.
"""
function equations_toplevel(sys::AbstractSystem)
iscomplete(sys) && error("Cannot apply `equations_toplevel` to complete systems.")
if has_parent(sys) && (parent = get_parent(sys)) !== nothing
return equations_toplevel(parent)
end
return get_eqs(sys)
end

"""
$(TYPEDSIGNATURES)

Expand Down
58 changes: 44 additions & 14 deletions src/systems/callbacks.jl
Original file line number Diff line number Diff line change
Expand Up @@ -94,17 +94,17 @@ A [`ContinuousCallback`](@ref SciMLBase.ContinuousCallback) specified symbolical
as well as the positive-edge `affect` and negative-edge `affect_neg` that apply when *any* of `eq` are satisfied.
By default `affect_neg = affect`; to only get rising edges specify `affect_neg = nothing`.

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`.
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`.
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))`.
A condition edge will be detected and the callback will be invoked iff `prev_sign * cur_sign <= 0`.
A condition edge will be detected and the callback will be invoked iff `prev_sign * cur_sign <= 0`.
The positive edge `affect` will be triggered iff an edge is detected and if `prev_sign < 0`; similarly, `affect_neg` will be
triggered iff an edge is detected and `prev_sign > 0`.
triggered iff an edge is detected and `prev_sign > 0`.

Inter-sample condition activation is not guaranteed; for example if we use the dirac delta function as `c` to insert a
Inter-sample condition activation is not guaranteed; for example if we use the dirac delta function as `c` to insert a
sharp discontinuity between integrator steps (which in this example would not normally be identified by adaptivity) then the condition is not
guaranteed to be triggered.

Once detected the integrator will "wind back" through a root-finding process to identify the point when the condition became active; the method used
Once detected the integrator will "wind back" through a root-finding process to identify the point when the condition became active; the method used
is specified by `rootfind` from [`SciMLBase.RootfindOpt`](@ref). If we denote the time when the condition becomes active as `tc`,
the value in the integrator after windback will be:
* `u[tc-epsilon], p[tc-epsilon], tc` if `LeftRootFind` is used,
Expand All @@ -116,7 +116,7 @@ it passed through 0.

Multiple callbacks in the same system with different `rootfind` operations will be grouped
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
active at the same instant. See the `SciMLBase` documentation for more information on the semantic rules.
active at the same instant. See the `SciMLBase` documentation for more information on the semantic rules.

Affects (i.e. `affect` and `affect_neg`) can be specified as either:
* 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.
Expand Down Expand Up @@ -328,13 +328,13 @@ The `SymbolicContinuousCallback`s in the returned vector are structs with two fi
`eqs => affect`.
"""
function continuous_events(sys::AbstractSystem)
obs = get_continuous_events(sys)
filter(!isempty, obs)
cbs = get_continuous_events(sys)
filter(!isempty, cbs)

systems = get_systems(sys)
cbs = [obs;
cbs = [cbs;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is a leftover from the code being copied from the observed getter (?), in which case it would make sense to use cbs and not obs throughout the function.

reduce(vcat,
(map(o -> namespace_callback(o, s), continuous_events(s))
(map(cb -> namespace_callback(cb, s), continuous_events(s))
for s in systems),
init = SymbolicContinuousCallback[])]
filter(!isempty, cbs)
Expand All @@ -356,6 +356,21 @@ function vars!(vars, cb::SymbolicContinuousCallback; op = Differential)
return vars
end

"""
continuous_events_toplevel(sys::AbstractSystem)

Replicates the behaviour of `continuous_events`, but ignores events of subsystems.

Notes:
- Cannot be applied to non-complete systems.
"""
function continuous_events_toplevel(sys::AbstractSystem)
if has_parent(sys) && (parent = get_parent(sys)) !== nothing
return continuous_events_toplevel(parent)
end
return get_continuous_events(sys)
end

#################################### discrete events #####################################

struct SymbolicDiscreteCallback
Expand Down Expand Up @@ -483,11 +498,11 @@ The `SymbolicDiscreteCallback`s in the returned vector are structs with two fiel
`condition => affect`.
"""
function discrete_events(sys::AbstractSystem)
obs = get_discrete_events(sys)
cbs = get_discrete_events(sys)
systems = get_systems(sys)
cbs = [obs;
cbs = [cbs;
reduce(vcat,
(map(o -> namespace_callback(o, s), discrete_events(s)) for s in systems),
(map(cb -> namespace_callback(cb, s), discrete_events(s)) for s in systems),
init = SymbolicDiscreteCallback[])]
cbs
end
Expand All @@ -514,6 +529,21 @@ function vars!(vars, cb::SymbolicDiscreteCallback; op = Differential)
return vars
end

"""
discrete_events_toplevel(sys::AbstractSystem)

Replicates the behaviour of `discrete_events`, but ignores events of subsystems.

Notes:
- Cannot be applied to non-complete systems.
"""
function discrete_events_toplevel(sys::AbstractSystem)
if has_parent(sys) && (parent = get_parent(sys)) !== nothing
return discrete_events_toplevel(parent)
end
return get_discrete_events(sys)
end

################################# compilation functions ####################################

# handles ensuring that affect! functions work with integrator arguments
Expand Down Expand Up @@ -688,7 +718,7 @@ function generate_rootfinding_callback(sys::AbstractTimeDependentSystem,
generate_rootfinding_callback(cbs, sys, dvs, ps; kwargs...)
end
"""
Generate a single rootfinding callback; this happens if there is only one equation in `cbs` passed to
Generate a single rootfinding callback; this happens if there is only one equation in `cbs` passed to
generate_rootfinding_callback and thus we can produce a ContinuousCallback instead of a VectorContinuousCallback.
"""
function generate_single_rootfinding_callback(
Expand Down
171 changes: 171 additions & 0 deletions test/accessor_functions.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
### Preparations ###

# Fetch packages.
using ModelingToolkit, Test
using ModelingToolkit: t_nounits as t, D_nounits as D
import ModelingToolkit: get_ps, get_unknowns, get_observed, get_eqs, get_continuous_events,
get_discrete_events, namespace_equations
import ModelingToolkit: parameters_toplevel, unknowns_toplevel, equations_toplevel,
continuous_events_toplevel, discrete_events_toplevel

# Creates helper functions.
function all_sets_equal(args...)
for arg in args[2:end]
issetequal(args[1], arg) || return false
end
return true
end
function sym_issubset(set1, set2)
for sym1 in set1
any(isequal(sym1, sym2) for sym2 in set2) || return false
end
return true
end

### Basic Tests ###

# Checks `toplevel = false` argument for various accessors (currently only for `ODESystem`s).
# Compares to `` version, and `get_` functions.
# Checks accessors for parameters, unknowns, equations, observables, and events.
# Some tests looks funny (caused by the formatter).
let
# Prepares model components.
@parameters p_top p_mid1 p_mid2 p_bot d
@variables X_top(t) X_mid1(t) X_mid2(t) X_bot(t) Y(t) O(t)

# Creates the systems (individual and hierarchical).
eqs_top = [
D(X_top) ~ p_top - d * X_top,
D(Y) ~ log(X_top) - Y^2 + 3.0,
O ~ (p_top + d) * X_top + Y
]
eqs_mid1 = [
D(X_mid1) ~ p_mid1 - d * X_mid1^2,
D(Y) ~ D(X_mid1) - Y^3,
O ~ (p_mid1 + d) * X_mid1 + Y
]
eqs_mid2 = [
D(X_mid2) ~ p_mid2 - d * X_mid2,
X_mid2^3 ~ log(X_mid2 + Y) - Y^2 + 3.0,
O ~ (p_mid2 + d) * X_mid2 + Y
]
eqs_bot = [
D(X_bot) ~ p_bot - d * X_bot,
D(Y) ~ -Y^3,
O ~ (p_bot + d) * X_bot + Y
]
cevs = [[t ~ 1.0] => [Y ~ Y + 2.0]]
devs = [(t == 2.0) => [Y ~ Y + 2.0]]
@named sys_bot = ODESystem(
eqs_bot, t; systems = [], continuous_events = cevs, discrete_events = devs)
@named sys_mid2 = ODESystem(
eqs_mid2, t; systems = [], continuous_events = cevs, discrete_events = devs)
@named sys_mid1 = ODESystem(
eqs_mid1, t; systems = [sys_bot], continuous_events = cevs, discrete_events = devs)
@named sys_top = ODESystem(eqs_top, t; systems = [sys_mid1, sys_mid2],
continuous_events = cevs, discrete_events = devs)
sys_bot_comp = complete(sys_bot)
sys_mid2_comp = complete(sys_mid2)
sys_mid1_comp = complete(sys_mid1)
sys_top_comp = complete(sys_top)
sys_bot_ss = structural_simplify(sys_bot)
sys_mid2_ss = structural_simplify(sys_mid2)
sys_mid1_ss = structural_simplify(sys_mid1)
sys_top_ss = structural_simplify(sys_top)

# Checks `parameters1.
@test all_sets_equal(parameters.([sys_bot, sys_bot_comp, sys_bot_ss])..., [d, p_bot])
@test all_sets_equal(parameters.([sys_mid1, sys_mid1_comp, sys_mid1_ss])...,
[d, p_mid1, sys_bot.d, sys_bot.p_bot])
@test all_sets_equal(
parameters.([sys_mid2, sys_mid2_comp, sys_mid2_ss])..., [d, p_mid2])
@test all_sets_equal(parameters.([sys_top, sys_top_comp, sys_top_ss])...,
[d, p_top, sys_mid1.d, sys_mid1.p_mid1, sys_mid1.sys_bot.d,
sys_mid1.sys_bot.p_bot, sys_mid2.d, sys_mid2.p_mid2])

# Checks `parameters_toplevel`. Compares to known parameters and also checks that
# these are subset of what `get_ps` returns.
@test all_sets_equal(
parameters_toplevel.([sys_bot, sys_bot_comp, sys_bot_ss])..., [d, p_bot])
@test_broken all_sets_equal(
parameters_toplevel.([sys_mid1, sys_mid1_comp, sys_mid1_ss])...,
[d, p_mid1])
@test all_sets_equal(
parameters_toplevel.([sys_mid2, sys_mid2_comp, sys_mid2_ss])...,
[d, p_mid2])
@test_broken all_sets_equal(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this broken?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because #3464 makes parameters return an internal parameter DEF.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fix for that just got merged and tagged. Could you rebase and try?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will do

parameters_toplevel.([sys_top, sys_top_comp, sys_top_ss])..., [d, p_top])
@test all(sym_issubset(parameters_toplevel(sys), get_ps(sys))
for sys in [sys_bot, sys_mid2, sys_mid1, sys_top])

# Checks `unknowns`. O(t) is eliminated by `structural_simplify` and
# must be considered separately.
@test all_sets_equal(unknowns.([sys_bot, sys_bot_comp])..., [O, Y, X_bot])
@test all_sets_equal(unknowns.([sys_bot_ss])..., [Y, X_bot])
@test all_sets_equal(unknowns.([sys_mid1, sys_mid1_comp])...,
[O, Y, X_mid1, sys_bot.Y, sys_bot.O, sys_bot.X_bot])
@test all_sets_equal(unknowns.([sys_mid1_ss])..., [Y, X_mid1, sys_bot.Y, sys_bot.X_bot])
@test all_sets_equal(unknowns.([sys_mid2, sys_mid2_comp])..., [O, Y, X_mid2])
@test all_sets_equal(unknowns.([sys_mid2_ss])..., [Y, X_mid2])
@test all_sets_equal(unknowns.([sys_top, sys_top_comp])...,
[O, Y, X_top, sys_mid1.O, sys_mid1.Y, sys_mid1.X_mid1,
sys_mid1.sys_bot.O, sys_mid1.sys_bot.Y, sys_mid1.sys_bot.X_bot,
sys_mid2.O, sys_mid2.Y, sys_mid2.X_mid2])
@test all_sets_equal(unknowns.([sys_top_ss])...,
[Y, X_top, sys_mid1.Y, sys_mid1.X_mid1, sys_mid1.sys_bot.Y,
sys_mid1.sys_bot.X_bot, sys_mid2.Y, sys_mid2.X_mid2])

# Checks `unknowns_toplevel`. Note that O is not eliminated here (as we go back
# to original parent system). Also checks that outputs are subsets of what `get_unknowns` returns.
@test all_sets_equal(
unknowns_toplevel.([sys_bot, sys_bot_comp, sys_bot_ss])..., [O, Y, X_bot])
@test all_sets_equal(
unknowns_toplevel.([sys_mid1, sys_mid1_comp])..., [O, Y, X_mid1])
@test all_sets_equal(
unknowns_toplevel.([sys_mid2, sys_mid2_comp])..., [O, Y, X_mid2])
@test all_sets_equal(
unknowns_toplevel.([sys_top, sys_top_comp])..., [O, Y, X_top])
@test all(sym_issubset(unknowns_toplevel(sys), get_unknowns(sys))
for sys in [sys_bot, sys_mid1, sys_mid2, sys_top])

# Checks `equations`. Do not check ss equations as these might potentially
# be structurally simplified to new equations.
@test all_sets_equal(equations.([sys_bot, sys_bot_comp])..., eqs_bot)
@test all_sets_equal(
equations.([sys_mid1, sys_mid1_comp])..., [eqs_mid1; namespace_equations(sys_bot)])
@test all_sets_equal(equations.([sys_mid2, sys_mid2_comp])..., eqs_mid2)
@test all_sets_equal(equations.([sys_top, sys_top_comp])...,
[eqs_top; namespace_equations(sys_mid1); namespace_equations(sys_mid2)])

# Checks `equations_toplevel`. Do not check ss equations directly as these
# might potentially be structurally simplified to new equations. Do not check
@test all_sets_equal(equations_toplevel.([sys_bot])..., eqs_bot)
@test all_sets_equal(
equations_toplevel.([sys_mid1])..., eqs_mid1)
@test all_sets_equal(
equations_toplevel.([sys_mid2])..., eqs_mid2)
@test all_sets_equal(equations_toplevel.([sys_top])..., eqs_top)
@test all(sym_issubset(equations_toplevel(sys), get_eqs(sys))
for sys in [sys_bot, sys_mid2, sys_mid1, sys_top])

# Checks `continuous_events_toplevel` and `discrete_events_toplevel` (straightforward
# as I stored the same singe event in all systems). Don't check for non-toplevel cases as
# technically not needed for these tests and name spacing the events is a mess.
mtk_cev = ModelingToolkit.SymbolicContinuousCallback.(cevs)[1]
mtk_dev = ModelingToolkit.SymbolicDiscreteCallback.(devs)[1]
@test all_sets_equal(
continuous_events_toplevel.(
[sys_bot, sys_bot_comp, sys_bot_ss, sys_mid1, sys_mid1_comp, sys_mid1_ss,
sys_mid2, sys_mid2_comp, sys_mid2_ss, sys_top, sys_top_comp, sys_top_ss])...,
[mtk_cev])
@test all_sets_equal(
discrete_events_toplevel.(
[sys_bot, sys_bot_comp, sys_bot_ss, sys_mid1, sys_mid1_comp, sys_mid1_ss,
sys_mid2, sys_mid2_comp, sys_mid2_ss, sys_top, sys_top_comp, sys_top_ss])...,
[mtk_dev])
@test all(sym_issubset(
continuous_events_toplevel(sys), get_continuous_events(sys))
for sys in [sys_bot, sys_mid2, sys_mid1, sys_top])
@test all(sym_issubset(discrete_events_toplevel(sys), get_discrete_events(sys))
for sys in [sys_bot, sys_mid2, sys_mid1, sys_top])
end
1 change: 1 addition & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ end
@safetestset "Constants Test" include("constants.jl")
@safetestset "Parameter Dependency Test" include("parameter_dependencies.jl")
@safetestset "Equation Type Accessors Test" include("equation_type_accessors.jl")
@safetestset "System Accessor Functions Test" include("accessor_functions.jl")
@safetestset "Equations with complex values" include("complex.jl")
end
end
Expand Down
Loading