From 6411a8d2d3e499160a9cb8f6a00d1da741eefdcd Mon Sep 17 00:00:00 2001 From: vyudu Date: Sat, 1 Mar 2025 17:35:49 -0500 Subject: [PATCH 01/52] init --- src/systems/callbacks.jl | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/src/systems/callbacks.jl b/src/systems/callbacks.jl index 937264d083..e0b4c85759 100644 --- a/src/systems/callbacks.jl +++ b/src/systems/callbacks.jl @@ -1,4 +1,4 @@ -#################################### system operations ##################################### +#################################### System operations ##################################### has_continuous_events(sys::AbstractSystem) = isdefined(sys, :continuous_events) function get_continuous_events(sys::AbstractSystem) has_continuous_events(sys) || return SymbolicContinuousCallback[] @@ -11,6 +11,35 @@ function get_discrete_events(sys::AbstractSystem) getfield(sys, :discrete_events) end +struct Callback + eqs::Vector{Equation} + initialize::Union{ImplicitDiscreteSystem, FunctionalAffect, ImperativeAffect} + finalize::ImplicitDiscreteSystem + affect::ImplicitDiscreteSystem + affect_neg::ImplicitDiscreteSystem + rootfind::Union{Nothing, SciMLBase.RootfindOpt} +end + +# Callbacks: +# mapping (cond) => ImplicitDiscreteSystem +function generate_continuous_callbacks(events, sys) + algeeqs = alg_equations(sys) + callbacks = Callback[] + for (cond, aff) in events + @mtkbuild affect = ImplicitDiscreteSystem([aff, algeeqs], t) + push!(callbacks, Callback(cond, NULL_AFFECT, NULL_AFFECT, affect, affect, SciMLBase.LeftRootFind)) + end + callbacks +end + +function generate_discrete_callback_system(events, sys) +end + +function generate_callback_function() + +end + +############# Old implementation ### struct FunctionalAffect f::Any sts::Vector From aa5e95abcc3f67c00165e42e96872f70fd148a5b Mon Sep 17 00:00:00 2001 From: vyudu Date: Mon, 3 Mar 2025 17:07:52 -0500 Subject: [PATCH 02/52] refactor: refactor affect codegen --- src/ModelingToolkit.jl | 10 +-- src/systems/callbacks.jl | 165 ++++++++++++++++++++++++++++++----- src/systems/codegen_utils.jl | 1 + 3 files changed, 147 insertions(+), 29 deletions(-) diff --git a/src/ModelingToolkit.jl b/src/ModelingToolkit.jl index b53d3ec098..259b5a310c 100644 --- a/src/ModelingToolkit.jl +++ b/src/ModelingToolkit.jl @@ -157,7 +157,6 @@ include("systems/model_parsing.jl") include("systems/connectors.jl") include("systems/analysis_points.jl") include("systems/imperative_affect.jl") -include("systems/callbacks.jl") include("systems/codegen_utils.jl") include("systems/problem_utils.jl") include("linearization.jl") @@ -167,19 +166,20 @@ include("systems/optimization/optimizationsystem.jl") include("systems/optimization/modelingtoolkitize.jl") include("systems/nonlinear/nonlinearsystem.jl") -include("systems/nonlinear/homotopy_continuation.jl") +include("systems/discrete_system/discrete_system.jl") +include("systems/discrete_system/implicit_discrete_system.jl") +include("systems/callbacks.jl") + include("systems/diffeqs/odesystem.jl") include("systems/diffeqs/sdesystem.jl") include("systems/diffeqs/abstractodesystem.jl") +include("systems/nonlinear/homotopy_continuation.jl") include("systems/nonlinear/modelingtoolkitize.jl") include("systems/nonlinear/initializesystem.jl") include("systems/diffeqs/first_order_transform.jl") include("systems/diffeqs/modelingtoolkitize.jl") include("systems/diffeqs/basic_transformations.jl") -include("systems/discrete_system/discrete_system.jl") -include("systems/discrete_system/implicit_discrete_system.jl") - include("systems/jumps/jumpsystem.jl") include("systems/pde/pdesystem.jl") diff --git a/src/systems/callbacks.jl b/src/systems/callbacks.jl index e0b4c85759..b86e36813c 100644 --- a/src/systems/callbacks.jl +++ b/src/systems/callbacks.jl @@ -11,35 +11,139 @@ function get_discrete_events(sys::AbstractSystem) getfield(sys, :discrete_events) end -struct Callback - eqs::Vector{Equation} - initialize::Union{ImplicitDiscreteSystem, FunctionalAffect, ImperativeAffect} - finalize::ImplicitDiscreteSystem - affect::ImplicitDiscreteSystem - affect_neg::ImplicitDiscreteSystem - rootfind::Union{Nothing, SciMLBase.RootfindOpt} -end +abstract type Callback end + +const Affect = Union{ImplicitDiscreteSystem, FunctionalAffect, ImperativeAffect} # Callbacks: # mapping (cond) => ImplicitDiscreteSystem function generate_continuous_callbacks(events, sys) algeeqs = alg_equations(sys) - callbacks = Callback[] - for (cond, aff) in events - @mtkbuild affect = ImplicitDiscreteSystem([aff, algeeqs], t) - push!(callbacks, Callback(cond, NULL_AFFECT, NULL_AFFECT, affect, affect, SciMLBase.LeftRootFind)) + callbacks = MTKContinuousCallback[] + for (cond, affs) in events + @mtkbuild affect = ImplicitDiscreteSystem([affs, algeeqs], t) + push!(callbacks, MTKContinuousCallback(cond, NULL_AFFECT, NULL_AFFECT, affect, affect, SciMLBase.LeftRootFind)) end callbacks end -function generate_discrete_callback_system(events, sys) +function generate_discrete_callbacks(events, sys) + algeeqs = alg_equations(sys) + callbacks = MTKDiscreteCallback[] + for (cond, affs) in events + @mtkbuild affect = ImplicitDiscreteSystem([affs, algeeqs], t) + push!(callbacks, MTKDiscreteCallback(cond, NULL_AFFECT, NULL_AFFECT, affect)) + end + callbacks end -function generate_callback_function() - +""" +Create a DifferentialEquations callback. A set of continuous callbacks becomes a VectorContinuousCallback. +""" +function create_callback(cbs::Vector{MTKContinuousCallback}, sys; is_discrete = false) + eqs = flatten_equations(cbs) + _, f_iip = generate_custom_function( + sys, [eq.lhs - eq.rhs for eq in eqs], unknowns(sys), parameters(sys); + expression = Val{false}) + trigger = (out, u, t, integ) -> f_iip(out, u, parameter_values(integ), t) + + affects = [] + affect_negs = [] + inits = [] + finals = [] + for cb in cbs + affect = compile_affect(cb.affect) + push!(affects, affect) + isnothing(cb.affect_neg) ? push!(affect_negs, affect) : push!(affect_negs, compile_affect(cb.affect_neg)) + push!(inits, compile_affect(cb.initialize, default = SciMLBase.INITALIZE_DEFAULT)) + push!(finals, compile_affect(cb.finalize, default = SciMLBase.FINALIZE_DEFAULT)) + end + + # since there may be different number of conditions and affects, + # we build a map that translates the condition eq. number to the affect number + num_eqs = length.(eqs) + eq2affect = reduce(vcat, + [fill(i, num_eqs[i]) for i in eachindex(affects)]) + @assert length(eq2affect) == length(eqs) + @assert maximum(eq2affect) == length(affect_functions) + + affect = function (integ, idx) + affects[eq2affect[idx]](integ) + end + affect_neg = function (integ, idx) + f = affect_negs[eq2affect[idx]] + isnothing(f) && return + f(integ) + end + initialize = compile_optional_setup(inits, SciMLBase.INITIALIZE_DEFAULT) + finalize = compile_optional_setup(finals, SciMLBase.FINALIZE_DEFAULT) + + return VectorContinuousCallback(trigger, affect; affect_neg, initialize, finalize, rootfind = callback.rootfind, initializealg = SciMLBase.NoInit) +end + +function create_callback(cb, sys; is_discrete = false) + is_timed = is_timed_condition(cb) + + trigger = if is_discrete + is_timed ? condition(cb) : + compile_condition(callback, sys, unknowns(sys), parameters(sys)) + else + _, f_iip = generate_custom_function( + sys, [eq.rhs - eq.lhs for eq in equations(cb)], unknowns(sys), parameters(sys); + expression = Val{false}) + (out, u, t, integ) -> f_iip(out, u, parameter_values(integ), t) + end + + affect = compile_affect(cb.affect) + affect_neg = isnothing(cb.affect_neg) ? affect_fn : compile_affect(cb.affect_neg) + initialize = compile_affect(cb.initialize, default = SciMLBase.INITIALIZE_DEFAULT) + finalize = compile_affect(cb.finalize, default = SciMLBase.FINALIZE_DEFAULT) + + if is_discrete + if is_timed && condition(cb) isa AbstractVector + return PresetTimeCallback(trigger, affect; affect_neg, initialize, finalize, initializealg = SciMLBase.NoInit) + elseif is_timed + return PeriodicCallback(affect, trigger; initialize, finalize) + else + return DiscreteCallback(trigger, affect; affect_neg, initialize, finalize, initializealg = SciMLBase.NoInit) + end + else + return ContinuousCallback(trigger, affect; affect_neg, initialize, finalize, rootfind = callback.rootfind, initializealg = SciMLBase.NoInit) + end +end + +function compile_affect(aff; default = nothing) + if aff isa ImplicitDiscreteSystem + function affect!(integrator) + u0map = [u => integrator[u] for u in unknowns(aff)] + pmap = [p => integrator[p] for p in parameters(aff)] + prob = ImplicitDiscreteProblem(aff, u0map, (0, 1), pmap) + sol = solve(prob) + for u in unknowns(aff) + integrator[u] = sol[u][end] + end + for p in parameters(aff) + integrator[p] = sol[p][end] + end + end + elseif aff isa FunctionalAffect || aff isa ImperativeAffect + compile_user_affect(aff, callback, sys, unknowns(sys), parameters(sys)) + else + default + end +end + +function compile_setup_funcs(funs, default) + all(isnothing, funs) && return default + return let funs = funs + function (cb, u, t, integ) + for func in funs + isnothing(func) ? continue : func(integ) + end + end + end end -############# Old implementation ### struct FunctionalAffect f::Any sts::Vector @@ -50,6 +154,22 @@ struct FunctionalAffect ctx::Any end +struct MTKContinuousCallback <: Callback + eqs::Vector{Equation} + initialize::Union{Affect, Nothing} + finalize::Union{Affect, Nothing} + affect::Affect + affect_neg::Union{Affect, Nothing} + rootfind::Union{Nothing, SciMLBase.RootfindOpt} +end + +struct MTKDiscreteCallback <: Callback + conds::Vector{Equation} + initialize::Union{Affect, Nothing} + finalize::Union{Affect, Nothing} + affect::Affect +end + function FunctionalAffect(f, sts, pars, discretes, ctx = nothing) # sts & pars contain either pairs: resistor.R => R, or Syms: R vs = [x isa Pair ? x.first : x for x in sts] @@ -67,7 +187,7 @@ function FunctionalAffect(; f, sts, pars, discretes, ctx = nothing) FunctionalAffect(f, sts, pars, discretes, ctx) end -func(f::FunctionalAffect) = f.f +func(a::FunctionalAffect) = a.f context(a::FunctionalAffect) = a.ctx parameters(a::FunctionalAffect) = a.pars parameters_syms(a::FunctionalAffect) = a.pars_syms @@ -729,6 +849,7 @@ function compile_affect(eqs::Vector{Equation}, cb, sys, dvs, ps; outputidxs = no outputidxs = update_inds, create_bindings = false, kwargs...) + @show rf_oop # applied user-provided function to the generated expression if postprocess_affect_expr! !== nothing postprocess_affect_expr!(rf_ip, integ) @@ -899,13 +1020,7 @@ function compile_affect_fn(cb, sys::AbstractTimeDependentSystem, dvs, ps, kwargs eq_aff = affects(cb) eq_neg_aff = affect_negs(cb) affect = compile_affect(eq_aff, cb, sys, dvs, ps; expression = Val{false}, kwargs...) - function compile_optional_affect(aff, default = nothing) - if isnothing(aff) || aff == default - return nothing - else - return compile_affect(aff, cb, sys, dvs, ps; expression = Val{false}, kwargs...) - end - end + if eq_neg_aff === eq_aff affect_neg = affect else @@ -1047,6 +1162,7 @@ end function compile_affect(affect::FunctionalAffect, cb, sys, dvs, ps; kwargs...) compile_user_affect(affect, cb, sys, dvs, ps; kwargs...) end + function _compile_optional_affect(default, aff, cb, sys, dvs, ps; kwargs...) if isnothing(aff) || aff == default return nothing @@ -1054,6 +1170,7 @@ function _compile_optional_affect(default, aff, cb, sys, dvs, ps; kwargs...) return compile_affect(aff, cb, sys, dvs, ps; expression = Val{false}, kwargs...) end end + function generate_timed_callback(cb, sys, dvs, ps; postprocess_affect_expr! = nothing, kwargs...) cond = condition(cb) diff --git a/src/systems/codegen_utils.jl b/src/systems/codegen_utils.jl index 1eeb7e026b..c32ee68d21 100644 --- a/src/systems/codegen_utils.jl +++ b/src/systems/codegen_utils.jl @@ -234,6 +234,7 @@ function build_function_wrapper(sys::AbstractSystem, expr, args...; p_start = 2, if wrap_code isa Tuple && symbolic_type(expr) == ScalarSymbolic() wrap_code = wrap_code[1] end + @show build_function(expr, args...)[1] return build_function(expr, args...; wrap_code, similarto, kwargs...) end From 203657dbe8618f42c8d27832a5299182ecc94d42 Mon Sep 17 00:00:00 2001 From: vyudu Date: Wed, 5 Mar 2025 15:22:28 -0500 Subject: [PATCH 03/52] feat: correct affect system generation --- src/systems/callbacks.jl | 1370 +++++++++--------------------- src/systems/diffeqs/odesystem.jl | 6 +- src/systems/imperative_affect.jl | 45 +- src/systems/systems.jl | 8 +- 4 files changed, 448 insertions(+), 981 deletions(-) diff --git a/src/systems/callbacks.jl b/src/systems/callbacks.jl index b86e36813c..42ab150c92 100644 --- a/src/systems/callbacks.jl +++ b/src/systems/callbacks.jl @@ -1,148 +1,4 @@ -#################################### System operations ##################################### -has_continuous_events(sys::AbstractSystem) = isdefined(sys, :continuous_events) -function get_continuous_events(sys::AbstractSystem) - has_continuous_events(sys) || return SymbolicContinuousCallback[] - getfield(sys, :continuous_events) -end - -has_discrete_events(sys::AbstractSystem) = isdefined(sys, :discrete_events) -function get_discrete_events(sys::AbstractSystem) - has_discrete_events(sys) || return SymbolicDiscreteCallback[] - getfield(sys, :discrete_events) -end - -abstract type Callback end - -const Affect = Union{ImplicitDiscreteSystem, FunctionalAffect, ImperativeAffect} - -# Callbacks: -# mapping (cond) => ImplicitDiscreteSystem -function generate_continuous_callbacks(events, sys) - algeeqs = alg_equations(sys) - callbacks = MTKContinuousCallback[] - for (cond, affs) in events - @mtkbuild affect = ImplicitDiscreteSystem([affs, algeeqs], t) - push!(callbacks, MTKContinuousCallback(cond, NULL_AFFECT, NULL_AFFECT, affect, affect, SciMLBase.LeftRootFind)) - end - callbacks -end - -function generate_discrete_callbacks(events, sys) - algeeqs = alg_equations(sys) - callbacks = MTKDiscreteCallback[] - for (cond, affs) in events - @mtkbuild affect = ImplicitDiscreteSystem([affs, algeeqs], t) - push!(callbacks, MTKDiscreteCallback(cond, NULL_AFFECT, NULL_AFFECT, affect)) - end - callbacks -end - -""" -Create a DifferentialEquations callback. A set of continuous callbacks becomes a VectorContinuousCallback. -""" -function create_callback(cbs::Vector{MTKContinuousCallback}, sys; is_discrete = false) - eqs = flatten_equations(cbs) - _, f_iip = generate_custom_function( - sys, [eq.lhs - eq.rhs for eq in eqs], unknowns(sys), parameters(sys); - expression = Val{false}) - trigger = (out, u, t, integ) -> f_iip(out, u, parameter_values(integ), t) - - affects = [] - affect_negs = [] - inits = [] - finals = [] - for cb in cbs - affect = compile_affect(cb.affect) - push!(affects, affect) - isnothing(cb.affect_neg) ? push!(affect_negs, affect) : push!(affect_negs, compile_affect(cb.affect_neg)) - push!(inits, compile_affect(cb.initialize, default = SciMLBase.INITALIZE_DEFAULT)) - push!(finals, compile_affect(cb.finalize, default = SciMLBase.FINALIZE_DEFAULT)) - end - - # since there may be different number of conditions and affects, - # we build a map that translates the condition eq. number to the affect number - num_eqs = length.(eqs) - eq2affect = reduce(vcat, - [fill(i, num_eqs[i]) for i in eachindex(affects)]) - @assert length(eq2affect) == length(eqs) - @assert maximum(eq2affect) == length(affect_functions) - - affect = function (integ, idx) - affects[eq2affect[idx]](integ) - end - affect_neg = function (integ, idx) - f = affect_negs[eq2affect[idx]] - isnothing(f) && return - f(integ) - end - initialize = compile_optional_setup(inits, SciMLBase.INITIALIZE_DEFAULT) - finalize = compile_optional_setup(finals, SciMLBase.FINALIZE_DEFAULT) - - return VectorContinuousCallback(trigger, affect; affect_neg, initialize, finalize, rootfind = callback.rootfind, initializealg = SciMLBase.NoInit) -end - -function create_callback(cb, sys; is_discrete = false) - is_timed = is_timed_condition(cb) - - trigger = if is_discrete - is_timed ? condition(cb) : - compile_condition(callback, sys, unknowns(sys), parameters(sys)) - else - _, f_iip = generate_custom_function( - sys, [eq.rhs - eq.lhs for eq in equations(cb)], unknowns(sys), parameters(sys); - expression = Val{false}) - (out, u, t, integ) -> f_iip(out, u, parameter_values(integ), t) - end - - affect = compile_affect(cb.affect) - affect_neg = isnothing(cb.affect_neg) ? affect_fn : compile_affect(cb.affect_neg) - initialize = compile_affect(cb.initialize, default = SciMLBase.INITIALIZE_DEFAULT) - finalize = compile_affect(cb.finalize, default = SciMLBase.FINALIZE_DEFAULT) - - if is_discrete - if is_timed && condition(cb) isa AbstractVector - return PresetTimeCallback(trigger, affect; affect_neg, initialize, finalize, initializealg = SciMLBase.NoInit) - elseif is_timed - return PeriodicCallback(affect, trigger; initialize, finalize) - else - return DiscreteCallback(trigger, affect; affect_neg, initialize, finalize, initializealg = SciMLBase.NoInit) - end - else - return ContinuousCallback(trigger, affect; affect_neg, initialize, finalize, rootfind = callback.rootfind, initializealg = SciMLBase.NoInit) - end -end - -function compile_affect(aff; default = nothing) - if aff isa ImplicitDiscreteSystem - function affect!(integrator) - u0map = [u => integrator[u] for u in unknowns(aff)] - pmap = [p => integrator[p] for p in parameters(aff)] - prob = ImplicitDiscreteProblem(aff, u0map, (0, 1), pmap) - sol = solve(prob) - for u in unknowns(aff) - integrator[u] = sol[u][end] - end - for p in parameters(aff) - integrator[p] = sol[p][end] - end - end - elseif aff isa FunctionalAffect || aff isa ImperativeAffect - compile_user_affect(aff, callback, sys, unknowns(sys), parameters(sys)) - else - default - end -end - -function compile_setup_funcs(funs, default) - all(isnothing, funs) && return default - return let funs = funs - function (cb, u, t, integ) - for func in funs - isnothing(func) ? continue : func(integ) - end - end - end -end +abstract type AbstractCallback end struct FunctionalAffect f::Any @@ -154,22 +10,6 @@ struct FunctionalAffect ctx::Any end -struct MTKContinuousCallback <: Callback - eqs::Vector{Equation} - initialize::Union{Affect, Nothing} - finalize::Union{Affect, Nothing} - affect::Affect - affect_neg::Union{Affect, Nothing} - rootfind::Union{Nothing, SciMLBase.RootfindOpt} -end - -struct MTKDiscreteCallback <: Callback - conds::Vector{Equation} - initialize::Union{Affect, Nothing} - finalize::Union{Affect, Nothing} - affect::Affect -end - function FunctionalAffect(f, sts, pars, discretes, ctx = nothing) # sts & pars contain either pairs: resistor.R => R, or Syms: R vs = [x isa Pair ? x.first : x for x in sts] @@ -211,31 +51,16 @@ function Base.hash(a::FunctionalAffect, s::UInt) hash(a.ctx, s) end -namespace_affect(affect, s) = namespace_equation(affect, s) -function namespace_affect(affect::FunctionalAffect, s) - FunctionalAffect(func(affect), - renamespace.((s,), unknowns(affect)), - unknowns_syms(affect), - renamespace.((s,), parameters(affect)), - parameters_syms(affect), - renamespace.((s,), discretes(affect)), - context(affect)) -end - function has_functional_affect(cb) (affects(cb) isa FunctionalAffect || affects(cb) isa ImperativeAffect) end -function vars!(vars, aff::FunctionalAffect; op = Differential) - for var in Iterators.flatten((unknowns(aff), parameters(aff), discretes(aff))) - vars!(vars, var) - end - return vars -end -#################################### continuous events ##################################### +############################### +###### Continuous events ###### +############################### +const Affect = Union{ImplicitDiscreteSystem, FunctionalAffect, ImperativeAffect} -const NULL_AFFECT = Equation[] """ SymbolicContinuousCallback(eqs::Vector{Equation}, affect, affect_neg, rootfind) @@ -277,54 +102,73 @@ Affects (i.e. `affect` and `affect_neg`) can be specified as either: + `ctx` is a user-defined context object passed to `f!` when invoked. This value is aliased for each problem. * A [`ImperativeAffect`](@ref); refer to its documentation for details. -DAEs will be reinitialized using `reinitializealg` (which defaults to `SciMLBase.CheckInit`) after callbacks are applied. -This reinitialization algorithm ensures that the DAE is satisfied after the callback runs. The default value of `CheckInit` will simply validate -that the newly-assigned values indeed satisfy the algebraic system; see the documentation on DAE initialization for a more detailed discussion of -initialization. +DAEs will automatically be reinitialized. Initial and final affects can also be specified with SCC, which are specified identically to positive and negative edge affects. Initialization affects will run as soon as the solver starts, while finalization affects will be executed after termination. """ -struct SymbolicContinuousCallback - eqs::Vector{Equation} - initialize::Union{Vector{Equation}, FunctionalAffect, ImperativeAffect} - finalize::Union{Vector{Equation}, FunctionalAffect, ImperativeAffect} - affect::Union{Vector{Equation}, FunctionalAffect, ImperativeAffect} - affect_neg::Union{Vector{Equation}, FunctionalAffect, ImperativeAffect, Nothing} - rootfind::SciMLBase.RootfindOpt - reinitializealg::SciMLBase.DAEInitializationAlgorithm - function SymbolicContinuousCallback(; - eqs::Vector{Equation}, - affect = NULL_AFFECT, +struct SymbolicContinuousCallback <: AbstractCallback + conditions::Vector{Equation} + affect::Union{Affect, Nothing} + affect_neg::Union{Affect, Nothing} + initialize::Union{Affect, Nothing} + finalize::Union{Affect, Nothing} + rootfind::Union{Nothing, SciMLBase.RootfindOpt} + + function SymbolicContinuousCallback( + conditions::Vector{Equation}, + affect = nothing; affect_neg = affect, - initialize = NULL_AFFECT, - finalize = NULL_AFFECT, - rootfind = SciMLBase.LeftRootFind, - reinitializealg = SciMLBase.CheckInit()) + initialize = nothing, + finalize = nothing, + rootfind = SciMLBase.LeftRootFind) new(eqs, initialize, finalize, make_affect(affect), - make_affect(affect_neg), rootfind, reinitializealg) + make_affect(affect_neg), rootfind) end # Default affect to nothing end -make_affect(affect) = affect -make_affect(affect::Tuple) = FunctionalAffect(affect...) -make_affect(affect::NamedTuple) = FunctionalAffect(; affect...) -function Base.:(==)(e1::SymbolicContinuousCallback, e2::SymbolicContinuousCallback) - isequal(e1.eqs, e2.eqs) && isequal(e1.affect, e2.affect) && - isequal(e1.initialize, e2.initialize) && isequal(e1.finalize, e2.finalize) && - isequal(e1.affect_neg, e2.affect_neg) && isequal(e1.rootfind, e2.rootfind) +make_affect(affect::Tuple, iv) = FunctionalAffect(affects...) +make_affect(affect::NamedTuple, iv) = FunctionalAffect(; affects...) +make_affect(affect::FunctionalAffect, iv) = affect + +# Default behavior: if no shifts are provided, then it is assumed that the RHS is the previous. +function make_affect(affect::Vector{Equation}, iv) + affect = scalarize(affect) + unknowns = OrderedSet() + params = OrderedSet() + for eq in affect + collect_vars!(unknowns, params, eq, iv) + end + affect = map(affect) do eq + ModelingToolkit.hasshift(eq) ? eq : + eq.lhs ~ distribute_shift(Prev(eq.rhs)) + end + params = map(params) do p + p = value(p) + Sym{FnType{Tuple{symtype(iv)}, Real}}(nameof(p))(iv) + end + + @mtkbuild affect = ImplicitDiscreteSystem(affect, iv, vcat(unknowns, params), []) end -Base.isempty(cb::SymbolicContinuousCallback) = isempty(cb.eqs) -function Base.hash(cb::SymbolicContinuousCallback, s::UInt) - hash_affect(affect::AbstractVector, s) = foldr(hash, affect, init = s) - hash_affect(affect, s) = hash(affect, s) - s = foldr(hash, cb.eqs, init = s) - s = hash_affect(cb.affect, s) - s = hash_affect(cb.affect_neg, s) - s = hash_affect(cb.initialize, s) - s = hash_affect(cb.finalize, s) - s = hash(cb.reinitializealg, s) - hash(cb.rootfind, s) + +make_affect(affect, iv) = error("Malformed affect $(affect). This should be a vector of equations or a tuple specifying a functional affect.") + +""" +Generate continuous callbacks. +""" +function SymbolicContinuousCallbacks(events, algeeqs, iv) + callbacks = MTKContinuousCallback[] + (isnothing(events) || isempty(events)) && return callbacks + + events isa AbstractVector || (events = [events]) + for (cond, affs) in events + if affs isa AbstractVector + affs = vcat(affs, algeeqs) + end + affect = make_affect(affs, iv) + push!(callbacks, SymbolicContinuousCallback(cond, affect, affect, nothing, nothing, SciMLBase.LeftRootFind)) + end + callbacks end function Base.show(io::IO, cb::SymbolicContinuousCallback) @@ -385,326 +229,192 @@ function Base.show(io::IO, mime::MIME"text/plain", cb::SymbolicContinuousCallbac end end -to_equation_vector(eq::Equation) = [eq] -to_equation_vector(eqs::Vector{Equation}) = eqs -function to_equation_vector(eqs::Vector{Any}) - isempty(eqs) || error("This should never happen") - Equation[] -end +################################ +######## Discrete events ####### +################################ -function SymbolicContinuousCallback(args...) - SymbolicContinuousCallback(to_equation_vector.(args)...) -end # wrap eq in vector -SymbolicContinuousCallback(p::Pair) = SymbolicContinuousCallback(p[1], p[2]) -SymbolicContinuousCallback(cb::SymbolicContinuousCallback) = cb # passthrough -function SymbolicContinuousCallback(eqs::Equation, affect = NULL_AFFECT; - initialize = NULL_AFFECT, finalize = NULL_AFFECT, - affect_neg = affect, rootfind = SciMLBase.LeftRootFind) - SymbolicContinuousCallback( - eqs = [eqs], affect = affect, affect_neg = affect_neg, - initialize = initialize, finalize = finalize, rootfind = rootfind) -end -function SymbolicContinuousCallback(eqs::Vector{Equation}, affect = NULL_AFFECT; - affect_neg = affect, initialize = NULL_AFFECT, finalize = NULL_AFFECT, - rootfind = SciMLBase.LeftRootFind) - SymbolicContinuousCallback( - eqs = eqs, affect = affect, affect_neg = affect_neg, - initialize = initialize, finalize = finalize, rootfind = rootfind) -end +# TODO: Iterative callbacks +""" + SymbolicDiscreteCallback(conditions::Vector{Equation}, affect) -SymbolicContinuousCallbacks(cb::SymbolicContinuousCallback) = [cb] -SymbolicContinuousCallbacks(cbs::Vector{<:SymbolicContinuousCallback}) = cbs -SymbolicContinuousCallbacks(cbs::Vector) = SymbolicContinuousCallback.(cbs) -function SymbolicContinuousCallbacks(ve::Vector{Equation}) - SymbolicContinuousCallbacks(SymbolicContinuousCallback(ve)) -end -function SymbolicContinuousCallbacks(others) - SymbolicContinuousCallbacks(SymbolicContinuousCallback(others)) -end -SymbolicContinuousCallbacks(::Nothing) = SymbolicContinuousCallback[] +A callback that triggers at the first timestep that the conditions are satisfied. -equations(cb::SymbolicContinuousCallback) = cb.eqs -function equations(cbs::Vector{<:SymbolicContinuousCallback}) - mapreduce(equations, vcat, cbs, init = Equation[]) -end +The condition can be one of: +- Real - periodic events with period Δt +- Vector{Real} - events trigger at these preset times +- Vector{Equation} - events trigger when the condition evaluates to true +""" +struct SymbolicDiscreteCallback{R} <: AbstractCallback where R <: Real + conditions::Union{R, Vector{R}, Vector{Equation}} + affect::Affect + initialize::Union{Affect, Nothing} + finalize::Union{Affect, Nothing} -affects(cb::SymbolicContinuousCallback) = cb.affect -function affects(cbs::Vector{SymbolicContinuousCallback}) - mapreduce(affects, vcat, cbs, init = Equation[]) + function SymbolicDiscreteCallback( + condition, affect = nothing; + initialize = nothing, finalize = nothing) + c = is_timed_condition(condition) ? condition : value(scalarize(condition)) + new(c, make_affect(affect), make_affect(initialize), + make_affect(finalize)) + end # Default affect to nothing end -affect_negs(cb::SymbolicContinuousCallback) = cb.affect_neg -function affect_negs(cbs::Vector{SymbolicContinuousCallback}) - mapreduce(affect_negs, vcat, cbs, init = Equation[]) +""" +Generate discrete callbacks. +""" +function SymbolicDiscreteCallbacks(events, algeeqs, iv) + callbacks = SymbolicDiscreteCallback[] + (isnothing(events) || isempty(events)) && return callbacks + events isa AbstractVector || (events = [events]) + + for (cond, aff) in events + if aff isa AbstractVector + aff = vcat(aff, algeeqs) + end + affect = make_affect(aff, iv) + push!(callbacks, SymbolicDiscreteCallback(cond, affect, nothing, nothing)) + end + callbacks end -reinitialization_alg(cb::SymbolicContinuousCallback) = cb.reinitializealg -function reinitialization_algs(cbs::Vector{SymbolicContinuousCallback}) - mapreduce( - reinitialization_alg, vcat, cbs, init = SciMLBase.DAEInitializationAlgorithm[]) +function is_timed_condition(condition::T) + if T <: Real + true + elseif T <: AbstractVector + eltype(V) <: Real + else + false + end end -initialize_affects(cb::SymbolicContinuousCallback) = cb.initialize -function initialize_affects(cbs::Vector{SymbolicContinuousCallback}) - mapreduce(initialize_affects, vcat, cbs, init = Equation[]) +function Base.show(io::IO, db::SymbolicDiscreteCallback) + indent = get(io, :indent, 0) + iio = IOContext(io, :indent => indent + 1) + println(io, "SymbolicDiscreteCallback:") + println(iio, "Conditions:") + print(iio, "; ") + if affects(db) != nothing + print(iio, "Affect:") + show(iio, affects(db)) + print(iio, ", ") + end + if affect_negs(db) != nothing + print(iio, "Negative-edge affect:") + show(iio, affect_negs(db)) + print(iio, ", ") + end + if initialize_affects(db) != nothing + print(iio, "Initialization affect:") + show(iio, initialize_affects(db)) + print(iio, ", ") + end + if finalize_affects(db) != nothing + print(iio, "Finalization affect:") + show(iio, finalize_affects(db)) + end + print(iio, ")") end -finalize_affects(cb::SymbolicContinuousCallback) = cb.finalize -function finalize_affects(cbs::Vector{SymbolicContinuousCallback}) - mapreduce(finalize_affects, vcat, cbs, init = Equation[]) +############################################ +########## Namespacing Utilities ########### +############################################ + +function namespace_affect(affect::FunctionalAffect, s) + FunctionalAffect(func(affect), + renamespace.((s,), unknowns(affect)), + unknowns_syms(affect), + renamespace.((s,), parameters(affect)), + parameters_syms(affect), + renamespace.((s,), discretes(affect)), + context(affect)) end -namespace_affects(af::Vector, s) = Equation[namespace_affect(a, s) for a in af] -namespace_affects(af::FunctionalAffect, s) = namespace_affect(af, s) -namespace_affects(::Nothing, s) = nothing +function namespace_affects(af::Affect, s) + if af isa ImplicitDiscreteSystem + af + elseif af isa FunctionalAffect || af isa ImperativeAffect + namespace_affect(af, s) + else + nothing + end +end function namespace_callback(cb::SymbolicContinuousCallback, s)::SymbolicContinuousCallback - SymbolicContinuousCallback(; - eqs = namespace_equation.(equations(cb), (s,)), - affect = namespace_affects(affects(cb), s), - affect_neg = namespace_affects(affect_negs(cb), s), - initialize = namespace_affects(initialize_affects(cb), s), - finalize = namespace_affects(finalize_affects(cb), s), - rootfind = cb.rootfind) + SymbolicContinuousCallback( + namespace_equation.(equations(cb), (s,)), + namespace_affects(affects(cb), s), + namespace_affects(affect_negs(cb), s), + namespace_affects(initialize_affects(cb), s), + namespace_affects(finalize_affects(cb), s), + cb.rootfind) end -""" - continuous_events(sys::AbstractSystem)::Vector{SymbolicContinuousCallback} +function namespace_condition(condition, s) + is_timed_condition(condition) ? condition : namespace_expr(condition, s) +end -Returns a vector of all the `continuous_events` in an abstract system and its component subsystems. -The `SymbolicContinuousCallback`s in the returned vector are structs with two fields: `eqs` and -`affect` which correspond to the first and second elements of a `Pair` used to define an event, i.e. -`eqs => affect`. -""" -function continuous_events(sys::AbstractSystem) - cbs = get_continuous_events(sys) - filter(!isempty, cbs) +function namespace_callback(cb::SymbolicDiscreteCallback, s)::SymbolicDiscreteCallback + SymbolicDiscreteCallback( + namespace_condition(condition(cb), s), + namespace_affects(affects(cb), s), + namespace_affects(initialize_affects(cb), s), + namespace_affects(finalize_affects(cb), s)) +end - systems = get_systems(sys) - cbs = [cbs; - reduce(vcat, - (map(cb -> namespace_callback(cb, s), continuous_events(s)) - for s in systems), - init = SymbolicContinuousCallback[])] - filter(!isempty, cbs) +function Base.hash(cb::SymbolicContinuousCallback, s::UInt) + s = foldr(hash, cb.eqs, init = s) + s = hash(cb.affect, s) + s = hash(cb.affect_neg, s) + s = hash(cb.initialize, s) + s = hash(cb.finalize, s) + hash(cb.rootfind, s) end -function vars!(vars, cb::SymbolicContinuousCallback; op = Differential) - for eq in equations(cb) - vars!(vars, eq; op) - end - for aff in (affects(cb), affect_negs(cb), initialize_affects(cb), finalize_affects(cb)) - if aff isa Vector{Equation} - for eq in aff - vars!(vars, eq; op) - end - elseif aff !== nothing - vars!(vars, aff; op) - end - end - return vars +function Base.hash(cb::SymbolicDiscreteCallback, s::UInt) + s = hash(cb.condition, s) + s = hash(cb.affects, s) + s = hash(cb.initialize, s) + hash(cb.finalize, s) end -""" - continuous_events_toplevel(sys::AbstractSystem) +########################### +######### Helpers ######### +########################### -Replicates the behaviour of `continuous_events`, but ignores events of subsystems. +conditions(cb::AbstractCallback) = cb.conditions +conditions(cbs::Vector{<:AbstractCallback}) = reduce(vcat, conditions(cb) for cb in cbs) +equations(cb::AbstractCallback) = conditions(cb) +equations(cb::Vector{<:AbstractCallback}) = conditions(cb) -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 +affects(cb::AbstractCallback) = cb.affect +affects(cbs::Vector{<:AbstractCallback}) = reduce(vcat, affects(cb) for cb in cbs; init = []) -#################################### discrete events ##################################### +affect_negs(cb::SymbolicContinuousCallback) = cb.affect_neg +affect_negs(cbs::Vector{SymbolicContinuousCallback})= mapreduce(affect_negs, vcat, cbs, init = Equation[]) -struct SymbolicDiscreteCallback - # condition can be one of: - # Δt::Real - Periodic with period Δt - # Δts::Vector{Real} - events trigger in this times (Preset) - # condition::Vector{Equation} - event triggered when condition is true - # TODO: Iterative - condition::Any - affects::Any - initialize::Any - finalize::Any - reinitializealg::SciMLBase.DAEInitializationAlgorithm +initialize_affects(cb::AbstractCallback) = cb.initialize +initialize_affects(cbs::Vector{<:AbstractCallback}) = mapreduce(initialize_affects, vcat, cbs, init = Equation[]) - function SymbolicDiscreteCallback( - condition, affects = NULL_AFFECT; reinitializealg = SciMLBase.CheckInit(), - initialize = NULL_AFFECT, finalize = NULL_AFFECT) - c = scalarize_condition(condition) - a = scalarize_affects(affects) - new(c, a, scalarize_affects(initialize), - scalarize_affects(finalize), reinitializealg) - end # Default affect to nothing +finalize_affects(cb::AbstractCallback) = cb.finalize +finalize_affects(cbs::Vector{<:AbstractCallback}) = mapreduce(finalize_affects, vcat, cbs, init = Equation[]) + +function Base.:(==)(e1::SymbolicDiscreteCallback, e2::SymbolicDiscreteCallback) + isequal(e1.condition, e2.condition) && isequal(e1.affects, e2.affects) && + isequal(e1.initialize, e2.initialize) && isequal(e1.finalize, e2.finalize) end -is_timed_condition(cb) = false -is_timed_condition(::R) where {R <: Real} = true -is_timed_condition(::V) where {V <: AbstractVector} = eltype(V) <: Real -is_timed_condition(::Num) = false -is_timed_condition(cb::SymbolicDiscreteCallback) = is_timed_condition(condition(cb)) - -function scalarize_condition(condition) - is_timed_condition(condition) ? condition : value(scalarize(condition)) -end -function namespace_condition(condition, s) - is_timed_condition(condition) ? condition : namespace_expr(condition, s) -end - -scalarize_affects(affects) = scalarize(affects) -scalarize_affects(affects::Tuple) = FunctionalAffect(affects...) -scalarize_affects(affects::NamedTuple) = FunctionalAffect(; affects...) -scalarize_affects(affects::FunctionalAffect) = affects - -SymbolicDiscreteCallback(p::Pair) = SymbolicDiscreteCallback(p[1], p[2]) -SymbolicDiscreteCallback(cb::SymbolicDiscreteCallback) = cb # passthrough - -function Base.show(io::IO, db::SymbolicDiscreteCallback) - println(io, "condition: ", db.condition) - println(io, "affects:") - if db.affects isa FunctionalAffect || db.affects isa ImperativeAffect - # TODO - println(io, " ", db.affects) - else - for affect in db.affects - println(io, " ", affect) - end - end -end - -function Base.:(==)(e1::SymbolicDiscreteCallback, e2::SymbolicDiscreteCallback) - isequal(e1.condition, e2.condition) && isequal(e1.affects, e2.affects) && - isequal(e1.initialize, e2.initialize) && isequal(e1.finalize, e2.finalize) -end -function Base.hash(cb::SymbolicDiscreteCallback, s::UInt) - s = hash(cb.condition, s) - s = cb.affects isa AbstractVector ? foldr(hash, cb.affects, init = s) : - hash(cb.affects, s) - s = cb.initialize isa AbstractVector ? foldr(hash, cb.initialize, init = s) : - hash(cb.initialize, s) - s = cb.finalize isa AbstractVector ? foldr(hash, cb.finalize, init = s) : - hash(cb.finalize, s) - s = hash(cb.reinitializealg, s) - return s -end - -condition(cb::SymbolicDiscreteCallback) = cb.condition -function conditions(cbs::Vector{<:SymbolicDiscreteCallback}) - reduce(vcat, condition(cb) for cb in cbs) -end - -affects(cb::SymbolicDiscreteCallback) = cb.affects - -function affects(cbs::Vector{SymbolicDiscreteCallback}) - reduce(vcat, affects(cb) for cb in cbs; init = []) -end - -reinitialization_alg(cb::SymbolicDiscreteCallback) = cb.reinitializealg -function reinitialization_algs(cbs::Vector{SymbolicDiscreteCallback}) - mapreduce( - reinitialization_alg, vcat, cbs, init = SciMLBase.DAEInitializationAlgorithm[]) -end - -initialize_affects(cb::SymbolicDiscreteCallback) = cb.initialize -function initialize_affects(cbs::Vector{SymbolicDiscreteCallback}) - mapreduce(initialize_affects, vcat, cbs, init = Equation[]) -end - -finalize_affects(cb::SymbolicDiscreteCallback) = cb.finalize -function finalize_affects(cbs::Vector{SymbolicDiscreteCallback}) - mapreduce(finalize_affects, vcat, cbs, init = Equation[]) -end - -function namespace_callback(cb::SymbolicDiscreteCallback, s)::SymbolicDiscreteCallback - function namespace_affects(af) - return af isa AbstractVector ? namespace_affect.(af, Ref(s)) : - namespace_affect(af, s) - end - SymbolicDiscreteCallback( - namespace_condition(condition(cb), s), namespace_affects(affects(cb)), - reinitializealg = cb.reinitializealg, initialize = namespace_affects(initialize_affects(cb)), - finalize = namespace_affects(finalize_affects(cb))) -end - -SymbolicDiscreteCallbacks(cb::Pair) = SymbolicDiscreteCallback[SymbolicDiscreteCallback(cb)] -SymbolicDiscreteCallbacks(cbs::Vector) = SymbolicDiscreteCallback.(cbs) -SymbolicDiscreteCallbacks(cb::SymbolicDiscreteCallback) = [cb] -SymbolicDiscreteCallbacks(cbs::Vector{<:SymbolicDiscreteCallback}) = cbs -SymbolicDiscreteCallbacks(::Nothing) = SymbolicDiscreteCallback[] - -""" - discrete_events(sys::AbstractSystem) :: Vector{SymbolicDiscreteCallback} - -Returns a vector of all the `discrete_events` in an abstract system and its component subsystems. -The `SymbolicDiscreteCallback`s in the returned vector are structs with two fields: `condition` and -`affect` which correspond to the first and second elements of a `Pair` used to define an event, i.e. -`condition => affect`. -""" -function discrete_events(sys::AbstractSystem) - cbs = get_discrete_events(sys) - systems = get_systems(sys) - cbs = [cbs; - reduce(vcat, - (map(cb -> namespace_callback(cb, s), discrete_events(s)) for s in systems), - init = SymbolicDiscreteCallback[])] - cbs -end - -function vars!(vars, cb::SymbolicDiscreteCallback; op = Differential) - if symbolic_type(cb.condition) == NotSymbolic - if cb.condition isa AbstractArray - for eq in cb.condition - vars!(vars, eq; op) - end - end - else - vars!(vars, cb.condition; op) - end - for aff in (cb.affects, cb.initialize, cb.finalize) - if aff isa Vector{Equation} - for eq in aff - vars!(vars, eq; op) - end - elseif aff !== nothing - vars!(vars, aff; op) - end - end - 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) +function Base.:(==)(e1::SymbolicContinuousCallback, e2::SymbolicContinuousCallback) + isequal(e1.eqs, e2.eqs) && isequal(e1.affect, e2.affect) && + isequal(e1.initialize, e2.initialize) && isequal(e1.finalize, e2.finalize) && + isequal(e1.affect_neg, e2.affect_neg) && isequal(e1.rootfind, e2.rootfind) end -################################# compilation functions #################################### - -# handles ensuring that affect! functions work with integrator arguments -function add_integrator_header( - sys::AbstractSystem, integrator = gensym(:MTKIntegrator), out = :u) - expr -> Func([DestructuredArgs(expr.args, integrator, inds = [:u, :p, :t])], [], - expr.body), - expr -> Func( - [DestructuredArgs(expr.args, integrator, inds = [out, :u, :p, :t])], [], - expr.body) -end +Base.isempty(cb::AbstractCallback) = isempty(cb.conditions) +#################################### +####### Compilation functions ###### +#################################### +# function condition_header(sys::AbstractSystem, integrator = gensym(:MTKIntegrator)) expr -> Func( [expr.args[1], expr.args[2], @@ -713,27 +423,6 @@ function condition_header(sys::AbstractSystem, integrator = gensym(:MTKIntegrato expr.body) end -function callback_save_header(sys::AbstractSystem, cb) - if !(has_index_cache(sys) && (ic = get_index_cache(sys)) !== nothing) - return (identity, identity) - end - save_idxs = get(ic.callback_to_clocks, cb, Int[]) - isempty(save_idxs) && return (identity, identity) - - wrapper = function (expr) - return Func(expr.args, [], - LiteralExpr(quote - $(expr.body) - save_idxs = $(save_idxs) - for idx in save_idxs - $(SciMLBase.save_discretes!)($(expr.args[1]), idx) - end - end)) - end - - return wrapper, wrapper -end - """ compile_condition(cb::SymbolicDiscreteCallback, sys, dvs, ps; expression, kwargs...) @@ -767,332 +456,20 @@ function compile_condition(cb::SymbolicDiscreteCallback, sys, dvs, ps; return eval_or_rgf(expr; eval_expression, eval_module) end -function compile_affect(cb::SymbolicContinuousCallback, args...; kwargs...) - compile_affect(affects(cb), cb, args...; kwargs...) -end - -""" - compile_affect(eqs::Vector{Equation}, sys, dvs, ps; expression, outputidxs, kwargs...) - compile_affect(cb::SymbolicContinuousCallback, args...; kwargs...) - -Returns a function that takes an integrator as argument and modifies the state with the -affect. The generated function has the signature `affect!(integrator)`. - -Notes - - - `expression = Val{true}`, causes the generated function to be returned as an expression. - If set to `Val{false}` a `RuntimeGeneratedFunction` will be returned. - - `outputidxs`, a vector of indices of the output variables which should correspond to - `unknowns(sys)`. If provided, checks that the LHS of affect equations are variables are - dropped, i.e. it is assumed these indices are correct and affect equations are - well-formed. - - `kwargs` are passed through to `Symbolics.build_function`. -""" -function compile_affect(eqs::Vector{Equation}, cb, sys, dvs, ps; outputidxs = nothing, - expression = Val{true}, checkvars = true, eval_expression = false, - eval_module = @__MODULE__, - postprocess_affect_expr! = nothing, kwargs...) - if isempty(eqs) - if expression == Val{true} - return :((args...) -> ()) - else - return (args...) -> () # We don't do anything in the callback, we're just after the event - end - else - eqs = flatten_equations(eqs) - rhss = map(x -> x.rhs, eqs) - outvar = :u - if outputidxs === nothing - lhss = map(x -> x.lhs, eqs) - all(isvariable, lhss) || - error("Non-variable symbolic expression found on the left hand side of an affect equation. Such equations must be of the form variable ~ symbolic expression for the new value of the variable.") - update_vars = collect(Iterators.flatten(map(ModelingToolkit.vars, lhss))) # these are the ones we're changing - length(update_vars) == length(unique(update_vars)) == length(eqs) || - error("affected variables not unique, each unknown can only be affected by one equation for a single `root_eqs => affects` pair.") - alleq = all(isequal(isparameter(first(update_vars))), - Iterators.map(isparameter, update_vars)) - if !isparameter(first(lhss)) && alleq - unknownind = Dict(reverse(en) for en in enumerate(dvs)) - update_inds = map(sym -> unknownind[sym], update_vars) - elseif isparameter(first(lhss)) && alleq - if has_index_cache(sys) && get_index_cache(sys) !== nothing - update_inds = map(update_vars) do sym - return parameter_index(sys, sym) - end - else - psind = Dict(reverse(en) for en in enumerate(ps)) - update_inds = map(sym -> psind[sym], update_vars) - end - outvar = :p - else - error("Error, building an affect function for a callback that wants to modify both parameters and unknowns. This is not currently allowed in one individual callback.") - end - else - update_inds = outputidxs - end - - _ps = ps - ps = reorder_parameters(sys, ps) - if checkvars - u = map(x -> time_varying_as_func(value(x), sys), dvs) - p = map.(x -> time_varying_as_func(value(x), sys), ps) - else - u = dvs - p = ps - end - t = get_iv(sys) - integ = gensym(:MTKIntegrator) - rf_oop, rf_ip = build_function_wrapper( - sys, rhss, u, p..., t; expression = Val{true}, - wrap_code = callback_save_header(sys, cb) .∘ - add_integrator_header(sys, integ, outvar), - outputidxs = update_inds, - create_bindings = false, - kwargs...) - @show rf_oop - # applied user-provided function to the generated expression - if postprocess_affect_expr! !== nothing - postprocess_affect_expr!(rf_ip, integ) - end - if expression == Val{false} - return eval_or_rgf(rf_ip; eval_expression, eval_module) - end - return rf_ip - end -end - -function generate_rootfinding_callback(sys::AbstractTimeDependentSystem, - dvs = unknowns(sys), ps = parameters(sys; initial_parameters = true); kwargs...) - cbs = continuous_events(sys) - isempty(cbs) && return nothing - 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_rootfinding_callback and thus we can produce a ContinuousCallback instead of a VectorContinuousCallback. -""" -function generate_single_rootfinding_callback( - eq, cb, sys::AbstractTimeDependentSystem, dvs = unknowns(sys), - ps = parameters(sys; initial_parameters = true); kwargs...) - if !isequal(eq.lhs, 0) - eq = 0 ~ eq.lhs - eq.rhs - end - - rf_oop, rf_ip = generate_custom_function( - sys, [eq.rhs], dvs, ps; expression = Val{false}, kwargs...) - affect_function = compile_affect_fn(cb, sys, dvs, ps, kwargs) - cond = function (u, t, integ) - if DiffEqBase.isinplace(integ.sol.prob) - tmp, = DiffEqBase.get_tmp_cache(integ) - rf_ip(tmp, u, parameter_values(integ), t) - tmp[1] - else - rf_oop(u, parameter_values(integ), t) - end - end - user_initfun = isnothing(affect_function.initialize) ? SciMLBase.INITIALIZE_DEFAULT : - (c, u, t, i) -> affect_function.initialize(i) - if has_index_cache(sys) && (ic = get_index_cache(sys)) !== nothing && - (save_idxs = get(ic.callback_to_clocks, cb, nothing)) !== nothing - initfn = let save_idxs = save_idxs - function (cb, u, t, integrator) - user_initfun(cb, u, t, integrator) - for idx in save_idxs - SciMLBase.save_discretes!(integrator, idx) - end - end - end - else - initfn = user_initfun - end - - return ContinuousCallback( - cond, affect_function.affect, affect_function.affect_neg, rootfind = cb.rootfind, - initialize = initfn, - finalize = isnothing(affect_function.finalize) ? SciMLBase.FINALIZE_DEFAULT : - (c, u, t, i) -> affect_function.finalize(i), - initializealg = reinitialization_alg(cb)) -end - -function generate_vector_rootfinding_callback( - cbs, sys::AbstractTimeDependentSystem, dvs = unknowns(sys), - ps = parameters(sys; initial_parameters = true); rootfind = SciMLBase.RightRootFind, - reinitialization = SciMLBase.CheckInit(), kwargs...) - eqs = map(cb -> flatten_equations(cb.eqs), cbs) - num_eqs = length.(eqs) - # fuse equations to create VectorContinuousCallback - eqs = reduce(vcat, eqs) - # rewrite all equations as 0 ~ interesting stuff - eqs = map(eqs) do eq - isequal(eq.lhs, 0) && return eq - 0 ~ eq.lhs - eq.rhs - end - - rhss = map(x -> x.rhs, eqs) - _, rf_ip = generate_custom_function( - sys, rhss, dvs, ps; expression = Val{false}, kwargs...) - - affect_functions = @NamedTuple{ - affect::Function, - affect_neg::Union{Function, Nothing}, - initialize::Union{Function, Nothing}, - finalize::Union{Function, Nothing}}[ - compile_affect_fn(cb, sys, dvs, ps, kwargs) - for cb in cbs] - cond = function (out, u, t, integ) - rf_ip(out, u, parameter_values(integ), t) - end - - # since there may be different number of conditions and affects, - # we build a map that translates the condition eq. number to the affect number - eq_ind2affect = reduce(vcat, - [fill(i, num_eqs[i]) for i in eachindex(affect_functions)]) - @assert length(eq_ind2affect) == length(eqs) - @assert maximum(eq_ind2affect) == length(affect_functions) - - affect = let affect_functions = affect_functions, eq_ind2affect = eq_ind2affect - function (integ, eq_ind) # eq_ind refers to the equation index that triggered the event, each event has num_eqs[i] equations - affect_functions[eq_ind2affect[eq_ind]].affect(integ) - end - end - affect_neg = let affect_functions = affect_functions, eq_ind2affect = eq_ind2affect - function (integ, eq_ind) # eq_ind refers to the equation index that triggered the event, each event has num_eqs[i] equations - affect_neg = affect_functions[eq_ind2affect[eq_ind]].affect_neg - if isnothing(affect_neg) - return # skip if the neg function doesn't exist - don't want to split this into a separate VCC because that'd break ordering - end - affect_neg(integ) - end - end - function handle_optional_setup_fn(funs, default) - if all(isnothing, funs) - return default - else - return let funs = funs - function (cb, u, t, integ) - for func in funs - if isnothing(func) - continue - else - func(integ) - end - end - end - end - end - end - initialize = nothing - if has_index_cache(sys) && (ic = get_index_cache(sys)) !== nothing - initialize = handle_optional_setup_fn( - map(cbs, affect_functions) do cb, fn - if (save_idxs = get(ic.callback_to_clocks, cb, nothing)) !== nothing - let save_idxs = save_idxs - custom_init = fn.initialize - (i) -> begin - !isnothing(custom_init) && custom_init(i) - for idx in save_idxs - SciMLBase.save_discretes!(i, idx) - end - end - end - else - fn.initialize - end - end, - SciMLBase.INITIALIZE_DEFAULT) - - else - initialize = handle_optional_setup_fn( - map(fn -> fn.initialize, affect_functions), SciMLBase.INITIALIZE_DEFAULT) - end - - finalize = handle_optional_setup_fn( - map(fn -> fn.finalize, affect_functions), SciMLBase.FINALIZE_DEFAULT) - return VectorContinuousCallback( - cond, affect, affect_neg, length(eqs), rootfind = rootfind, - initialize = initialize, finalize = finalize, initializealg = reinitialization) -end - """ -Compile a single continuous callback affect function(s). +Compile user-defined functional affect. """ -function compile_affect_fn(cb, sys::AbstractTimeDependentSystem, dvs, ps, kwargs) - eq_aff = affects(cb) - eq_neg_aff = affect_negs(cb) - affect = compile_affect(eq_aff, cb, sys, dvs, ps; expression = Val{false}, kwargs...) - - if eq_neg_aff === eq_aff - affect_neg = affect - else - affect_neg = _compile_optional_affect( - NULL_AFFECT, eq_neg_aff, cb, sys, dvs, ps; kwargs...) - end - initialize = _compile_optional_affect( - NULL_AFFECT, initialize_affects(cb), cb, sys, dvs, ps; kwargs...) - finalize = _compile_optional_affect( - NULL_AFFECT, finalize_affects(cb), cb, sys, dvs, ps; kwargs...) - (affect = affect, affect_neg = affect_neg, initialize = initialize, finalize = finalize) -end - -function generate_rootfinding_callback(cbs, sys::AbstractTimeDependentSystem, - dvs = unknowns(sys), ps = parameters(sys; initial_parameters = true); kwargs...) - eqs = map(cb -> flatten_equations(cb.eqs), cbs) - num_eqs = length.(eqs) - total_eqs = sum(num_eqs) - (isempty(eqs) || total_eqs == 0) && return nothing - if total_eqs == 1 - # find the callback with only one eq - cb_ind = findfirst(>(0), num_eqs) - if isnothing(cb_ind) - error("Inconsistent state in affect compilation; one equation but no callback with equations?") - end - cb = cbs[cb_ind] - return generate_single_rootfinding_callback(cb.eqs[], cb, sys, dvs, ps; kwargs...) - end - - # group the cbs by what rootfind op they use - # groupby would be very useful here, but alas - cb_classes = Dict{ - @NamedTuple{ - rootfind::SciMLBase.RootfindOpt, - reinitialization::SciMLBase.DAEInitializationAlgorithm}, Vector{SymbolicContinuousCallback}}() - for cb in cbs - push!( - get!(() -> SymbolicContinuousCallback[], cb_classes, - ( - rootfind = cb.rootfind, - reinitialization = reinitialization_alg(cb))), - cb) - end - - # generate the callbacks out; we sort by the equivalence class to ensure a deterministic preference order - compiled_callbacks = map(collect(pairs(sort!( - OrderedDict(cb_classes); by = p -> p.rootfind)))) do (equiv_class, cbs_in_class) - return generate_vector_rootfinding_callback( - cbs_in_class, sys, dvs, ps; rootfind = equiv_class.rootfind, - reinitialization = equiv_class.reinitialization, kwargs...) - end - if length(compiled_callbacks) == 1 - return compiled_callbacks[] - else - return CallbackSet(compiled_callbacks...) - end -end - -function compile_user_affect(affect::FunctionalAffect, cb, sys, dvs, ps; kwargs...) +function compile_functional_affect(affect::FunctionalAffect, cb, sys, dvs, ps; kwargs...) dvs_ind = Dict(reverse(en) for en in enumerate(dvs)) v_inds = map(sym -> dvs_ind[sym], unknowns(affect)) if has_index_cache(sys) && get_index_cache(sys) !== nothing - p_inds = [if (pind = parameter_index(sys, sym)) === nothing - sym - else - pind - end - for sym in parameters(affect)] + p_inds = [(pind = parameter_index(sys, sym)) === nothing ? sym : pind for sym in parameters(affect)] + save_idxs = get(ic. callback_to_clocks, cb, Int[]) else ps_ind = Dict(reverse(en) for en in enumerate(ps)) p_inds = map(sym -> get(ps_ind, sym, sym), parameters(affect)) + save_idxs = Int[] end # HACK: filter out eliminated symbols. Not clear this is the right thing to do # (MTK should keep these symbols) @@ -1101,11 +478,6 @@ function compile_user_affect(affect::FunctionalAffect, cb, sys, dvs, ps; kwargs. p = filter(x -> !isnothing(x[2]), collect(zip(parameters_syms(affect), p_inds))) |> NamedTuple - if has_index_cache(sys) && (ic = get_index_cache(sys)) !== nothing - save_idxs = get(ic.callback_to_clocks, cb, Int[]) - else - save_idxs = Int[] - end let u = u, p = p, user_affect = func(affect), ctx = context(affect), save_idxs = save_idxs @@ -1118,151 +490,146 @@ function compile_user_affect(affect::FunctionalAffect, cb, sys, dvs, ps; kwargs. end end -function invalid_variables(sys, expr) - filter(x -> !any(isequal(x), all_symbols(sys)), reduce(vcat, vars(expr); init = [])) -end -function unassignable_variables(sys, expr) - assignable_syms = reduce( - vcat, Symbolics.scalarize.(vcat( - unknowns(sys), parameters(sys; initial_parameters = true))); - init = []) - written = reduce(vcat, Symbolics.scalarize.(vars(expr)); init = []) - return filter( - x -> !any(isequal(x), assignable_syms), written) -end +""" +Codegen a DifferentialEquations callback. A set of continuous callbacks becomes a VectorContinuousCallback. +Individual callbacks become DiscreteCallback, PresetTimeCallback, PeriodicCallback, or ContinuousCallback +depending on the case. +""" +function generate_callback(cbs::Vector{SymbolicContinuousCallback}, sys; is_discrete = false) + is_discrete && error() + eqs = map(cb -> flatten_equations(cb.eqs), cbs) + _, f_iip = generate_custom_function( + sys, [eq.lhs - eq.rhs for eq in eqs], unknowns(sys), parameters(sys); + expression = Val{false}) + trigger = (out, u, t, integ) -> f_iip(out, u, parameter_values(integ), t) -@generated function _generated_writeback(integ, setters::NamedTuple{NS1, <:Tuple}, - values::NamedTuple{NS2, <:Tuple}) where {NS1, NS2} - setter_exprs = [] - for name in NS2 - if !(name in NS1) - missing_name = "Tried to write back to $name from affect; only declared states ($NS1) may be written to." - error(missing_name) - end - push!(setter_exprs, :(setters.$name(integ, values.$name))) - end - return :(begin - $(setter_exprs...) - end) -end + affects = [] + affect_negs = [] + inits = [] + finals = [] + for cb in cbs + affect = compile_affect(cb.affect) + push!(affects, affect) -function check_assignable(sys, sym) - if symbolic_type(sym) == ScalarSymbolic() - is_variable(sys, sym) || is_parameter(sys, sym) - elseif symbolic_type(sym) == ArraySymbolic() - is_variable(sys, sym) || is_parameter(sys, sym) || - all(x -> check_assignable(sys, x), collect(sym)) - elseif sym isa Union{AbstractArray, Tuple} - all(x -> check_assignable(sys, x), sym) - else - false + isnothing(cb.affect_neg) ? + push!(affect_negs, affect) : + push!(affect_negs, compile_affect(cb.affect_neg)) + + push!(inits, compile_affect(cb.initialize, default = SciMLBase.INITALIZE_DEFAULT)) + push!(finals, compile_affect(cb.finalize, default = SciMLBase.FINALIZE_DEFAULT)) end -end -function compile_affect(affect::FunctionalAffect, cb, sys, dvs, ps; kwargs...) - compile_user_affect(affect, cb, sys, dvs, ps; kwargs...) -end + # Since there may be different number of conditions and affects, + # we build a map that translates the condition eq. number to the affect number + num_eqs = length.(eqs) + eq2affect = reduce(vcat, + [fill(i, num_eqs[i]) for i in eachindex(affects)]) + @assert length(eq2affect) == length(eqs) + @assert maximum(eq2affect) == length(affect_functions) -function _compile_optional_affect(default, aff, cb, sys, dvs, ps; kwargs...) - if isnothing(aff) || aff == default - return nothing - else - return compile_affect(aff, cb, sys, dvs, ps; expression = Val{false}, kwargs...) + affect = function (integ, idx) + affects[eq2affect[idx]](integ) + end + affect_neg = function (integ, idx) + f = affect_negs[eq2affect[idx]] + isnothing(f) && return + f(integ) end + initialize = compile_vector_optional_affect(inits, SciMLBase.INITIALIZE_DEFAULT) + finalize = compile_vector_optional_affect(finals, SciMLBase.FINALIZE_DEFAULT) + + return VectorContinuousCallback(trigger, affect; affect_neg, initialize, finalize, rootfind = callback.rootfind, initializealg = SciMLBase.NoInit) end -function generate_timed_callback(cb, sys, dvs, ps; postprocess_affect_expr! = nothing, - kwargs...) - cond = condition(cb) - as = compile_affect(affects(cb), cb, sys, dvs, ps; expression = Val{false}, - postprocess_affect_expr!, kwargs...) - - user_initfun = _compile_optional_affect( - NULL_AFFECT, initialize_affects(cb), cb, sys, dvs, ps; kwargs...) - user_finfun = _compile_optional_affect( - NULL_AFFECT, finalize_affects(cb), cb, sys, dvs, ps; kwargs...) - if has_index_cache(sys) && (ic = get_index_cache(sys)) !== nothing && - (save_idxs = get(ic.callback_to_clocks, cb, nothing)) !== nothing - initfn = let - save_idxs = save_idxs - initfun = user_initfun - function (cb, u, t, integrator) - if !isnothing(initfun) - initfun(integrator) - end - for idx in save_idxs - SciMLBase.save_discretes!(integrator, idx) - end - end +function generate_callback(cb, sys; is_discrete = false) + is_timed = is_timed_condition(conditions(cb)) + + trigger = if is_discrete + is_timed ? condition(cb) : + compile_condition(callback, sys, unknowns(sys), parameters(sys)) + else + _, f_iip = generate_custom_function( + sys, [eq.rhs - eq.lhs for eq in equations(cb)], unknowns(sys), parameters(sys); + expression = Val{false}) + (out, u, t, integ) -> f_iip(out, u, parameter_values(integ), t) + end + + affect = compile_affect(cb.affect) + affect_neg = isnothing(cb.affect_neg) ? affect_fn : compile_affect(cb.affect_neg) + initialize = compile_affect(cb.initialize, default = SciMLBase.INITIALIZE_DEFAULT) + finalize = compile_affect(cb.finalize, default = SciMLBase.FINALIZE_DEFAULT) + + if is_discrete + if is_timed && condition(cb) isa AbstractVector + return PresetTimeCallback(trigger, affect; affect_neg, initialize, finalize, initializealg = SciMLBase.NoInit) + elseif is_timed + return PeriodicCallback(affect, trigger; initialize, finalize) + else + return DiscreteCallback(trigger, affect; affect_neg, initialize, finalize, initializealg = SciMLBase.NoInit) end else - initfn = isnothing(user_initfun) ? SciMLBase.INITIALIZE_DEFAULT : - (_, _, _, i) -> user_initfun(i) - end - finfun = isnothing(user_finfun) ? SciMLBase.FINALIZE_DEFAULT : - (_, _, _, i) -> user_finfun(i) - if cond isa AbstractVector - # Preset Time - return PresetTimeCallback( - cond, as; initialize = initfn, finalize = finfun, - initializealg = reinitialization_alg(cb)) - else - # Periodic - return PeriodicCallback( - as, cond; initialize = initfn, finalize = finfun, - initializealg = reinitialization_alg(cb)) + return ContinuousCallback(trigger, affect; affect_neg, initialize, finalize, rootfind = callback.rootfind, initializealg = SciMLBase.NoInit) end end -function generate_discrete_callback(cb, sys, dvs, ps; postprocess_affect_expr! = nothing, - kwargs...) - if is_timed_condition(cb) - return generate_timed_callback(cb, sys, dvs, ps; postprocess_affect_expr!, - kwargs...) +""" + compile_affect(cb::AbstractCallback, sys::AbstractSystem, dvs, ps; expression, outputidxs, kwargs...) + +Returns a function that takes an integrator as argument and modifies the state with the +affect. The generated function has the signature `affect!(integrator)`. + +Notes + + - `expression = Val{true}`, causes the generated function to be returned as an expression. + If set to `Val{false}` a `RuntimeGeneratedFunction` will be returned. + - `outputidxs`, a vector of indices of the output variables which should correspond to + `unknowns(sys)`. If provided, checks that the LHS of affect equations are variables are + dropped, i.e. it is assumed these indices are correct and affect equations are + well-formed. + - `kwargs` are passed through to `Symbolics.build_function`. +""" +function compile_affect(aff::Affect, cb::AbstractCallback, sys::AbstractSystem; default = nothing) + save_idxs = if !(has_index_cache(sys) && (ic = get_index_cache(sys)) !== nothing) + Int[] else - c = compile_condition(cb, sys, dvs, ps; expression = Val{false}, kwargs...) - as = compile_affect(affects(cb), cb, sys, dvs, ps; expression = Val{false}, - postprocess_affect_expr!, kwargs...) - - user_initfun = _compile_optional_affect( - NULL_AFFECT, initialize_affects(cb), cb, sys, dvs, ps; kwargs...) - user_finfun = _compile_optional_affect( - NULL_AFFECT, finalize_affects(cb), cb, sys, dvs, ps; kwargs...) - if has_index_cache(sys) && (ic = get_index_cache(sys)) !== nothing && - (save_idxs = get(ic.callback_to_clocks, cb, nothing)) !== nothing - initfn = let save_idxs = save_idxs, initfun = user_initfun - function (cb, u, t, integrator) - if !isnothing(initfun) - initfun(integrator) - end - for idx in save_idxs - SciMLBase.save_discretes!(integrator, idx) - end - end + get(ic.callback_to_clocks, cb, Int[]) + end + + isnothing(aff) && return default + + ps = parameters(aff) + dvs = unknowns(aff) + + if aff isa ImplicitDiscreteSystem + function affect!(integrator) + u0map = [u => integrator[u] for u in dvs] + prob = ImplicitDiscreteProblem(aff, u0map, (0, 1), []) + sol = solve(prob) + for u in dvs + integrator[u] = sol[u][end] + end + + for idx in save_idxs + SciMLBase.save_discretes!(integ, idx) end - else - initfn = isnothing(user_initfun) ? SciMLBase.INITIALIZE_DEFAULT : - (_, _, _, i) -> user_initfun(i) end - finfun = isnothing(user_finfun) ? SciMLBase.FINALIZE_DEFAULT : - (_, _, _, i) -> user_finfun(i) - return DiscreteCallback( - c, as; initialize = initfn, finalize = finfun, - initializealg = reinitialization_alg(cb)) + elseif aff isa FunctionalAffect || aff isa ImperativeAffect + compile_functional_affect(aff, callback, sys, dvs, ps) end end -function generate_discrete_callbacks(sys::AbstractSystem, dvs = unknowns(sys), - ps = parameters(sys; initial_parameters = true); kwargs...) - has_discrete_events(sys) || return nothing - symcbs = discrete_events(sys) - isempty(symcbs) && return nothing - - dbs = map(symcbs) do cb - generate_discrete_callback(cb, sys, dvs, ps; kwargs...) +""" +Initialize and Finalize for VectorContinuousCallback. +""" +function compile_vector_optional_affect(funs, default) + all(isnothing, funs) && return default + return let funs = funs + function (cb, u, t, integ) + for func in funs + isnothing(func) ? continue : func(integ) + end + end end - - dbs end merge_cb(::Nothing, ::Nothing) = nothing @@ -1272,12 +639,12 @@ merge_cb(x, y) = CallbackSet(x, y) function process_events(sys; callback = nothing, kwargs...) if has_continuous_events(sys) && !isempty(continuous_events(sys)) - contin_cb = generate_rootfinding_callback(sys; kwargs...) + contin_cb = generate_callback(sys; kwargs...) else contin_cb = nothing end if has_discrete_events(sys) && !isempty(discrete_events(sys)) - discrete_cb = generate_discrete_callbacks(sys; kwargs...) + discrete_cb = generate_callback(sys; is_discrete = true, kwargs...) else discrete_cb = nothing end @@ -1285,3 +652,58 @@ function process_events(sys; callback = nothing, kwargs...) cb = merge_cb(contin_cb, callback) (discrete_cb === nothing) ? cb : CallbackSet(contin_cb, discrete_cb...) end + +""" + discrete_events(sys::AbstractSystem) :: Vector{SymbolicDiscreteCallback} + +Returns a vector of all the `discrete_events` in an abstract system and its component subsystems. +The `SymbolicDiscreteCallback`s in the returned vector are structs with two fields: `condition` and +`affect` which correspond to the first and second elements of a `Pair` used to define an event, i.e. +`condition => affect`. + +See also `get_discrete_events`, which only returns the events of the top-level system. +""" +function discrete_events(sys::AbstractSystem) + obs = get_discrete_events(sys) + systems = get_systems(sys) + cbs = [obs; + reduce(vcat, + (map(o -> namespace_callback(o, s), discrete_events(s)) for s in systems), + init = SymbolicDiscreteCallback[])] + cbs +end + +has_discrete_events(sys::AbstractSystem) = isdefined(sys, :discrete_events) +function get_discrete_events(sys::AbstractSystem) + has_discrete_events(sys) || return SymbolicDiscreteCallback[] + getfield(sys, :discrete_events) +end + +""" + continuous_events(sys::AbstractSystem)::Vector{SymbolicContinuousCallback} + +Returns a vector of all the `continuous_events` in an abstract system and its component subsystems. +The `SymbolicContinuousCallback`s in the returned vector are structs with two fields: `eqs` and +`affect` which correspond to the first and second elements of a `Pair` used to define an event, i.e. +`eqs => affect`. + +See also `get_continuous_events`, which only returns the events of the top-level system. +""" +function continuous_events(sys::AbstractSystem) + obs = get_continuous_events(sys) + filter(!isempty, obs) + + systems = get_systems(sys) + cbs = [obs; + reduce(vcat, + (map(o -> namespace_callback(o, s), continuous_events(s)) + for s in systems), + init = SymbolicContinuousCallback[])] + filter(!isempty, cbs) +end + +has_continuous_events(sys::AbstractSystem) = isdefined(sys, :continuous_events) +function get_continuous_events(sys::AbstractSystem) + has_continuous_events(sys) || return SymbolicContinuousCallback[] + getfield(sys, :continuous_events) +end diff --git a/src/systems/diffeqs/odesystem.jl b/src/systems/diffeqs/odesystem.jl index bd0a58e10a..3833b061da 100644 --- a/src/systems/diffeqs/odesystem.jl +++ b/src/systems/diffeqs/odesystem.jl @@ -310,8 +310,10 @@ function ODESystem(deqs::AbstractVector{<:Equation}, iv, dvs, ps; if length(unique(sysnames)) != length(sysnames) throw(ArgumentError("System names must be unique.")) end - cont_callbacks = SymbolicContinuousCallbacks(continuous_events) - disc_callbacks = SymbolicDiscreteCallbacks(discrete_events) + + algeeqs = filter(is_alg_equation, deqs) + cont_callbacks = generate_continuous_callbacks(continuous_events, algeeqs) + disc_callbacks = generate_discrete_callbacks(discrete_events, algeeqs) if is_dde === nothing is_dde = _check_if_dde(deqs, iv′, systems) diff --git a/src/systems/imperative_affect.jl b/src/systems/imperative_affect.jl index 4c9ff3d248..a58c608233 100644 --- a/src/systems/imperative_affect.jl +++ b/src/systems/imperative_affect.jl @@ -99,7 +99,6 @@ function Base.hash(a::ImperativeAffect, s::UInt) hash(a.ctx, s) end -namespace_affects(af::ImperativeAffect, s) = namespace_affect(af, s) function namespace_affect(affect::ImperativeAffect, s) ImperativeAffect(func(affect), namespace_expr.(observed(affect), (s,)), @@ -114,6 +113,49 @@ function compile_affect(affect::ImperativeAffect, cb, sys, dvs, ps; kwargs...) compile_user_affect(affect, cb, sys, dvs, ps; kwargs...) end +function invalid_variables(sys, expr) + filter(x -> !any(isequal(x), all_symbols(sys)), reduce(vcat, vars(expr); init = [])) +end + +function unassignable_variables(sys, expr) + assignable_syms = reduce( + vcat, Symbolics.scalarize.(vcat( + unknowns(sys), parameters(sys; initial_parameters = true))); + init = []) + written = reduce(vcat, Symbolics.scalarize.(vars(expr)); init = []) + return filter( + x -> !any(isequal(x), assignable_syms), written) +end + +@generated function _generated_writeback(integ, setters::NamedTuple{NS1, <:Tuple}, + values::NamedTuple{NS2, <:Tuple}) where {NS1, NS2} + setter_exprs = [] + for name in NS2 + if !(name in NS1) + missing_name = "Tried to write back to $name from affect; only declared states ($NS1) may be written to." + error(missing_name) + end + push!(setter_exprs, :(setters.$name(integ, values.$name))) + end + return :(begin + $(setter_exprs...) + end) +end + +function check_assignable(sys, sym) + if symbolic_type(sym) == ScalarSymbolic() + is_variable(sys, sym) || is_parameter(sys, sym) + elseif symbolic_type(sym) == ArraySymbolic() + is_variable(sys, sym) || is_parameter(sys, sym) || + all(x -> check_assignable(sys, x), collect(sym)) + elseif sym isa Union{AbstractArray, Tuple} + all(x -> check_assignable(sys, x), sym) + else + false + end +end + + function compile_user_affect(affect::ImperativeAffect, cb, sys, dvs, ps; kwargs...) #= Implementation sketch: @@ -238,3 +280,4 @@ function vars!(vars, aff::ImperativeAffect; op = Differential) end return vars end + diff --git a/src/systems/systems.jl b/src/systems/systems.jl index 367e1cd4cd..6490c31ac2 100644 --- a/src/systems/systems.jl +++ b/src/systems/systems.jl @@ -41,10 +41,10 @@ function structural_simplify( end if newsys isa DiscreteSystem && any(eq -> symbolic_type(eq.lhs) == NotSymbolic(), equations(newsys)) - error(""" - Encountered algebraic equations when simplifying discrete system. Please construct \ - an ImplicitDiscreteSystem instead. - """) + #error(""" + # Encountered algebraic equations when simplifying discrete system. Please construct \ + # an ImplicitDiscreteSystem instead. + #""") end for pass in additional_passes newsys = pass(newsys) From 3f051bd2318e02538a6dcf8837d08380b7cfd92f Mon Sep 17 00:00:00 2001 From: vyudu Date: Mon, 10 Mar 2025 16:23:40 -0400 Subject: [PATCH 04/52] use Pre in the affect definition --- src/ModelingToolkit.jl | 1 + src/systems/callbacks.jl | 231 ++++++++++++++++++++++--------- src/systems/diffeqs/odesystem.jl | 4 +- src/systems/imperative_affect.jl | 4 +- 4 files changed, 169 insertions(+), 71 deletions(-) diff --git a/src/ModelingToolkit.jl b/src/ModelingToolkit.jl index 259b5a310c..76698d3329 100644 --- a/src/ModelingToolkit.jl +++ b/src/ModelingToolkit.jl @@ -302,6 +302,7 @@ export initialization_equations, guesses, defaults, parameter_dependencies, hier export structural_simplify, expand_connections, linearize, linearization_function, LinearizationProblem export solve +export Pre export calculate_jacobian, generate_jacobian, generate_function, generate_custom_function export calculate_control_jacobian, generate_control_jacobian diff --git a/src/systems/callbacks.jl b/src/systems/callbacks.jl index 42ab150c92..30b1d4bd29 100644 --- a/src/systems/callbacks.jl +++ b/src/systems/callbacks.jl @@ -55,6 +55,62 @@ function has_functional_affect(cb) (affects(cb) isa FunctionalAffect || affects(cb) isa ImperativeAffect) end +function vars!(vars, aff::FunctionalAffect; op = Differential) + for var in Iterators.flatten((unknowns(aff), parameters(aff), discretes(aff))) + vars!(vars, var) + end + return vars +end + +""" + Pre(x) + +The `Pre` operator. Used by the callback system to indicate the value of a parameter or variable +before the callback is triggered. +""" +struct Pre <: Symbolics.Operator end +Pre(x) = Pre()(x) +SymbolicUtils.promote_symtype(::Type{Pre}, T) = T +SymbolicUtils.isbinop(::Pre) = false +Base.nameof(::Pre) = :Pre +Base.show(io::IO, x::Pre) = print(io, "Pre") +input_timedomain(::Pre, _ = nothing) = ContinuousClock() +output_timedomain(::Pre, _ = nothing) = ContinuousClock() + +function (p::Pre)(x) + iw = Symbolics.iswrapped(x) + x = unwrap(x) + # non-symbolic values don't change + if symbolic_type(x) == NotSymbolic() + return x + end + # differential variables are default-toterm-ed + if iscall(x) && operation(x) isa Differential + x = default_toterm(x) + end + # don't double wrap + iscall(x) && operation(x) isa Pre && return x + result = if symbolic_type(x) == ArraySymbolic() + # create an array for `Pre(array)` + Symbolics.array_term(p, toparam(x)) + elseif iscall(x) && operation(x) == getindex + # instead of `Pre(x[1])` create `Pre(x)[1]` + # which allows parameter indexing to handle this case automatically. + arr = arguments(x)[1] + term(getindex, p(toparam(arr)), arguments(x)[2:end]...) + else + term(p, toparam(x)) + end + # the result should be a parameter + result = toparam(result) + if iw + result = wrap(result) + end + return result +end + +haspre(eq::Equation) = haspre(eq.lhs) || haspre(eq.rhs) +haspre(O) = recursive_hasoperator(Pre, O) ############################### ###### Continuous events ###### @@ -131,24 +187,30 @@ make_affect(affect::Tuple, iv) = FunctionalAffect(affects...) make_affect(affect::NamedTuple, iv) = FunctionalAffect(; affects...) make_affect(affect::FunctionalAffect, iv) = affect -# Default behavior: if no shifts are provided, then it is assumed that the RHS is the previous. -function make_affect(affect::Vector{Equation}, iv) +function make_affect(affect::Vector{Equation}, iv; warn = true) affect = scalarize(affect) unknowns = OrderedSet() params = OrderedSet() for eq in affect - collect_vars!(unknowns, params, eq, iv) + !haspre(eq) && warn && @warn "Equation $eq has no `Pre` operator. As such it will be interpreted as an algebraic equation to be satisfied after the callback. + If you intended to use the value of a variable x before the affect, use Pre(x)." + collect_vars!(unknowns, params, eq, iv; op = Pre) end - affect = map(affect) do eq - ModelingToolkit.hasshift(eq) ? eq : - eq.lhs ~ distribute_shift(Prev(eq.rhs)) - end - params = map(params) do p - p = value(p) - Sym{FnType{Tuple{symtype(iv)}, Real}}(nameof(p))(iv) + + # System parameters should become unknowns. + cb_params = OrderedSet() + sys_params = OrderedSet() + for p in params + if iscall(p) && (operator(p) isa Pre) + push!(cb_params, p) + else + p = Sym{FnType{Tuple{symtype(iv)}, Real}}(nameof(p))(iv) + p = wrap(tovar(p)) + push!(sys_params, p) + end end - @mtkbuild affect = ImplicitDiscreteSystem(affect, iv, vcat(unknowns, params), []) + @mtkbuild affect = ImplicitDiscreteSystem(affect, iv, vcat(unknowns, sys_params), cb_params) end make_affect(affect, iv) = error("Malformed affect $(affect). This should be a vector of equations or a tuple specifying a functional affect.") @@ -157,7 +219,7 @@ make_affect(affect, iv) = error("Malformed affect $(affect). This should be a ve Generate continuous callbacks. """ function SymbolicContinuousCallbacks(events, algeeqs, iv) - callbacks = MTKContinuousCallback[] + callbacks = SymbolicContinuousCallback[] (isnothing(events) || isempty(events)) && return callbacks events isa AbstractVector || (events = [events]) @@ -229,6 +291,22 @@ function Base.show(io::IO, mime::MIME"text/plain", cb::SymbolicContinuousCallbac end end +function vars!(vars, cb::SymbolicContinuousCallback; op = Differential) + for eq in equations(cb) + vars!(vars, eq; op) + end + for aff in (affects(cb), affect_negs(cb), initialize_affects(cb), finalize_affects(cb)) + if aff isa Vector{Equation} + for eq in aff + vars!(vars, eq; op) + end + elseif aff !== nothing + vars!(vars, aff; op) + end + end + return vars +end + ################################ ######## Discrete events ####### ################################ @@ -240,12 +318,12 @@ end A callback that triggers at the first timestep that the conditions are satisfied. The condition can be one of: -- Real - periodic events with period Δt -- Vector{Real} - events trigger at these preset times -- Vector{Equation} - events trigger when the condition evaluates to true +- Δt::Real - periodic events with period Δt +- ts::Vector{Real} - events trigger at these preset times given by `ts` +- eqs::Vector{Equation} - events trigger when the condition evaluates to true """ -struct SymbolicDiscreteCallback{R} <: AbstractCallback where R <: Real - conditions::Union{R, Vector{R}, Vector{Equation}} +struct SymbolicDiscreteCallback <: AbstractCallback + conditions::Any affect::Affect initialize::Union{Affect, Nothing} finalize::Union{Affect, Nothing} @@ -277,11 +355,11 @@ function SymbolicDiscreteCallbacks(events, algeeqs, iv) callbacks end -function is_timed_condition(condition::T) +function is_timed_condition(condition::T) where T if T <: Real true elseif T <: AbstractVector - eltype(V) <: Real + eltype(condition) <: Real else false end @@ -298,11 +376,6 @@ function Base.show(io::IO, db::SymbolicDiscreteCallback) show(iio, affects(db)) print(iio, ", ") end - if affect_negs(db) != nothing - print(iio, "Negative-edge affect:") - show(iio, affect_negs(db)) - print(iio, ", ") - end if initialize_affects(db) != nothing print(iio, "Initialization affect:") show(iio, initialize_affects(db)) @@ -315,6 +388,28 @@ function Base.show(io::IO, db::SymbolicDiscreteCallback) print(iio, ")") end +function vars!(vars, cb::SymbolicDiscreteCallback; op = Differential) + if symbolic_type(cb.condition) == NotSymbolic + if cb.condition isa AbstractArray + for eq in cb.condition + vars!(vars, eq; op) + end + end + else + vars!(vars, cb.condition; op) + end + for aff in (cb.affects, cb.initialize, cb.finalize) + if aff isa Vector{Equation} + for eq in aff + vars!(vars, eq; op) + end + elseif aff !== nothing + vars!(vars, aff; op) + end + end + return vars +end + ############################################ ########## Namespacing Utilities ########### ############################################ @@ -382,7 +477,7 @@ end ########################### conditions(cb::AbstractCallback) = cb.conditions -conditions(cbs::Vector{<:AbstractCallback}) = reduce(vcat, conditions(cb) for cb in cbs) +conditions(cbs::Vector{<:AbstractCallback}) = reduce(vcat, conditions(cb) for cb in cbs; init = []) equations(cb::AbstractCallback) = conditions(cb) equations(cb::Vector{<:AbstractCallback}) = conditions(cb) @@ -390,13 +485,13 @@ affects(cb::AbstractCallback) = cb.affect affects(cbs::Vector{<:AbstractCallback}) = reduce(vcat, affects(cb) for cb in cbs; init = []) affect_negs(cb::SymbolicContinuousCallback) = cb.affect_neg -affect_negs(cbs::Vector{SymbolicContinuousCallback})= mapreduce(affect_negs, vcat, cbs, init = Equation[]) +affect_negs(cbs::Vector{SymbolicContinuousCallback}) = reduce(vcat, affect_negs(cb) for cb in cbs; init = []) initialize_affects(cb::AbstractCallback) = cb.initialize -initialize_affects(cbs::Vector{<:AbstractCallback}) = mapreduce(initialize_affects, vcat, cbs, init = Equation[]) +initialize_affects(cbs::Vector{<:AbstractCallback}) = reduce(initialize_affects, vcat, cbs; init = []) finalize_affects(cb::AbstractCallback) = cb.finalize -finalize_affects(cbs::Vector{<:AbstractCallback}) = mapreduce(finalize_affects, vcat, cbs, init = Equation[]) +finalize_affects(cbs::Vector{<:AbstractCallback}) = reduce(finalize_affects, vcat, cbs; init = []) function Base.:(==)(e1::SymbolicDiscreteCallback, e2::SymbolicDiscreteCallback) isequal(e1.condition, e2.condition) && isequal(e1.affects, e2.affects) && @@ -414,7 +509,6 @@ Base.isempty(cb::AbstractCallback) = isempty(cb.conditions) #################################### ####### Compilation functions ###### #################################### -# function condition_header(sys::AbstractSystem, integrator = gensym(:MTKIntegrator)) expr -> Func( [expr.args[1], expr.args[2], @@ -424,7 +518,7 @@ function condition_header(sys::AbstractSystem, integrator = gensym(:MTKIntegrato end """ - compile_condition(cb::SymbolicDiscreteCallback, sys, dvs, ps; expression, kwargs...) + compile_condition(cb::AbstractCallback, sys, dvs, ps; expression, kwargs...) Returns a function `condition(u,t,integrator)` returning the `condition(cb)`. @@ -434,12 +528,12 @@ Notes If set to `Val{false}` a `RuntimeGeneratedFunction` will be returned. - `kwargs` are passed through to `Symbolics.build_function`. """ -function compile_condition(cb::SymbolicDiscreteCallback, sys, dvs, ps; +function compile_condition(cb::AbstractCallback, sys, dvs, ps; expression = Val{true}, eval_expression = false, eval_module = @__MODULE__, kwargs...) u = map(x -> time_varying_as_func(value(x), sys), dvs) p = map.(x -> time_varying_as_func(value(x), sys), reorder_parameters(sys, ps)) t = get_iv(sys) - condit = condition(cb) + condit = conditions(cb) cs = collect_constants(condit) if !isempty(cs) cmap = map(x -> x => getdefault(x), cs) @@ -490,17 +584,20 @@ function compile_functional_affect(affect::FunctionalAffect, cb, sys, dvs, ps; k end end +is_discrete(cb::AbstractCallback) = cb isa SymbolicDiscreteCallback + """ Codegen a DifferentialEquations callback. A set of continuous callbacks becomes a VectorContinuousCallback. Individual callbacks become DiscreteCallback, PresetTimeCallback, PeriodicCallback, or ContinuousCallback depending on the case. """ -function generate_callback(cbs::Vector{SymbolicContinuousCallback}, sys; is_discrete = false) - is_discrete && error() +function generate_callback(cbs::Vector{SymbolicContinuousCallback}, sys; kwargs...) + length(cbs) == 1 && return generate_callback(only(cbs), sys) eqs = map(cb -> flatten_equations(cb.eqs), cbs) + _, f_iip = generate_custom_function( sys, [eq.lhs - eq.rhs for eq in eqs], unknowns(sys), parameters(sys); - expression = Val{false}) + expression = Val{false}, kwargs...) trigger = (out, u, t, integ) -> f_iip(out, u, parameter_values(integ), t) affects = [] @@ -509,12 +606,9 @@ function generate_callback(cbs::Vector{SymbolicContinuousCallback}, sys; is_disc finals = [] for cb in cbs affect = compile_affect(cb.affect) - push!(affects, affect) - - isnothing(cb.affect_neg) ? - push!(affect_negs, affect) : - push!(affect_negs, compile_affect(cb.affect_neg)) + push!(affects, affect) + push!(affect_negs, compile_affect(cb.affect_neg, default = affect)) push!(inits, compile_affect(cb.initialize, default = SciMLBase.INITALIZE_DEFAULT)) push!(finals, compile_affect(cb.finalize, default = SciMLBase.FINALIZE_DEFAULT)) end @@ -538,28 +632,21 @@ function generate_callback(cbs::Vector{SymbolicContinuousCallback}, sys; is_disc initialize = compile_vector_optional_affect(inits, SciMLBase.INITIALIZE_DEFAULT) finalize = compile_vector_optional_affect(finals, SciMLBase.FINALIZE_DEFAULT) - return VectorContinuousCallback(trigger, affect; affect_neg, initialize, finalize, rootfind = callback.rootfind, initializealg = SciMLBase.NoInit) + return VectorContinuousCallback(trigger, affect, length(cbs); affect_neg, initialize, finalize, rootfind = callback.rootfind, initializealg = SciMLBase.NoInit) end -function generate_callback(cb, sys; is_discrete = false) +function generate_callback(cb, sys; kwargs...) is_timed = is_timed_condition(conditions(cb)) + dvs = unknowns(sys) + ps = parameters(sys; initial_parameters = true) - trigger = if is_discrete - is_timed ? condition(cb) : - compile_condition(callback, sys, unknowns(sys), parameters(sys)) - else - _, f_iip = generate_custom_function( - sys, [eq.rhs - eq.lhs for eq in equations(cb)], unknowns(sys), parameters(sys); - expression = Val{false}) - (out, u, t, integ) -> f_iip(out, u, parameter_values(integ), t) - end - + trigger = is_timed ? conditions(cb) : compile_condition(cb, sys, dvs, ps; kwargs...) affect = compile_affect(cb.affect) - affect_neg = isnothing(cb.affect_neg) ? affect_fn : compile_affect(cb.affect_neg) + affect_neg = hasfield(cb, :affect_neg) ? compile_affect(cb.affect_neg, default = affect) : nothing initialize = compile_affect(cb.initialize, default = SciMLBase.INITIALIZE_DEFAULT) finalize = compile_affect(cb.finalize, default = SciMLBase.FINALIZE_DEFAULT) - if is_discrete + if is_discrete(cb) if is_timed && condition(cb) isa AbstractVector return PresetTimeCallback(trigger, affect; affect_neg, initialize, finalize, initializealg = SciMLBase.NoInit) elseif is_timed @@ -568,7 +655,7 @@ function generate_callback(cb, sys; is_discrete = false) return DiscreteCallback(trigger, affect; affect_neg, initialize, finalize, initializealg = SciMLBase.NoInit) end else - return ContinuousCallback(trigger, affect; affect_neg, initialize, finalize, rootfind = callback.rootfind, initializealg = SciMLBase.NoInit) + return ContinuousCallback(trigger, affect; affect_neg, initialize, finalize, rootfind = cb.rootfind, initializealg = SciMLBase.NoInit) end end @@ -597,16 +684,21 @@ function compile_affect(aff::Affect, cb::AbstractCallback, sys::AbstractSystem; isnothing(aff) && return default - ps = parameters(aff) + ps = parameters(aff; initial_parameters = true) dvs = unknowns(aff) if aff isa ImplicitDiscreteSystem function affect!(integrator) - u0map = [u => integrator[u] for u in dvs] - prob = ImplicitDiscreteProblem(aff, u0map, (0, 1), []) - sol = solve(prob) + pmap = [] + for pre_p in ps + p = only(arguments(unwrap(pre_p))) + push!(pmap, pre_p => integrator[p]) + end + guesses = [u => integrator[u] for u in dvs] + prob = ImplicitDiscreteProblem(aff, [], (0, 1), pmap; guesses) + sol = init(prob, SimpleIDSolve()) for u in dvs - integrator[u] = sol[u][end] + integrator[u] = sol[u] end for idx in save_idxs @@ -614,7 +706,7 @@ function compile_affect(aff::Affect, cb::AbstractCallback, sys::AbstractSystem; end end elseif aff isa FunctionalAffect || aff isa ImperativeAffect - compile_functional_affect(aff, callback, sys, dvs, ps) + compile_functional_affect(aff, callback, sys, dvs, ps; kwargs...) end end @@ -637,20 +729,25 @@ merge_cb(::Nothing, x) = merge_cb(x, nothing) merge_cb(x, ::Nothing) = x merge_cb(x, y) = CallbackSet(x, y) +""" +Generate the CallbackSet for a ODESystem or SDESystem. +""" function process_events(sys; callback = nothing, kwargs...) if has_continuous_events(sys) && !isempty(continuous_events(sys)) - contin_cb = generate_callback(sys; kwargs...) + cbs = continuous_events(sys) + contin_cbs = generate_callback(cbs, sys; kwargs...) else - contin_cb = nothing + contin_cbs = nothing end if has_discrete_events(sys) && !isempty(discrete_events(sys)) - discrete_cb = generate_callback(sys; is_discrete = true, kwargs...) + dbs = discrete_events(sys) + discrete_cbs = [generate_callback(db, sys; kwargs...) for db in dbs] else - discrete_cb = nothing + discrete_cbs = nothing end - cb = merge_cb(contin_cb, callback) - (discrete_cb === nothing) ? cb : CallbackSet(contin_cb, discrete_cb...) + cb = merge_cb(contin_cbs, callback) + (discrete_cbs === nothing) ? cb : CallbackSet(contin_cbs, discrete_cbs...) end """ diff --git a/src/systems/diffeqs/odesystem.jl b/src/systems/diffeqs/odesystem.jl index 3833b061da..d5239df40a 100644 --- a/src/systems/diffeqs/odesystem.jl +++ b/src/systems/diffeqs/odesystem.jl @@ -312,8 +312,8 @@ function ODESystem(deqs::AbstractVector{<:Equation}, iv, dvs, ps; end algeeqs = filter(is_alg_equation, deqs) - cont_callbacks = generate_continuous_callbacks(continuous_events, algeeqs) - disc_callbacks = generate_discrete_callbacks(discrete_events, algeeqs) + cont_callbacks = SymbolicContinuousCallbacks(continuous_events, algeeqs, iv) + disc_callbacks = SymbolicDiscreteCallbacks(discrete_events, algeeqs, iv) if is_dde === nothing is_dde = _check_if_dde(deqs, iv′, systems) diff --git a/src/systems/imperative_affect.jl b/src/systems/imperative_affect.jl index a58c608233..0b578f55c5 100644 --- a/src/systems/imperative_affect.jl +++ b/src/systems/imperative_affect.jl @@ -110,7 +110,7 @@ function namespace_affect(affect::ImperativeAffect, s) end function compile_affect(affect::ImperativeAffect, cb, sys, dvs, ps; kwargs...) - compile_user_affect(affect, cb, sys, dvs, ps; kwargs...) + compile_functional_affect(affect, cb, sys, dvs, ps; kwargs...) end function invalid_variables(sys, expr) @@ -156,7 +156,7 @@ function check_assignable(sys, sym) end -function compile_user_affect(affect::ImperativeAffect, cb, sys, dvs, ps; kwargs...) +function compile_functional_affect(affect::ImperativeAffect, cb, sys, dvs, ps; kwargs...) #= Implementation sketch: generate observed function (oop), should save to a component array under obs_syms From 677c426ce3b78fe4fdd6b76ead68432f9ccddee2 Mon Sep 17 00:00:00 2001 From: vyudu Date: Tue, 11 Mar 2025 18:19:58 -0400 Subject: [PATCH 05/52] refactor: correct condition generation in --- src/systems/callbacks.jl | 344 ++++++++++------- src/systems/codegen_utils.jl | 1 - src/systems/diffeqs/odesystem.jl | 4 +- src/systems/imperative_affect.jl | 2 - src/systems/index_cache.jl | 2 + test/symbolic_events.jl | 634 +++++++++++++------------------ 6 files changed, 479 insertions(+), 508 deletions(-) diff --git a/src/systems/callbacks.jl b/src/systems/callbacks.jl index 30b1d4bd29..b25d64d59c 100644 --- a/src/systems/callbacks.jl +++ b/src/systems/callbacks.jl @@ -62,6 +62,26 @@ function vars!(vars, aff::FunctionalAffect; op = Differential) return vars end +struct AffectSystem + system::ImplicitDiscreteSystem + unknowns::Vector + parameters::Vector + discretes::Vector + """Maps the unknowns in the ImplicitDiscreteSystem to the corresponding parameter or unknown in the parent system.""" + affu_to_sysu::Dict +end + +system(a::AffectSystem) = a.system +discretes(a::AffectSystem) = a.discretes +unknowns(a::AffectSystem) = a.unknowns +parameters(a::AffectSystem) = a.parameters +affu_to_sysu(a::AffectSystem) = a.affu_to_sysu + +function Base.show(iio::IO, aff::AffectSystem) + eqs = vcat(equations(system(aff)), observed(system(aff))) + show(iio, eqs) +end + """ Pre(x) @@ -77,7 +97,7 @@ Base.show(io::IO, x::Pre) = print(io, "Pre") input_timedomain(::Pre, _ = nothing) = ContinuousClock() output_timedomain(::Pre, _ = nothing) = ContinuousClock() -function (p::Pre)(x) +function (p::Pre)(x) iw = Symbolics.iswrapped(x) x = unwrap(x) # non-symbolic values don't change @@ -115,7 +135,7 @@ haspre(O) = recursive_hasoperator(Pre, O) ############################### ###### Continuous events ###### ############################### -const Affect = Union{ImplicitDiscreteSystem, FunctionalAffect, ImperativeAffect} +const Affect = Union{AffectSystem, FunctionalAffect, ImperativeAffect} """ SymbolicContinuousCallback(eqs::Vector{Equation}, affect, affect_neg, rootfind) @@ -172,63 +192,87 @@ struct SymbolicContinuousCallback <: AbstractCallback rootfind::Union{Nothing, SciMLBase.RootfindOpt} function SymbolicContinuousCallback( - conditions::Vector{Equation}, + conditions::Union{Equation, Vector{Equation}}, affect = nothing; affect_neg = affect, initialize = nothing, finalize = nothing, rootfind = SciMLBase.LeftRootFind) - new(eqs, initialize, finalize, make_affect(affect), - make_affect(affect_neg), rootfind) + + conditions = (conditions isa AbstractVector) ? conditions : [conditions] + new(conditions, make_affect(affect), make_affect(affect_neg), + initialize, finalize, rootfind) end # Default affect to nothing end -make_affect(affect::Tuple, iv) = FunctionalAffect(affects...) -make_affect(affect::NamedTuple, iv) = FunctionalAffect(; affects...) -make_affect(affect::FunctionalAffect, iv) = affect +SymbolicContinuousCallback(p::Pair) = SymbolicContinuousCallback(p[1], p[2]) +SymbolicContinuousCallback(cb::SymbolicContinuousCallback, args...) = cb + +make_affect(affect::Nothing) = nothing +make_affect(affect::Tuple) = FunctionalAffect(affects...) +make_affect(affect::NamedTuple) = FunctionalAffect(; affects...) +make_affect(affect::FunctionalAffect) = affect +make_affect(affect::AffectSystem) = affect -function make_affect(affect::Vector{Equation}, iv; warn = true) +function make_affect(affect::Vector{Equation}; warn = true) affect = scalarize(affect) unknowns = OrderedSet() params = OrderedSet() + for eq in affect - !haspre(eq) && warn && @warn "Equation $eq has no `Pre` operator. As such it will be interpreted as an algebraic equation to be satisfied after the callback. - If you intended to use the value of a variable x before the affect, use Pre(x)." - collect_vars!(unknowns, params, eq, iv; op = Pre) + !haspre(eq) && warn && + @warn "Equation $eq has no `Pre` operator. As such it will be interpreted as an algebraic equation to be satisfied after the callback. If you intended to use the value of a variable x before the affect, use Pre(x)." + collect_vars!(unknowns, params, eq, nothing; op = Pre) end + iv = isempty(unknowns) ? t_nounits : only(arguments(unknowns[1])) - # System parameters should become unknowns. - cb_params = OrderedSet() - sys_params = OrderedSet() + # System parameters should become unknowns in the ImplicitDiscreteSystem. + cb_params = Any[] + discretes = Any[] + p_as_unknowns = Any[] for p in params if iscall(p) && (operator(p) isa Pre) push!(cb_params, p) + elseif iscall(p) && length(arguments(p)) == 1 && + isequal(only(arguments(p)), iv) + push!(discretes, p) + push!(p_as_unknowns, tovar(p)) else - p = Sym{FnType{Tuple{symtype(iv)}, Real}}(nameof(p))(iv) - p = wrap(tovar(p)) - push!(sys_params, p) + push!(discretes, p) + p = iscall(p) ? wrap(Sym{FnType{Tuple{symtype(iv)}, Real}}(nameof(operation(p)))(iv)) : + wrap(Sym{FnType{Tuple{symtype(iv)}, Real}}(nameof(p))(iv)) + push!(p_as_unknowns, p) end end - - @mtkbuild affect = ImplicitDiscreteSystem(affect, iv, vcat(unknowns, sys_params), cb_params) + @mtkbuild affectsys = ImplicitDiscreteSystem( + affect, iv, collect(union(unknowns, p_as_unknowns)), cb_params) + params = map(x -> only(arguments(unwrap(x))), cb_params) + affmap = Dict(zip([p_as_unknowns, unknowns], [discretes, unknowns])) + + return AffectSystem(affectsys, collect(unknowns), params, discretes, affmap) end -make_affect(affect, iv) = error("Malformed affect $(affect). This should be a vector of equations or a tuple specifying a functional affect.") +function make_affect(affect) + error("Malformed affect $(affect). This should be a vector of equations or a tuple specifying a functional affect.") +end """ Generate continuous callbacks. -""" -function SymbolicContinuousCallbacks(events, algeeqs, iv) +""" +function SymbolicContinuousCallbacks(events, algeeqs::Vector{Equation} = Equation[]) callbacks = SymbolicContinuousCallback[] - (isnothing(events) || isempty(events)) && return callbacks + isnothing(events) && return callbacks events isa AbstractVector || (events = [events]) - for (cond, affs) in events + isempty(events) && return callbacks + + for event in events + cond, affs = event isa Pair ? (event[1], event[2]) : (event, nothing) if affs isa AbstractVector affs = vcat(affs, algeeqs) end - affect = make_affect(affs, iv) - push!(callbacks, SymbolicContinuousCallback(cond, affect, affect, nothing, nothing, SciMLBase.LeftRootFind)) + affect = make_affect(affs) + push!(callbacks, SymbolicContinuousCallback(cond, affect)) end callbacks end @@ -240,22 +284,22 @@ function Base.show(io::IO, cb::SymbolicContinuousCallback) print(iio, "Equations:") show(iio, equations(cb)) print(iio, "; ") - if affects(cb) != NULL_AFFECT + if affects(cb) != nothing print(iio, "Affect:") show(iio, affects(cb)) print(iio, ", ") end - if affect_negs(cb) != NULL_AFFECT + if affect_negs(cb) != nothing print(iio, "Negative-edge affect:") show(iio, affect_negs(cb)) print(iio, ", ") end - if initialize_affects(cb) != NULL_AFFECT + if initialize_affects(cb) != nothing print(iio, "Initialization affect:") show(iio, initialize_affects(cb)) print(iio, ", ") end - if finalize_affects(cb) != NULL_AFFECT + if finalize_affects(cb) != nothing print(iio, "Finalization affect:") show(iio, finalize_affects(cb)) end @@ -269,22 +313,22 @@ function Base.show(io::IO, mime::MIME"text/plain", cb::SymbolicContinuousCallbac println(iio, "Equations:") show(iio, mime, equations(cb)) print(iio, "\n") - if affects(cb) != NULL_AFFECT + if affects(cb) != nothing println(iio, "Affect:") show(iio, mime, affects(cb)) print(iio, "\n") end - if affect_negs(cb) != NULL_AFFECT - println(iio, "Negative-edge affect:") + if affect_negs(cb) != nothing + print(iio, "Negative-edge affect:\n") show(iio, mime, affect_negs(cb)) print(iio, "\n") end - if initialize_affects(cb) != NULL_AFFECT + if initialize_affects(cb) != nothing println(iio, "Initialization affect:") show(iio, mime, initialize_affects(cb)) print(iio, "\n") end - if finalize_affects(cb) != NULL_AFFECT + if finalize_affects(cb) != nothing println(iio, "Finalization affect:") show(iio, mime, finalize_affects(cb)) print(iio, "\n") @@ -322,7 +366,7 @@ The condition can be one of: - ts::Vector{Real} - events trigger at these preset times given by `ts` - eqs::Vector{Equation} - events trigger when the condition evaluates to true """ -struct SymbolicDiscreteCallback <: AbstractCallback +struct SymbolicDiscreteCallback <: AbstractCallback conditions::Any affect::Affect initialize::Union{Affect, Nothing} @@ -340,22 +384,25 @@ end """ Generate discrete callbacks. """ -function SymbolicDiscreteCallbacks(events, algeeqs, iv) +function SymbolicDiscreteCallbacks(events, algeeqs::Vector{Equation} = Equation[]) callbacks = SymbolicDiscreteCallback[] - (isnothing(events) || isempty(events)) && return callbacks + + isnothing(events) && return callbacks events isa AbstractVector || (events = [events]) + isempty(events) && return callbacks - for (cond, aff) in events + for event in events + cond, affs = event isa Pair ? (event[1], event[2]) : (event, nothing) if aff isa AbstractVector aff = vcat(aff, algeeqs) end - affect = make_affect(aff, iv) + affect = make_affect(aff) push!(callbacks, SymbolicDiscreteCallback(cond, affect, nothing, nothing)) end callbacks end -function is_timed_condition(condition::T) where T +function is_timed_condition(condition::T) where {T} if T <: Real true elseif T <: AbstractVector @@ -371,17 +418,17 @@ function Base.show(io::IO, db::SymbolicDiscreteCallback) println(io, "SymbolicDiscreteCallback:") println(iio, "Conditions:") print(iio, "; ") - if affects(db) != nothing + if affects(db) != nothing print(iio, "Affect:") show(iio, affects(db)) print(iio, ", ") end - if initialize_affects(db) != nothing + if initialize_affects(db) != nothing print(iio, "Initialization affect:") show(iio, initialize_affects(db)) print(iio, ", ") end - if finalize_affects(db) != nothing + if finalize_affects(db) != nothing print(iio, "Finalization affect:") show(iio, finalize_affects(db)) end @@ -424,24 +471,17 @@ function namespace_affect(affect::FunctionalAffect, s) context(affect)) end -function namespace_affects(af::Affect, s) - if af isa ImplicitDiscreteSystem - af - elseif af isa FunctionalAffect || af isa ImperativeAffect - namespace_affect(af, s) - else - nothing - end -end +namespace_affect(affect::AffectSystem, s) = AffectSystem(system(affect), renamespace.((s,), discretes(affect))) +namespace_affects(af::Union{Nothing, Affect}, s) = af isa Affect ? namespace_affect(af, s) : nothing function namespace_callback(cb::SymbolicContinuousCallback, s)::SymbolicContinuousCallback SymbolicContinuousCallback( namespace_equation.(equations(cb), (s,)), namespace_affects(affects(cb), s), - namespace_affects(affect_negs(cb), s), - namespace_affects(initialize_affects(cb), s), - namespace_affects(finalize_affects(cb), s), - cb.rootfind) + affect_neg = namespace_affects(affect_negs(cb), s), + initialize = namespace_affects(initialize_affects(cb), s), + finalize = namespace_affects(finalize_affects(cb), s), + rootfind = cb.rootfind) end function namespace_condition(condition, s) @@ -450,7 +490,7 @@ end function namespace_callback(cb::SymbolicDiscreteCallback, s)::SymbolicDiscreteCallback SymbolicDiscreteCallback( - namespace_condition(condition(cb), s), + namespace_condition(condition(cb), s), namespace_affects(affects(cb), s), namespace_affects(initialize_affects(cb), s), namespace_affects(finalize_affects(cb), s)) @@ -477,29 +517,39 @@ end ########################### conditions(cb::AbstractCallback) = cb.conditions -conditions(cbs::Vector{<:AbstractCallback}) = reduce(vcat, conditions(cb) for cb in cbs; init = []) +function conditions(cbs::Vector{<:AbstractCallback}) + reduce(vcat, conditions(cb) for cb in cbs; init = []) +end equations(cb::AbstractCallback) = conditions(cb) equations(cb::Vector{<:AbstractCallback}) = conditions(cb) affects(cb::AbstractCallback) = cb.affect -affects(cbs::Vector{<:AbstractCallback}) = reduce(vcat, affects(cb) for cb in cbs; init = []) +function affects(cbs::Vector{<:AbstractCallback}) + reduce(vcat, affects(cb) for cb in cbs; init = []) +end affect_negs(cb::SymbolicContinuousCallback) = cb.affect_neg -affect_negs(cbs::Vector{SymbolicContinuousCallback}) = reduce(vcat, affect_negs(cb) for cb in cbs; init = []) +function affect_negs(cbs::Vector{SymbolicContinuousCallback}) + reduce(vcat, affect_negs(cb) for cb in cbs; init = []) +end initialize_affects(cb::AbstractCallback) = cb.initialize -initialize_affects(cbs::Vector{<:AbstractCallback}) = reduce(initialize_affects, vcat, cbs; init = []) +function initialize_affects(cbs::Vector{<:AbstractCallback}) + reduce(initialize_affects, vcat, cbs; init = []) +end finalize_affects(cb::AbstractCallback) = cb.finalize -finalize_affects(cbs::Vector{<:AbstractCallback}) = reduce(finalize_affects, vcat, cbs; init = []) +function finalize_affects(cbs::Vector{<:AbstractCallback}) + reduce(finalize_affects, vcat, cbs; init = []) +end function Base.:(==)(e1::SymbolicDiscreteCallback, e2::SymbolicDiscreteCallback) - isequal(e1.condition, e2.condition) && isequal(e1.affects, e2.affects) && + isequal(e1.conditions, e2.conditions) && isequal(e1.affects, e2.affects) && isequal(e1.initialize, e2.initialize) && isequal(e1.finalize, e2.finalize) end function Base.:(==)(e1::SymbolicContinuousCallback, e2::SymbolicContinuousCallback) - isequal(e1.eqs, e2.eqs) && isequal(e1.affect, e2.affect) && + isequal(e1.conditions, e2.conditions) && isequal(e1.affect, e2.affect) && isequal(e1.initialize, e2.initialize) && isequal(e1.finalize, e2.finalize) && isequal(e1.affect_neg, e2.affect_neg) && isequal(e1.rootfind, e2.rootfind) end @@ -509,18 +559,10 @@ Base.isempty(cb::AbstractCallback) = isempty(cb.conditions) #################################### ####### Compilation functions ###### #################################### -function condition_header(sys::AbstractSystem, integrator = gensym(:MTKIntegrator)) - expr -> Func( - [expr.args[1], expr.args[2], - DestructuredArgs(expr.args[3:end], integrator, inds = [:p])], - [], - expr.body) -end - """ compile_condition(cb::AbstractCallback, sys, dvs, ps; expression, kwargs...) -Returns a function `condition(u,t,integrator)` returning the `condition(cb)`. +Returns a function `condition(u,t,integrator)`, condition(out,u,t,integrator)` returning the `condition(cb)`. Notes @@ -528,26 +570,40 @@ Notes If set to `Val{false}` a `RuntimeGeneratedFunction` will be returned. - `kwargs` are passed through to `Symbolics.build_function`. """ -function compile_condition(cb::AbstractCallback, sys, dvs, ps; - expression = Val{true}, eval_expression = false, eval_module = @__MODULE__, kwargs...) +function compile_condition(cbs::Union{AbstractCallback, Vector{<:AbstractCallback}}, sys, dvs, ps; + expression = Val{false}, eval_expression = false, eval_module = @__MODULE__, kwargs...) u = map(x -> time_varying_as_func(value(x), sys), dvs) p = map.(x -> time_varying_as_func(value(x), sys), reorder_parameters(sys, ps)) t = get_iv(sys) - condit = conditions(cb) + condit = conditions(cbs) cs = collect_constants(condit) if !isempty(cs) cmap = map(x -> x => getdefault(x), cs) condit = substitute(condit, cmap) end - expr = build_function_wrapper(sys, - condit, u, t, p...; expression = Val{true}, - p_start = 3, p_end = length(p) + 2, - wrap_code = condition_header(sys), - kwargs...) - if expression == Val{true} - return expr + + f_oop, f_iip = build_function_wrapper(sys, + condit, u, t, p...; expression = Val{true}, + p_start = 3, p_end = length(p) + 2, + kwargs...) + + if cbs isa AbstractVector + cond(out, u, t, integ) = f_iip(out, u, t, parameter_values(integ)) + elseif is_discrete(cbs) + cond(u, t, integ) = f_oop(u, t, parameter_values(integ)) + else + cond = function (u, t, integ) + if DiffEqBase.isinplace(integ.sol.prob) + tmp, = DiffEqBase.get_tmp_cache(integ) + f_iip(tmp, u, t, parameter_values(integ)) + tmp[1] + else + f_oop(u, t, parameter_values(integ)) + end + end end - return eval_or_rgf(expr; eval_expression, eval_module) + + cond end """ @@ -558,8 +614,9 @@ function compile_functional_affect(affect::FunctionalAffect, cb, sys, dvs, ps; k v_inds = map(sym -> dvs_ind[sym], unknowns(affect)) if has_index_cache(sys) && get_index_cache(sys) !== nothing - p_inds = [(pind = parameter_index(sys, sym)) === nothing ? sym : pind for sym in parameters(affect)] - save_idxs = get(ic. callback_to_clocks, cb, Int[]) + p_inds = [(pind = parameter_index(sys, sym)) === nothing ? sym : pind + for sym in parameters(affect)] + save_idxs = get(ic.callback_to_clocks, cb, Int[]) else ps_ind = Dict(reverse(en) for en in enumerate(ps)) p_inds = map(sym -> get(ps_ind, sym, sym), parameters(affect)) @@ -574,7 +631,6 @@ function compile_functional_affect(affect::FunctionalAffect, cb, sys, dvs, ps; k let u = u, p = p, user_affect = func(affect), ctx = context(affect), save_idxs = save_idxs - function (integ) user_affect(integ, u, p, ctx) for idx in save_idxs @@ -586,31 +642,44 @@ end is_discrete(cb::AbstractCallback) = cb isa SymbolicDiscreteCallback +function generate_continuous_callbacks(sys::AbstractSystem, dvs = unknowns(sys), ps = parameters(sys; initial_parameters = true); kwargs...) + cbs = continuous_events(sys) + isempty(cbs) && return nothing + generate_callback(cbs, sys; kwargs...) +end + +function generate_discrete_callbacks(sys::AbstractSystem, dvs = unknowns(sys), ps = parameters(sys; initial_parameters = true); kwargs...) + dbs = discrete_events(sys) + isempty(dbs) && return nothing + [generate_callback(db, sys; kwargs...) for db in dbs] +end + """ -Codegen a DifferentialEquations callback. A set of continuous callbacks becomes a VectorContinuousCallback. -Individual callbacks become DiscreteCallback, PresetTimeCallback, PeriodicCallback, or ContinuousCallback -depending on the case. +Codegen a DifferentialEquations callback. A (set of) continuous callback with multiple equations becomes a VectorContinuousCallback. +Continuous callbacks with only one equation will become a ContinuousCallback. +Individual discrete callbacks become DiscreteCallback, PresetTimeCallback, PeriodicCallback depending on the case. """ function generate_callback(cbs::Vector{SymbolicContinuousCallback}, sys; kwargs...) - length(cbs) == 1 && return generate_callback(only(cbs), sys) - eqs = map(cb -> flatten_equations(cb.eqs), cbs) - - _, f_iip = generate_custom_function( - sys, [eq.lhs - eq.rhs for eq in eqs], unknowns(sys), parameters(sys); - expression = Val{false}, kwargs...) - trigger = (out, u, t, integ) -> f_iip(out, u, parameter_values(integ), t) - + eqs = map(cb -> flatten_equations(equations(cb)), cbs) + num_eqs = length.(eqs) + (isempty(eqs) || sum(num_eqs) == 0) && return nothing + if sum(num_eqs) == 1 + cb_ind = findfirst(>(0), num_eqs) + return generate_callback(cbs[cb_ind], sys; kwargs...) + end + + trigger = compile_condition(cbs, sys, dvs, ps; kwargs...) affects = [] affect_negs = [] inits = [] finals = [] for cb in cbs - affect = compile_affect(cb.affect) + affect = compile_affect(cb.affect, cb, sys) push!(affects, affect) - push!(affect_negs, compile_affect(cb.affect_neg, default = affect)) - push!(inits, compile_affect(cb.initialize, default = SciMLBase.INITALIZE_DEFAULT)) - push!(finals, compile_affect(cb.finalize, default = SciMLBase.FINALIZE_DEFAULT)) + push!(affect_negs, compile_affect(cb.affect_neg, cb, sys, default = affect)) + push!(inits, compile_affect(cb.initialize, cb, sys, default = SciMLBase.INITALIZE_DEFAULT)) + push!(finals, compile_affect(cb.finalize, cb, sys, default = SciMLBase.FINALIZE_DEFAULT)) end # Since there may be different number of conditions and affects, @@ -632,7 +701,9 @@ function generate_callback(cbs::Vector{SymbolicContinuousCallback}, sys; kwargs. initialize = compile_vector_optional_affect(inits, SciMLBase.INITIALIZE_DEFAULT) finalize = compile_vector_optional_affect(finals, SciMLBase.FINALIZE_DEFAULT) - return VectorContinuousCallback(trigger, affect, length(cbs); affect_neg, initialize, finalize, rootfind = callback.rootfind, initializealg = SciMLBase.NoInit) + return VectorContinuousCallback( + trigger, affect, length(cbs); affect_neg, initialize, finalize, + rootfind = callback.rootfind, initializealg = SciMLBase.NoInit) end function generate_callback(cb, sys; kwargs...) @@ -641,21 +712,25 @@ function generate_callback(cb, sys; kwargs...) ps = parameters(sys; initial_parameters = true) trigger = is_timed ? conditions(cb) : compile_condition(cb, sys, dvs, ps; kwargs...) - affect = compile_affect(cb.affect) - affect_neg = hasfield(cb, :affect_neg) ? compile_affect(cb.affect_neg, default = affect) : nothing - initialize = compile_affect(cb.initialize, default = SciMLBase.INITIALIZE_DEFAULT) - finalize = compile_affect(cb.finalize, default = SciMLBase.FINALIZE_DEFAULT) + affect = compile_affect(cb.affect, cb, sys) + affect_neg = hasfield(typeof(cb), :affect_neg) ? + compile_affect(cb.affect_neg, cb, sys, default = affect) : nothing + initialize = compile_affect(cb.initialize, cb, sys, default = SciMLBase.INITIALIZE_DEFAULT) + finalize = compile_affect(cb.finalize, cb, sys, default = SciMLBase.FINALIZE_DEFAULT) if is_discrete(cb) if is_timed && condition(cb) isa AbstractVector - return PresetTimeCallback(trigger, affect; affect_neg, initialize, finalize, initializealg = SciMLBase.NoInit) + return PresetTimeCallback(trigger, affect; affect_neg, initialize, + finalize, initializealg = SciMLBase.NoInit) elseif is_timed return PeriodicCallback(affect, trigger; initialize, finalize) else - return DiscreteCallback(trigger, affect; affect_neg, initialize, finalize, initializealg = SciMLBase.NoInit) + return DiscreteCallback(trigger, affect; initialize, + finalize, initializealg = SciMLBase.NoInit) end else - return ContinuousCallback(trigger, affect; affect_neg, initialize, finalize, rootfind = cb.rootfind, initializealg = SciMLBase.NoInit) + return ContinuousCallback(trigger, affect, affect_neg; initialize, finalize, + rootfind = cb.rootfind, initializealg = SciMLBase.NoInit) end end @@ -675,7 +750,8 @@ Notes well-formed. - `kwargs` are passed through to `Symbolics.build_function`. """ -function compile_affect(aff::Affect, cb::AbstractCallback, sys::AbstractSystem; default = nothing) +function compile_affect( + aff::Union{Nothing, Affect}, cb::AbstractCallback, sys::AbstractSystem; default = nothing) save_idxs = if !(has_index_cache(sys) && (ic = get_index_cache(sys)) !== nothing) Int[] else @@ -684,21 +760,22 @@ function compile_affect(aff::Affect, cb::AbstractCallback, sys::AbstractSystem; isnothing(aff) && return default - ps = parameters(aff; initial_parameters = true) + ps = parameters(aff) dvs = unknowns(aff) - if aff isa ImplicitDiscreteSystem - function affect!(integrator) - pmap = [] - for pre_p in ps + if aff isa AffectSystem + aff_map = affu_to_sysu(aff) + function affect!(integrator) + pmap = [] + for pre_p in parameters(system(affect)) p = only(arguments(unwrap(pre_p))) push!(pmap, pre_p => integrator[p]) end - guesses = [u => integrator[u] for u in dvs] - prob = ImplicitDiscreteProblem(aff, [], (0, 1), pmap; guesses) + guesses = [u => integrator[aff_map[u]] for u in unknowns(system(affect))] + prob = ImplicitDiscreteProblem(system(affect), [], (0, 1), pmap; guesses) sol = init(prob, SimpleIDSolve()) - for u in dvs - integrator[u] = sol[u] + for u in unknowns(system(affect)) + integrator[aff_map[u]] = sol[u] end for idx in save_idxs @@ -706,7 +783,7 @@ function compile_affect(aff::Affect, cb::AbstractCallback, sys::AbstractSystem; end end elseif aff isa FunctionalAffect || aff isa ImperativeAffect - compile_functional_affect(aff, callback, sys, dvs, ps; kwargs...) + compile_functional_affect(aff, cb, sys, dvs, ps; kwargs...) end end @@ -717,9 +794,9 @@ function compile_vector_optional_affect(funs, default) all(isnothing, funs) && return default return let funs = funs function (cb, u, t, integ) - for func in funs - isnothing(func) ? continue : func(integ) - end + for func in funs + isnothing(func) ? continue : func(integ) + end end end end @@ -733,19 +810,8 @@ merge_cb(x, y) = CallbackSet(x, y) Generate the CallbackSet for a ODESystem or SDESystem. """ function process_events(sys; callback = nothing, kwargs...) - if has_continuous_events(sys) && !isempty(continuous_events(sys)) - cbs = continuous_events(sys) - contin_cbs = generate_callback(cbs, sys; kwargs...) - else - contin_cbs = nothing - end - if has_discrete_events(sys) && !isempty(discrete_events(sys)) - dbs = discrete_events(sys) - discrete_cbs = [generate_callback(db, sys; kwargs...) for db in dbs] - else - discrete_cbs = nothing - end - + contin_cbs = generate_continuous_callbacks(sys; kwargs...) + discrete_cbs = generate_discrete_callbacks(sys; kwargs...) cb = merge_cb(contin_cbs, callback) (discrete_cbs === nothing) ? cb : CallbackSet(contin_cbs, discrete_cbs...) end diff --git a/src/systems/codegen_utils.jl b/src/systems/codegen_utils.jl index c32ee68d21..1eeb7e026b 100644 --- a/src/systems/codegen_utils.jl +++ b/src/systems/codegen_utils.jl @@ -234,7 +234,6 @@ function build_function_wrapper(sys::AbstractSystem, expr, args...; p_start = 2, if wrap_code isa Tuple && symbolic_type(expr) == ScalarSymbolic() wrap_code = wrap_code[1] end - @show build_function(expr, args...)[1] return build_function(expr, args...; wrap_code, similarto, kwargs...) end diff --git a/src/systems/diffeqs/odesystem.jl b/src/systems/diffeqs/odesystem.jl index d5239df40a..dc6c2709bb 100644 --- a/src/systems/diffeqs/odesystem.jl +++ b/src/systems/diffeqs/odesystem.jl @@ -312,8 +312,8 @@ function ODESystem(deqs::AbstractVector{<:Equation}, iv, dvs, ps; end algeeqs = filter(is_alg_equation, deqs) - cont_callbacks = SymbolicContinuousCallbacks(continuous_events, algeeqs, iv) - disc_callbacks = SymbolicDiscreteCallbacks(discrete_events, algeeqs, iv) + cont_callbacks = SymbolicContinuousCallbacks(continuous_events, algeeqs) + disc_callbacks = SymbolicDiscreteCallbacks(discrete_events, algeeqs) if is_dde === nothing is_dde = _check_if_dde(deqs, iv′, systems) diff --git a/src/systems/imperative_affect.jl b/src/systems/imperative_affect.jl index 0b578f55c5..991a16a23a 100644 --- a/src/systems/imperative_affect.jl +++ b/src/systems/imperative_affect.jl @@ -155,7 +155,6 @@ function check_assignable(sys, sym) end end - function compile_functional_affect(affect::ImperativeAffect, cb, sys, dvs, ps; kwargs...) #= Implementation sketch: @@ -280,4 +279,3 @@ function vars!(vars, aff::ImperativeAffect; op = Differential) end return vars end - diff --git a/src/systems/index_cache.jl b/src/systems/index_cache.jl index 47a784c00b..c12835d969 100644 --- a/src/systems/index_cache.jl +++ b/src/systems/index_cache.jl @@ -121,6 +121,8 @@ function IndexCache(sys::AbstractSystem) is_parameter(sys, affect.lhs) && push!(discs, affect.lhs) elseif affect isa FunctionalAffect || affect isa ImperativeAffect union!(discs, unwrap.(discretes(affect))) + elseif isnothing(affect) + continue else error("Unhandled affect type $(typeof(affect))") end diff --git a/test/symbolic_events.jl b/test/symbolic_events.jl index 804432408b..2a690cb7f4 100644 --- a/test/symbolic_events.jl +++ b/test/symbolic_events.jl @@ -1,10 +1,11 @@ using ModelingToolkit, OrdinaryDiffEq, StochasticDiffEq, JumpProcesses, Test using SciMLStructures: canonicalize, Discrete using ModelingToolkit: SymbolicContinuousCallback, - SymbolicContinuousCallbacks, NULL_AFFECT, + SymbolicContinuousCallbacks, get_callback, t_nounits as t, - D_nounits as D + D_nounits as D, + affects, affect_negs, system, observed, AffectSystem using StableRNGs import SciMLBase using SymbolicIndexingInterface @@ -17,215 +18,110 @@ eqs = [D(x) ~ 1] affect = [x ~ 0] affect_neg = [x ~ 1] -## Test SymbolicContinuousCallback @testset "SymbolicContinuousCallback constructors" begin e = SymbolicContinuousCallback(eqs[]) @test e isa SymbolicContinuousCallback - @test isequal(e.eqs, eqs) - @test e.affect == NULL_AFFECT - @test e.affect_neg == NULL_AFFECT + @test isequal(equations(e), eqs) + @test e.affect == nothing + @test e.affect_neg == nothing @test e.rootfind == SciMLBase.LeftRootFind e = SymbolicContinuousCallback(eqs) @test e isa SymbolicContinuousCallback - @test isequal(e.eqs, eqs) - @test e.affect == NULL_AFFECT - @test e.affect_neg == NULL_AFFECT + @test isequal(equations(e), eqs) + @test e.affect == nothing + @test e.affect_neg == nothing @test e.rootfind == SciMLBase.LeftRootFind - e = SymbolicContinuousCallback(eqs, NULL_AFFECT) + e = SymbolicContinuousCallback(eqs, nothing) @test e isa SymbolicContinuousCallback - @test isequal(e.eqs, eqs) - @test e.affect == NULL_AFFECT - @test e.affect_neg == NULL_AFFECT + @test isequal(equations(e), eqs) + @test e.affect == nothing + @test e.affect_neg == nothing @test e.rootfind == SciMLBase.LeftRootFind - e = SymbolicContinuousCallback(eqs[], NULL_AFFECT) + e = SymbolicContinuousCallback(eqs[], nothing) @test e isa SymbolicContinuousCallback - @test isequal(e.eqs, eqs) - @test e.affect == NULL_AFFECT - @test e.affect_neg == NULL_AFFECT + @test isequal(equations(e), eqs) + @test e.affect == nothing + @test e.affect_neg == nothing @test e.rootfind == SciMLBase.LeftRootFind - e = SymbolicContinuousCallback(eqs => NULL_AFFECT) + e = SymbolicContinuousCallback(eqs => nothing) @test e isa SymbolicContinuousCallback - @test isequal(e.eqs, eqs) - @test e.affect == NULL_AFFECT - @test e.affect_neg == NULL_AFFECT + @test isequal(equations(e), eqs) + @test e.affect == nothing + @test e.affect_neg == nothing @test e.rootfind == SciMLBase.LeftRootFind - e = SymbolicContinuousCallback(eqs[] => NULL_AFFECT) + e = SymbolicContinuousCallback(eqs[] => nothing) @test e isa SymbolicContinuousCallback - @test isequal(e.eqs, eqs) - @test e.affect == NULL_AFFECT - @test e.affect_neg == NULL_AFFECT + @test isequal(equations(e), eqs) + @test e.affect == nothing + @test e.affect_neg == nothing @test e.rootfind == SciMLBase.LeftRootFind ## With affect - - e = SymbolicContinuousCallback(eqs[], affect) - @test e isa SymbolicContinuousCallback - @test isequal(e.eqs, eqs) - @test e.affect == affect - @test e.affect_neg == affect - @test e.rootfind == SciMLBase.LeftRootFind - - e = SymbolicContinuousCallback(eqs, affect) - @test e isa SymbolicContinuousCallback - @test isequal(e.eqs, eqs) - @test e.affect == affect - @test e.affect_neg == affect - @test e.rootfind == SciMLBase.LeftRootFind - - e = SymbolicContinuousCallback(eqs, affect) - @test e isa SymbolicContinuousCallback - @test isequal(e.eqs, eqs) - @test e.affect == affect - @test e.affect_neg == affect - @test e.rootfind == SciMLBase.LeftRootFind - e = SymbolicContinuousCallback(eqs[], affect) @test e isa SymbolicContinuousCallback - @test isequal(e.eqs, eqs) - @test e.affect == affect - @test e.affect_neg == affect - @test e.rootfind == SciMLBase.LeftRootFind - - e = SymbolicContinuousCallback(eqs => affect) - @test e isa SymbolicContinuousCallback - @test isequal(e.eqs, eqs) - @test e.affect == affect - @test e.affect_neg == affect - @test e.rootfind == SciMLBase.LeftRootFind - - e = SymbolicContinuousCallback(eqs[] => affect) - @test e isa SymbolicContinuousCallback - @test isequal(e.eqs, eqs) - @test e.affect == affect - @test e.affect_neg == affect + @test isequal(equations(e), eqs) + @test observed(system(affects(e))) == affect + @test observed(system(affect_negs(e))) == affect @test e.rootfind == SciMLBase.LeftRootFind # with only positive edge affect - - e = SymbolicContinuousCallback(eqs[], affect, affect_neg = nothing) - @test e isa SymbolicContinuousCallback - @test isequal(e.eqs, eqs) - @test e.affect == affect - @test isnothing(e.affect_neg) - @test e.rootfind == SciMLBase.LeftRootFind - - e = SymbolicContinuousCallback(eqs, affect, affect_neg = nothing) - @test e isa SymbolicContinuousCallback - @test isequal(e.eqs, eqs) - @test e.affect == affect - @test isnothing(e.affect_neg) - @test e.rootfind == SciMLBase.LeftRootFind - - e = SymbolicContinuousCallback(eqs, affect, affect_neg = nothing) - @test e isa SymbolicContinuousCallback - @test isequal(e.eqs, eqs) - @test e.affect == affect - @test isnothing(e.affect_neg) - @test e.rootfind == SciMLBase.LeftRootFind - e = SymbolicContinuousCallback(eqs[], affect, affect_neg = nothing) @test e isa SymbolicContinuousCallback - @test isequal(e.eqs, eqs) - @test e.affect == affect + @test isequal(equations(e), eqs) + @test observed(system(affects(e))) == affect @test isnothing(e.affect_neg) @test e.rootfind == SciMLBase.LeftRootFind # with explicit edge affects - - e = SymbolicContinuousCallback(eqs[], affect, affect_neg = affect_neg) - @test e isa SymbolicContinuousCallback - @test isequal(e.eqs, eqs) - @test e.affect == affect - @test e.affect_neg == affect_neg - @test e.rootfind == SciMLBase.LeftRootFind - - e = SymbolicContinuousCallback(eqs, affect, affect_neg = affect_neg) - @test e isa SymbolicContinuousCallback - @test isequal(e.eqs, eqs) - @test e.affect == affect - @test e.affect_neg == affect_neg - @test e.rootfind == SciMLBase.LeftRootFind - - e = SymbolicContinuousCallback(eqs, affect, affect_neg = affect_neg) - @test e isa SymbolicContinuousCallback - @test isequal(e.eqs, eqs) - @test e.affect == affect - @test e.affect_neg == affect_neg - @test e.rootfind == SciMLBase.LeftRootFind - e = SymbolicContinuousCallback(eqs[], affect, affect_neg = affect_neg) @test e isa SymbolicContinuousCallback - @test isequal(e.eqs, eqs) - @test e.affect == affect - @test e.affect_neg == affect_neg + @test isequal(equations(e), eqs) + @test observed(system(affects(e))) == affect + @test observed(system(affect_negs(e))) == affect_neg @test e.rootfind == SciMLBase.LeftRootFind # with different root finding ops - e = SymbolicContinuousCallback( eqs[], affect, affect_neg = affect_neg, rootfind = SciMLBase.LeftRootFind) @test e isa SymbolicContinuousCallback - @test isequal(e.eqs, eqs) - @test e.affect == affect - @test e.affect_neg == affect_neg + @test isequal(equations(e), eqs) @test e.rootfind == SciMLBase.LeftRootFind - e = SymbolicContinuousCallback( - eqs[], affect, affect_neg = affect_neg, rootfind = SciMLBase.RightRootFind) - @test e isa SymbolicContinuousCallback - @test isequal(e.eqs, eqs) - @test e.affect == affect - @test e.affect_neg == affect_neg - @test e.rootfind == SciMLBase.RightRootFind - - e = SymbolicContinuousCallback( - eqs[], affect, affect_neg = affect_neg, rootfind = SciMLBase.NoRootFind) - @test e isa SymbolicContinuousCallback - @test isequal(e.eqs, eqs) - @test e.affect == affect - @test e.affect_neg == affect_neg - @test e.rootfind == SciMLBase.NoRootFind # test plural constructor - e = SymbolicContinuousCallbacks(eqs[]) @test e isa Vector{SymbolicContinuousCallback} - @test isequal(e[].eqs, eqs) - @test e[].affect == NULL_AFFECT + @test isequal(equations(e[]), eqs) + @test e[].affect == nothing e = SymbolicContinuousCallbacks(eqs) @test e isa Vector{SymbolicContinuousCallback} - @test isequal(e[].eqs, eqs) - @test e[].affect == NULL_AFFECT + @test isequal(equations(e[]), eqs) + @test e[].affect == nothing e = SymbolicContinuousCallbacks(eqs[] => affect) @test e isa Vector{SymbolicContinuousCallback} - @test isequal(e[].eqs, eqs) - @test e[].affect == affect + @test isequal(equations(e[]), eqs) + @test e[].affect isa AffectSystem e = SymbolicContinuousCallbacks(eqs => affect) @test e isa Vector{SymbolicContinuousCallback} - @test isequal(e[].eqs, eqs) - @test e[].affect == affect + @test isequal(equations(e[]), eqs) + @test e[].affect isa AffectSystem e = SymbolicContinuousCallbacks([eqs[] => affect]) @test e isa Vector{SymbolicContinuousCallback} - @test isequal(e[].eqs, eqs) - @test e[].affect == affect + @test isequal(equations(e[]), eqs) + @test e[].affect isa AffectSystem e = SymbolicContinuousCallbacks([eqs => affect]) @test e isa Vector{SymbolicContinuousCallback} - @test isequal(e[].eqs, eqs) - @test e[].affect == affect - - e = SymbolicContinuousCallbacks(SymbolicContinuousCallbacks([eqs => affect])) - @test e isa Vector{SymbolicContinuousCallback} - @test isequal(e[].eqs, eqs) - @test e[].affect == affect + @test isequal(equations(e[]), eqs) + @test e[].affect isa AffectSystem end @testset "ImperativeAffect constructors" begin @@ -341,159 +237,162 @@ end @test m.ctx === 3 end -## - -@named sys = ODESystem(eqs, t, continuous_events = [x ~ 1]) -@test getfield(sys, :continuous_events)[] == - SymbolicContinuousCallback(Equation[x ~ 1], NULL_AFFECT) -@test isequal(equations(getfield(sys, :continuous_events))[], x ~ 1) -fsys = flatten(sys) -@test isequal(equations(getfield(fsys, :continuous_events))[], x ~ 1) - -@named sys2 = ODESystem([D(x) ~ 1], t, continuous_events = [x ~ 2], systems = [sys]) -@test getfield(sys2, :continuous_events)[] == - SymbolicContinuousCallback(Equation[x ~ 2], NULL_AFFECT) -@test all(ModelingToolkit.continuous_events(sys2) .== [ - SymbolicContinuousCallback(Equation[x ~ 2], NULL_AFFECT), - SymbolicContinuousCallback(Equation[sys.x ~ 1], NULL_AFFECT) -]) - -@test isequal(equations(getfield(sys2, :continuous_events))[1], x ~ 2) -@test length(ModelingToolkit.continuous_events(sys2)) == 2 -@test isequal(ModelingToolkit.continuous_events(sys2)[1].eqs[], x ~ 2) -@test isequal(ModelingToolkit.continuous_events(sys2)[2].eqs[], sys.x ~ 1) - -sys = complete(sys) -sys_nosplit = complete(sys; split = false) -sys2 = complete(sys2) -# Functions should be generated for root-finding equations -prob = ODEProblem(sys, Pair[], (0.0, 2.0)) -p0 = 0 -t0 = 0 -@test get_callback(prob) isa ModelingToolkit.DiffEqCallbacks.ContinuousCallback -cb = ModelingToolkit.generate_rootfinding_callback(sys) -cond = cb.condition -out = [0.0] -cond.rf_ip(out, [0], p0, t0) -@test out[] ≈ -1 # signature is u,p,t -cond.rf_ip(out, [1], p0, t0) -@test out[] ≈ 0 # signature is u,p,t -cond.rf_ip(out, [2], p0, t0) -@test out[] ≈ 1 # signature is u,p,t - -prob = ODEProblem(sys, Pair[], (0.0, 2.0)) -prob_nosplit = ODEProblem(sys_nosplit, Pair[], (0.0, 2.0)) -sol = solve(prob, Tsit5()) -sol_nosplit = solve(prob_nosplit, Tsit5()) -@test minimum(t -> abs(t - 1), sol.t) < 1e-10 # test that the solver stepped at the root -@test minimum(t -> abs(t - 1), sol_nosplit.t) < 1e-10 # test that the solver stepped at the root - -# Test that a user provided callback is respected -test_callback = DiscreteCallback(x -> x, x -> x) -prob = ODEProblem(sys, Pair[], (0.0, 2.0), callback = test_callback) -prob_nosplit = ODEProblem(sys_nosplit, Pair[], (0.0, 2.0), callback = test_callback) -cbs = get_callback(prob) -cbs_nosplit = get_callback(prob_nosplit) -@test cbs isa CallbackSet -@test cbs.discrete_callbacks[1] == test_callback -@test cbs_nosplit isa CallbackSet -@test cbs_nosplit.discrete_callbacks[1] == test_callback - -prob = ODEProblem(sys2, Pair[], (0.0, 3.0)) -cb = get_callback(prob) -@test cb isa ModelingToolkit.DiffEqCallbacks.VectorContinuousCallback - -cond = cb.condition -out = [0.0, 0.0] -# the root to find is 2 -cond.rf_ip(out, [0, 0], p0, t0) -@test out[1] ≈ -2 # signature is u,p,t -cond.rf_ip(out, [1, 0], p0, t0) -@test out[1] ≈ -1 # signature is u,p,t -cond.rf_ip(out, [2, 0], p0, t0) # this should return 0 -@test out[1] ≈ 0 # signature is u,p,t - -# the root to find is 1 -out = [0.0, 0.0] -cond.rf_ip(out, [0, 0], p0, t0) -@test out[2] ≈ -1 # signature is u,p,t -cond.rf_ip(out, [0, 1], p0, t0) # this should return 0 -@test out[2] ≈ 0 # signature is u,p,t -cond.rf_ip(out, [0, 2], p0, t0) -@test out[2] ≈ 1 # signature is u,p,t - -sol = solve(prob, Tsit5(); abstol = 1e-14, reltol = 1e-14) -@test minimum(t -> abs(t - 1), sol.t) < 1e-10 # test that the solver stepped at the first root -@test minimum(t -> abs(t - 2), sol.t) < 1e-10 # test that the solver stepped at the second root - -@named sys = ODESystem(eqs, t, continuous_events = [x ~ 1, x ~ 2]) # two root eqs using the same unknown -sys = complete(sys) -prob = ODEProblem(sys, Pair[], (0.0, 3.0)) -@test get_callback(prob) isa ModelingToolkit.DiffEqCallbacks.VectorContinuousCallback -sol = solve(prob, Tsit5(); abstol = 1e-14, reltol = 1e-14) -@test minimum(t -> abs(t - 1), sol.t) < 1e-10 # test that the solver stepped at the first root -@test minimum(t -> abs(t - 2), sol.t) < 1e-10 # test that the solver stepped at the second root - -## Test bouncing ball with equation affect -@variables x(t)=1 v(t)=0 - -root_eqs = [x ~ 0] -affect = [v ~ -v] - -@named ball = ODESystem([D(x) ~ v - D(v) ~ -9.8], t, continuous_events = root_eqs => affect) - -@test getfield(ball, :continuous_events)[] == - SymbolicContinuousCallback(Equation[x ~ 0], Equation[v ~ -v]) -ball = structural_simplify(ball) - -@test length(ModelingToolkit.continuous_events(ball)) == 1 - -tspan = (0.0, 5.0) -prob = ODEProblem(ball, Pair[], tspan) -sol = solve(prob, Tsit5()) -@test 0 <= minimum(sol[x]) <= 1e-10 # the ball never went through the floor but got very close -# plot(sol) - -## Test bouncing ball in 2D with walls -@variables x(t)=1 y(t)=0 vx(t)=0 vy(t)=1 - -continuous_events = [[x ~ 0] => [vx ~ -vx] - [y ~ -1.5, y ~ 1.5] => [vy ~ -vy]] - -@named ball = ODESystem( - [D(x) ~ vx - D(y) ~ vy - D(vx) ~ -9.8 - D(vy) ~ -0.01vy], t; continuous_events) - -_ball = ball -ball = structural_simplify(_ball) -ball_nosplit = structural_simplify(_ball; split = false) - -tspan = (0.0, 5.0) -prob = ODEProblem(ball, Pair[], tspan) -prob_nosplit = ODEProblem(ball_nosplit, Pair[], tspan) - -cb = get_callback(prob) -@test cb isa ModelingToolkit.DiffEqCallbacks.VectorContinuousCallback -@test getfield(ball, :continuous_events)[1] == - SymbolicContinuousCallback(Equation[x ~ 0], Equation[vx ~ -vx]) -@test getfield(ball, :continuous_events)[2] == - SymbolicContinuousCallback(Equation[y ~ -1.5, y ~ 1.5], Equation[vy ~ -vy]) -cond = cb.condition -out = [0.0, 0.0, 0.0] -cond.rf_ip(out, [0, 0, 0, 0], p0, t0) -@test out ≈ [0, 1.5, -1.5] +@testset "Basic ODESystem Tests" begin + @named sys = ODESystem(eqs, t, continuous_events = [x ~ 1]) + @test getfield(sys, :continuous_events)[] == + SymbolicContinuousCallback(Equation[x ~ 1], nothing) + @test isequal(equations(getfield(sys, :continuous_events))[], x ~ 1) + fsys = flatten(sys) + @test isequal(equations(getfield(fsys, :continuous_events))[], x ~ 1) + + @named sys2 = ODESystem([D(x) ~ 1], t, continuous_events = [x ~ 2], systems = [sys]) + @test getfield(sys2, :continuous_events)[] == + SymbolicContinuousCallback(Equation[x ~ 2], nothing) + @test all(ModelingToolkit.continuous_events(sys2) .== [ + SymbolicContinuousCallback(Equation[x ~ 2], nothing), + SymbolicContinuousCallback(Equation[sys.x ~ 1], nothing) + ]) + + @test isequal(equations(getfield(sys2, :continuous_events))[1], x ~ 2) + @test length(ModelingToolkit.continuous_events(sys2)) == 2 + @test isequal(equations(ModelingToolkit.continuous_events(sys2)[1])[], x ~ 2) + @test isequal(equations(ModelingToolkit.continuous_events(sys2)[2])[], sys.x ~ 1) + + sys = complete(sys) + sys_nosplit = complete(sys; split = false) + sys2 = complete(sys2) + + # Test proper rootfinding + prob = ODEProblem(sys, Pair[], (0.0, 2.0)) + p0 = 0 + t0 = 0 + @test get_callback(prob) isa ModelingToolkit.DiffEqCallbacks.ContinuousCallback + cb = ModelingToolkit.generate_continuous_callbacks(sys) + cond = cb.condition + out = [0.0] + cond(out, [0], p0, t0) + @test out[] ≈ -1 # signature is u,p,t + cond.rf_ip(out, [1], p0, t0) + @test out[] ≈ 0 # signature is u,p,t + cond.rf_ip(out, [2], p0, t0) + @test out[] ≈ 1 # signature is u,p,t + + prob = ODEProblem(sys, Pair[], (0.0, 2.0)) + prob_nosplit = ODEProblem(sys_nosplit, Pair[], (0.0, 2.0)) + sol = solve(prob, Tsit5()) + sol_nosplit = solve(prob_nosplit, Tsit5()) + @test minimum(t -> abs(t - 1), sol.t) < 1e-10 # test that the solver stepped at the root + @test minimum(t -> abs(t - 1), sol_nosplit.t) < 1e-10 # test that the solver stepped at the root + + # Test user-provided callback is respected + test_callback = DiscreteCallback(x -> x, x -> x) + prob = ODEProblem(sys, Pair[], (0.0, 2.0), callback = test_callback) + prob_nosplit = ODEProblem(sys_nosplit, Pair[], (0.0, 2.0), callback = test_callback) + cbs = get_callback(prob) + cbs_nosplit = get_callback(prob_nosplit) + @test cbs isa CallbackSet + @test cbs.discrete_callbacks[1] == test_callback + @test cbs_nosplit isa CallbackSet + @test cbs_nosplit.discrete_callbacks[1] == test_callback + + prob = ODEProblem(sys2, Pair[], (0.0, 3.0)) + cb = get_callback(prob) + @test cb isa ModelingToolkit.DiffEqCallbacks.VectorContinuousCallback + + cond = cb.condition + out = [0.0, 0.0] + # the root to find is 2 + cond.rf_ip(out, [0, 0], p0, t0) + @test out[1] ≈ -2 # signature is u,p,t + cond.rf_ip(out, [1, 0], p0, t0) + @test out[1] ≈ -1 # signature is u,p,t + cond.rf_ip(out, [2, 0], p0, t0) # this should return 0 + @test out[1] ≈ 0 # signature is u,p,t + + # the root to find is 1 + out = [0.0, 0.0] + cond.rf_ip(out, [0, 0], p0, t0) + @test out[2] ≈ -1 # signature is u,p,t + cond.rf_ip(out, [0, 1], p0, t0) # this should return 0 + @test out[2] ≈ 0 # signature is u,p,t + cond.rf_ip(out, [0, 2], p0, t0) + @test out[2] ≈ 1 # signature is u,p,t -sol = solve(prob, Tsit5()) -sol_nosplit = solve(prob_nosplit, Tsit5()) -@test 0 <= minimum(sol[x]) <= 1e-10 # the ball never went through the floor but got very close -@test minimum(sol[y]) ≈ -1.5 # check wall conditions -@test maximum(sol[y]) ≈ 1.5 # check wall conditions -@test 0 <= minimum(sol_nosplit[x]) <= 1e-10 # the ball never went through the floor but got very close -@test minimum(sol_nosplit[y]) ≈ -1.5 # check wall conditions -@test maximum(sol_nosplit[y]) ≈ 1.5 # check wall conditions + sol = solve(prob, Tsit5()) + @test minimum(t -> abs(t - 1), sol.t) < 1e-10 # test that the solver stepped at the first root + @test minimum(t -> abs(t - 2), sol.t) < 1e-10 # test that the solver stepped at the second root + + @named sys = ODESystem(eqs, t, continuous_events = [x ~ 1, x ~ 2]) # two root eqs using the same unknown + sys = complete(sys) + prob = ODEProblem(sys, Pair[], (0.0, 3.0)) + @test get_callback(prob) isa ModelingToolkit.DiffEqCallbacks.VectorContinuousCallback + sol = solve(prob, Tsit5()) + @test minimum(t -> abs(t - 1), sol.t) < 1e-10 # test that the solver stepped at the first root + @test minimum(t -> abs(t - 2), sol.t) < 1e-10 # test that the solver stepped at the second root +end + +@testset "Bouncing Ball" begin + ###### 1D Bounce + @variables x(t)=1 v(t)=0 + + root_eqs = [x ~ 0] + affect = [v ~ -v] + + @named ball = ODESystem( + [D(x) ~ v + D(v) ~ -9.8], t, continuous_events = root_eqs => affect) + + @test getfield(ball, :continuous_events)[] == + SymbolicContinuousCallback(Equation[x ~ 0], Equation[v ~ -v]) + ball = structural_simplify(ball) + + @test length(ModelingToolkit.continuous_events(ball)) == 1 + + tspan = (0.0, 5.0) + prob = ODEProblem(ball, Pair[], tspan) + sol = solve(prob, Tsit5()) + @test 0 <= minimum(sol[x]) <= 1e-10 # the ball never went through the floor but got very close + + ###### 2D bouncing ball + @variables x(t)=1 y(t)=0 vx(t)=0 vy(t)=1 + + continuous_events = [[x ~ 0] => [vx ~ -vx] + [y ~ -1.5, y ~ 1.5] => [vy ~ -vy]] + + @named ball = ODESystem( + [D(x) ~ vx + D(y) ~ vy + D(vx) ~ -9.8 + D(vy) ~ -0.01vy], t; continuous_events) + + _ball = ball + ball = structural_simplify(_ball) + ball_nosplit = structural_simplify(_ball; split = false) + + tspan = (0.0, 5.0) + prob = ODEProblem(ball, Pair[], tspan) + prob_nosplit = ODEProblem(ball_nosplit, Pair[], tspan) + + cb = get_callback(prob) + @test cb isa ModelingToolkit.DiffEqCallbacks.VectorContinuousCallback + @test getfield(ball, :continuous_events)[1] == + SymbolicContinuousCallback(Equation[x ~ 0], Equation[vx ~ -vx]) + @test getfield(ball, :continuous_events)[2] == + SymbolicContinuousCallback(Equation[y ~ -1.5, y ~ 1.5], Equation[vy ~ -vy]) + cond = cb.condition + out = [0.0, 0.0, 0.0] + cond.rf_ip(out, [0, 0, 0, 0], p0, t0) + @test out ≈ [0, 1.5, -1.5] + + sol = solve(prob, Tsit5()) + sol_nosplit = solve(prob_nosplit, Tsit5()) + @test 0 <= minimum(sol[x]) <= 1e-10 # the ball never went through the floor but got very close + @test minimum(sol[y]) ≈ -1.5 # check wall conditions + @test maximum(sol[y]) ≈ 1.5 # check wall conditions + @test 0 <= minimum(sol_nosplit[x]) <= 1e-10 # the ball never went through the floor but got very close + @test minimum(sol_nosplit[y]) ≈ -1.5 # check wall conditions + @test maximum(sol_nosplit[y]) ≈ 1.5 # check wall conditions +end # tv = sort([LinRange(0, 5, 200); sol.t]) # plot(sol(tv)[y], sol(tv)[x], line_z=tv) @@ -502,27 +401,29 @@ sol_nosplit = solve(prob_nosplit, Tsit5()) ## Test multi-variable affect # in this test, there are two variables affected by a single event. -continuous_events = [ - [x ~ 0] => [vx ~ -vx, vy ~ -vy] -] +@testset "Multi-variable affect" begin + continuous_events = [ + [x ~ 0] => [vx ~ -vx, vy ~ -vy] + ] -@named ball = ODESystem([D(x) ~ vx - D(y) ~ vy - D(vx) ~ -1 - D(vy) ~ 0], t; continuous_events) + @named ball = ODESystem([D(x) ~ vx + D(y) ~ vy + D(vx) ~ -1 + D(vy) ~ 0], t; continuous_events) -ball_nosplit = structural_simplify(ball) -ball = structural_simplify(ball) + ball_nosplit = structural_simplify(ball) + ball = structural_simplify(ball) -tspan = (0.0, 5.0) -prob = ODEProblem(ball, Pair[], tspan) -prob_nosplit = ODEProblem(ball_nosplit, Pair[], tspan) -sol = solve(prob, Tsit5()) -sol_nosplit = solve(prob_nosplit, Tsit5()) -@test 0 <= minimum(sol[x]) <= 1e-10 # the ball never went through the floor but got very close -@test -minimum(sol[y]) ≈ maximum(sol[y]) ≈ sqrt(2) # the ball will never go further than √2 in either direction (gravity was changed to 1 to get this particular number) -@test 0 <= minimum(sol_nosplit[x]) <= 1e-10 # the ball never went through the floor but got very close -@test -minimum(sol_nosplit[y]) ≈ maximum(sol_nosplit[y]) ≈ sqrt(2) # the ball will never go further than √2 in either direction (gravity was changed to 1 to get this particular number) + tspan = (0.0, 5.0) + prob = ODEProblem(ball, Pair[], tspan) + prob_nosplit = ODEProblem(ball_nosplit, Pair[], tspan) + sol = solve(prob, Tsit5()) + sol_nosplit = solve(prob_nosplit, Tsit5()) + @test 0 <= minimum(sol[x]) <= 1e-10 # the ball never went through the floor but got very close + @test -minimum(sol[y]) ≈ maximum(sol[y]) ≈ sqrt(2) # the ball will never go further than √2 in either direction (gravity was changed to 1 to get this particular number) + @test 0 <= minimum(sol_nosplit[x]) <= 1e-10 # the ball never went through the floor but got very close + @test -minimum(sol_nosplit[y]) ≈ maximum(sol_nosplit[y]) ≈ sqrt(2) # the ball will never go further than √2 in either direction (gravity was changed to 1 to get this particular number) +end # tv = sort([LinRange(0, 5, 200); sol.t]) # plot(sol(tv)[y], sol(tv)[x], line_z=tv) @@ -544,50 +445,53 @@ sol = solve(prob, Tsit5()) @test sol([0.25 - eps()])[vmeasured][] == sol([0.23])[vmeasured][] # test the hold property ## https://github.com/SciML/ModelingToolkit.jl/issues/1528 -Dₜ = D +@testset "Handle Empty Events" begin + Dₜ = D -@parameters u(t) [input = true] # Indicate that this is a controlled input -@parameters y(t) [output = true] # Indicate that this is a measured output + @parameters u(t) [input = true] # Indicate that this is a controlled input + @parameters y(t) [output = true] # Indicate that this is a measured output -function Mass(; name, m = 1.0, p = 0, v = 0) - ps = @parameters m = m - sts = @variables pos(t)=p vel(t)=v - eqs = Dₜ(pos) ~ vel - ODESystem(eqs, t, [pos, vel], ps; name) -end -function Spring(; name, k = 1e4) - ps = @parameters k = k - @variables x(t) = 0 # Spring deflection - ODESystem(Equation[], t, [x], ps; name) -end -function Damper(; name, c = 10) - ps = @parameters c = c - @variables vel(t) = 0 - ODESystem(Equation[], t, [vel], ps; name) -end -function SpringDamper(; name, k = false, c = false) - spring = Spring(; name = :spring, k) - damper = Damper(; name = :damper, c) - compose(ODESystem(Equation[], t; name), - spring, damper) -end -connect_sd(sd, m1, m2) = [sd.spring.x ~ m1.pos - m2.pos, sd.damper.vel ~ m1.vel - m2.vel] -sd_force(sd) = -sd.spring.k * sd.spring.x - sd.damper.c * sd.damper.vel -@named mass1 = Mass(; m = 1) -@named mass2 = Mass(; m = 1) -@named sd = SpringDamper(; k = 1000, c = 10) -function Model(u, d = 0) - eqs = [connect_sd(sd, mass1, mass2) - Dₜ(mass1.vel) ~ (sd_force(sd) + u) / mass1.m - Dₜ(mass2.vel) ~ (-sd_force(sd) + d) / mass2.m] - @named _model = ODESystem(eqs, t; observed = [y ~ mass2.pos]) - @named model = compose(_model, mass1, mass2, sd) + function Mass(; name, m = 1.0, p = 0, v = 0) + ps = @parameters m = m + sts = @variables pos(t)=p vel(t)=v + eqs = Dₜ(pos) ~ vel + ODESystem(eqs, t, [pos, vel], ps; name) + end + function Spring(; name, k = 1e4) + ps = @parameters k = k + @variables x(t) = 0 # Spring deflection + ODESystem(Equation[], t, [x], ps; name) + end + function Damper(; name, c = 10) + ps = @parameters c = c + @variables vel(t) = 0 + ODESystem(Equation[], t, [vel], ps; name) + end + function SpringDamper(; name, k = false, c = false) + spring = Spring(; name = :spring, k) + damper = Damper(; name = :damper, c) + compose(ODESystem(Equation[], t; name), + spring, damper) + end + connect_sd(sd, m1, m2) = [ + sd.spring.x ~ m1.pos - m2.pos, sd.damper.vel ~ m1.vel - m2.vel] + sd_force(sd) = -sd.spring.k * sd.spring.x - sd.damper.c * sd.damper.vel + @named mass1 = Mass(; m = 1) + @named mass2 = Mass(; m = 1) + @named sd = SpringDamper(; k = 1000, c = 10) + function Model(u, d = 0) + eqs = [connect_sd(sd, mass1, mass2) + Dₜ(mass1.vel) ~ (sd_force(sd) + u) / mass1.m + Dₜ(mass2.vel) ~ (-sd_force(sd) + d) / mass2.m] + @named _model = ODESystem(eqs, t; observed = [y ~ mass2.pos]) + @named model = compose(_model, mass1, mass2, sd) + end + model = Model(sin(30t)) + sys = structural_simplify(model) + @test isempty(ModelingToolkit.continuous_events(sys)) end -model = Model(sin(30t)) -sys = structural_simplify(model) -@test isempty(ModelingToolkit.continuous_events(sys)) -let +@testset "ODESystem Discrete Callbacks" begin function testsol(osys, u0, p, tspan; tstops = Float64[], paramtotest = nothing, kwargs...) oprob = ODEProblem(complete(osys), u0, tspan, p; kwargs...) @@ -662,7 +566,7 @@ let @test isapprox(sol(10.0)[1], 0.1; atol = 1e-10, rtol = 1e-10) end -let +@testset "SDESystem Discrete Callbacks" begin function testsol(ssys, u0, p, tspan; tstops = Float64[], paramtotest = nothing, kwargs...) sprob = SDEProblem(complete(ssys), u0, tspan, p; kwargs...) @@ -743,7 +647,8 @@ let @test isapprox(sol(10.0)[1], 0.1; atol = 1e-10, rtol = 1e-10) end -let rng = rng +@testset "JumpSystem Discrete Callbacks" begin + rng = rng function testsol(jsys, u0, p, tspan; tstops = Float64[], paramtotest = nothing, N = 40000, kwargs...) jsys = complete(jsys) @@ -810,7 +715,7 @@ let rng = rng testsol(jsys6, u0, p, tspan; tstops = [1.0, 2.0], rng, paramtotest = k) end -let +@testset "Oscillator" begin function oscillator_ce(k = 1.0; name) sts = @variables x(t)=1.0 v(t)=0.0 F(t) ps = @parameters k=k Θ=0.5 @@ -1083,6 +988,7 @@ end @test sol[b] == [2.0, 5.0, 5.0] @test sol[c] == [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0] end + @testset "Heater" begin @variables temp(t) params = @parameters furnace_on_threshold=0.5 furnace_off_threshold=0.7 furnace_power=1.0 leakage=0.1 furnace_on::Bool=false @@ -1378,7 +1284,7 @@ end @test_nowarn solve(prob, Tsit5(), tstops = [1.0]) end -@testset "Array parameter updates in ImperativeEffect" begin +@testset "Array parameter updates in ImperativeAffect" begin function weird1(max_time; name) params = @parameters begin θ(t) = 0.0 From 32702ed7c1b68d979dad14f79692e23a1201316d Mon Sep 17 00:00:00 2001 From: vyudu Date: Wed, 12 Mar 2025 03:59:29 -0400 Subject: [PATCH 06/52] some tests working --- src/linearization.jl | 1 - src/systems/callbacks.jl | 132 ++++++++++++------ src/systems/diffeqs/odesystem.jl | 1 - .../discrete_system/discrete_system.jl | 10 ++ .../implicit_discrete_system.jl | 10 ++ src/systems/index_cache.jl | 4 +- src/systems/problem_utils.jl | 4 +- test/symbolic_events.jl | 86 ++++++------ 8 files changed, 147 insertions(+), 101 deletions(-) diff --git a/src/linearization.jl b/src/linearization.jl index 57b171e874..77f4422b63 100644 --- a/src/linearization.jl +++ b/src/linearization.jl @@ -535,7 +535,6 @@ function linearize_symbolic(sys::AbstractSystem, inputs, if !iszero(Bs) if !allow_input_derivatives der_inds = findall(vec(any(!iszero, Bs, dims = 1))) - @show typeof(der_inds) error("Input derivatives appeared in expressions (-g_z\\g_u != 0), the following inputs appeared differentiated: $(ModelingToolkit.inputs(sys)[der_inds]). Call `linearize_symbolic` with keyword argument `allow_input_derivatives = true` to allow this and have the returned `B` matrix be of double width ($(2nu)), where the last $nu inputs are the derivatives of the first $nu inputs.") end B = [B [zeros(nx, nu); Bs]] diff --git a/src/systems/callbacks.jl b/src/systems/callbacks.jl index b25d64d59c..76e5a0d47f 100644 --- a/src/systems/callbacks.jl +++ b/src/systems/callbacks.jl @@ -67,21 +67,31 @@ struct AffectSystem unknowns::Vector parameters::Vector discretes::Vector - """Maps the unknowns in the ImplicitDiscreteSystem to the corresponding parameter or unknown in the parent system.""" - affu_to_sysu::Dict + """Maps the symbols of unknowns/observed in the ImplicitDiscreteSystem to its corresponding unknown/parameter in the parent system.""" + aff_to_sys::Dict end system(a::AffectSystem) = a.system discretes(a::AffectSystem) = a.discretes unknowns(a::AffectSystem) = a.unknowns parameters(a::AffectSystem) = a.parameters -affu_to_sysu(a::AffectSystem) = a.affu_to_sysu +aff_to_sys(a::AffectSystem) = a.aff_to_sys +previous_vals(a::AffectSystem) = parameters(system(a)) +updated_vals(a::AffectSystem) = unknowns(system(a)) function Base.show(iio::IO, aff::AffectSystem) eqs = vcat(equations(system(aff)), observed(system(aff))) show(iio, eqs) end +function Base.:(==)(a1::AffectSystem, a2::AffectSystem) + isequal(system(a1), system(a2)) && + isequal(discretes(a1), discretes(a2)) && + isequal(unknowns(a1), unknowns(a2)) && + isequal(parameters(a1), parameters(a2)) && + isequal(aff_to_sys(a1), aff_to_sys(a2)) +end + """ Pre(x) @@ -112,14 +122,14 @@ function (p::Pre)(x) iscall(x) && operation(x) isa Pre && return x result = if symbolic_type(x) == ArraySymbolic() # create an array for `Pre(array)` - Symbolics.array_term(p, toparam(x)) + Symbolics.array_term(p, x) elseif iscall(x) && operation(x) == getindex # instead of `Pre(x[1])` create `Pre(x)[1]` # which allows parameter indexing to handle this case automatically. arr = arguments(x)[1] - term(getindex, p(toparam(arr)), arguments(x)[2:end]...) + term(getindex, p(arr), arguments(x)[2:end]...) else - term(p, toparam(x)) + term(p, x) end # the result should be a parameter result = toparam(result) @@ -231,7 +241,7 @@ function make_affect(affect::Vector{Equation}; warn = true) discretes = Any[] p_as_unknowns = Any[] for p in params - if iscall(p) && (operator(p) isa Pre) + if iscall(p) && (operation(p) isa Pre) push!(cb_params, p) elseif iscall(p) && length(arguments(p)) == 1 && isequal(only(arguments(p)), iv) @@ -239,17 +249,28 @@ function make_affect(affect::Vector{Equation}; warn = true) push!(p_as_unknowns, tovar(p)) else push!(discretes, p) - p = iscall(p) ? wrap(Sym{FnType{Tuple{symtype(iv)}, Real}}(nameof(operation(p)))(iv)) : - wrap(Sym{FnType{Tuple{symtype(iv)}, Real}}(nameof(p))(iv)) + name = iscall(p) ? nameof(operation(p)) : nameof(p) + p = wrap(Sym{FnType{Tuple{symtype(iv)}, Real}}(name)(iv)) + p = setmetadata(p, Symbolics.VariableSource, (:variables, name)) push!(p_as_unknowns, p) end end + aff_map = Dict(zip(p_as_unknowns, discretes)) + rev_map = Dict([v => k for (k, v) in aff_map]) + affect = Symbolics.substitute(affect, rev_map) @mtkbuild affectsys = ImplicitDiscreteSystem( affect, iv, collect(union(unknowns, p_as_unknowns)), cb_params) - params = map(x -> only(arguments(unwrap(x))), cb_params) - affmap = Dict(zip([p_as_unknowns, unknowns], [discretes, unknowns])) + params = filter(isparameter, map(x -> only(arguments(unwrap(x))), cb_params)) + @show params + + for u in unknowns + aff_map[u] = u + end + + @show unknowns + @show params - return AffectSystem(affectsys, collect(unknowns), params, discretes, affmap) + return AffectSystem(affectsys, collect(unknowns), params, discretes, aff_map) end function make_affect(affect) @@ -393,17 +414,19 @@ function SymbolicDiscreteCallbacks(events, algeeqs::Vector{Equation} = Equation[ for event in events cond, affs = event isa Pair ? (event[1], event[2]) : (event, nothing) - if aff isa AbstractVector - aff = vcat(aff, algeeqs) + if affs isa AbstractVector + affs = vcat(affs, algeeqs) end - affect = make_affect(aff) - push!(callbacks, SymbolicDiscreteCallback(cond, affect, nothing, nothing)) + affect = make_affect(affs) + push!(callbacks, SymbolicDiscreteCallback(cond, affect)) end callbacks end function is_timed_condition(condition::T) where {T} - if T <: Real + if T === Num + false + elseif T <: Real true elseif T <: AbstractVector eltype(condition) <: Real @@ -582,23 +605,31 @@ function compile_condition(cbs::Union{AbstractCallback, Vector{<:AbstractCallbac condit = substitute(condit, cmap) end - f_oop, f_iip = build_function_wrapper(sys, - condit, u, t, p...; expression = Val{true}, - p_start = 3, p_end = length(p) + 2, + if !is_discrete(cbs) + condit = [cond.lhs - cond.rhs for cond in condit] + end + + fs = build_function_wrapper(sys, + condit, u, p..., t; expression, kwargs...) - if cbs isa AbstractVector - cond(out, u, t, integ) = f_iip(out, u, t, parameter_values(integ)) + if expression == Val{true} + fs = eval_or_rgf.(fs; eval_expression, eval_module) + end + is_discrete(cbs) ? (f_oop = fs) : (f_oop, f_iip = fs) + + cond = if cbs isa AbstractVector + (out, u, t, integ) -> f_iip(out, u, parameter_values(integ), t) elseif is_discrete(cbs) - cond(u, t, integ) = f_oop(u, t, parameter_values(integ)) + (u, t, integ) -> f_oop(u, parameter_values(integ), t) else - cond = function (u, t, integ) + function (u, t, integ) if DiffEqBase.isinplace(integ.sol.prob) tmp, = DiffEqBase.get_tmp_cache(integ) - f_iip(tmp, u, t, parameter_values(integ)) + f_iip(tmp, u, parameter_values(integ), t) tmp[1] else - f_oop(u, t, parameter_values(integ)) + f_oop(u, parameter_values(integ), t) end end end @@ -641,6 +672,7 @@ function compile_functional_affect(affect::FunctionalAffect, cb, sys, dvs, ps; k end is_discrete(cb::AbstractCallback) = cb isa SymbolicDiscreteCallback +is_discrete(cb::Vector{<:AbstractCallback}) = eltype(cb) isa SymbolicDiscreteCallback function generate_continuous_callbacks(sys::AbstractSystem, dvs = unknowns(sys), ps = parameters(sys; initial_parameters = true); kwargs...) cbs = continuous_events(sys) @@ -668,27 +700,27 @@ function generate_callback(cbs::Vector{SymbolicContinuousCallback}, sys; kwargs. return generate_callback(cbs[cb_ind], sys; kwargs...) end - trigger = compile_condition(cbs, sys, dvs, ps; kwargs...) + trigger = compile_condition(cbs, sys, unknowns(sys), parameters(sys; initial_parameters = true); kwargs...) affects = [] affect_negs = [] inits = [] finals = [] for cb in cbs - affect = compile_affect(cb.affect, cb, sys) + affect = compile_affect(cb.affect, cb, sys, default = (args...) -> ()) push!(affects, affect) push!(affect_negs, compile_affect(cb.affect_neg, cb, sys, default = affect)) - push!(inits, compile_affect(cb.initialize, cb, sys, default = SciMLBase.INITALIZE_DEFAULT)) - push!(finals, compile_affect(cb.finalize, cb, sys, default = SciMLBase.FINALIZE_DEFAULT)) + push!(inits, compile_affect(cb.initialize, cb, sys, default = nothing)) + push!(finals, compile_affect(cb.finalize, cb, sys, default = nothing)) end # Since there may be different number of conditions and affects, # we build a map that translates the condition eq. number to the affect number - num_eqs = length.(eqs) eq2affect = reduce(vcat, [fill(i, num_eqs[i]) for i in eachindex(affects)]) + eqs = reduce(vcat, eqs) @assert length(eq2affect) == length(eqs) - @assert maximum(eq2affect) == length(affect_functions) + @assert maximum(eq2affect) == length(affects) affect = function (integ, idx) affects[eq2affect[idx]](integ) @@ -702,8 +734,8 @@ function generate_callback(cbs::Vector{SymbolicContinuousCallback}, sys; kwargs. finalize = compile_vector_optional_affect(finals, SciMLBase.FINALIZE_DEFAULT) return VectorContinuousCallback( - trigger, affect, length(cbs); affect_neg, initialize, finalize, - rootfind = callback.rootfind, initializealg = SciMLBase.NoInit) + trigger, affect, affect_neg, length(eqs); initialize, finalize, + rootfind = cbs[1].rootfind, initializealg = SciMLBase.NoInit) end function generate_callback(cb, sys; kwargs...) @@ -712,14 +744,14 @@ function generate_callback(cb, sys; kwargs...) ps = parameters(sys; initial_parameters = true) trigger = is_timed ? conditions(cb) : compile_condition(cb, sys, dvs, ps; kwargs...) - affect = compile_affect(cb.affect, cb, sys) + affect = compile_affect(cb.affect, cb, sys, default = (args...) -> ()) affect_neg = hasfield(typeof(cb), :affect_neg) ? compile_affect(cb.affect_neg, cb, sys, default = affect) : nothing initialize = compile_affect(cb.initialize, cb, sys, default = SciMLBase.INITIALIZE_DEFAULT) finalize = compile_affect(cb.finalize, cb, sys, default = SciMLBase.FINALIZE_DEFAULT) if is_discrete(cb) - if is_timed && condition(cb) isa AbstractVector + if is_timed && conditions(cb) isa AbstractVector return PresetTimeCallback(trigger, affect; affect_neg, initialize, finalize, initializealg = SciMLBase.NoInit) elseif is_timed @@ -762,22 +794,30 @@ function compile_affect( ps = parameters(aff) dvs = unknowns(aff) + @show ps if aff isa AffectSystem - aff_map = affu_to_sysu(aff) + aff_map = aff_to_sys(aff) + sys_map = Dict([v => k for (k, v) in aff_map]) + build_initializeprob = has_alg_eqs(sys) + function affect!(integrator) - pmap = [] - for pre_p in parameters(system(affect)) + pmap = Pair[] + for pre_p in previous_vals(aff) p = only(arguments(unwrap(pre_p))) - push!(pmap, pre_p => integrator[p]) - end - guesses = [u => integrator[aff_map[u]] for u in unknowns(system(affect))] - prob = ImplicitDiscreteProblem(system(affect), [], (0, 1), pmap; guesses) - sol = init(prob, SimpleIDSolve()) - for u in unknowns(system(affect)) - integrator[aff_map[u]] = sol[u] + pval = isparameter(p) ? integrator.ps[p] : integrator[p] + push!(pmap, pre_p => pval) end + guesses = Pair[u => integrator[aff_map[u]] for u in updated_vals(aff)] + affprob = ImplicitDiscreteProblem(system(aff), Pair[], (0, 1), pmap; guesses, build_initializeprob) + affsol = init(affprob, SimpleIDSolve()) + for u in unknowns(aff) + integrator[u] = affsol[u] + end + for p in discretes(aff) + integrator.ps[p] = affsol[sys_map[p]] + end for idx in save_idxs SciMLBase.save_discretes!(integ, idx) end diff --git a/src/systems/diffeqs/odesystem.jl b/src/systems/diffeqs/odesystem.jl index dc6c2709bb..d4fcf961cc 100644 --- a/src/systems/diffeqs/odesystem.jl +++ b/src/systems/diffeqs/odesystem.jl @@ -325,7 +325,6 @@ function ODESystem(deqs::AbstractVector{<:Equation}, iv, dvs, ps; cons = get_constraintsystem(sys) cons !== nothing && push!(conssystems, cons) end - @show conssystems @set! constraintsystem.systems = conssystems end diff --git a/src/systems/discrete_system/discrete_system.jl b/src/systems/discrete_system/discrete_system.jl index 40f01769ee..b20ad32008 100644 --- a/src/systems/discrete_system/discrete_system.jl +++ b/src/systems/discrete_system/discrete_system.jl @@ -428,4 +428,14 @@ function DiscreteFunctionExpr(sys::DiscreteSystem, args...; kwargs...) DiscreteFunctionExpr{true}(sys, args...; kwargs...) end +function Base.:(==)(sys1::DiscreteSystem, sys2::DiscreteSystem) + sys1 === sys2 && return true + isequal(nameof(sys1), nameof(sys2)) && + isequal(get_iv(sys1), get_iv(sys2)) && + _eq_unordered(get_eqs(sys1), get_eqs(sys2)) && + _eq_unordered(get_unknowns(sys1), get_unknowns(sys2)) && + _eq_unordered(get_ps(sys1), get_ps(sys2)) && + all(s1 == s2 for (s1, s2) in zip(get_systems(sys1), get_systems(sys2))) +end + supports_initialization(::DiscreteSystem) = false diff --git a/src/systems/discrete_system/implicit_discrete_system.jl b/src/systems/discrete_system/implicit_discrete_system.jl index ebae78384a..60ee09cf4d 100644 --- a/src/systems/discrete_system/implicit_discrete_system.jl +++ b/src/systems/discrete_system/implicit_discrete_system.jl @@ -441,3 +441,13 @@ end function ImplicitDiscreteFunctionExpr(sys::ImplicitDiscreteSystem, args...; kwargs...) ImplicitDiscreteFunctionExpr{true}(sys, args...; kwargs...) end + +function Base.:(==)(sys1::ImplicitDiscreteSystem, sys2::ImplicitDiscreteSystem) + sys1 === sys2 && return true + isequal(nameof(sys1), nameof(sys2)) && + isequal(get_iv(sys1), get_iv(sys2)) && + _eq_unordered(get_eqs(sys1), get_eqs(sys2)) && + _eq_unordered(get_unknowns(sys1), get_unknowns(sys2)) && + _eq_unordered(get_ps(sys1), get_ps(sys2)) && + all(s1 == s2 for (s1, s2) in zip(get_systems(sys1), get_systems(sys2))) +end diff --git a/src/systems/index_cache.jl b/src/systems/index_cache.jl index c12835d969..5141f71e76 100644 --- a/src/systems/index_cache.jl +++ b/src/systems/index_cache.jl @@ -117,9 +117,7 @@ function IndexCache(sys::AbstractSystem) affs = [affs] end for affect in affs - if affect isa Equation - is_parameter(sys, affect.lhs) && push!(discs, affect.lhs) - elseif affect isa FunctionalAffect || affect isa ImperativeAffect + if affect isa AffectSystem || affect isa FunctionalAffect || affect isa ImperativeAffect union!(discs, unwrap.(discretes(affect))) elseif isnothing(affect) continue diff --git a/src/systems/problem_utils.jl b/src/systems/problem_utils.jl index d0ae0d174f..b90f667c5f 100644 --- a/src/systems/problem_utils.jl +++ b/src/systems/problem_utils.jl @@ -396,9 +396,7 @@ function better_varmap_to_vars(varmap::AbstractDict, vars::Vector; vals = promote_to_concrete(vals; tofloat = tofloat, use_union = false) end - if isempty(vals) - return nothing - elseif container_type <: Tuple + if container_type <: Tuple return (vals...,) else return SymbolicUtils.Code.create_array(container_type, eltype(vals), Val{1}(), diff --git a/test/symbolic_events.jl b/test/symbolic_events.jl index 2a690cb7f4..5aac8365e1 100644 --- a/test/symbolic_events.jl +++ b/test/symbolic_events.jl @@ -237,7 +237,7 @@ end @test m.ctx === 3 end -@testset "Basic ODESystem Tests" begin +@testset "Condition Compilation" begin @named sys = ODESystem(eqs, t, continuous_events = [x ~ 1]) @test getfield(sys, :continuous_events)[] == SymbolicContinuousCallback(Equation[x ~ 1], nothing) @@ -270,11 +270,11 @@ end cb = ModelingToolkit.generate_continuous_callbacks(sys) cond = cb.condition out = [0.0] - cond(out, [0], p0, t0) + cond.f_iip.contents(out, [0], p0, t0) @test out[] ≈ -1 # signature is u,p,t - cond.rf_ip(out, [1], p0, t0) + cond.f_iip.contents(out, [1], p0, t0) @test out[] ≈ 0 # signature is u,p,t - cond.rf_ip(out, [2], p0, t0) + cond.f_iip.contents(out, [2], p0, t0) @test out[] ≈ 1 # signature is u,p,t prob = ODEProblem(sys, Pair[], (0.0, 2.0)) @@ -302,20 +302,20 @@ end cond = cb.condition out = [0.0, 0.0] # the root to find is 2 - cond.rf_ip(out, [0, 0], p0, t0) + cond.f_iip.contents(out, [0, 0], p0, t0) @test out[1] ≈ -2 # signature is u,p,t - cond.rf_ip(out, [1, 0], p0, t0) + cond.f_iip.contents(out, [1, 0], p0, t0) @test out[1] ≈ -1 # signature is u,p,t - cond.rf_ip(out, [2, 0], p0, t0) # this should return 0 + cond.f_iip.contents(out, [2, 0], p0, t0) # this should return 0 @test out[1] ≈ 0 # signature is u,p,t # the root to find is 1 out = [0.0, 0.0] - cond.rf_ip(out, [0, 0], p0, t0) + cond.f_iip.contents(out, [0, 0], p0, t0) @test out[2] ≈ -1 # signature is u,p,t - cond.rf_ip(out, [0, 1], p0, t0) # this should return 0 + cond.f_iip.contents(out, [0, 1], p0, t0) # this should return 0 @test out[2] ≈ 0 # signature is u,p,t - cond.rf_ip(out, [0, 2], p0, t0) + cond.f_iip.contents(out, [0, 2], p0, t0) @test out[2] ≈ 1 # signature is u,p,t sol = solve(prob, Tsit5()) @@ -336,14 +336,14 @@ end @variables x(t)=1 v(t)=0 root_eqs = [x ~ 0] - affect = [v ~ -v] + affect = [v ~ -Pre(v)] @named ball = ODESystem( [D(x) ~ v D(v) ~ -9.8], t, continuous_events = root_eqs => affect) - @test getfield(ball, :continuous_events)[] == - SymbolicContinuousCallback(Equation[x ~ 0], Equation[v ~ -v]) + @test only(continuous_events(ball)) == + SymbolicContinuousCallback(Equation[x ~ 0], Equation[v ~ -Pre(v)]) ball = structural_simplify(ball) @test length(ModelingToolkit.continuous_events(ball)) == 1 @@ -356,14 +356,14 @@ end ###### 2D bouncing ball @variables x(t)=1 y(t)=0 vx(t)=0 vy(t)=1 - continuous_events = [[x ~ 0] => [vx ~ -vx] - [y ~ -1.5, y ~ 1.5] => [vy ~ -vy]] + events = [[x ~ 0] => [vx ~ -Pre(vx)] + [y ~ -1.5, y ~ 1.5] => [vy ~ -Pre(vy)]] @named ball = ODESystem( [D(x) ~ vx D(y) ~ vy D(vx) ~ -9.8 - D(vy) ~ -0.01vy], t; continuous_events) + D(vy) ~ -0.01vy], t; continuous_events = events) _ball = ball ball = structural_simplify(_ball) @@ -381,7 +381,9 @@ end SymbolicContinuousCallback(Equation[y ~ -1.5, y ~ 1.5], Equation[vy ~ -vy]) cond = cb.condition out = [0.0, 0.0, 0.0] - cond.rf_ip(out, [0, 0, 0, 0], p0, t0) + p0 = 0. + t0 = 0. + cond.f_iip.contents(out, [0, 0, 0, 0], p0, t0) @test out ≈ [0, 1.5, -1.5] sol = solve(prob, Tsit5()) @@ -394,22 +396,15 @@ end @test maximum(sol_nosplit[y]) ≈ 1.5 # check wall conditions end -# tv = sort([LinRange(0, 5, 200); sol.t]) -# plot(sol(tv)[y], sol(tv)[x], line_z=tv) -# vline!([-1.5, 1.5], l=(:black, 5), primary=false) -# hline!([0], l=(:black, 5), primary=false) - ## Test multi-variable affect # in this test, there are two variables affected by a single event. @testset "Multi-variable affect" begin - continuous_events = [ - [x ~ 0] => [vx ~ -vx, vy ~ -vy] - ] + events = [[x ~ 0] => [vx ~ -Pre(vx), vy ~ -Pre(vy)]] @named ball = ODESystem([D(x) ~ vx D(y) ~ vy D(vx) ~ -1 - D(vy) ~ 0], t; continuous_events) + D(vy) ~ 0], t; continuous_events = events) ball_nosplit = structural_simplify(ball) ball = structural_simplify(ball) @@ -425,24 +420,21 @@ end @test -minimum(sol_nosplit[y]) ≈ maximum(sol_nosplit[y]) ≈ sqrt(2) # the ball will never go further than √2 in either direction (gravity was changed to 1 to get this particular number) end -# tv = sort([LinRange(0, 5, 200); sol.t]) -# plot(sol(tv)[y], sol(tv)[x], line_z=tv) -# vline!([-1.5, 1.5], l=(:black, 5), primary=false) -# hline!([0], l=(:black, 5), primary=false) - # issue https://github.com/SciML/ModelingToolkit.jl/issues/1386 # tests that it works for ODAESystem -@variables vs(t) v(t) vmeasured(t) -eq = [vs ~ sin(2pi * t) - D(v) ~ vs - v - D(vmeasured) ~ 0.0] -ev = [sin(20pi * t) ~ 0.0] => [vmeasured ~ v] -@named sys = ODESystem(eq, t, continuous_events = ev) -sys = structural_simplify(sys) -prob = ODEProblem(sys, zeros(2), (0.0, 5.1)) -sol = solve(prob, Tsit5()) -@test all(minimum((0:0.05:5) .- sol.t', dims = 2) .< 0.0001) # test that the solver stepped every 0.05s as dictated by event -@test sol([0.25 - eps()])[vmeasured][] == sol([0.23])[vmeasured][] # test the hold property +@testset "ODAESystem" begin + @variables vs(t) v(t) vmeasured(t) + eq = [vs ~ sin(2pi * t) + D(v) ~ vs - v + D(vmeasured) ~ 0.0] + ev = [sin(20pi * t) ~ 0.0] => [vmeasured ~ Pre(v)] + @named sys = ODESystem(eq, t, continuous_events = ev) + sys = structural_simplify(sys) + prob = ODEProblem(sys, zeros(2), (0.0, 5.1)) + sol = solve(prob, Tsit5()) + @test all(minimum((0:0.1:5) .- sol.t', dims = 2) .< 0.0001) # test that the solver stepped every 0.1s as dictated by event + @test sol([0.25])[vmeasured][] == sol([0.23])[vmeasured][] # test the hold property +end ## https://github.com/SciML/ModelingToolkit.jl/issues/1528 @testset "Handle Empty Events" begin @@ -506,7 +498,7 @@ end @variables A(t) B(t) cond1 = (t == t1) - affect1 = [A ~ A + 1] + affect1 = [A ~ Pre(A) + 1] cb1 = cond1 => affect1 cond2 = (t == t2) affect2 = [k ~ 1.0] @@ -581,7 +573,7 @@ end @variables A(t) B(t) cond1 = (t == t1) - affect1 = [A ~ A + 1] + affect1 = [A ~ Pre(A) + 1] cb1 = cond1 => affect1 cond2 = (t == t2) affect2 = [k ~ 1.0] @@ -597,7 +589,7 @@ end testsol(ssys, u0, p, tspan; tstops = [1.0, 2.0], paramtotest = k) cond1a = (t == t1) - affect1a = [A ~ A + 1, B ~ A] + affect1a = [A ~ Pre(A) + 1, B ~ Pre(A)] cb1a = cond1a => affect1a @named ssys1 = SDESystem(eqs, [0.0], t, [A, B], [k, t1, t2], discrete_events = [cb1a, cb2]) @@ -665,7 +657,7 @@ end @variables A(t) B(t) cond1 = (t == t1) - affect1 = [A ~ A + 1] + affect1 = [A ~ Pre(A) + 1] cb1 = cond1 => affect1 cond2 = (t == t2) affect2 = [k ~ 1.0] @@ -679,7 +671,7 @@ end testsol(jsys, u0, p, tspan; tstops = [1.0, 2.0], rng, paramtotest = k) cond1a = (t == t1) - affect1a = [A ~ A + 1, B ~ A] + affect1a = [A ~ Pre(A) + 1, B ~ Pre(A)] cb1a = cond1a => affect1a @named jsys1 = JumpSystem(eqs, t, [A, B], [k, t1, t2], discrete_events = [cb1a, cb2]) u0′ = [A => 1, B => 0] From 0106e067e52587d7eb69d590512ce7ca59fcc9cd Mon Sep 17 00:00:00 2001 From: vyudu Date: Wed, 12 Mar 2025 11:37:33 -0400 Subject: [PATCH 07/52] fix: modify constructor for SDESystem and JUmpSystem --- src/systems/diffeqs/odesystem.jl | 4 ---- src/systems/diffeqs/sdesystem.jl | 6 ++++-- src/systems/jumps/jumpsystem.jl | 4 ++-- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/systems/diffeqs/odesystem.jl b/src/systems/diffeqs/odesystem.jl index d4fcf961cc..6eb9854d54 100644 --- a/src/systems/diffeqs/odesystem.jl +++ b/src/systems/diffeqs/odesystem.jl @@ -311,10 +311,6 @@ function ODESystem(deqs::AbstractVector{<:Equation}, iv, dvs, ps; throw(ArgumentError("System names must be unique.")) end - algeeqs = filter(is_alg_equation, deqs) - cont_callbacks = SymbolicContinuousCallbacks(continuous_events, algeeqs) - disc_callbacks = SymbolicDiscreteCallbacks(discrete_events, algeeqs) - if is_dde === nothing is_dde = _check_if_dde(deqs, iv′, systems) end diff --git a/src/systems/diffeqs/sdesystem.jl b/src/systems/diffeqs/sdesystem.jl index f51529a559..2f01de5009 100644 --- a/src/systems/diffeqs/sdesystem.jl +++ b/src/systems/diffeqs/sdesystem.jl @@ -267,8 +267,10 @@ function SDESystem(deqs::AbstractVector{<:Equation}, neqs::AbstractArray, iv, dv ctrl_jac = RefValue{Any}(EMPTY_JAC) Wfact = RefValue(EMPTY_JAC) Wfact_t = RefValue(EMPTY_JAC) - cont_callbacks = SymbolicContinuousCallbacks(continuous_events) - disc_callbacks = SymbolicDiscreteCallbacks(discrete_events) + + algeeqs = filter(is_alg_equation, deqs) + cont_callbacks = SymbolicContinuousCallbacks(continuous_events, algeeqs) + disc_callbacks = SymbolicDiscreteCallbacks(discrete_events, algeeqs) if is_dde === nothing is_dde = _check_if_dde(deqs, iv′, systems) end diff --git a/src/systems/jumps/jumpsystem.jl b/src/systems/jumps/jumpsystem.jl index 9da32a4305..877d4dc315 100644 --- a/src/systems/jumps/jumpsystem.jl +++ b/src/systems/jumps/jumpsystem.jl @@ -230,8 +230,8 @@ function JumpSystem(eqs, iv, unknowns, ps; end end - cont_callbacks = SymbolicContinuousCallbacks(continuous_events) - disc_callbacks = SymbolicDiscreteCallbacks(discrete_events) + cont_callbacks = SymbolicContinuousCallbacks(continuous_events, Equation[]) + disc_callbacks = SymbolicDiscreteCallbacks(discrete_events, Equation[]) JumpSystem{typeof(ap)}(Threads.atomic_add!(SYSTEM_COUNT, UInt(1)), ap, iv′, us′, ps′, var_to_name, observed, name, description, systems, From 9b4e3adb932438b5b3fdc26fe149746fc83ce968 Mon Sep 17 00:00:00 2001 From: vyudu Date: Wed, 12 Mar 2025 12:13:23 -0400 Subject: [PATCH 08/52] test: make more tests pass --- src/systems/callbacks.jl | 12 +++--- src/systems/diffeqs/odesystem.jl | 4 ++ test/symbolic_events.jl | 63 +++++++++++++++----------------- 3 files changed, 40 insertions(+), 39 deletions(-) diff --git a/src/systems/callbacks.jl b/src/systems/callbacks.jl index 76e5a0d47f..a197d978cc 100644 --- a/src/systems/callbacks.jl +++ b/src/systems/callbacks.jl @@ -219,8 +219,8 @@ SymbolicContinuousCallback(p::Pair) = SymbolicContinuousCallback(p[1], p[2]) SymbolicContinuousCallback(cb::SymbolicContinuousCallback, args...) = cb make_affect(affect::Nothing) = nothing -make_affect(affect::Tuple) = FunctionalAffect(affects...) -make_affect(affect::NamedTuple) = FunctionalAffect(; affects...) +make_affect(affect::Tuple) = FunctionalAffect(affect...) +make_affect(affect::NamedTuple) = FunctionalAffect(; affect...) make_affect(affect::FunctionalAffect) = affect make_affect(affect::AffectSystem) = affect @@ -616,7 +616,7 @@ function compile_condition(cbs::Union{AbstractCallback, Vector{<:AbstractCallbac if expression == Val{true} fs = eval_or_rgf.(fs; eval_expression, eval_module) end - is_discrete(cbs) ? (f_oop = fs) : (f_oop, f_iip = fs) + f_oop, f_iip = is_discrete(cbs) ? (fs, nothing) : fs # no iip function for discrete condition. cond = if cbs isa AbstractVector (out, u, t, integ) -> f_iip(out, u, parameter_values(integ), t) @@ -644,7 +644,7 @@ function compile_functional_affect(affect::FunctionalAffect, cb, sys, dvs, ps; k dvs_ind = Dict(reverse(en) for en in enumerate(dvs)) v_inds = map(sym -> dvs_ind[sym], unknowns(affect)) - if has_index_cache(sys) && get_index_cache(sys) !== nothing + if has_index_cache(sys) && (ic = get_index_cache(sys)) !== nothing p_inds = [(pind = parameter_index(sys, sym)) === nothing ? sym : pind for sym in parameters(affect)] save_idxs = get(ic.callback_to_clocks, cb, Int[]) @@ -752,7 +752,7 @@ function generate_callback(cb, sys; kwargs...) if is_discrete(cb) if is_timed && conditions(cb) isa AbstractVector - return PresetTimeCallback(trigger, affect; affect_neg, initialize, + return PresetTimeCallback(trigger, affect; initialize, finalize, initializealg = SciMLBase.NoInit) elseif is_timed return PeriodicCallback(affect, trigger; initialize, finalize) @@ -783,7 +783,7 @@ Notes - `kwargs` are passed through to `Symbolics.build_function`. """ function compile_affect( - aff::Union{Nothing, Affect}, cb::AbstractCallback, sys::AbstractSystem; default = nothing) + aff::Union{Nothing, Affect}, cb::AbstractCallback, sys::AbstractSystem; default = nothing, kwargs...) save_idxs = if !(has_index_cache(sys) && (ic = get_index_cache(sys)) !== nothing) Int[] else diff --git a/src/systems/diffeqs/odesystem.jl b/src/systems/diffeqs/odesystem.jl index 6eb9854d54..d4fcf961cc 100644 --- a/src/systems/diffeqs/odesystem.jl +++ b/src/systems/diffeqs/odesystem.jl @@ -311,6 +311,10 @@ function ODESystem(deqs::AbstractVector{<:Equation}, iv, dvs, ps; throw(ArgumentError("System names must be unique.")) end + algeeqs = filter(is_alg_equation, deqs) + cont_callbacks = SymbolicContinuousCallbacks(continuous_events, algeeqs) + disc_callbacks = SymbolicDiscreteCallbacks(discrete_events, algeeqs) + if is_dde === nothing is_dde = _check_if_dde(deqs, iv′, systems) end diff --git a/test/symbolic_events.jl b/test/symbolic_events.jl index 5aac8365e1..27522815de 100644 --- a/test/symbolic_events.jl +++ b/test/symbolic_events.jl @@ -270,11 +270,11 @@ end cb = ModelingToolkit.generate_continuous_callbacks(sys) cond = cb.condition out = [0.0] - cond.f_iip.contents(out, [0], p0, t0) + cond.f_iip(out, [0], p0, t0) @test out[] ≈ -1 # signature is u,p,t - cond.f_iip.contents(out, [1], p0, t0) + cond.f_iip(out, [1], p0, t0) @test out[] ≈ 0 # signature is u,p,t - cond.f_iip.contents(out, [2], p0, t0) + cond.f_iip(out, [2], p0, t0) @test out[] ≈ 1 # signature is u,p,t prob = ODEProblem(sys, Pair[], (0.0, 2.0)) @@ -302,20 +302,20 @@ end cond = cb.condition out = [0.0, 0.0] # the root to find is 2 - cond.f_iip.contents(out, [0, 0], p0, t0) + cond.f_iip(out, [0, 0], p0, t0) @test out[1] ≈ -2 # signature is u,p,t - cond.f_iip.contents(out, [1, 0], p0, t0) + cond.f_iip(out, [1, 0], p0, t0) @test out[1] ≈ -1 # signature is u,p,t - cond.f_iip.contents(out, [2, 0], p0, t0) # this should return 0 + cond.f_iip(out, [2, 0], p0, t0) # this should return 0 @test out[1] ≈ 0 # signature is u,p,t # the root to find is 1 out = [0.0, 0.0] - cond.f_iip.contents(out, [0, 0], p0, t0) + cond.f_iip(out, [0, 0], p0, t0) @test out[2] ≈ -1 # signature is u,p,t - cond.f_iip.contents(out, [0, 1], p0, t0) # this should return 0 + cond.f_iip(out, [0, 1], p0, t0) # this should return 0 @test out[2] ≈ 0 # signature is u,p,t - cond.f_iip.contents(out, [0, 2], p0, t0) + cond.f_iip(out, [0, 2], p0, t0) @test out[2] ≈ 1 # signature is u,p,t sol = solve(prob, Tsit5()) @@ -376,14 +376,14 @@ end cb = get_callback(prob) @test cb isa ModelingToolkit.DiffEqCallbacks.VectorContinuousCallback @test getfield(ball, :continuous_events)[1] == - SymbolicContinuousCallback(Equation[x ~ 0], Equation[vx ~ -vx]) + SymbolicContinuousCallback(Equation[x ~ 0], Equation[vx ~ -Pre(vx)]) @test getfield(ball, :continuous_events)[2] == - SymbolicContinuousCallback(Equation[y ~ -1.5, y ~ 1.5], Equation[vy ~ -vy]) + SymbolicContinuousCallback(Equation[y ~ -1.5, y ~ 1.5], Equation[vy ~ -Pre(vy)]) cond = cb.condition out = [0.0, 0.0, 0.0] p0 = 0. t0 = 0. - cond.f_iip.contents(out, [0, 0, 0, 0], p0, t0) + cond.f_iip(out, [0, 0, 0, 0], p0, t0) @test out ≈ [0, 1.5, -1.5] sol = solve(prob, Tsit5()) @@ -394,11 +394,9 @@ end @test 0 <= minimum(sol_nosplit[x]) <= 1e-10 # the ball never went through the floor but got very close @test minimum(sol_nosplit[y]) ≈ -1.5 # check wall conditions @test maximum(sol_nosplit[y]) ≈ 1.5 # check wall conditions -end -## Test multi-variable affect -# in this test, there are two variables affected by a single event. -@testset "Multi-variable affect" begin + ## Test multi-variable affect + # in this test, there are two variables affected by a single event. events = [[x ~ 0] => [vx ~ -Pre(vx), vy ~ -Pre(vy)]] @named ball = ODESystem([D(x) ~ vx @@ -422,19 +420,19 @@ end # issue https://github.com/SciML/ModelingToolkit.jl/issues/1386 # tests that it works for ODAESystem -@testset "ODAESystem" begin - @variables vs(t) v(t) vmeasured(t) - eq = [vs ~ sin(2pi * t) - D(v) ~ vs - v - D(vmeasured) ~ 0.0] - ev = [sin(20pi * t) ~ 0.0] => [vmeasured ~ Pre(v)] - @named sys = ODESystem(eq, t, continuous_events = ev) - sys = structural_simplify(sys) - prob = ODEProblem(sys, zeros(2), (0.0, 5.1)) - sol = solve(prob, Tsit5()) - @test all(minimum((0:0.1:5) .- sol.t', dims = 2) .< 0.0001) # test that the solver stepped every 0.1s as dictated by event - @test sol([0.25])[vmeasured][] == sol([0.23])[vmeasured][] # test the hold property -end +#@testset "ODAESystem" begin +# @variables vs(t) v(t) vmeasured(t) +# eq = [vs ~ sin(2pi * t) +# D(v) ~ vs - v +# D(vmeasured) ~ 0.0] +# ev = [sin(20pi * t) ~ 0.0] => [vmeasured ~ Pre(v)] +# @named sys = ODESystem(eq, t, continuous_events = ev) +# sys = structural_simplify(sys) +# prob = ODEProblem(sys, zeros(2), (0.0, 5.1)) +# sol = solve(prob, Tsit5()) +# @test all(minimum((0:0.1:5) .- sol.t', dims = 2) .< 0.0001) # test that the solver stepped every 0.1s as dictated by event +# @test sol([0.25])[vmeasured][] == sol([0.23])[vmeasured][] # test the hold property +#end ## https://github.com/SciML/ModelingToolkit.jl/issues/1528 @testset "Handle Empty Events" begin @@ -513,7 +511,7 @@ end testsol(osys, u0, p, tspan; tstops = [1.0, 2.0], paramtotest = k) cond1a = (t == t1) - affect1a = [A ~ A + 1, B ~ A] + affect1a = [A ~ Pre(A) + 1, B ~ A] cb1a = cond1a => affect1a @named osys1 = ODESystem(eqs, t, [A, B], [k, t1, t2], discrete_events = [cb1a, cb2]) u0′ = [A => 1.0, B => 0.0] @@ -589,7 +587,7 @@ end testsol(ssys, u0, p, tspan; tstops = [1.0, 2.0], paramtotest = k) cond1a = (t == t1) - affect1a = [A ~ Pre(A) + 1, B ~ Pre(A)] + affect1a = [A ~ Pre(A) + 1, B ~ A] cb1a = cond1a => affect1a @named ssys1 = SDESystem(eqs, [0.0], t, [A, B], [k, t1, t2], discrete_events = [cb1a, cb2]) @@ -640,7 +638,6 @@ end end @testset "JumpSystem Discrete Callbacks" begin - rng = rng function testsol(jsys, u0, p, tspan; tstops = Float64[], paramtotest = nothing, N = 40000, kwargs...) jsys = complete(jsys) @@ -671,7 +668,7 @@ end testsol(jsys, u0, p, tspan; tstops = [1.0, 2.0], rng, paramtotest = k) cond1a = (t == t1) - affect1a = [A ~ Pre(A) + 1, B ~ Pre(A)] + affect1a = [A ~ Pre(A) + 1, B ~ A] cb1a = cond1a => affect1a @named jsys1 = JumpSystem(eqs, t, [A, B], [k, t1, t2], discrete_events = [cb1a, cb2]) u0′ = [A => 1, B => 0] From 9759d668da72bf9f7b8a2f8b819db6360e284c01 Mon Sep 17 00:00:00 2001 From: vyudu Date: Wed, 12 Mar 2025 15:29:00 -0400 Subject: [PATCH 09/52] test: fix namespacing --- src/systems/callbacks.jl | 36 ++++++++++++++++++------------------ test/symbolic_events.jl | 7 ++++--- 2 files changed, 22 insertions(+), 21 deletions(-) diff --git a/src/systems/callbacks.jl b/src/systems/callbacks.jl index a197d978cc..813e5412c7 100644 --- a/src/systems/callbacks.jl +++ b/src/systems/callbacks.jl @@ -258,18 +258,12 @@ function make_affect(affect::Vector{Equation}; warn = true) aff_map = Dict(zip(p_as_unknowns, discretes)) rev_map = Dict([v => k for (k, v) in aff_map]) affect = Symbolics.substitute(affect, rev_map) - @mtkbuild affectsys = ImplicitDiscreteSystem( - affect, iv, collect(union(unknowns, p_as_unknowns)), cb_params) + @mtkbuild affectsys = ImplicitDiscreteSystem(affect, iv, collect(union(unknowns, p_as_unknowns)), cb_params) params = filter(isparameter, map(x -> only(arguments(unwrap(x))), cb_params)) - @show params - for u in unknowns aff_map[u] = u end - @show unknowns - @show params - return AffectSystem(affectsys, collect(unknowns), params, discretes, aff_map) end @@ -494,16 +488,22 @@ function namespace_affect(affect::FunctionalAffect, s) context(affect)) end -namespace_affect(affect::AffectSystem, s) = AffectSystem(system(affect), renamespace.((s,), discretes(affect))) -namespace_affects(af::Union{Nothing, Affect}, s) = af isa Affect ? namespace_affect(af, s) : nothing +function namespace_affect(affect::AffectSystem, s) + AffectSystem(renamespace(s, system(affect)), + renamespace.((s,), unknowns(affect)), + renamespace.((s,), parameters(affect)), + renamespace.((s,), discretes(affect)), + Dict([k => renamespace(s, v) for (k, v) in aff_to_sys(affect)])) +end +namespace_affect(af::Nothing, s) = nothing function namespace_callback(cb::SymbolicContinuousCallback, s)::SymbolicContinuousCallback SymbolicContinuousCallback( namespace_equation.(equations(cb), (s,)), - namespace_affects(affects(cb), s), - affect_neg = namespace_affects(affect_negs(cb), s), - initialize = namespace_affects(initialize_affects(cb), s), - finalize = namespace_affects(finalize_affects(cb), s), + namespace_affect(affects(cb), s), + affect_neg = namespace_affect(affect_negs(cb), s), + initialize = namespace_affect(initialize_affects(cb), s), + finalize = namespace_affect(finalize_affects(cb), s), rootfind = cb.rootfind) end @@ -794,9 +794,9 @@ function compile_affect( ps = parameters(aff) dvs = unknowns(aff) - @show ps if aff isa AffectSystem + affsys = system(aff) aff_map = aff_to_sys(aff) sys_map = Dict([v => k for (k, v) in aff_map]) build_initializeprob = has_alg_eqs(sys) @@ -809,11 +809,11 @@ function compile_affect( push!(pmap, pre_p => pval) end guesses = Pair[u => integrator[aff_map[u]] for u in updated_vals(aff)] - affprob = ImplicitDiscreteProblem(system(aff), Pair[], (0, 1), pmap; guesses, build_initializeprob) + affprob = ImplicitDiscreteProblem(affsys, Pair[], (0, 1), pmap; guesses, build_initializeprob) affsol = init(affprob, SimpleIDSolve()) for u in unknowns(aff) - integrator[u] = affsol[u] + integrator[u] = affsol[sys_map[u]] end for p in discretes(aff) integrator.ps[p] = affsol[sys_map[p]] @@ -899,9 +899,9 @@ function continuous_events(sys::AbstractSystem) systems = get_systems(sys) cbs = [obs; reduce(vcat, - (map(o -> namespace_callback(o, s), continuous_events(s)) - for s in systems), + (map(o -> namespace_callback(o, s), continuous_events(s)) for s in systems), init = SymbolicContinuousCallback[])] + @show cbs filter(!isempty, cbs) end diff --git a/test/symbolic_events.jl b/test/symbolic_events.jl index 27522815de..fbd5a5776e 100644 --- a/test/symbolic_events.jl +++ b/test/symbolic_events.jl @@ -644,6 +644,7 @@ end dprob = DiscreteProblem(jsys, u0, tspan, p) jprob = JumpProblem(jsys, dprob, Direct(); kwargs...) sol = solve(jprob, SSAStepper(); tstops = tstops) + @show sol @test (sol(1.000000000001)[1] - sol(0.99999999999)[1]) == 1 paramtotest === nothing || (@test sol.ps[paramtotest] == 1.0) @test sol(40.0)[1] == 0 @@ -654,7 +655,7 @@ end @variables A(t) B(t) cond1 = (t == t1) - affect1 = [A ~ Pre(A) + 1] + affect1 = [A ~ A + 1] cb1 = cond1 => affect1 cond2 = (t == t2) affect2 = [k ~ 1.0] @@ -704,7 +705,7 @@ end testsol(jsys6, u0, p, tspan; tstops = [1.0, 2.0], rng, paramtotest = k) end -@testset "Oscillator" begin +@testset "Namespacing" begin function oscillator_ce(k = 1.0; name) sts = @variables x(t)=1.0 v(t)=0.0 F(t) ps = @parameters k=k Θ=0.5 @@ -1152,7 +1153,7 @@ end f = ModelingToolkit.FunctionalAffect( f = (i, u, p, c) -> seen = true, sts = [], pars = [], discretes = []) cb1 = ModelingToolkit.SymbolicContinuousCallback( - [x ~ 0], Equation[], initialize = [x ~ 1.5], finalize = f) + [x ~ 0], nothing, initialize = [x ~ 1.5], finalize = f) @mtkbuild sys = ODESystem(D(x) ~ -1, t, [x], []; continuous_events = [cb1]) prob = ODEProblem(sys, [x => 1.0], (0.0, 2), []) sol = solve(prob, Tsit5(); dtmax = 0.01) From 7b11c4c961c67f6c7bc8713905fd0b5d645234f6 Mon Sep 17 00:00:00 2001 From: vyudu Date: Wed, 12 Mar 2025 17:11:33 -0400 Subject: [PATCH 10/52] fix: fix JumpSystem and don't use is_diff_equation --- src/systems/callbacks.jl | 22 ++++++++++++---------- src/systems/diffeqs/odesystem.jl | 2 +- src/systems/diffeqs/sdesystem.jl | 2 +- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/systems/callbacks.jl b/src/systems/callbacks.jl index 813e5412c7..814d43d679 100644 --- a/src/systems/callbacks.jl +++ b/src/systems/callbacks.jl @@ -355,8 +355,8 @@ function vars!(vars, cb::SymbolicContinuousCallback; op = Differential) vars!(vars, eq; op) end for aff in (affects(cb), affect_negs(cb), initialize_affects(cb), finalize_affects(cb)) - if aff isa Vector{Equation} - for eq in aff + if aff isa AffectSystem + for eq in vcat(observed(system(aff)), equations(system(aff))) vars!(vars, eq; op) end elseif aff !== nothing @@ -453,18 +453,18 @@ function Base.show(io::IO, db::SymbolicDiscreteCallback) end function vars!(vars, cb::SymbolicDiscreteCallback; op = Differential) - if symbolic_type(cb.condition) == NotSymbolic - if cb.condition isa AbstractArray - for eq in cb.condition + if symbolic_type(conditions(cb)) == NotSymbolic + if conditions(cb) isa AbstractArray + for eq in conditions(cb) vars!(vars, eq; op) end end else - vars!(vars, cb.condition; op) + vars!(vars, conditions(cb); op) end - for aff in (cb.affects, cb.initialize, cb.finalize) - if aff isa Vector{Equation} - for eq in aff + for aff in (affects(cb), initialize_affects(cb), finalize_affects(cb)) + if aff isa AffectSystem + for eq in vcat(observed(system(aff)), equations(system(aff))) vars!(vars, eq; op) end elseif aff !== nothing @@ -709,7 +709,7 @@ function generate_callback(cbs::Vector{SymbolicContinuousCallback}, sys; kwargs. affect = compile_affect(cb.affect, cb, sys, default = (args...) -> ()) push!(affects, affect) - push!(affect_negs, compile_affect(cb.affect_neg, cb, sys, default = affect)) + push!(affect_negs, compile_affect(cb.affect_neg, cb, sys, default = affect) push!(inits, compile_affect(cb.initialize, cb, sys, default = nothing)) push!(finals, compile_affect(cb.finalize, cb, sys, default = nothing)) end @@ -821,6 +821,8 @@ function compile_affect( for idx in save_idxs SciMLBase.save_discretes!(integ, idx) end + + sys isa JumpSystem && reset_aggregated_jumps!(integrator) end elseif aff isa FunctionalAffect || aff isa ImperativeAffect compile_functional_affect(aff, cb, sys, dvs, ps; kwargs...) diff --git a/src/systems/diffeqs/odesystem.jl b/src/systems/diffeqs/odesystem.jl index d4fcf961cc..ff91c6be01 100644 --- a/src/systems/diffeqs/odesystem.jl +++ b/src/systems/diffeqs/odesystem.jl @@ -311,7 +311,7 @@ function ODESystem(deqs::AbstractVector{<:Equation}, iv, dvs, ps; throw(ArgumentError("System names must be unique.")) end - algeeqs = filter(is_alg_equation, deqs) + alg_eqs = filter(eq -> eq.lhs isa Union{Symbolic, Number} && !isdiffeq(eq), deqs) cont_callbacks = SymbolicContinuousCallbacks(continuous_events, algeeqs) disc_callbacks = SymbolicDiscreteCallbacks(discrete_events, algeeqs) diff --git a/src/systems/diffeqs/sdesystem.jl b/src/systems/diffeqs/sdesystem.jl index 2f01de5009..b0de2db817 100644 --- a/src/systems/diffeqs/sdesystem.jl +++ b/src/systems/diffeqs/sdesystem.jl @@ -268,7 +268,7 @@ function SDESystem(deqs::AbstractVector{<:Equation}, neqs::AbstractArray, iv, dv Wfact = RefValue(EMPTY_JAC) Wfact_t = RefValue(EMPTY_JAC) - algeeqs = filter(is_alg_equation, deqs) + alg_eqs = filter(eq -> eq.lhs isa Union{Symbolic, Number} && !isdiffeq(eq), deqs) cont_callbacks = SymbolicContinuousCallbacks(continuous_events, algeeqs) disc_callbacks = SymbolicDiscreteCallbacks(discrete_events, algeeqs) if is_dde === nothing From 4a07e30cc9465afc989ee27de06b6364c6d3d60b Mon Sep 17 00:00:00 2001 From: vyudu Date: Wed, 12 Mar 2025 17:18:19 -0400 Subject: [PATCH 11/52] typo: add ) --- src/systems/callbacks.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/systems/callbacks.jl b/src/systems/callbacks.jl index 814d43d679..2f2846e362 100644 --- a/src/systems/callbacks.jl +++ b/src/systems/callbacks.jl @@ -709,7 +709,7 @@ function generate_callback(cbs::Vector{SymbolicContinuousCallback}, sys; kwargs. affect = compile_affect(cb.affect, cb, sys, default = (args...) -> ()) push!(affects, affect) - push!(affect_negs, compile_affect(cb.affect_neg, cb, sys, default = affect) + push!(affect_negs, compile_affect(cb.affect_neg, cb, sys, default = affect)) push!(inits, compile_affect(cb.initialize, cb, sys, default = nothing)) push!(finals, compile_affect(cb.finalize, cb, sys, default = nothing)) end From 932a48a27b8b802a5165dfa92c99177387c67ba2 Mon Sep 17 00:00:00 2001 From: vyudu Date: Wed, 12 Mar 2025 17:22:00 -0400 Subject: [PATCH 12/52] typo: algeeqs --- src/systems/diffeqs/odesystem.jl | 4 ++-- src/systems/diffeqs/sdesystem.jl | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/systems/diffeqs/odesystem.jl b/src/systems/diffeqs/odesystem.jl index ff91c6be01..35742a2116 100644 --- a/src/systems/diffeqs/odesystem.jl +++ b/src/systems/diffeqs/odesystem.jl @@ -312,8 +312,8 @@ function ODESystem(deqs::AbstractVector{<:Equation}, iv, dvs, ps; end alg_eqs = filter(eq -> eq.lhs isa Union{Symbolic, Number} && !isdiffeq(eq), deqs) - cont_callbacks = SymbolicContinuousCallbacks(continuous_events, algeeqs) - disc_callbacks = SymbolicDiscreteCallbacks(discrete_events, algeeqs) + cont_callbacks = SymbolicContinuousCallbacks(continuous_events, alg_eqs) + disc_callbacks = SymbolicDiscreteCallbacks(discrete_events, alg_eqs) if is_dde === nothing is_dde = _check_if_dde(deqs, iv′, systems) diff --git a/src/systems/diffeqs/sdesystem.jl b/src/systems/diffeqs/sdesystem.jl index b0de2db817..58adbf261e 100644 --- a/src/systems/diffeqs/sdesystem.jl +++ b/src/systems/diffeqs/sdesystem.jl @@ -269,8 +269,8 @@ function SDESystem(deqs::AbstractVector{<:Equation}, neqs::AbstractArray, iv, dv Wfact_t = RefValue(EMPTY_JAC) alg_eqs = filter(eq -> eq.lhs isa Union{Symbolic, Number} && !isdiffeq(eq), deqs) - cont_callbacks = SymbolicContinuousCallbacks(continuous_events, algeeqs) - disc_callbacks = SymbolicDiscreteCallbacks(discrete_events, algeeqs) + cont_callbacks = SymbolicContinuousCallbacks(continuous_events, alg_eqs) + disc_callbacks = SymbolicDiscreteCallbacks(discrete_events, alg_eqs) if is_dde === nothing is_dde = _check_if_dde(deqs, iv′, systems) end From 1974ae8204c1740415495fde0e9d888063917b53 Mon Sep 17 00:00:00 2001 From: vyudu Date: Thu, 13 Mar 2025 20:53:56 -0400 Subject: [PATCH 13/52] fix --- src/systems/callbacks.jl | 1 - 1 file changed, 1 deletion(-) diff --git a/src/systems/callbacks.jl b/src/systems/callbacks.jl index 2f2846e362..ad36dab9a2 100644 --- a/src/systems/callbacks.jl +++ b/src/systems/callbacks.jl @@ -903,7 +903,6 @@ function continuous_events(sys::AbstractSystem) reduce(vcat, (map(o -> namespace_callback(o, s), continuous_events(s)) for s in systems), init = SymbolicContinuousCallback[])] - @show cbs filter(!isempty, cbs) end From 2c7ca7f77348b25935234b71e5a293ad285652ab Mon Sep 17 00:00:00 2001 From: vyudu Date: Mon, 17 Mar 2025 10:06:00 -0400 Subject: [PATCH 14/52] more test fixes --- Project.toml | 1 + src/systems/callbacks.jl | 74 ++++++++++++++++++-------------- src/systems/diffeqs/odesystem.jl | 4 +- src/systems/diffeqs/sdesystem.jl | 4 +- test/symbolic_events.jl | 26 +++++------ 5 files changed, 59 insertions(+), 50 deletions(-) diff --git a/Project.toml b/Project.toml index a9290b037a..7d4ec0ef00 100644 --- a/Project.toml +++ b/Project.toml @@ -51,6 +51,7 @@ SciMLBase = "0bca4576-84f4-4d90-8ffe-ffa030f20462" SciMLStructures = "53ae85a6-f571-4167-b2af-e1d143709226" Serialization = "9e88b42a-f829-5b0c-bbe9-9e923198166b" Setfield = "efcf1570-3423-57d1-acb7-fd33fddbac46" +SimpleImplicitDiscreteSolve = "3263718b-31ed-49cf-8a0f-35a466e8af96" SimpleNonlinearSolve = "727e6d20-b764-4bd8-a329-72de5adea6c7" SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" SpecialFunctions = "276daf66-3868-5448-9aa4-cd146d93841b" diff --git a/src/systems/callbacks.jl b/src/systems/callbacks.jl index ad36dab9a2..3f3defea88 100644 --- a/src/systems/callbacks.jl +++ b/src/systems/callbacks.jl @@ -203,68 +203,79 @@ struct SymbolicContinuousCallback <: AbstractCallback function SymbolicContinuousCallback( conditions::Union{Equation, Vector{Equation}}, - affect = nothing; + affect = nothing, iv = nothing; affect_neg = affect, initialize = nothing, finalize = nothing, - rootfind = SciMLBase.LeftRootFind) + rootfind = SciMLBase.LeftRootFind, + algeeqs = Equation[]) + affect isa AbstractVector && isnothing(iv) && @warn "No independent variable specified. If t appears in an affect equation explicitly, like x ~ t + 1, then this must be specified. Otherwise this can be disregarded." conditions = (conditions isa AbstractVector) ? conditions : [conditions] - new(conditions, make_affect(affect), make_affect(affect_neg), - initialize, finalize, rootfind) + new(conditions, make_affect(affect, iv; algeeqs), make_affect(affect_neg, iv; algeeqs), + make_affect(initialize, iv; algeeqs), make_affect(finalize, iv; algeeqs), rootfind) end # Default affect to nothing end SymbolicContinuousCallback(p::Pair) = SymbolicContinuousCallback(p[1], p[2]) SymbolicContinuousCallback(cb::SymbolicContinuousCallback, args...) = cb -make_affect(affect::Nothing) = nothing -make_affect(affect::Tuple) = FunctionalAffect(affect...) -make_affect(affect::NamedTuple) = FunctionalAffect(; affect...) -make_affect(affect::FunctionalAffect) = affect -make_affect(affect::AffectSystem) = affect +make_affect(affect::Nothing, iv; kwargs...) = nothing +make_affect(affect::Tuple, iv; kwargs...) = FunctionalAffect(affect...) +make_affect(affect::NamedTuple, iv; kwargs...) = FunctionalAffect(; affect...) +make_affect(affect::FunctionalAffect, iv; kwargs...) = affect +make_affect(affect::AffectSystem, iv; kwargs...) = affect + +function make_affect(affect::Vector{Equation}, iv; algeeqs = Equation[]) + isempty(affect) && return nothing + isempty(algeeqs) && @warn "No algebraic equations were found. If the system has no algebraic equations, this can be disregarded. Otherwise consider passing in `algeeqs` to the SymbolicContinuousCallbacks constructor." -function make_affect(affect::Vector{Equation}; warn = true) affect = scalarize(affect) - unknowns = OrderedSet() + dvs = OrderedSet() params = OrderedSet() - for eq in affect - !haspre(eq) && warn && - @warn "Equation $eq has no `Pre` operator. As such it will be interpreted as an algebraic equation to be satisfied after the callback. If you intended to use the value of a variable x before the affect, use Pre(x)." - collect_vars!(unknowns, params, eq, nothing; op = Pre) + !haspre(eq) && + @warn "Affect equation $eq has no `Pre` operator. As such it will be interpreted as an algebraic equation to be satisfied after the callback. If you intended to use the value of a variable x before the affect, use Pre(x)." + collect_vars!(dvs, params, eq, iv; op = Pre) + end + for eq in algeeqs + collect_vars!(dvs, params, eq, iv) + end + if isnothing(iv) + iv = isempty(dvs) ? iv : only(arguments(dvs[1])) end - iv = isempty(unknowns) ? t_nounits : only(arguments(unknowns[1])) # System parameters should become unknowns in the ImplicitDiscreteSystem. cb_params = Any[] discretes = Any[] - p_as_unknowns = Any[] + p_as_dvs = Any[] for p in params if iscall(p) && (operation(p) isa Pre) push!(cb_params, p) elseif iscall(p) && length(arguments(p)) == 1 && isequal(only(arguments(p)), iv) push!(discretes, p) - push!(p_as_unknowns, tovar(p)) + push!(p_as_dvs, tovar(p)) else push!(discretes, p) name = iscall(p) ? nameof(operation(p)) : nameof(p) p = wrap(Sym{FnType{Tuple{symtype(iv)}, Real}}(name)(iv)) p = setmetadata(p, Symbolics.VariableSource, (:variables, name)) - push!(p_as_unknowns, p) + push!(p_as_dvs, p) end end - aff_map = Dict(zip(p_as_unknowns, discretes)) + aff_map = Dict(zip(p_as_dvs, discretes)) rev_map = Dict([v => k for (k, v) in aff_map]) affect = Symbolics.substitute(affect, rev_map) - @mtkbuild affectsys = ImplicitDiscreteSystem(affect, iv, collect(union(unknowns, p_as_unknowns)), cb_params) + @mtkbuild affectsys = ImplicitDiscreteSystem(vcat(affect, algeeqs), iv, collect(union(dvs, p_as_dvs)), cb_params) + # get accessed parameters p from Pre(p) in the callback parameters params = filter(isparameter, map(x -> only(arguments(unwrap(x))), cb_params)) - for u in unknowns + # add unknowns to the map + for u in unknowns(affectsys) aff_map[u] = u end - return AffectSystem(affectsys, collect(unknowns), params, discretes, aff_map) + return AffectSystem(affectsys, unknowns(affectsys), params, discretes, aff_map) end function make_affect(affect) @@ -274,7 +285,7 @@ end """ Generate continuous callbacks. """ -function SymbolicContinuousCallbacks(events, algeeqs::Vector{Equation} = Equation[]) +function SymbolicContinuousCallbacks(events, algeeqs::Vector{Equation} = Equation[], iv = nothing) callbacks = SymbolicContinuousCallback[] isnothing(events) && return callbacks @@ -283,10 +294,7 @@ function SymbolicContinuousCallbacks(events, algeeqs::Vector{Equation} = Equatio for event in events cond, affs = event isa Pair ? (event[1], event[2]) : (event, nothing) - if affs isa AbstractVector - affs = vcat(affs, algeeqs) - end - affect = make_affect(affs) + affect = make_affect(affs, iv; algeeqs) push!(callbacks, SymbolicContinuousCallback(cond, affect)) end callbacks @@ -391,6 +399,8 @@ struct SymbolicDiscreteCallback <: AbstractCallback condition, affect = nothing; initialize = nothing, finalize = nothing) c = is_timed_condition(condition) ? condition : value(scalarize(condition)) + + isnothing(iv) && @warn "No independent variable specified. If t appears in an affect equation explicitly, like x ~ t + 1, then this must be specified. Otherwise this can be disregarded." new(c, make_affect(affect), make_affect(initialize), make_affect(finalize)) end # Default affect to nothing @@ -399,7 +409,7 @@ end """ Generate discrete callbacks. """ -function SymbolicDiscreteCallbacks(events, algeeqs::Vector{Equation} = Equation[]) +function SymbolicDiscreteCallbacks(events, algeeqs::Vector{Equation} = Equation[], iv = nothing) callbacks = SymbolicDiscreteCallback[] isnothing(events) && return callbacks @@ -408,10 +418,7 @@ function SymbolicDiscreteCallbacks(events, algeeqs::Vector{Equation} = Equation[ for event in events cond, affs = event isa Pair ? (event[1], event[2]) : (event, nothing) - if affs isa AbstractVector - affs = vcat(affs, algeeqs) - end - affect = make_affect(affs) + affect = make_affect(affs, iv; algeeqs) push!(callbacks, SymbolicDiscreteCallback(cond, affect)) end callbacks @@ -813,6 +820,7 @@ function compile_affect( affsol = init(affprob, SimpleIDSolve()) for u in unknowns(aff) + @show u integrator[u] = affsol[sys_map[u]] end for p in discretes(aff) diff --git a/src/systems/diffeqs/odesystem.jl b/src/systems/diffeqs/odesystem.jl index 35742a2116..712a8995fe 100644 --- a/src/systems/diffeqs/odesystem.jl +++ b/src/systems/diffeqs/odesystem.jl @@ -312,8 +312,8 @@ function ODESystem(deqs::AbstractVector{<:Equation}, iv, dvs, ps; end alg_eqs = filter(eq -> eq.lhs isa Union{Symbolic, Number} && !isdiffeq(eq), deqs) - cont_callbacks = SymbolicContinuousCallbacks(continuous_events, alg_eqs) - disc_callbacks = SymbolicDiscreteCallbacks(discrete_events, alg_eqs) + cont_callbacks = SymbolicContinuousCallbacks(continuous_events, alg_eqs, iv) + disc_callbacks = SymbolicDiscreteCallbacks(discrete_events, alg_eqs, iv) if is_dde === nothing is_dde = _check_if_dde(deqs, iv′, systems) diff --git a/src/systems/diffeqs/sdesystem.jl b/src/systems/diffeqs/sdesystem.jl index 58adbf261e..829ac03426 100644 --- a/src/systems/diffeqs/sdesystem.jl +++ b/src/systems/diffeqs/sdesystem.jl @@ -269,8 +269,8 @@ function SDESystem(deqs::AbstractVector{<:Equation}, neqs::AbstractArray, iv, dv Wfact_t = RefValue(EMPTY_JAC) alg_eqs = filter(eq -> eq.lhs isa Union{Symbolic, Number} && !isdiffeq(eq), deqs) - cont_callbacks = SymbolicContinuousCallbacks(continuous_events, alg_eqs) - disc_callbacks = SymbolicDiscreteCallbacks(discrete_events, alg_eqs) + cont_callbacks = SymbolicContinuousCallbacks(continuous_events, alg_eqs, iv) + disc_callbacks = SymbolicDiscreteCallbacks(discrete_events, alg_eqs, iv) if is_dde === nothing is_dde = _check_if_dde(deqs, iv′, systems) end diff --git a/test/symbolic_events.jl b/test/symbolic_events.jl index fbd5a5776e..8bb9606ff9 100644 --- a/test/symbolic_events.jl +++ b/test/symbolic_events.jl @@ -420,19 +420,19 @@ end # issue https://github.com/SciML/ModelingToolkit.jl/issues/1386 # tests that it works for ODAESystem -#@testset "ODAESystem" begin -# @variables vs(t) v(t) vmeasured(t) -# eq = [vs ~ sin(2pi * t) -# D(v) ~ vs - v -# D(vmeasured) ~ 0.0] -# ev = [sin(20pi * t) ~ 0.0] => [vmeasured ~ Pre(v)] -# @named sys = ODESystem(eq, t, continuous_events = ev) -# sys = structural_simplify(sys) -# prob = ODEProblem(sys, zeros(2), (0.0, 5.1)) -# sol = solve(prob, Tsit5()) -# @test all(minimum((0:0.1:5) .- sol.t', dims = 2) .< 0.0001) # test that the solver stepped every 0.1s as dictated by event -# @test sol([0.25])[vmeasured][] == sol([0.23])[vmeasured][] # test the hold property -#end +@testset "ODAESystem" begin + @variables vs(t) v(t) vmeasured(t) + eq = [vs ~ sin(2pi * t) + D(v) ~ vs - v + D(vmeasured) ~ 0.0] + ev = [sin(20pi * t) ~ 0.0] => [vmeasured ~ Pre(v)] + @named sys = ODESystem(eq, t, continuous_events = ev) + sys = structural_simplify(sys) + prob = ODEProblem(sys, zeros(2), (0.0, 5.1)) + sol = solve(prob, Tsit5()) + @test all(minimum((0:0.1:5) .- sol.t', dims = 2) .< 0.0001) # test that the solver stepped every 0.1s as dictated by event + @test sol([0.25])[vmeasured][] == sol([0.23])[vmeasured][] # test the hold property +end ## https://github.com/SciML/ModelingToolkit.jl/issues/1528 @testset "Handle Empty Events" begin From 9ca45c5db861f67677269062cf883fdeb79e76b6 Mon Sep 17 00:00:00 2001 From: vyudu Date: Tue, 18 Mar 2025 12:36:20 -0400 Subject: [PATCH 15/52] refactor: make iv, algeeqs kwargs --- src/systems/callbacks.jl | 70 +- src/systems/diffeqs/odesystem.jl | 6 +- src/systems/diffeqs/sdesystem.jl | 6 +- src/systems/jumps/jumpsystem.jl | 4 +- test/symbolic_events.jl | 1422 +++++++++++++++--------------- 5 files changed, 757 insertions(+), 751 deletions(-) diff --git a/src/systems/callbacks.jl b/src/systems/callbacks.jl index 3f3defea88..0207f552a1 100644 --- a/src/systems/callbacks.jl +++ b/src/systems/callbacks.jl @@ -77,7 +77,6 @@ unknowns(a::AffectSystem) = a.unknowns parameters(a::AffectSystem) = a.parameters aff_to_sys(a::AffectSystem) = a.aff_to_sys previous_vals(a::AffectSystem) = parameters(system(a)) -updated_vals(a::AffectSystem) = unknowns(system(a)) function Base.show(iio::IO, aff::AffectSystem) eqs = vcat(equations(system(aff)), observed(system(aff))) @@ -148,7 +147,8 @@ haspre(O) = recursive_hasoperator(Pre, O) const Affect = Union{AffectSystem, FunctionalAffect, ImperativeAffect} """ - SymbolicContinuousCallback(eqs::Vector{Equation}, affect, affect_neg, rootfind) + SymbolicContinuousCallback(eqs::Vector{Equation}, affect = nothing, iv = nothing; + affect_neg = affect, initialize = nothing, finalize = nothing, rootfind = SciMLBase.LeftRootFind, algeeqs = Equation[]) A [`ContinuousCallback`](@ref SciMLBase.ContinuousCallback) specified symbolically. Takes a vector of equations `eq` as well as the positive-edge `affect` and negative-edge `affect_neg` that apply when *any* of `eq` are satisfied. @@ -203,32 +203,31 @@ struct SymbolicContinuousCallback <: AbstractCallback function SymbolicContinuousCallback( conditions::Union{Equation, Vector{Equation}}, - affect = nothing, iv = nothing; + affect = nothing; affect_neg = affect, initialize = nothing, finalize = nothing, rootfind = SciMLBase.LeftRootFind, + iv = nothing, algeeqs = Equation[]) - affect isa AbstractVector && isnothing(iv) && @warn "No independent variable specified. If t appears in an affect equation explicitly, like x ~ t + 1, then this must be specified. Otherwise this can be disregarded." conditions = (conditions isa AbstractVector) ? conditions : [conditions] - new(conditions, make_affect(affect, iv; algeeqs), make_affect(affect_neg, iv; algeeqs), - make_affect(initialize, iv; algeeqs), make_affect(finalize, iv; algeeqs), rootfind) + new(conditions, make_affect(affect; iv, algeeqs), make_affect(affect_neg; iv, algeeqs), + make_affect(initialize; iv, algeeqs), make_affect(finalize; iv, algeeqs), rootfind) end # Default affect to nothing end -SymbolicContinuousCallback(p::Pair) = SymbolicContinuousCallback(p[1], p[2]) -SymbolicContinuousCallback(cb::SymbolicContinuousCallback, args...) = cb +SymbolicContinuousCallback(p::Pair, args...; kwargs...) = SymbolicContinuousCallback(p[1], p[2]) +SymbolicContinuousCallback(cb::SymbolicContinuousCallback, args...; kwargs...) = cb -make_affect(affect::Nothing, iv; kwargs...) = nothing -make_affect(affect::Tuple, iv; kwargs...) = FunctionalAffect(affect...) -make_affect(affect::NamedTuple, iv; kwargs...) = FunctionalAffect(; affect...) -make_affect(affect::FunctionalAffect, iv; kwargs...) = affect -make_affect(affect::AffectSystem, iv; kwargs...) = affect +make_affect(affect::Nothing; kwargs...) = nothing +make_affect(affect::Tuple; kwargs...) = FunctionalAffect(affect...) +make_affect(affect::NamedTuple; kwargs...) = FunctionalAffect(; affect...) +make_affect(affect::Affect; kwargs...) = affect -function make_affect(affect::Vector{Equation}, iv; algeeqs = Equation[]) +function make_affect(affect::Vector{Equation}; iv = nothing, algeeqs = Equation[]) isempty(affect) && return nothing - isempty(algeeqs) && @warn "No algebraic equations were found. If the system has no algebraic equations, this can be disregarded. Otherwise consider passing in `algeeqs` to the SymbolicContinuousCallbacks constructor." + isempty(algeeqs) && @warn "No algebraic equations were found. If the system has no algebraic equations, this can be disregarded. Otherwise pass in `algeeqs` to the SymbolicContinuousCallback constructor." affect = scalarize(affect) dvs = OrderedSet() @@ -243,6 +242,7 @@ function make_affect(affect::Vector{Equation}, iv; algeeqs = Equation[]) end if isnothing(iv) iv = isempty(dvs) ? iv : only(arguments(dvs[1])) + isnothing(iv) && @warn "No independent variable specified and could not be inferred. If the iv appears in an affect equation explicitly, like x ~ t + 1, then it must be specified as an argument to the SymbolicContinuousCallback or SymbolicDiscreteCallback constructor. Otherwise this warning can be disregarded." end # System parameters should become unknowns in the ImplicitDiscreteSystem. @@ -271,21 +271,21 @@ function make_affect(affect::Vector{Equation}, iv; algeeqs = Equation[]) # get accessed parameters p from Pre(p) in the callback parameters params = filter(isparameter, map(x -> only(arguments(unwrap(x))), cb_params)) # add unknowns to the map - for u in unknowns(affectsys) + for u in dvs aff_map[u] = u end - return AffectSystem(affectsys, unknowns(affectsys), params, discretes, aff_map) + return AffectSystem(affectsys, collect(dvs), params, discretes, aff_map) end -function make_affect(affect) +function make_affect(affect; kwargs...) error("Malformed affect $(affect). This should be a vector of equations or a tuple specifying a functional affect.") end """ Generate continuous callbacks. """ -function SymbolicContinuousCallbacks(events, algeeqs::Vector{Equation} = Equation[], iv = nothing) +function SymbolicContinuousCallbacks(events; algeeqs::Vector{Equation} = Equation[], iv = nothing) callbacks = SymbolicContinuousCallback[] isnothing(events) && return callbacks @@ -294,8 +294,7 @@ function SymbolicContinuousCallbacks(events, algeeqs::Vector{Equation} = Equatio for event in events cond, affs = event isa Pair ? (event[1], event[2]) : (event, nothing) - affect = make_affect(affs, iv; algeeqs) - push!(callbacks, SymbolicContinuousCallback(cond, affect)) + push!(callbacks, SymbolicContinuousCallback(cond, affs; iv, algeeqs)) end callbacks end @@ -380,7 +379,8 @@ end # TODO: Iterative callbacks """ - SymbolicDiscreteCallback(conditions::Vector{Equation}, affect) + SymbolicDiscreteCallback(conditions::Vector{Equation}, affect = nothing, iv = nothing; + initialize = nothing, finalize = nothing, algeeqs = Equation[]) A callback that triggers at the first timestep that the conditions are satisfied. @@ -388,6 +388,10 @@ The condition can be one of: - Δt::Real - periodic events with period Δt - ts::Vector{Real} - events trigger at these preset times given by `ts` - eqs::Vector{Equation} - events trigger when the condition evaluates to true + +Arguments: +- iv: The independent variable of the system. This must be specified if the independent variable appaers in one of the equations explicitly, as in x ~ t + 1. +- algeeqs: Algebraic equations of the system that must be satisfied after the callback occurs. """ struct SymbolicDiscreteCallback <: AbstractCallback conditions::Any @@ -397,19 +401,18 @@ struct SymbolicDiscreteCallback <: AbstractCallback function SymbolicDiscreteCallback( condition, affect = nothing; - initialize = nothing, finalize = nothing) + initialize = nothing, finalize = nothing, iv = nothing, algeeqs = Equation[]) c = is_timed_condition(condition) ? condition : value(scalarize(condition)) - isnothing(iv) && @warn "No independent variable specified. If t appears in an affect equation explicitly, like x ~ t + 1, then this must be specified. Otherwise this can be disregarded." - new(c, make_affect(affect), make_affect(initialize), - make_affect(finalize)) + new(c, make_affect(affect; iv, algeeqs), make_affect(initialize; iv, algeeqs), + make_affect(finalize; iv, algeeqs)) end # Default affect to nothing end """ Generate discrete callbacks. """ -function SymbolicDiscreteCallbacks(events, algeeqs::Vector{Equation} = Equation[], iv = nothing) +function SymbolicDiscreteCallbacks(events; algeeqs::Vector{Equation} = Equation[], iv = nothing) callbacks = SymbolicDiscreteCallback[] isnothing(events) && return callbacks @@ -418,8 +421,7 @@ function SymbolicDiscreteCallbacks(events, algeeqs::Vector{Equation} = Equation[ for event in events cond, affs = event isa Pair ? (event[1], event[2]) : (event, nothing) - affect = make_affect(affs, iv; algeeqs) - push!(callbacks, SymbolicDiscreteCallback(cond, affect)) + push!(callbacks, SymbolicDiscreteCallback(cond, affs; iv, algeeqs)) end callbacks end @@ -801,12 +803,13 @@ function compile_affect( ps = parameters(aff) dvs = unknowns(aff) + dvs_to_modify = setdiff(dvs, getfield.(observed(sys), :lhs)) if aff isa AffectSystem affsys = system(aff) aff_map = aff_to_sys(aff) sys_map = Dict([v => k for (k, v) in aff_map]) - build_initializeprob = has_alg_eqs(sys) + reinit = has_alg_eqs(sys) function affect!(integrator) pmap = Pair[] @@ -815,12 +818,11 @@ function compile_affect( pval = isparameter(p) ? integrator.ps[p] : integrator[p] push!(pmap, pre_p => pval) end - guesses = Pair[u => integrator[aff_map[u]] for u in updated_vals(aff)] - affprob = ImplicitDiscreteProblem(affsys, Pair[], (0, 1), pmap; guesses, build_initializeprob) + guesses = Pair[u => integrator[aff_map[u]] for u in unknowns(affsys)] + affprob = ImplicitDiscreteProblem(affsys, Pair[], (0, 1), pmap; guesses, build_initializeprob = reinit) affsol = init(affprob, SimpleIDSolve()) - for u in unknowns(aff) - @show u + for u in dvs_to_modify integrator[u] = affsol[sys_map[u]] end for p in discretes(aff) diff --git a/src/systems/diffeqs/odesystem.jl b/src/systems/diffeqs/odesystem.jl index 712a8995fe..f10c62f94d 100644 --- a/src/systems/diffeqs/odesystem.jl +++ b/src/systems/diffeqs/odesystem.jl @@ -311,9 +311,9 @@ function ODESystem(deqs::AbstractVector{<:Equation}, iv, dvs, ps; throw(ArgumentError("System names must be unique.")) end - alg_eqs = filter(eq -> eq.lhs isa Union{Symbolic, Number} && !isdiffeq(eq), deqs) - cont_callbacks = SymbolicContinuousCallbacks(continuous_events, alg_eqs, iv) - disc_callbacks = SymbolicDiscreteCallbacks(discrete_events, alg_eqs, iv) + algeeqs = filter(eq -> eq.lhs isa Union{Symbolic, Number} && !isdiffeq(eq), deqs) + cont_callbacks = SymbolicContinuousCallbacks(continuous_events; algeeqs, iv) + disc_callbacks = SymbolicDiscreteCallbacks(discrete_events; algeeqs, iv) if is_dde === nothing is_dde = _check_if_dde(deqs, iv′, systems) diff --git a/src/systems/diffeqs/sdesystem.jl b/src/systems/diffeqs/sdesystem.jl index 829ac03426..5a4c966ad1 100644 --- a/src/systems/diffeqs/sdesystem.jl +++ b/src/systems/diffeqs/sdesystem.jl @@ -268,9 +268,9 @@ function SDESystem(deqs::AbstractVector{<:Equation}, neqs::AbstractArray, iv, dv Wfact = RefValue(EMPTY_JAC) Wfact_t = RefValue(EMPTY_JAC) - alg_eqs = filter(eq -> eq.lhs isa Union{Symbolic, Number} && !isdiffeq(eq), deqs) - cont_callbacks = SymbolicContinuousCallbacks(continuous_events, alg_eqs, iv) - disc_callbacks = SymbolicDiscreteCallbacks(discrete_events, alg_eqs, iv) + algeeqs = filter(eq -> eq.lhs isa Union{Symbolic, Number} && !isdiffeq(eq), deqs) + cont_callbacks = SymbolicContinuousCallbacks(continuous_events; algeeqs, iv) + disc_callbacks = SymbolicDiscreteCallbacks(discrete_events; algeeqs, iv) if is_dde === nothing is_dde = _check_if_dde(deqs, iv′, systems) end diff --git a/src/systems/jumps/jumpsystem.jl b/src/systems/jumps/jumpsystem.jl index 877d4dc315..3ace018377 100644 --- a/src/systems/jumps/jumpsystem.jl +++ b/src/systems/jumps/jumpsystem.jl @@ -230,8 +230,8 @@ function JumpSystem(eqs, iv, unknowns, ps; end end - cont_callbacks = SymbolicContinuousCallbacks(continuous_events, Equation[]) - disc_callbacks = SymbolicDiscreteCallbacks(discrete_events, Equation[]) + cont_callbacks = SymbolicContinuousCallbacks(continuous_events; iv) + disc_callbacks = SymbolicDiscreteCallbacks(discrete_events; iv) JumpSystem{typeof(ap)}(Threads.atomic_add!(SYSTEM_COUNT, UInt(1)), ap, iv′, us′, ps′, var_to_name, observed, name, description, systems, diff --git a/test/symbolic_events.jl b/test/symbolic_events.jl index 8bb9606ff9..28d6d644d0 100644 --- a/test/symbolic_events.jl +++ b/test/symbolic_events.jl @@ -18,715 +18,715 @@ eqs = [D(x) ~ 1] affect = [x ~ 0] affect_neg = [x ~ 1] -@testset "SymbolicContinuousCallback constructors" begin - e = SymbolicContinuousCallback(eqs[]) - @test e isa SymbolicContinuousCallback - @test isequal(equations(e), eqs) - @test e.affect == nothing - @test e.affect_neg == nothing - @test e.rootfind == SciMLBase.LeftRootFind - - e = SymbolicContinuousCallback(eqs) - @test e isa SymbolicContinuousCallback - @test isequal(equations(e), eqs) - @test e.affect == nothing - @test e.affect_neg == nothing - @test e.rootfind == SciMLBase.LeftRootFind - - e = SymbolicContinuousCallback(eqs, nothing) - @test e isa SymbolicContinuousCallback - @test isequal(equations(e), eqs) - @test e.affect == nothing - @test e.affect_neg == nothing - @test e.rootfind == SciMLBase.LeftRootFind - - e = SymbolicContinuousCallback(eqs[], nothing) - @test e isa SymbolicContinuousCallback - @test isequal(equations(e), eqs) - @test e.affect == nothing - @test e.affect_neg == nothing - @test e.rootfind == SciMLBase.LeftRootFind - - e = SymbolicContinuousCallback(eqs => nothing) - @test e isa SymbolicContinuousCallback - @test isequal(equations(e), eqs) - @test e.affect == nothing - @test e.affect_neg == nothing - @test e.rootfind == SciMLBase.LeftRootFind - - e = SymbolicContinuousCallback(eqs[] => nothing) - @test e isa SymbolicContinuousCallback - @test isequal(equations(e), eqs) - @test e.affect == nothing - @test e.affect_neg == nothing - @test e.rootfind == SciMLBase.LeftRootFind - - ## With affect - e = SymbolicContinuousCallback(eqs[], affect) - @test e isa SymbolicContinuousCallback - @test isequal(equations(e), eqs) - @test observed(system(affects(e))) == affect - @test observed(system(affect_negs(e))) == affect - @test e.rootfind == SciMLBase.LeftRootFind - - # with only positive edge affect - e = SymbolicContinuousCallback(eqs[], affect, affect_neg = nothing) - @test e isa SymbolicContinuousCallback - @test isequal(equations(e), eqs) - @test observed(system(affects(e))) == affect - @test isnothing(e.affect_neg) - @test e.rootfind == SciMLBase.LeftRootFind - - # with explicit edge affects - e = SymbolicContinuousCallback(eqs[], affect, affect_neg = affect_neg) - @test e isa SymbolicContinuousCallback - @test isequal(equations(e), eqs) - @test observed(system(affects(e))) == affect - @test observed(system(affect_negs(e))) == affect_neg - @test e.rootfind == SciMLBase.LeftRootFind - - # with different root finding ops - e = SymbolicContinuousCallback( - eqs[], affect, affect_neg = affect_neg, rootfind = SciMLBase.LeftRootFind) - @test e isa SymbolicContinuousCallback - @test isequal(equations(e), eqs) - @test e.rootfind == SciMLBase.LeftRootFind - - # test plural constructor - e = SymbolicContinuousCallbacks(eqs[]) - @test e isa Vector{SymbolicContinuousCallback} - @test isequal(equations(e[]), eqs) - @test e[].affect == nothing - - e = SymbolicContinuousCallbacks(eqs) - @test e isa Vector{SymbolicContinuousCallback} - @test isequal(equations(e[]), eqs) - @test e[].affect == nothing - - e = SymbolicContinuousCallbacks(eqs[] => affect) - @test e isa Vector{SymbolicContinuousCallback} - @test isequal(equations(e[]), eqs) - @test e[].affect isa AffectSystem - - e = SymbolicContinuousCallbacks(eqs => affect) - @test e isa Vector{SymbolicContinuousCallback} - @test isequal(equations(e[]), eqs) - @test e[].affect isa AffectSystem - - e = SymbolicContinuousCallbacks([eqs[] => affect]) - @test e isa Vector{SymbolicContinuousCallback} - @test isequal(equations(e[]), eqs) - @test e[].affect isa AffectSystem - - e = SymbolicContinuousCallbacks([eqs => affect]) - @test e isa Vector{SymbolicContinuousCallback} - @test isequal(equations(e[]), eqs) - @test e[].affect isa AffectSystem -end - -@testset "ImperativeAffect constructors" begin - fmfa(o, x, i, c) = nothing - m = ModelingToolkit.ImperativeAffect(fmfa) - @test m isa ModelingToolkit.ImperativeAffect - @test m.f == fmfa - @test m.obs == [] - @test m.obs_syms == [] - @test m.modified == [] - @test m.mod_syms == [] - @test m.ctx === nothing - - m = ModelingToolkit.ImperativeAffect(fmfa, (;)) - @test m isa ModelingToolkit.ImperativeAffect - @test m.f == fmfa - @test m.obs == [] - @test m.obs_syms == [] - @test m.modified == [] - @test m.mod_syms == [] - @test m.ctx === nothing - - m = ModelingToolkit.ImperativeAffect(fmfa, (; x)) - @test m isa ModelingToolkit.ImperativeAffect - @test m.f == fmfa - @test isequal(m.obs, []) - @test m.obs_syms == [] - @test isequal(m.modified, [x]) - @test m.mod_syms == [:x] - @test m.ctx === nothing - - m = ModelingToolkit.ImperativeAffect(fmfa, (; y = x)) - @test m isa ModelingToolkit.ImperativeAffect - @test m.f == fmfa - @test isequal(m.obs, []) - @test m.obs_syms == [] - @test isequal(m.modified, [x]) - @test m.mod_syms == [:y] - @test m.ctx === nothing - - m = ModelingToolkit.ImperativeAffect(fmfa; observed = (; y = x)) - @test m isa ModelingToolkit.ImperativeAffect - @test m.f == fmfa - @test isequal(m.obs, [x]) - @test m.obs_syms == [:y] - @test m.modified == [] - @test m.mod_syms == [] - @test m.ctx === nothing - - m = ModelingToolkit.ImperativeAffect(fmfa; modified = (; x)) - @test m isa ModelingToolkit.ImperativeAffect - @test m.f == fmfa - @test isequal(m.obs, []) - @test m.obs_syms == [] - @test isequal(m.modified, [x]) - @test m.mod_syms == [:x] - @test m.ctx === nothing - - m = ModelingToolkit.ImperativeAffect(fmfa; modified = (; y = x)) - @test m isa ModelingToolkit.ImperativeAffect - @test m.f == fmfa - @test isequal(m.obs, []) - @test m.obs_syms == [] - @test isequal(m.modified, [x]) - @test m.mod_syms == [:y] - @test m.ctx === nothing - - m = ModelingToolkit.ImperativeAffect(fmfa, (; x), (; x)) - @test m isa ModelingToolkit.ImperativeAffect - @test m.f == fmfa - @test isequal(m.obs, [x]) - @test m.obs_syms == [:x] - @test isequal(m.modified, [x]) - @test m.mod_syms == [:x] - @test m.ctx === nothing - - m = ModelingToolkit.ImperativeAffect(fmfa, (; y = x), (; y = x)) - @test m isa ModelingToolkit.ImperativeAffect - @test m.f == fmfa - @test isequal(m.obs, [x]) - @test m.obs_syms == [:y] - @test isequal(m.modified, [x]) - @test m.mod_syms == [:y] - @test m.ctx === nothing - - m = ModelingToolkit.ImperativeAffect( - fmfa; modified = (; y = x), observed = (; y = x)) - @test m isa ModelingToolkit.ImperativeAffect - @test m.f == fmfa - @test isequal(m.obs, [x]) - @test m.obs_syms == [:y] - @test isequal(m.modified, [x]) - @test m.mod_syms == [:y] - @test m.ctx === nothing - - m = ModelingToolkit.ImperativeAffect( - fmfa; modified = (; y = x), observed = (; y = x), ctx = 3) - @test m isa ModelingToolkit.ImperativeAffect - @test m.f == fmfa - @test isequal(m.obs, [x]) - @test m.obs_syms == [:y] - @test isequal(m.modified, [x]) - @test m.mod_syms == [:y] - @test m.ctx === 3 - - m = ModelingToolkit.ImperativeAffect(fmfa, (; x), (; x), 3) - @test m isa ModelingToolkit.ImperativeAffect - @test m.f == fmfa - @test isequal(m.obs, [x]) - @test m.obs_syms == [:x] - @test isequal(m.modified, [x]) - @test m.mod_syms == [:x] - @test m.ctx === 3 -end - -@testset "Condition Compilation" begin - @named sys = ODESystem(eqs, t, continuous_events = [x ~ 1]) - @test getfield(sys, :continuous_events)[] == - SymbolicContinuousCallback(Equation[x ~ 1], nothing) - @test isequal(equations(getfield(sys, :continuous_events))[], x ~ 1) - fsys = flatten(sys) - @test isequal(equations(getfield(fsys, :continuous_events))[], x ~ 1) - - @named sys2 = ODESystem([D(x) ~ 1], t, continuous_events = [x ~ 2], systems = [sys]) - @test getfield(sys2, :continuous_events)[] == - SymbolicContinuousCallback(Equation[x ~ 2], nothing) - @test all(ModelingToolkit.continuous_events(sys2) .== [ - SymbolicContinuousCallback(Equation[x ~ 2], nothing), - SymbolicContinuousCallback(Equation[sys.x ~ 1], nothing) - ]) - - @test isequal(equations(getfield(sys2, :continuous_events))[1], x ~ 2) - @test length(ModelingToolkit.continuous_events(sys2)) == 2 - @test isequal(equations(ModelingToolkit.continuous_events(sys2)[1])[], x ~ 2) - @test isequal(equations(ModelingToolkit.continuous_events(sys2)[2])[], sys.x ~ 1) - - sys = complete(sys) - sys_nosplit = complete(sys; split = false) - sys2 = complete(sys2) - - # Test proper rootfinding - prob = ODEProblem(sys, Pair[], (0.0, 2.0)) - p0 = 0 - t0 = 0 - @test get_callback(prob) isa ModelingToolkit.DiffEqCallbacks.ContinuousCallback - cb = ModelingToolkit.generate_continuous_callbacks(sys) - cond = cb.condition - out = [0.0] - cond.f_iip(out, [0], p0, t0) - @test out[] ≈ -1 # signature is u,p,t - cond.f_iip(out, [1], p0, t0) - @test out[] ≈ 0 # signature is u,p,t - cond.f_iip(out, [2], p0, t0) - @test out[] ≈ 1 # signature is u,p,t - - prob = ODEProblem(sys, Pair[], (0.0, 2.0)) - prob_nosplit = ODEProblem(sys_nosplit, Pair[], (0.0, 2.0)) - sol = solve(prob, Tsit5()) - sol_nosplit = solve(prob_nosplit, Tsit5()) - @test minimum(t -> abs(t - 1), sol.t) < 1e-10 # test that the solver stepped at the root - @test minimum(t -> abs(t - 1), sol_nosplit.t) < 1e-10 # test that the solver stepped at the root - - # Test user-provided callback is respected - test_callback = DiscreteCallback(x -> x, x -> x) - prob = ODEProblem(sys, Pair[], (0.0, 2.0), callback = test_callback) - prob_nosplit = ODEProblem(sys_nosplit, Pair[], (0.0, 2.0), callback = test_callback) - cbs = get_callback(prob) - cbs_nosplit = get_callback(prob_nosplit) - @test cbs isa CallbackSet - @test cbs.discrete_callbacks[1] == test_callback - @test cbs_nosplit isa CallbackSet - @test cbs_nosplit.discrete_callbacks[1] == test_callback - - prob = ODEProblem(sys2, Pair[], (0.0, 3.0)) - cb = get_callback(prob) - @test cb isa ModelingToolkit.DiffEqCallbacks.VectorContinuousCallback - - cond = cb.condition - out = [0.0, 0.0] - # the root to find is 2 - cond.f_iip(out, [0, 0], p0, t0) - @test out[1] ≈ -2 # signature is u,p,t - cond.f_iip(out, [1, 0], p0, t0) - @test out[1] ≈ -1 # signature is u,p,t - cond.f_iip(out, [2, 0], p0, t0) # this should return 0 - @test out[1] ≈ 0 # signature is u,p,t - - # the root to find is 1 - out = [0.0, 0.0] - cond.f_iip(out, [0, 0], p0, t0) - @test out[2] ≈ -1 # signature is u,p,t - cond.f_iip(out, [0, 1], p0, t0) # this should return 0 - @test out[2] ≈ 0 # signature is u,p,t - cond.f_iip(out, [0, 2], p0, t0) - @test out[2] ≈ 1 # signature is u,p,t - - sol = solve(prob, Tsit5()) - @test minimum(t -> abs(t - 1), sol.t) < 1e-10 # test that the solver stepped at the first root - @test minimum(t -> abs(t - 2), sol.t) < 1e-10 # test that the solver stepped at the second root - - @named sys = ODESystem(eqs, t, continuous_events = [x ~ 1, x ~ 2]) # two root eqs using the same unknown - sys = complete(sys) - prob = ODEProblem(sys, Pair[], (0.0, 3.0)) - @test get_callback(prob) isa ModelingToolkit.DiffEqCallbacks.VectorContinuousCallback - sol = solve(prob, Tsit5()) - @test minimum(t -> abs(t - 1), sol.t) < 1e-10 # test that the solver stepped at the first root - @test minimum(t -> abs(t - 2), sol.t) < 1e-10 # test that the solver stepped at the second root -end - -@testset "Bouncing Ball" begin - ###### 1D Bounce - @variables x(t)=1 v(t)=0 - - root_eqs = [x ~ 0] - affect = [v ~ -Pre(v)] - - @named ball = ODESystem( - [D(x) ~ v - D(v) ~ -9.8], t, continuous_events = root_eqs => affect) - - @test only(continuous_events(ball)) == - SymbolicContinuousCallback(Equation[x ~ 0], Equation[v ~ -Pre(v)]) - ball = structural_simplify(ball) - - @test length(ModelingToolkit.continuous_events(ball)) == 1 - - tspan = (0.0, 5.0) - prob = ODEProblem(ball, Pair[], tspan) - sol = solve(prob, Tsit5()) - @test 0 <= minimum(sol[x]) <= 1e-10 # the ball never went through the floor but got very close - - ###### 2D bouncing ball - @variables x(t)=1 y(t)=0 vx(t)=0 vy(t)=1 - - events = [[x ~ 0] => [vx ~ -Pre(vx)] - [y ~ -1.5, y ~ 1.5] => [vy ~ -Pre(vy)]] - - @named ball = ODESystem( - [D(x) ~ vx - D(y) ~ vy - D(vx) ~ -9.8 - D(vy) ~ -0.01vy], t; continuous_events = events) - - _ball = ball - ball = structural_simplify(_ball) - ball_nosplit = structural_simplify(_ball; split = false) - - tspan = (0.0, 5.0) - prob = ODEProblem(ball, Pair[], tspan) - prob_nosplit = ODEProblem(ball_nosplit, Pair[], tspan) - - cb = get_callback(prob) - @test cb isa ModelingToolkit.DiffEqCallbacks.VectorContinuousCallback - @test getfield(ball, :continuous_events)[1] == - SymbolicContinuousCallback(Equation[x ~ 0], Equation[vx ~ -Pre(vx)]) - @test getfield(ball, :continuous_events)[2] == - SymbolicContinuousCallback(Equation[y ~ -1.5, y ~ 1.5], Equation[vy ~ -Pre(vy)]) - cond = cb.condition - out = [0.0, 0.0, 0.0] - p0 = 0. - t0 = 0. - cond.f_iip(out, [0, 0, 0, 0], p0, t0) - @test out ≈ [0, 1.5, -1.5] - - sol = solve(prob, Tsit5()) - sol_nosplit = solve(prob_nosplit, Tsit5()) - @test 0 <= minimum(sol[x]) <= 1e-10 # the ball never went through the floor but got very close - @test minimum(sol[y]) ≈ -1.5 # check wall conditions - @test maximum(sol[y]) ≈ 1.5 # check wall conditions - @test 0 <= minimum(sol_nosplit[x]) <= 1e-10 # the ball never went through the floor but got very close - @test minimum(sol_nosplit[y]) ≈ -1.5 # check wall conditions - @test maximum(sol_nosplit[y]) ≈ 1.5 # check wall conditions - - ## Test multi-variable affect - # in this test, there are two variables affected by a single event. - events = [[x ~ 0] => [vx ~ -Pre(vx), vy ~ -Pre(vy)]] - - @named ball = ODESystem([D(x) ~ vx - D(y) ~ vy - D(vx) ~ -1 - D(vy) ~ 0], t; continuous_events = events) - - ball_nosplit = structural_simplify(ball) - ball = structural_simplify(ball) - - tspan = (0.0, 5.0) - prob = ODEProblem(ball, Pair[], tspan) - prob_nosplit = ODEProblem(ball_nosplit, Pair[], tspan) - sol = solve(prob, Tsit5()) - sol_nosplit = solve(prob_nosplit, Tsit5()) - @test 0 <= minimum(sol[x]) <= 1e-10 # the ball never went through the floor but got very close - @test -minimum(sol[y]) ≈ maximum(sol[y]) ≈ sqrt(2) # the ball will never go further than √2 in either direction (gravity was changed to 1 to get this particular number) - @test 0 <= minimum(sol_nosplit[x]) <= 1e-10 # the ball never went through the floor but got very close - @test -minimum(sol_nosplit[y]) ≈ maximum(sol_nosplit[y]) ≈ sqrt(2) # the ball will never go further than √2 in either direction (gravity was changed to 1 to get this particular number) -end - -# issue https://github.com/SciML/ModelingToolkit.jl/issues/1386 -# tests that it works for ODAESystem -@testset "ODAESystem" begin - @variables vs(t) v(t) vmeasured(t) - eq = [vs ~ sin(2pi * t) - D(v) ~ vs - v - D(vmeasured) ~ 0.0] - ev = [sin(20pi * t) ~ 0.0] => [vmeasured ~ Pre(v)] - @named sys = ODESystem(eq, t, continuous_events = ev) - sys = structural_simplify(sys) - prob = ODEProblem(sys, zeros(2), (0.0, 5.1)) - sol = solve(prob, Tsit5()) - @test all(minimum((0:0.1:5) .- sol.t', dims = 2) .< 0.0001) # test that the solver stepped every 0.1s as dictated by event - @test sol([0.25])[vmeasured][] == sol([0.23])[vmeasured][] # test the hold property -end - -## https://github.com/SciML/ModelingToolkit.jl/issues/1528 -@testset "Handle Empty Events" begin - Dₜ = D - - @parameters u(t) [input = true] # Indicate that this is a controlled input - @parameters y(t) [output = true] # Indicate that this is a measured output - - function Mass(; name, m = 1.0, p = 0, v = 0) - ps = @parameters m = m - sts = @variables pos(t)=p vel(t)=v - eqs = Dₜ(pos) ~ vel - ODESystem(eqs, t, [pos, vel], ps; name) - end - function Spring(; name, k = 1e4) - ps = @parameters k = k - @variables x(t) = 0 # Spring deflection - ODESystem(Equation[], t, [x], ps; name) - end - function Damper(; name, c = 10) - ps = @parameters c = c - @variables vel(t) = 0 - ODESystem(Equation[], t, [vel], ps; name) - end - function SpringDamper(; name, k = false, c = false) - spring = Spring(; name = :spring, k) - damper = Damper(; name = :damper, c) - compose(ODESystem(Equation[], t; name), - spring, damper) - end - connect_sd(sd, m1, m2) = [ - sd.spring.x ~ m1.pos - m2.pos, sd.damper.vel ~ m1.vel - m2.vel] - sd_force(sd) = -sd.spring.k * sd.spring.x - sd.damper.c * sd.damper.vel - @named mass1 = Mass(; m = 1) - @named mass2 = Mass(; m = 1) - @named sd = SpringDamper(; k = 1000, c = 10) - function Model(u, d = 0) - eqs = [connect_sd(sd, mass1, mass2) - Dₜ(mass1.vel) ~ (sd_force(sd) + u) / mass1.m - Dₜ(mass2.vel) ~ (-sd_force(sd) + d) / mass2.m] - @named _model = ODESystem(eqs, t; observed = [y ~ mass2.pos]) - @named model = compose(_model, mass1, mass2, sd) - end - model = Model(sin(30t)) - sys = structural_simplify(model) - @test isempty(ModelingToolkit.continuous_events(sys)) -end - -@testset "ODESystem Discrete Callbacks" begin - function testsol(osys, u0, p, tspan; tstops = Float64[], paramtotest = nothing, - kwargs...) - oprob = ODEProblem(complete(osys), u0, tspan, p; kwargs...) - sol = solve(oprob, Tsit5(); tstops = tstops, abstol = 1e-10, reltol = 1e-10) - @test isapprox(sol(1.0000000001)[1] - sol(0.999999999)[1], 1.0; rtol = 1e-6) - paramtotest === nothing || (@test sol.ps[paramtotest] == 1.0) - @test isapprox(sol(4.0)[1], 2 * exp(-2.0)) - sol - end - - @parameters k t1 t2 - @variables A(t) B(t) - - cond1 = (t == t1) - affect1 = [A ~ Pre(A) + 1] - cb1 = cond1 => affect1 - cond2 = (t == t2) - affect2 = [k ~ 1.0] - cb2 = cond2 => affect2 - - ∂ₜ = D - eqs = [∂ₜ(A) ~ -k * A] - @named osys = ODESystem(eqs, t, [A], [k, t1, t2], discrete_events = [cb1, cb2]) - u0 = [A => 1.0] - p = [k => 0.0, t1 => 1.0, t2 => 2.0] - tspan = (0.0, 4.0) - testsol(osys, u0, p, tspan; tstops = [1.0, 2.0], paramtotest = k) - - cond1a = (t == t1) - affect1a = [A ~ Pre(A) + 1, B ~ A] - cb1a = cond1a => affect1a - @named osys1 = ODESystem(eqs, t, [A, B], [k, t1, t2], discrete_events = [cb1a, cb2]) - u0′ = [A => 1.0, B => 0.0] - sol = testsol( - osys1, u0′, p, tspan; tstops = [1.0, 2.0], check_length = false, paramtotest = k) - @test sol(1.0000001, idxs = B) == 2.0 - - # same as above - but with set-time event syntax - cb1‵ = [1.0] => affect1 # needs to be a Vector for the event to happen only once - cb2‵ = [2.0] => affect2 - @named osys‵ = ODESystem(eqs, t, [A], [k], discrete_events = [cb1‵, cb2‵]) - testsol(osys‵, u0, p, tspan; paramtotest = k) - - # mixing discrete affects - @named osys3 = ODESystem(eqs, t, [A], [k, t1, t2], discrete_events = [cb1, cb2‵]) - testsol(osys3, u0, p, tspan; tstops = [1.0], paramtotest = k) - - # mixing with a func affect - function affect!(integrator, u, p, ctx) - integrator.ps[p.k] = 1.0 - nothing - end - cb2‵‵ = [2.0] => (affect!, [], [k], [k], nothing) - @named osys4 = ODESystem(eqs, t, [A], [k, t1], discrete_events = [cb1, cb2‵‵]) - oprob4 = ODEProblem(complete(osys4), u0, tspan, p) - testsol(osys4, u0, p, tspan; tstops = [1.0], paramtotest = k) - - # mixing with symbolic condition in the func affect - cb2‵‵‵ = (t == t2) => (affect!, [], [k], [k], nothing) - @named osys5 = ODESystem(eqs, t, [A], [k, t1, t2], discrete_events = [cb1, cb2‵‵‵]) - testsol(osys5, u0, p, tspan; tstops = [1.0, 2.0]) - @named osys6 = ODESystem(eqs, t, [A], [k, t1, t2], discrete_events = [cb2‵‵‵, cb1]) - testsol(osys6, u0, p, tspan; tstops = [1.0, 2.0], paramtotest = k) - - # mix a continuous event too - cond3 = A ~ 0.1 - affect3 = [k ~ 0.0] - cb3 = cond3 => affect3 - @named osys7 = ODESystem(eqs, t, [A], [k, t1, t2], discrete_events = [cb1, cb2‵‵‵], - continuous_events = [cb3]) - sol = testsol(osys7, u0, p, (0.0, 10.0); tstops = [1.0, 2.0]) - @test isapprox(sol(10.0)[1], 0.1; atol = 1e-10, rtol = 1e-10) -end - -@testset "SDESystem Discrete Callbacks" begin - function testsol(ssys, u0, p, tspan; tstops = Float64[], paramtotest = nothing, - kwargs...) - sprob = SDEProblem(complete(ssys), u0, tspan, p; kwargs...) - sol = solve(sprob, RI5(); tstops = tstops, abstol = 1e-10, reltol = 1e-10) - @test isapprox(sol(1.0000000001)[1] - sol(0.999999999)[1], 1.0; rtol = 1e-4) - paramtotest === nothing || (@test sol.ps[paramtotest] == 1.0) - @test isapprox(sol(4.0)[1], 2 * exp(-2.0), atol = 1e-4) - sol - end - - @parameters k t1 t2 - @variables A(t) B(t) - - cond1 = (t == t1) - affect1 = [A ~ Pre(A) + 1] - cb1 = cond1 => affect1 - cond2 = (t == t2) - affect2 = [k ~ 1.0] - cb2 = cond2 => affect2 - - ∂ₜ = D - eqs = [∂ₜ(A) ~ -k * A] - @named ssys = SDESystem(eqs, [0.0], t, [A], [k, t1, t2], - discrete_events = [cb1, cb2]) - u0 = [A => 1.0] - p = [k => 0.0, t1 => 1.0, t2 => 2.0] - tspan = (0.0, 4.0) - testsol(ssys, u0, p, tspan; tstops = [1.0, 2.0], paramtotest = k) - - cond1a = (t == t1) - affect1a = [A ~ Pre(A) + 1, B ~ A] - cb1a = cond1a => affect1a - @named ssys1 = SDESystem(eqs, [0.0], t, [A, B], [k, t1, t2], - discrete_events = [cb1a, cb2]) - u0′ = [A => 1.0, B => 0.0] - sol = testsol( - ssys1, u0′, p, tspan; tstops = [1.0, 2.0], check_length = false, paramtotest = k) - @test sol(1.0000001, idxs = 2) == 2.0 - - # same as above - but with set-time event syntax - cb1‵ = [1.0] => affect1 # needs to be a Vector for the event to happen only once - cb2‵ = [2.0] => affect2 - @named ssys‵ = SDESystem(eqs, [0.0], t, [A], [k], discrete_events = [cb1‵, cb2‵]) - testsol(ssys‵, u0, p, tspan; paramtotest = k) - - # mixing discrete affects - @named ssys3 = SDESystem(eqs, [0.0], t, [A], [k, t1, t2], - discrete_events = [cb1, cb2‵]) - testsol(ssys3, u0, p, tspan; tstops = [1.0], paramtotest = k) - - # mixing with a func affect - function affect!(integrator, u, p, ctx) - setp(integrator, p.k)(integrator, 1.0) - nothing - end - cb2‵‵ = [2.0] => (affect!, [], [k], [k], nothing) - @named ssys4 = SDESystem(eqs, [0.0], t, [A], [k, t1], - discrete_events = [cb1, cb2‵‵]) - testsol(ssys4, u0, p, tspan; tstops = [1.0], paramtotest = k) - - # mixing with symbolic condition in the func affect - cb2‵‵‵ = (t == t2) => (affect!, [], [k], [k], nothing) - @named ssys5 = SDESystem(eqs, [0.0], t, [A], [k, t1, t2], - discrete_events = [cb1, cb2‵‵‵]) - testsol(ssys5, u0, p, tspan; tstops = [1.0, 2.0], paramtotest = k) - @named ssys6 = SDESystem(eqs, [0.0], t, [A], [k, t1, t2], - discrete_events = [cb2‵‵‵, cb1]) - testsol(ssys6, u0, p, tspan; tstops = [1.0, 2.0], paramtotest = k) - - # mix a continuous event too - cond3 = A ~ 0.1 - affect3 = [k ~ 0.0] - cb3 = cond3 => affect3 - @named ssys7 = SDESystem(eqs, [0.0], t, [A], [k, t1, t2], - discrete_events = [cb1, cb2‵‵‵], - continuous_events = [cb3]) - sol = testsol(ssys7, u0, p, (0.0, 10.0); tstops = [1.0, 2.0]) - @test isapprox(sol(10.0)[1], 0.1; atol = 1e-10, rtol = 1e-10) -end - -@testset "JumpSystem Discrete Callbacks" begin - function testsol(jsys, u0, p, tspan; tstops = Float64[], paramtotest = nothing, - N = 40000, kwargs...) - jsys = complete(jsys) - dprob = DiscreteProblem(jsys, u0, tspan, p) - jprob = JumpProblem(jsys, dprob, Direct(); kwargs...) - sol = solve(jprob, SSAStepper(); tstops = tstops) - @show sol - @test (sol(1.000000000001)[1] - sol(0.99999999999)[1]) == 1 - paramtotest === nothing || (@test sol.ps[paramtotest] == 1.0) - @test sol(40.0)[1] == 0 - sol - end - - @parameters k t1 t2 - @variables A(t) B(t) - - cond1 = (t == t1) - affect1 = [A ~ A + 1] - cb1 = cond1 => affect1 - cond2 = (t == t2) - affect2 = [k ~ 1.0] - cb2 = cond2 => affect2 - - eqs = [MassActionJump(k, [A => 1], [A => -1])] - @named jsys = JumpSystem(eqs, t, [A], [k, t1, t2], discrete_events = [cb1, cb2]) - u0 = [A => 1] - p = [k => 0.0, t1 => 1.0, t2 => 2.0] - tspan = (0.0, 40.0) - testsol(jsys, u0, p, tspan; tstops = [1.0, 2.0], rng, paramtotest = k) - - cond1a = (t == t1) - affect1a = [A ~ Pre(A) + 1, B ~ A] - cb1a = cond1a => affect1a - @named jsys1 = JumpSystem(eqs, t, [A, B], [k, t1, t2], discrete_events = [cb1a, cb2]) - u0′ = [A => 1, B => 0] - sol = testsol(jsys1, u0′, p, tspan; tstops = [1.0, 2.0], - check_length = false, rng, paramtotest = k) - @test sol(1.000000001, idxs = B) == 2 - - # same as above - but with set-time event syntax - cb1‵ = [1.0] => affect1 # needs to be a Vector for the event to happen only once - cb2‵ = [2.0] => affect2 - @named jsys‵ = JumpSystem(eqs, t, [A], [k], discrete_events = [cb1‵, cb2‵]) - testsol(jsys‵, u0, [p[1]], tspan; rng, paramtotest = k) - - # mixing discrete affects - @named jsys3 = JumpSystem(eqs, t, [A], [k, t1, t2], discrete_events = [cb1, cb2‵]) - testsol(jsys3, u0, p, tspan; tstops = [1.0], rng, paramtotest = k) - - # mixing with a func affect - function affect!(integrator, u, p, ctx) - integrator.ps[p.k] = 1.0 - reset_aggregated_jumps!(integrator) - nothing - end - cb2‵‵ = [2.0] => (affect!, [], [k], [k], nothing) - @named jsys4 = JumpSystem(eqs, t, [A], [k, t1], discrete_events = [cb1, cb2‵‵]) - testsol(jsys4, u0, p, tspan; tstops = [1.0], rng, paramtotest = k) - - # mixing with symbolic condition in the func affect - cb2‵‵‵ = (t == t2) => (affect!, [], [k], [k], nothing) - @named jsys5 = JumpSystem(eqs, t, [A], [k, t1, t2], discrete_events = [cb1, cb2‵‵‵]) - testsol(jsys5, u0, p, tspan; tstops = [1.0, 2.0], rng, paramtotest = k) - @named jsys6 = JumpSystem(eqs, t, [A], [k, t1, t2], discrete_events = [cb2‵‵‵, cb1]) - testsol(jsys6, u0, p, tspan; tstops = [1.0, 2.0], rng, paramtotest = k) -end - -@testset "Namespacing" begin - function oscillator_ce(k = 1.0; name) - sts = @variables x(t)=1.0 v(t)=0.0 F(t) - ps = @parameters k=k Θ=0.5 - eqs = [D(x) ~ v, D(v) ~ -k * x + F] - ev = [x ~ Θ] => [x ~ 1.0, v ~ 0.0] - ODESystem(eqs, t, sts, ps, continuous_events = [ev]; name) - end - - @named oscce = oscillator_ce() - eqs = [oscce.F ~ 0] - @named eqs_sys = ODESystem(eqs, t) - @named oneosc_ce = compose(eqs_sys, oscce) - oneosc_ce_simpl = structural_simplify(oneosc_ce) - - prob = ODEProblem(oneosc_ce_simpl, [], (0.0, 2.0), []) - sol = solve(prob, Tsit5(), saveat = 0.1) - - @test typeof(oneosc_ce_simpl) == ODESystem - @test sol[1, 6] < 1.0 # test whether x(t) decreases over time - @test sol[1, 18] > 0.5 # test whether event happened -end +#@testset "SymbolicContinuousCallback constructors" begin +# e = SymbolicContinuousCallback(eqs[]) +# @test e isa SymbolicContinuousCallback +# @test isequal(equations(e), eqs) +# @test e.affect == nothing +# @test e.affect_neg == nothing +# @test e.rootfind == SciMLBase.LeftRootFind +# +# e = SymbolicContinuousCallback(eqs) +# @test e isa SymbolicContinuousCallback +# @test isequal(equations(e), eqs) +# @test e.affect == nothing +# @test e.affect_neg == nothing +# @test e.rootfind == SciMLBase.LeftRootFind +# +# e = SymbolicContinuousCallback(eqs, nothing) +# @test e isa SymbolicContinuousCallback +# @test isequal(equations(e), eqs) +# @test e.affect == nothing +# @test e.affect_neg == nothing +# @test e.rootfind == SciMLBase.LeftRootFind +# +# e = SymbolicContinuousCallback(eqs[], nothing) +# @test e isa SymbolicContinuousCallback +# @test isequal(equations(e), eqs) +# @test e.affect == nothing +# @test e.affect_neg == nothing +# @test e.rootfind == SciMLBase.LeftRootFind +# +# e = SymbolicContinuousCallback(eqs => nothing) +# @test e isa SymbolicContinuousCallback +# @test isequal(equations(e), eqs) +# @test e.affect == nothing +# @test e.affect_neg == nothing +# @test e.rootfind == SciMLBase.LeftRootFind +# +# e = SymbolicContinuousCallback(eqs[] => nothing) +# @test e isa SymbolicContinuousCallback +# @test isequal(equations(e), eqs) +# @test e.affect == nothing +# @test e.affect_neg == nothing +# @test e.rootfind == SciMLBase.LeftRootFind +# +# ## With affect +# e = SymbolicContinuousCallback(eqs[], affect) +# @test e isa SymbolicContinuousCallback +# @test isequal(equations(e), eqs) +# @test observed(system(affects(e))) == affect +# @test observed(system(affect_negs(e))) == affect +# @test e.rootfind == SciMLBase.LeftRootFind +# +# # with only positive edge affect +# e = SymbolicContinuousCallback(eqs[], affect, affect_neg = nothing) +# @test e isa SymbolicContinuousCallback +# @test isequal(equations(e), eqs) +# @test observed(system(affects(e))) == affect +# @test isnothing(e.affect_neg) +# @test e.rootfind == SciMLBase.LeftRootFind +# +# # with explicit edge affects +# e = SymbolicContinuousCallback(eqs[], affect, affect_neg = affect_neg) +# @test e isa SymbolicContinuousCallback +# @test isequal(equations(e), eqs) +# @test observed(system(affects(e))) == affect +# @test observed(system(affect_negs(e))) == affect_neg +# @test e.rootfind == SciMLBase.LeftRootFind +# +# # with different root finding ops +# e = SymbolicContinuousCallback( +# eqs[], affect, affect_neg = affect_neg, rootfind = SciMLBase.LeftRootFind) +# @test e isa SymbolicContinuousCallback +# @test isequal(equations(e), eqs) +# @test e.rootfind == SciMLBase.LeftRootFind +# +# # test plural constructor +# e = SymbolicContinuousCallbacks(eqs[]) +# @test e isa Vector{SymbolicContinuousCallback} +# @test isequal(equations(e[]), eqs) +# @test e[].affect == nothing +# +# e = SymbolicContinuousCallbacks(eqs) +# @test e isa Vector{SymbolicContinuousCallback} +# @test isequal(equations(e[]), eqs) +# @test e[].affect == nothing +# +# e = SymbolicContinuousCallbacks(eqs[] => affect) +# @test e isa Vector{SymbolicContinuousCallback} +# @test isequal(equations(e[]), eqs) +# @test e[].affect isa AffectSystem +# +# e = SymbolicContinuousCallbacks(eqs => affect) +# @test e isa Vector{SymbolicContinuousCallback} +# @test isequal(equations(e[]), eqs) +# @test e[].affect isa AffectSystem +# +# e = SymbolicContinuousCallbacks([eqs[] => affect]) +# @test e isa Vector{SymbolicContinuousCallback} +# @test isequal(equations(e[]), eqs) +# @test e[].affect isa AffectSystem +# +# e = SymbolicContinuousCallbacks([eqs => affect]) +# @test e isa Vector{SymbolicContinuousCallback} +# @test isequal(equations(e[]), eqs) +# @test e[].affect isa AffectSystem +#end +# +#@testset "ImperativeAffect constructors" begin +# fmfa(o, x, i, c) = nothing +# m = ModelingToolkit.ImperativeAffect(fmfa) +# @test m isa ModelingToolkit.ImperativeAffect +# @test m.f == fmfa +# @test m.obs == [] +# @test m.obs_syms == [] +# @test m.modified == [] +# @test m.mod_syms == [] +# @test m.ctx === nothing +# +# m = ModelingToolkit.ImperativeAffect(fmfa, (;)) +# @test m isa ModelingToolkit.ImperativeAffect +# @test m.f == fmfa +# @test m.obs == [] +# @test m.obs_syms == [] +# @test m.modified == [] +# @test m.mod_syms == [] +# @test m.ctx === nothing +# +# m = ModelingToolkit.ImperativeAffect(fmfa, (; x)) +# @test m isa ModelingToolkit.ImperativeAffect +# @test m.f == fmfa +# @test isequal(m.obs, []) +# @test m.obs_syms == [] +# @test isequal(m.modified, [x]) +# @test m.mod_syms == [:x] +# @test m.ctx === nothing +# +# m = ModelingToolkit.ImperativeAffect(fmfa, (; y = x)) +# @test m isa ModelingToolkit.ImperativeAffect +# @test m.f == fmfa +# @test isequal(m.obs, []) +# @test m.obs_syms == [] +# @test isequal(m.modified, [x]) +# @test m.mod_syms == [:y] +# @test m.ctx === nothing +# +# m = ModelingToolkit.ImperativeAffect(fmfa; observed = (; y = x)) +# @test m isa ModelingToolkit.ImperativeAffect +# @test m.f == fmfa +# @test isequal(m.obs, [x]) +# @test m.obs_syms == [:y] +# @test m.modified == [] +# @test m.mod_syms == [] +# @test m.ctx === nothing +# +# m = ModelingToolkit.ImperativeAffect(fmfa; modified = (; x)) +# @test m isa ModelingToolkit.ImperativeAffect +# @test m.f == fmfa +# @test isequal(m.obs, []) +# @test m.obs_syms == [] +# @test isequal(m.modified, [x]) +# @test m.mod_syms == [:x] +# @test m.ctx === nothing +# +# m = ModelingToolkit.ImperativeAffect(fmfa; modified = (; y = x)) +# @test m isa ModelingToolkit.ImperativeAffect +# @test m.f == fmfa +# @test isequal(m.obs, []) +# @test m.obs_syms == [] +# @test isequal(m.modified, [x]) +# @test m.mod_syms == [:y] +# @test m.ctx === nothing +# +# m = ModelingToolkit.ImperativeAffect(fmfa, (; x), (; x)) +# @test m isa ModelingToolkit.ImperativeAffect +# @test m.f == fmfa +# @test isequal(m.obs, [x]) +# @test m.obs_syms == [:x] +# @test isequal(m.modified, [x]) +# @test m.mod_syms == [:x] +# @test m.ctx === nothing +# +# m = ModelingToolkit.ImperativeAffect(fmfa, (; y = x), (; y = x)) +# @test m isa ModelingToolkit.ImperativeAffect +# @test m.f == fmfa +# @test isequal(m.obs, [x]) +# @test m.obs_syms == [:y] +# @test isequal(m.modified, [x]) +# @test m.mod_syms == [:y] +# @test m.ctx === nothing +# +# m = ModelingToolkit.ImperativeAffect( +# fmfa; modified = (; y = x), observed = (; y = x)) +# @test m isa ModelingToolkit.ImperativeAffect +# @test m.f == fmfa +# @test isequal(m.obs, [x]) +# @test m.obs_syms == [:y] +# @test isequal(m.modified, [x]) +# @test m.mod_syms == [:y] +# @test m.ctx === nothing +# +# m = ModelingToolkit.ImperativeAffect( +# fmfa; modified = (; y = x), observed = (; y = x), ctx = 3) +# @test m isa ModelingToolkit.ImperativeAffect +# @test m.f == fmfa +# @test isequal(m.obs, [x]) +# @test m.obs_syms == [:y] +# @test isequal(m.modified, [x]) +# @test m.mod_syms == [:y] +# @test m.ctx === 3 +# +# m = ModelingToolkit.ImperativeAffect(fmfa, (; x), (; x), 3) +# @test m isa ModelingToolkit.ImperativeAffect +# @test m.f == fmfa +# @test isequal(m.obs, [x]) +# @test m.obs_syms == [:x] +# @test isequal(m.modified, [x]) +# @test m.mod_syms == [:x] +# @test m.ctx === 3 +#end + +#@testset "Condition Compilation" begin +# @named sys = ODESystem(eqs, t, continuous_events = [x ~ 1]) +# @test getfield(sys, :continuous_events)[] == +# SymbolicContinuousCallback(Equation[x ~ 1], nothing) +# @test isequal(equations(getfield(sys, :continuous_events))[], x ~ 1) +# fsys = flatten(sys) +# @test isequal(equations(getfield(fsys, :continuous_events))[], x ~ 1) +# +# @named sys2 = ODESystem([D(x) ~ 1], t, continuous_events = [x ~ 2], systems = [sys]) +# @test getfield(sys2, :continuous_events)[] == +# SymbolicContinuousCallback(Equation[x ~ 2], nothing) +# @test all(ModelingToolkit.continuous_events(sys2) .== [ +# SymbolicContinuousCallback(Equation[x ~ 2], nothing), +# SymbolicContinuousCallback(Equation[sys.x ~ 1], nothing) +# ]) +# +# @test isequal(equations(getfield(sys2, :continuous_events))[1], x ~ 2) +# @test length(ModelingToolkit.continuous_events(sys2)) == 2 +# @test isequal(equations(ModelingToolkit.continuous_events(sys2)[1])[], x ~ 2) +# @test isequal(equations(ModelingToolkit.continuous_events(sys2)[2])[], sys.x ~ 1) +# +# sys = complete(sys) +# sys_nosplit = complete(sys; split = false) +# sys2 = complete(sys2) +# +# # Test proper rootfinding +# prob = ODEProblem(sys, Pair[], (0.0, 2.0)) +# p0 = 0 +# t0 = 0 +# @test get_callback(prob) isa ModelingToolkit.DiffEqCallbacks.ContinuousCallback +# cb = ModelingToolkit.generate_continuous_callbacks(sys) +# cond = cb.condition +# out = [0.0] +# cond.f_iip(out, [0], p0, t0) +# @test out[] ≈ -1 # signature is u,p,t +# cond.f_iip(out, [1], p0, t0) +# @test out[] ≈ 0 # signature is u,p,t +# cond.f_iip(out, [2], p0, t0) +# @test out[] ≈ 1 # signature is u,p,t +# +# prob = ODEProblem(sys, Pair[], (0.0, 2.0)) +# prob_nosplit = ODEProblem(sys_nosplit, Pair[], (0.0, 2.0)) +# sol = solve(prob, Tsit5()) +# sol_nosplit = solve(prob_nosplit, Tsit5()) +# @test minimum(t -> abs(t - 1), sol.t) < 1e-10 # test that the solver stepped at the root +# @test minimum(t -> abs(t - 1), sol_nosplit.t) < 1e-10 # test that the solver stepped at the root +# +# # Test user-provided callback is respected +# test_callback = DiscreteCallback(x -> x, x -> x) +# prob = ODEProblem(sys, Pair[], (0.0, 2.0), callback = test_callback) +# prob_nosplit = ODEProblem(sys_nosplit, Pair[], (0.0, 2.0), callback = test_callback) +# cbs = get_callback(prob) +# cbs_nosplit = get_callback(prob_nosplit) +# @test cbs isa CallbackSet +# @test cbs.discrete_callbacks[1] == test_callback +# @test cbs_nosplit isa CallbackSet +# @test cbs_nosplit.discrete_callbacks[1] == test_callback +# +# prob = ODEProblem(sys2, Pair[], (0.0, 3.0)) +# cb = get_callback(prob) +# @test cb isa ModelingToolkit.DiffEqCallbacks.VectorContinuousCallback +# +# cond = cb.condition +# out = [0.0, 0.0] +# # the root to find is 2 +# cond.f_iip(out, [0, 0], p0, t0) +# @test out[1] ≈ -2 # signature is u,p,t +# cond.f_iip(out, [1, 0], p0, t0) +# @test out[1] ≈ -1 # signature is u,p,t +# cond.f_iip(out, [2, 0], p0, t0) # this should return 0 +# @test out[1] ≈ 0 # signature is u,p,t +# +# # the root to find is 1 +# out = [0.0, 0.0] +# cond.f_iip(out, [0, 0], p0, t0) +# @test out[2] ≈ -1 # signature is u,p,t +# cond.f_iip(out, [0, 1], p0, t0) # this should return 0 +# @test out[2] ≈ 0 # signature is u,p,t +# cond.f_iip(out, [0, 2], p0, t0) +# @test out[2] ≈ 1 # signature is u,p,t +# +# sol = solve(prob, Tsit5()) +# @test minimum(t -> abs(t - 1), sol.t) < 1e-10 # test that the solver stepped at the first root +# @test minimum(t -> abs(t - 2), sol.t) < 1e-10 # test that the solver stepped at the second root +# +# @named sys = ODESystem(eqs, t, continuous_events = [x ~ 1, x ~ 2]) # two root eqs using the same unknown +# sys = complete(sys) +# prob = ODEProblem(sys, Pair[], (0.0, 3.0)) +# @test get_callback(prob) isa ModelingToolkit.DiffEqCallbacks.VectorContinuousCallback +# sol = solve(prob, Tsit5()) +# @test minimum(t -> abs(t - 1), sol.t) < 1e-10 # test that the solver stepped at the first root +# @test minimum(t -> abs(t - 2), sol.t) < 1e-10 # test that the solver stepped at the second root +#end + +#@testset "Bouncing Ball" begin +# ###### 1D Bounce +# @variables x(t)=1 v(t)=0 +# +# root_eqs = [x ~ 0] +# affect = [v ~ -Pre(v)] +# +# @named ball = ODESystem( +# [D(x) ~ v +# D(v) ~ -9.8], t, continuous_events = root_eqs => affect) +# +# @test only(continuous_events(ball)) == +# SymbolicContinuousCallback(Equation[x ~ 0], Equation[v ~ -Pre(v)]) +# ball = structural_simplify(ball) +# +# @test length(ModelingToolkit.continuous_events(ball)) == 1 +# +# tspan = (0.0, 5.0) +# prob = ODEProblem(ball, Pair[], tspan) +# sol = solve(prob, Tsit5()) +# @test 0 <= minimum(sol[x]) <= 1e-10 # the ball never went through the floor but got very close +# +# ###### 2D bouncing ball +# @variables x(t)=1 y(t)=0 vx(t)=0 vy(t)=1 +# +# events = [[x ~ 0] => [vx ~ -Pre(vx)] +# [y ~ -1.5, y ~ 1.5] => [vy ~ -Pre(vy)]] +# +# @named ball = ODESystem( +# [D(x) ~ vx +# D(y) ~ vy +# D(vx) ~ -9.8 +# D(vy) ~ -0.01vy], t; continuous_events = events) +# +# _ball = ball +# ball = structural_simplify(_ball) +# ball_nosplit = structural_simplify(_ball; split = false) +# +# tspan = (0.0, 5.0) +# prob = ODEProblem(ball, Pair[], tspan) +# prob_nosplit = ODEProblem(ball_nosplit, Pair[], tspan) +# +# cb = get_callback(prob) +# @test cb isa ModelingToolkit.DiffEqCallbacks.VectorContinuousCallback +# @test getfield(ball, :continuous_events)[1] == +# SymbolicContinuousCallback(Equation[x ~ 0], Equation[vx ~ -Pre(vx)]) +# @test getfield(ball, :continuous_events)[2] == +# SymbolicContinuousCallback(Equation[y ~ -1.5, y ~ 1.5], Equation[vy ~ -Pre(vy)]) +# cond = cb.condition +# out = [0.0, 0.0, 0.0] +# p0 = 0. +# t0 = 0. +# cond.f_iip(out, [0, 0, 0, 0], p0, t0) +# @test out ≈ [0, 1.5, -1.5] +# +# sol = solve(prob, Tsit5()) +# sol_nosplit = solve(prob_nosplit, Tsit5()) +# @test 0 <= minimum(sol[x]) <= 1e-10 # the ball never went through the floor but got very close +# @test minimum(sol[y]) ≈ -1.5 # check wall conditions +# @test maximum(sol[y]) ≈ 1.5 # check wall conditions +# @test 0 <= minimum(sol_nosplit[x]) <= 1e-10 # the ball never went through the floor but got very close +# @test minimum(sol_nosplit[y]) ≈ -1.5 # check wall conditions +# @test maximum(sol_nosplit[y]) ≈ 1.5 # check wall conditions +# +# ## Test multi-variable affect +# # in this test, there are two variables affected by a single event. +# events = [[x ~ 0] => [vx ~ -Pre(vx), vy ~ -Pre(vy)]] +# +# @named ball = ODESystem([D(x) ~ vx +# D(y) ~ vy +# D(vx) ~ -1 +# D(vy) ~ 0], t; continuous_events = events) +# +# ball_nosplit = structural_simplify(ball) +# ball = structural_simplify(ball) +# +# tspan = (0.0, 5.0) +# prob = ODEProblem(ball, Pair[], tspan) +# prob_nosplit = ODEProblem(ball_nosplit, Pair[], tspan) +# sol = solve(prob, Tsit5()) +# sol_nosplit = solve(prob_nosplit, Tsit5()) +# @test 0 <= minimum(sol[x]) <= 1e-10 # the ball never went through the floor but got very close +# @test -minimum(sol[y]) ≈ maximum(sol[y]) ≈ sqrt(2) # the ball will never go further than √2 in either direction (gravity was changed to 1 to get this particular number) +# @test 0 <= minimum(sol_nosplit[x]) <= 1e-10 # the ball never went through the floor but got very close +# @test -minimum(sol_nosplit[y]) ≈ maximum(sol_nosplit[y]) ≈ sqrt(2) # the ball will never go further than √2 in either direction (gravity was changed to 1 to get this particular number) +#end +# +## issue https://github.com/SciML/ModelingToolkit.jl/issues/1386 +## tests that it works for ODAESystem +#@testset "ODAESystem" begin +# @variables vs(t) v(t) vmeasured(t) +# eq = [vs ~ sin(2pi * t) +# D(v) ~ vs - v +# D(vmeasured) ~ 0.0] +# ev = [sin(20pi * t) ~ 0.0] => [vmeasured ~ Pre(v)] +# @named sys = ODESystem(eq, t, continuous_events = ev) +# sys = structural_simplify(sys) +# prob = ODEProblem(sys, zeros(2), (0.0, 5.1)) +# sol = solve(prob, Tsit5()) +# @test all(minimum((0:0.1:5) .- sol.t', dims = 2) .< 0.0001) # test that the solver stepped every 0.1s as dictated by event +# @test sol([0.25])[vmeasured][] == sol([0.23])[vmeasured][] # test the hold property +#end +# +### https://github.com/SciML/ModelingToolkit.jl/issues/1528 +#@testset "Handle Empty Events" begin +# Dₜ = D +# +# @parameters u(t) [input = true] # Indicate that this is a controlled input +# @parameters y(t) [output = true] # Indicate that this is a measured output +# +# function Mass(; name, m = 1.0, p = 0, v = 0) +# ps = @parameters m = m +# sts = @variables pos(t)=p vel(t)=v +# eqs = Dₜ(pos) ~ vel +# ODESystem(eqs, t, [pos, vel], ps; name) +# end +# function Spring(; name, k = 1e4) +# ps = @parameters k = k +# @variables x(t) = 0 # Spring deflection +# ODESystem(Equation[], t, [x], ps; name) +# end +# function Damper(; name, c = 10) +# ps = @parameters c = c +# @variables vel(t) = 0 +# ODESystem(Equation[], t, [vel], ps; name) +# end +# function SpringDamper(; name, k = false, c = false) +# spring = Spring(; name = :spring, k) +# damper = Damper(; name = :damper, c) +# compose(ODESystem(Equation[], t; name), +# spring, damper) +# end +# connect_sd(sd, m1, m2) = [ +# sd.spring.x ~ m1.pos - m2.pos, sd.damper.vel ~ m1.vel - m2.vel] +# sd_force(sd) = -sd.spring.k * sd.spring.x - sd.damper.c * sd.damper.vel +# @named mass1 = Mass(; m = 1) +# @named mass2 = Mass(; m = 1) +# @named sd = SpringDamper(; k = 1000, c = 10) +# function Model(u, d = 0) +# eqs = [connect_sd(sd, mass1, mass2) +# Dₜ(mass1.vel) ~ (sd_force(sd) + u) / mass1.m +# Dₜ(mass2.vel) ~ (-sd_force(sd) + d) / mass2.m] +# @named _model = ODESystem(eqs, t; observed = [y ~ mass2.pos]) +# @named model = compose(_model, mass1, mass2, sd) +# end +# model = Model(sin(30t)) +# sys = structural_simplify(model) +# @test isempty(ModelingToolkit.continuous_events(sys)) +#end +# +#@testset "ODESystem Discrete Callbacks" begin +# function testsol(osys, u0, p, tspan; tstops = Float64[], paramtotest = nothing, +# kwargs...) +# oprob = ODEProblem(complete(osys), u0, tspan, p; kwargs...) +# sol = solve(oprob, Tsit5(); tstops = tstops, abstol = 1e-10, reltol = 1e-10) +# @test isapprox(sol(1.0000000001)[1] - sol(0.999999999)[1], 1.0; rtol = 1e-6) +# paramtotest === nothing || (@test sol.ps[paramtotest] == 1.0) +# @test isapprox(sol(4.0)[1], 2 * exp(-2.0)) +# sol +# end +# +# @parameters k t1 t2 +# @variables A(t) B(t) +# +# cond1 = (t == t1) +# affect1 = [A ~ Pre(A) + 1] +# cb1 = cond1 => affect1 +# cond2 = (t == t2) +# affect2 = [k ~ 1.0] +# cb2 = cond2 => affect2 +# +# ∂ₜ = D +# eqs = [∂ₜ(A) ~ -k * A] +# @named osys = ODESystem(eqs, t, [A], [k, t1, t2], discrete_events = [cb1, cb2]) +# u0 = [A => 1.0] +# p = [k => 0.0, t1 => 1.0, t2 => 2.0] +# tspan = (0.0, 4.0) +# testsol(osys, u0, p, tspan; tstops = [1.0, 2.0], paramtotest = k) +# +# cond1a = (t == t1) +# affect1a = [A ~ Pre(A) + 1, B ~ A] +# cb1a = cond1a => affect1a +# @named osys1 = ODESystem(eqs, t, [A, B], [k, t1, t2], discrete_events = [cb1a, cb2]) +# u0′ = [A => 1.0, B => 0.0] +# sol = testsol( +# osys1, u0′, p, tspan; tstops = [1.0, 2.0], check_length = false, paramtotest = k) +# @test sol(1.0000001, idxs = B) == 2.0 +# +# # same as above - but with set-time event syntax +# cb1‵ = [1.0] => affect1 # needs to be a Vector for the event to happen only once +# cb2‵ = [2.0] => affect2 +# @named osys‵ = ODESystem(eqs, t, [A], [k], discrete_events = [cb1‵, cb2‵]) +# testsol(osys‵, u0, p, tspan; paramtotest = k) +# +# # mixing discrete affects +# @named osys3 = ODESystem(eqs, t, [A], [k, t1, t2], discrete_events = [cb1, cb2‵]) +# testsol(osys3, u0, p, tspan; tstops = [1.0], paramtotest = k) +# +# # mixing with a func affect +# function affect!(integrator, u, p, ctx) +# integrator.ps[p.k] = 1.0 +# nothing +# end +# cb2‵‵ = [2.0] => (affect!, [], [k], [k], nothing) +# @named osys4 = ODESystem(eqs, t, [A], [k, t1], discrete_events = [cb1, cb2‵‵]) +# oprob4 = ODEProblem(complete(osys4), u0, tspan, p) +# testsol(osys4, u0, p, tspan; tstops = [1.0], paramtotest = k) +# +# # mixing with symbolic condition in the func affect +# cb2‵‵‵ = (t == t2) => (affect!, [], [k], [k], nothing) +# @named osys5 = ODESystem(eqs, t, [A], [k, t1, t2], discrete_events = [cb1, cb2‵‵‵]) +# testsol(osys5, u0, p, tspan; tstops = [1.0, 2.0]) +# @named osys6 = ODESystem(eqs, t, [A], [k, t1, t2], discrete_events = [cb2‵‵‵, cb1]) +# testsol(osys6, u0, p, tspan; tstops = [1.0, 2.0], paramtotest = k) +# +# # mix a continuous event too +# cond3 = A ~ 0.1 +# affect3 = [k ~ 0.0] +# cb3 = cond3 => affect3 +# @named osys7 = ODESystem(eqs, t, [A], [k, t1, t2], discrete_events = [cb1, cb2‵‵‵], +# continuous_events = [cb3]) +# sol = testsol(osys7, u0, p, (0.0, 10.0); tstops = [1.0, 2.0]) +# @test isapprox(sol(10.0)[1], 0.1; atol = 1e-10, rtol = 1e-10) +#end +# +#@testset "SDESystem Discrete Callbacks" begin +# function testsol(ssys, u0, p, tspan; tstops = Float64[], paramtotest = nothing, +# kwargs...) +# sprob = SDEProblem(complete(ssys), u0, tspan, p; kwargs...) +# sol = solve(sprob, RI5(); tstops = tstops, abstol = 1e-10, reltol = 1e-10) +# @test isapprox(sol(1.0000000001)[1] - sol(0.999999999)[1], 1.0; rtol = 1e-4) +# paramtotest === nothing || (@test sol.ps[paramtotest] == 1.0) +# @test isapprox(sol(4.0)[1], 2 * exp(-2.0), atol = 1e-4) +# sol +# end +# +# @parameters k t1 t2 +# @variables A(t) B(t) +# +# cond1 = (t == t1) +# affect1 = [A ~ Pre(A) + 1] +# cb1 = cond1 => affect1 +# cond2 = (t == t2) +# affect2 = [k ~ 1.0] +# cb2 = cond2 => affect2 +# +# ∂ₜ = D +# eqs = [∂ₜ(A) ~ -k * A] +# @named ssys = SDESystem(eqs, [0.0], t, [A], [k, t1, t2], +# discrete_events = [cb1, cb2]) +# u0 = [A => 1.0] +# p = [k => 0.0, t1 => 1.0, t2 => 2.0] +# tspan = (0.0, 4.0) +# testsol(ssys, u0, p, tspan; tstops = [1.0, 2.0], paramtotest = k) +# +# cond1a = (t == t1) +# affect1a = [A ~ Pre(A) + 1, B ~ A] +# cb1a = cond1a => affect1a +# @named ssys1 = SDESystem(eqs, [0.0], t, [A, B], [k, t1, t2], +# discrete_events = [cb1a, cb2]) +# u0′ = [A => 1.0, B => 0.0] +# sol = testsol( +# ssys1, u0′, p, tspan; tstops = [1.0, 2.0], check_length = false, paramtotest = k) +# @test sol(1.0000001, idxs = 2) == 2.0 +# +# # same as above - but with set-time event syntax +# cb1‵ = [1.0] => affect1 # needs to be a Vector for the event to happen only once +# cb2‵ = [2.0] => affect2 +# @named ssys‵ = SDESystem(eqs, [0.0], t, [A], [k], discrete_events = [cb1‵, cb2‵]) +# testsol(ssys‵, u0, p, tspan; paramtotest = k) +# +# # mixing discrete affects +# @named ssys3 = SDESystem(eqs, [0.0], t, [A], [k, t1, t2], +# discrete_events = [cb1, cb2‵]) +# testsol(ssys3, u0, p, tspan; tstops = [1.0], paramtotest = k) +# +# # mixing with a func affect +# function affect!(integrator, u, p, ctx) +# setp(integrator, p.k)(integrator, 1.0) +# nothing +# end +# cb2‵‵ = [2.0] => (affect!, [], [k], [k], nothing) +# @named ssys4 = SDESystem(eqs, [0.0], t, [A], [k, t1], +# discrete_events = [cb1, cb2‵‵]) +# testsol(ssys4, u0, p, tspan; tstops = [1.0], paramtotest = k) +# +# # mixing with symbolic condition in the func affect +# cb2‵‵‵ = (t == t2) => (affect!, [], [k], [k], nothing) +# @named ssys5 = SDESystem(eqs, [0.0], t, [A], [k, t1, t2], +# discrete_events = [cb1, cb2‵‵‵]) +# testsol(ssys5, u0, p, tspan; tstops = [1.0, 2.0], paramtotest = k) +# @named ssys6 = SDESystem(eqs, [0.0], t, [A], [k, t1, t2], +# discrete_events = [cb2‵‵‵, cb1]) +# testsol(ssys6, u0, p, tspan; tstops = [1.0, 2.0], paramtotest = k) +# +# # mix a continuous event too +# cond3 = A ~ 0.1 +# affect3 = [k ~ 0.0] +# cb3 = cond3 => affect3 +# @named ssys7 = SDESystem(eqs, [0.0], t, [A], [k, t1, t2], +# discrete_events = [cb1, cb2‵‵‵], +# continuous_events = [cb3]) +# sol = testsol(ssys7, u0, p, (0.0, 10.0); tstops = [1.0, 2.0]) +# @test isapprox(sol(10.0)[1], 0.1; atol = 1e-10, rtol = 1e-10) +#end + +#@testset "JumpSystem Discrete Callbacks" begin +# function testsol(jsys, u0, p, tspan; tstops = Float64[], paramtotest = nothing, +# N = 40000, kwargs...) +# jsys = complete(jsys) +# dprob = DiscreteProblem(jsys, u0, tspan, p) +# jprob = JumpProblem(jsys, dprob, Direct(); kwargs...) +# sol = solve(jprob, SSAStepper(); tstops = tstops) +# @show sol +# @test (sol(1.000000000001)[1] - sol(0.99999999999)[1]) == 1 +# paramtotest === nothing || (@test sol.ps[paramtotest] == 1.0) +# @test sol(40.0)[1] == 0 +# sol +# end +# +# @parameters k t1 t2 +# @variables A(t) B(t) +# +# cond1 = (t == t1) +# affect1 = [A ~ Pre(A) + 1] +# cb1 = cond1 => affect1 +# cond2 = (t == t2) +# affect2 = [k ~ 1.0] +# cb2 = cond2 => affect2 +# +# eqs = [MassActionJump(k, [A => 1], [A => -1])] +# @named jsys = JumpSystem(eqs, t, [A], [k, t1, t2], discrete_events = [cb1, cb2]) +# u0 = [A => 1] +# p = [k => 0.0, t1 => 1.0, t2 => 2.0] +# tspan = (0.0, 40.0) +# testsol(jsys, u0, p, tspan; tstops = [1.0, 2.0], rng, paramtotest = k) +# +# cond1a = (t == t1) +# affect1a = [A ~ Pre(A) + 1, B ~ A] +# cb1a = cond1a => affect1a +# @named jsys1 = JumpSystem(eqs, t, [A, B], [k, t1, t2], discrete_events = [cb1a, cb2]) +# u0′ = [A => 1, B => 0] +# sol = testsol(jsys1, u0′, p, tspan; tstops = [1.0, 2.0], +# check_length = false, rng, paramtotest = k) +# @test sol(1.000000001, idxs = B) == 2 +# +# # same as above - but with set-time event syntax +# cb1‵ = [1.0] => affect1 # needs to be a Vector for the event to happen only once +# cb2‵ = [2.0] => affect2 +# @named jsys‵ = JumpSystem(eqs, t, [A], [k], discrete_events = [cb1‵, cb2‵]) +# testsol(jsys‵, u0, [p[1]], tspan; rng, paramtotest = k) +# +# # mixing discrete affects +# @named jsys3 = JumpSystem(eqs, t, [A], [k, t1, t2], discrete_events = [cb1, cb2‵]) +# testsol(jsys3, u0, p, tspan; tstops = [1.0], rng, paramtotest = k) +# +# # mixing with a func affect +# function affect!(integrator, u, p, ctx) +# integrator.ps[p.k] = 1.0 +# reset_aggregated_jumps!(integrator) +# nothing +# end +# cb2‵‵ = [2.0] => (affect!, [], [k], [k], nothing) +# @named jsys4 = JumpSystem(eqs, t, [A], [k, t1], discrete_events = [cb1, cb2‵‵]) +# testsol(jsys4, u0, p, tspan; tstops = [1.0], rng, paramtotest = k) +# +# # mixing with symbolic condition in the func affect +# cb2‵‵‵ = (t == t2) => (affect!, [], [k], [k], nothing) +# @named jsys5 = JumpSystem(eqs, t, [A], [k, t1, t2], discrete_events = [cb1, cb2‵‵‵]) +# testsol(jsys5, u0, p, tspan; tstops = [1.0, 2.0], rng, paramtotest = k) +# @named jsys6 = JumpSystem(eqs, t, [A], [k, t1, t2], discrete_events = [cb2‵‵‵, cb1]) +# testsol(jsys6, u0, p, tspan; tstops = [1.0, 2.0], rng, paramtotest = k) +#end +# +#@testset "Namespacing" begin +# function oscillator_ce(k = 1.0; name) +# sts = @variables x(t)=1.0 v(t)=0.0 F(t) +# ps = @parameters k=k Θ=0.5 +# eqs = [D(x) ~ v, D(v) ~ -k * x + F] +# ev = [x ~ Θ] => [x ~ 1.0, v ~ 0.0] +# ODESystem(eqs, t, sts, ps, continuous_events = [ev]; name) +# end +# +# @named oscce = oscillator_ce() +# eqs = [oscce.F ~ 0] +# @named eqs_sys = ODESystem(eqs, t) +# @named oneosc_ce = compose(eqs_sys, oscce) +# oneosc_ce_simpl = structural_simplify(oneosc_ce) +# +# prob = ODEProblem(oneosc_ce_simpl, [], (0.0, 2.0), []) +# sol = solve(prob, Tsit5(), saveat = 0.1) +# +# @test typeof(oneosc_ce_simpl) == ODESystem +# @test sol[1, 6] < 1.0 # test whether x(t) decreases over time +# @test sol[1, 18] > 0.5 # test whether event happened +#end @testset "Additional SymbolicContinuousCallback options" begin # baseline affect (pos + neg + left root find) @@ -1330,3 +1330,7 @@ end sol2 = solve(ODEProblem(sys2, [], (0.0, 1.0)), Tsit5()) @test 100.0 ∈ sol2[sys2.wd2.θ] end + +# TO teste: +# - Functional affects reinitialize correctly +# - explicit equation of t in a functional affect From a7b3cdd265ca3f23324996c304ecfd6462687768 Mon Sep 17 00:00:00 2001 From: vyudu Date: Tue, 18 Mar 2025 16:20:46 -0400 Subject: [PATCH 16/52] fix NoInit() error --- Project.toml | 3 +- src/ModelingToolkit.jl | 1 + src/systems/callbacks.jl | 104 +++++------ src/systems/imperative_affect.jl | 5 +- test/symbolic_events.jl | 296 +++++++++++++++---------------- 5 files changed, 202 insertions(+), 207 deletions(-) diff --git a/Project.toml b/Project.toml index 7d4ec0ef00..cf7896614b 100644 --- a/Project.toml +++ b/Project.toml @@ -30,6 +30,7 @@ ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210" FunctionWrappers = "069b7b12-0de2-55c6-9aab-29f3d0a68a2e" FunctionWrappersWrappers = "77dc65aa-8811-40c2-897b-53d922fa7daf" Graphs = "86223c79-3864-5bf0-83f7-82e725a168b6" +ImplicitDiscreteSolve = "3263718b-31ed-49cf-8a0f-35a466e8af96" InteractiveUtils = "b77e0a4c-d291-57a0-90e8-8db25a27a240" JuliaFormatter = "98e50ef6-434e-11e9-1051-2b60c6c9e899" JumpProcesses = "ccbc3e58-028d-4f4c-8cd5-9ae44345cda5" @@ -42,6 +43,7 @@ NaNMath = "77ba4419-2d1f-58cd-9bb1-8ffee604a2e3" NonlinearSolve = "8913a72c-1f9b-4ce2-8d82-65094dcecaec" OffsetArrays = "6fe1bfb0-de20-5000-8ca7-80f57d26f881" OrderedCollections = "bac558e1-5e72-5ebc-8fee-abe8a469f55d" +OrdinaryDiffEqCore = "bbf590c4-e513-4bbe-9b18-05decba2e5d8" PrecompileTools = "aea7be01-6a6a-4083-8856-8a6e6704d82a" RecursiveArrayTools = "731186ca-8d62-57ce-b412-fbd966d074cd" Reexport = "189a3867-3050-52da-a836-e630ba90ab69" @@ -51,7 +53,6 @@ SciMLBase = "0bca4576-84f4-4d90-8ffe-ffa030f20462" SciMLStructures = "53ae85a6-f571-4167-b2af-e1d143709226" Serialization = "9e88b42a-f829-5b0c-bbe9-9e923198166b" Setfield = "efcf1570-3423-57d1-acb7-fd33fddbac46" -SimpleImplicitDiscreteSolve = "3263718b-31ed-49cf-8a0f-35a466e8af96" SimpleNonlinearSolve = "727e6d20-b764-4bd8-a329-72de5adea6c7" SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" SpecialFunctions = "276daf66-3868-5448-9aa4-cd146d93841b" diff --git a/src/ModelingToolkit.jl b/src/ModelingToolkit.jl index 76698d3329..5d9e7dea3f 100644 --- a/src/ModelingToolkit.jl +++ b/src/ModelingToolkit.jl @@ -54,6 +54,7 @@ import Moshi using Moshi.Data: @data using NonlinearSolve import SCCNonlinearSolve +using ImplicitDiscreteSolve using Reexport using RecursiveArrayTools import Graphs: SimpleDiGraph, add_edge!, incidence_matrix diff --git a/src/systems/callbacks.jl b/src/systems/callbacks.jl index 0207f552a1..80556566a2 100644 --- a/src/systems/callbacks.jl +++ b/src/systems/callbacks.jl @@ -233,8 +233,9 @@ function make_affect(affect::Vector{Equation}; iv = nothing, algeeqs = Equation[ dvs = OrderedSet() params = OrderedSet() for eq in affect - !haspre(eq) && + if !haspre(eq) && !(symbolic_type(eq.rhs) === NotSymbolic()) @warn "Affect equation $eq has no `Pre` operator. As such it will be interpreted as an algebraic equation to be satisfied after the callback. If you intended to use the value of a variable x before the affect, use Pre(x)." + end collect_vars!(dvs, params, eq, iv; op = Pre) end for eq in algeeqs @@ -299,11 +300,11 @@ function SymbolicContinuousCallbacks(events; algeeqs::Vector{Equation} = Equatio callbacks end -function Base.show(io::IO, cb::SymbolicContinuousCallback) +function Base.show(io::IO, cb::AbstractCallback) indent = get(io, :indent, 0) iio = IOContext(io, :indent => indent + 1) - print(io, "SymbolicContinuousCallback(") - print(iio, "Equations:") + is_discrete(cb) ? print(io, "SymbolicDiscreteCallback(") : print(io, "SymbolicContinuousCallback(") + print(iio, "Conditions:") show(iio, equations(cb)) print(iio, "; ") if affects(cb) != nothing @@ -311,7 +312,7 @@ function Base.show(io::IO, cb::SymbolicContinuousCallback) show(iio, affects(cb)) print(iio, ", ") end - if affect_negs(cb) != nothing + if !is_discrete(cb) && affect_negs(cb) != nothing print(iio, "Negative-edge affect:") show(iio, affect_negs(cb)) print(iio, ", ") @@ -328,11 +329,11 @@ function Base.show(io::IO, cb::SymbolicContinuousCallback) print(iio, ")") end -function Base.show(io::IO, mime::MIME"text/plain", cb::SymbolicContinuousCallback) +function Base.show(io::IO, mime::MIME"text/plain", cb::AbstractCallback) indent = get(io, :indent, 0) iio = IOContext(io, :indent => indent + 1) - println(io, "SymbolicContinuousCallback:") - println(iio, "Equations:") + is_discrete(cb) ? println(io, "SymbolicDiscreteCallback:") : println(io, "SymbolicContinuousCallback:") + println(iio, "Conditions:") show(iio, mime, equations(cb)) print(iio, "\n") if affects(cb) != nothing @@ -340,7 +341,7 @@ function Base.show(io::IO, mime::MIME"text/plain", cb::SymbolicContinuousCallbac show(iio, mime, affects(cb)) print(iio, "\n") end - if affect_negs(cb) != nothing + if !is_discrete(cb) && affect_negs(cb) != nothing print(iio, "Negative-edge affect:\n") show(iio, mime, affect_negs(cb)) print(iio, "\n") @@ -394,8 +395,8 @@ Arguments: - algeeqs: Algebraic equations of the system that must be satisfied after the callback occurs. """ struct SymbolicDiscreteCallback <: AbstractCallback - conditions::Any - affect::Affect + conditions::Union{Real, Vector{<:Real}, Vector{Equation}} + affect::Union{Affect, Nothing} initialize::Union{Affect, Nothing} finalize::Union{Affect, Nothing} @@ -409,6 +410,9 @@ struct SymbolicDiscreteCallback <: AbstractCallback end # Default affect to nothing end +SymbolicDiscreteCallback(p::Pair, args...; kwargs...) = SymbolicDiscreteCallback(p[1], p[2]) +SymbolicDiscreteCallback(cb::SymbolicDiscreteCallback, args...; kwargs...) = cb + """ Generate discrete callbacks. """ @@ -438,29 +442,6 @@ function is_timed_condition(condition::T) where {T} end end -function Base.show(io::IO, db::SymbolicDiscreteCallback) - indent = get(io, :indent, 0) - iio = IOContext(io, :indent => indent + 1) - println(io, "SymbolicDiscreteCallback:") - println(iio, "Conditions:") - print(iio, "; ") - if affects(db) != nothing - print(iio, "Affect:") - show(iio, affects(db)) - print(iio, ", ") - end - if initialize_affects(db) != nothing - print(iio, "Initialization affect:") - show(iio, initialize_affects(db)) - print(iio, ", ") - end - if finalize_affects(db) != nothing - print(iio, "Finalization affect:") - show(iio, finalize_affects(db)) - end - print(iio, ")") -end - function vars!(vars, cb::SymbolicDiscreteCallback; op = Differential) if symbolic_type(conditions(cb)) == NotSymbolic if conditions(cb) isa AbstractArray @@ -529,7 +510,7 @@ function namespace_callback(cb::SymbolicDiscreteCallback, s)::SymbolicDiscreteCa end function Base.hash(cb::SymbolicContinuousCallback, s::UInt) - s = foldr(hash, cb.eqs, init = s) + s = foldr(hash, cb.conditions, init = s) s = hash(cb.affect, s) s = hash(cb.affect_neg, s) s = hash(cb.initialize, s) @@ -538,8 +519,8 @@ function Base.hash(cb::SymbolicContinuousCallback, s::UInt) end function Base.hash(cb::SymbolicDiscreteCallback, s::UInt) - s = hash(cb.condition, s) - s = hash(cb.affects, s) + s = foldr(hash, cb.conditions, init = s) + s = hash(cb.affect, s) s = hash(cb.initialize, s) hash(cb.finalize, s) end @@ -649,7 +630,9 @@ end """ Compile user-defined functional affect. """ -function compile_functional_affect(affect::FunctionalAffect, cb, sys, dvs, ps; kwargs...) +function compile_functional_affect(affect::FunctionalAffect, cb, sys; kwargs...) + dvs = unknowns(sys) + ps = parameters(sys) dvs_ind = Dict(reverse(en) for en in enumerate(dvs)) v_inds = map(sym -> dvs_ind[sym], unknowns(affect)) @@ -686,7 +669,18 @@ is_discrete(cb::Vector{<:AbstractCallback}) = eltype(cb) isa SymbolicDiscreteCal function generate_continuous_callbacks(sys::AbstractSystem, dvs = unknowns(sys), ps = parameters(sys; initial_parameters = true); kwargs...) cbs = continuous_events(sys) isempty(cbs) && return nothing - generate_callback(cbs, sys; kwargs...) + cb_classes = Dict{SciMLBase.RootfindOpt, Vector{SymbolicContinuousCallback}}() + for cb in cbs + _cbs = get!(() -> SymbolicContinuousCallback[], cb_classes, cb.rootfind) + push!(_cbs, cb) + end + cb_classes = sort!(OrderedDict(cb_classes)) + compiled_callbacks = [generate_callback(cb, sys; kwargs...) for (rf, cb) in cb_classes] + if length(compiled_callbacks) == 1 + return only(compiled_callbacks) + else + return CallbackSet(compiled_callbacks...) + end end function generate_discrete_callbacks(sys::AbstractSystem, dvs = unknowns(sys), ps = parameters(sys; initial_parameters = true); kwargs...) @@ -716,9 +710,9 @@ function generate_callback(cbs::Vector{SymbolicContinuousCallback}, sys; kwargs. finals = [] for cb in cbs affect = compile_affect(cb.affect, cb, sys, default = (args...) -> ()) - push!(affects, affect) - push!(affect_negs, compile_affect(cb.affect_neg, cb, sys, default = affect)) + affect_neg = (cb.affect_neg === cb.affect) ? affect : compile_affect(cb.affect_neg, cb, sys, default = (args...) -> ()) + push!(affect_negs, affect_neg) push!(inits, compile_affect(cb.initialize, cb, sys, default = nothing)) push!(finals, compile_affect(cb.finalize, cb, sys, default = nothing)) end @@ -728,8 +722,6 @@ function generate_callback(cbs::Vector{SymbolicContinuousCallback}, sys; kwargs. eq2affect = reduce(vcat, [fill(i, num_eqs[i]) for i in eachindex(affects)]) eqs = reduce(vcat, eqs) - @assert length(eq2affect) == length(eqs) - @assert maximum(eq2affect) == length(affects) affect = function (integ, idx) affects[eq2affect[idx]](integ) @@ -744,7 +736,7 @@ function generate_callback(cbs::Vector{SymbolicContinuousCallback}, sys; kwargs. return VectorContinuousCallback( trigger, affect, affect_neg, length(eqs); initialize, finalize, - rootfind = cbs[1].rootfind, initializealg = SciMLBase.NoInit) + rootfind = cbs[1].rootfind, initializealg = SciMLBase.NoInit()) end function generate_callback(cb, sys; kwargs...) @@ -762,16 +754,16 @@ function generate_callback(cb, sys; kwargs...) if is_discrete(cb) if is_timed && conditions(cb) isa AbstractVector return PresetTimeCallback(trigger, affect; initialize, - finalize, initializealg = SciMLBase.NoInit) + finalize, initializealg = SciMLBase.NoInit()) elseif is_timed return PeriodicCallback(affect, trigger; initialize, finalize) else return DiscreteCallback(trigger, affect; initialize, - finalize, initializealg = SciMLBase.NoInit) + finalize, initializealg = SciMLBase.NoInit()) end else return ContinuousCallback(trigger, affect, affect_neg; initialize, finalize, - rootfind = cb.rootfind, initializealg = SciMLBase.NoInit) + rootfind = cb.rootfind, initializealg = SciMLBase.NoInit()) end end @@ -793,27 +785,25 @@ Notes """ function compile_affect( aff::Union{Nothing, Affect}, cb::AbstractCallback, sys::AbstractSystem; default = nothing, kwargs...) + isnothing(aff) && return default + save_idxs = if !(has_index_cache(sys) && (ic = get_index_cache(sys)) !== nothing) Int[] else get(ic.callback_to_clocks, cb, Int[]) end - isnothing(aff) && return default - - ps = parameters(aff) - dvs = unknowns(aff) - dvs_to_modify = setdiff(dvs, getfield.(observed(sys), :lhs)) - if aff isa AffectSystem affsys = system(aff) aff_map = aff_to_sys(aff) sys_map = Dict([v => k for (k, v) in aff_map]) reinit = has_alg_eqs(sys) + ps_to_modify = discretes(aff) + dvs_to_modify = setdiff(unknowns(aff), getfield.(observed(sys), :lhs)) function affect!(integrator) pmap = Pair[] - for pre_p in previous_vals(aff) + for pre_p in parameters(affsys) p = only(arguments(unwrap(pre_p))) pval = isparameter(p) ? integrator.ps[p] : integrator[p] push!(pmap, pre_p => pval) @@ -825,17 +815,17 @@ function compile_affect( for u in dvs_to_modify integrator[u] = affsol[sys_map[u]] end - for p in discretes(aff) + for p in ps_to_modify integrator.ps[p] = affsol[sys_map[p]] end for idx in save_idxs - SciMLBase.save_discretes!(integ, idx) + SciMLBase.save_discretes!(integrator, idx) end sys isa JumpSystem && reset_aggregated_jumps!(integrator) end elseif aff isa FunctionalAffect || aff isa ImperativeAffect - compile_functional_affect(aff, cb, sys, dvs, ps; kwargs...) + compile_functional_affect(aff, cb, sys; kwargs...) end end diff --git a/src/systems/imperative_affect.jl b/src/systems/imperative_affect.jl index 991a16a23a..c36b250fbf 100644 --- a/src/systems/imperative_affect.jl +++ b/src/systems/imperative_affect.jl @@ -155,7 +155,7 @@ function check_assignable(sys, sym) end end -function compile_functional_affect(affect::ImperativeAffect, cb, sys, dvs, ps; kwargs...) +function compile_functional_affect(affect::ImperativeAffect, cb, sys; kwargs...) #= Implementation sketch: generate observed function (oop), should save to a component array under obs_syms @@ -179,6 +179,9 @@ function compile_functional_affect(affect::ImperativeAffect, cb, sys, dvs, ps; k return (syms_dedup, exprs_dedup) end + dvs = unknowns(sys) + ps = parameters(sys) + obs_exprs = observed(affect) if !affect.skip_checks for oexpr in obs_exprs diff --git a/test/symbolic_events.jl b/test/symbolic_events.jl index 28d6d644d0..80a15c2b6c 100644 --- a/test/symbolic_events.jl +++ b/test/symbolic_events.jl @@ -728,153 +728,153 @@ affect_neg = [x ~ 1] # @test sol[1, 18] > 0.5 # test whether event happened #end -@testset "Additional SymbolicContinuousCallback options" begin - # baseline affect (pos + neg + left root find) - @variables c1(t)=1.0 c2(t)=1.0 # c1 = cos(t), c2 = cos(3t) - eqs = [D(c1) ~ -sin(t); D(c2) ~ -3 * sin(3 * t)] - record_crossings(i, u, _, c) = push!(c, i.t => i.u[u.v]) - cr1 = [] - cr2 = [] - evt1 = ModelingToolkit.SymbolicContinuousCallback( - [c1 ~ 0], (record_crossings, [c1 => :v], [], [], cr1)) - evt2 = ModelingToolkit.SymbolicContinuousCallback( - [c2 ~ 0], (record_crossings, [c2 => :v], [], [], cr2)) - @named trigsys = ODESystem(eqs, t; continuous_events = [evt1, evt2]) - trigsys_ss = structural_simplify(trigsys) - prob = ODEProblem(trigsys_ss, [], (0.0, 2π)) - sol = solve(prob, Tsit5()) - required_crossings_c1 = [π / 2, 3 * π / 2] - required_crossings_c2 = [π / 6, π / 2, 5 * π / 6, 7 * π / 6, 3 * π / 2, 11 * π / 6] - @test maximum(abs.(first.(cr1) .- required_crossings_c1)) < 1e-4 - @test maximum(abs.(first.(cr2) .- required_crossings_c2)) < 1e-4 - @test sign.(cos.(required_crossings_c1 .- 1e-6)) == sign.(last.(cr1)) - @test sign.(cos.(3 * (required_crossings_c2 .- 1e-6))) == sign.(last.(cr2)) - - # with neg affect (pos * neg + left root find) - cr1p = [] - cr2p = [] - cr1n = [] - cr2n = [] - evt1 = ModelingToolkit.SymbolicContinuousCallback( - [c1 ~ 0], (record_crossings, [c1 => :v], [], [], cr1p); - affect_neg = (record_crossings, [c1 => :v], [], [], cr1n)) - evt2 = ModelingToolkit.SymbolicContinuousCallback( - [c2 ~ 0], (record_crossings, [c2 => :v], [], [], cr2p); - affect_neg = (record_crossings, [c2 => :v], [], [], cr2n)) - @named trigsys = ODESystem(eqs, t; continuous_events = [evt1, evt2]) - trigsys_ss = structural_simplify(trigsys) - prob = ODEProblem(trigsys_ss, [], (0.0, 2π)) - sol = solve(prob, Tsit5(); dtmax = 0.01) - c1_pc = filter((<=)(0) ∘ sin, required_crossings_c1) - c1_nc = filter((>=)(0) ∘ sin, required_crossings_c1) - c2_pc = filter(c -> -sin(3c) > 0, required_crossings_c2) - c2_nc = filter(c -> -sin(3c) < 0, required_crossings_c2) - @test maximum(abs.(c1_pc .- first.(cr1p))) < 1e-5 - @test maximum(abs.(c1_nc .- first.(cr1n))) < 1e-5 - @test maximum(abs.(c2_pc .- first.(cr2p))) < 1e-5 - @test maximum(abs.(c2_nc .- first.(cr2n))) < 1e-5 - @test sign.(cos.(c1_pc .- 1e-6)) == sign.(last.(cr1p)) - @test sign.(cos.(c1_nc .- 1e-6)) == sign.(last.(cr1n)) - @test sign.(cos.(3 * (c2_pc .- 1e-6))) == sign.(last.(cr2p)) - @test sign.(cos.(3 * (c2_nc .- 1e-6))) == sign.(last.(cr2n)) - - # with nothing neg affect (pos * neg + left root find) - cr1p = [] - cr2p = [] - evt1 = ModelingToolkit.SymbolicContinuousCallback( - [c1 ~ 0], (record_crossings, [c1 => :v], [], [], cr1p); affect_neg = nothing) - evt2 = ModelingToolkit.SymbolicContinuousCallback( - [c2 ~ 0], (record_crossings, [c2 => :v], [], [], cr2p); affect_neg = nothing) - @named trigsys = ODESystem(eqs, t; continuous_events = [evt1, evt2]) - trigsys_ss = structural_simplify(trigsys) - prob = ODEProblem(trigsys_ss, [], (0.0, 2π)) - sol = solve(prob, Tsit5(); dtmax = 0.01) - @test maximum(abs.(c1_pc .- first.(cr1p))) < 1e-5 - @test maximum(abs.(c2_pc .- first.(cr2p))) < 1e-5 - @test sign.(cos.(c1_pc .- 1e-6)) == sign.(last.(cr1p)) - @test sign.(cos.(3 * (c2_pc .- 1e-6))) == sign.(last.(cr2p)) - - #mixed - cr1p = [] - cr2p = [] - cr1n = [] - cr2n = [] - evt1 = ModelingToolkit.SymbolicContinuousCallback( - [c1 ~ 0], (record_crossings, [c1 => :v], [], [], cr1p); affect_neg = nothing) - evt2 = ModelingToolkit.SymbolicContinuousCallback( - [c2 ~ 0], (record_crossings, [c2 => :v], [], [], cr2p); - affect_neg = (record_crossings, [c2 => :v], [], [], cr2n)) - @named trigsys = ODESystem(eqs, t; continuous_events = [evt1, evt2]) - trigsys_ss = structural_simplify(trigsys) - prob = ODEProblem(trigsys_ss, [], (0.0, 2π)) - sol = solve(prob, Tsit5(); dtmax = 0.01) - c1_pc = filter((<=)(0) ∘ sin, required_crossings_c1) - c2_pc = filter(c -> -sin(3c) > 0, required_crossings_c2) - c2_nc = filter(c -> -sin(3c) < 0, required_crossings_c2) - @test maximum(abs.(c1_pc .- first.(cr1p))) < 1e-5 - @test maximum(abs.(c2_pc .- first.(cr2p))) < 1e-5 - @test maximum(abs.(c2_nc .- first.(cr2n))) < 1e-5 - @test sign.(cos.(c1_pc .- 1e-6)) == sign.(last.(cr1p)) - @test sign.(cos.(3 * (c2_pc .- 1e-6))) == sign.(last.(cr2p)) - @test sign.(cos.(3 * (c2_nc .- 1e-6))) == sign.(last.(cr2n)) - - # baseline affect w/ right rootfind (pos + neg + right root find) - @variables c1(t)=1.0 c2(t)=1.0 # c1 = cos(t), c2 = cos(3t) - cr1 = [] - cr2 = [] - evt1 = ModelingToolkit.SymbolicContinuousCallback( - [c1 ~ 0], (record_crossings, [c1 => :v], [], [], cr1); - rootfind = SciMLBase.RightRootFind) - evt2 = ModelingToolkit.SymbolicContinuousCallback( - [c2 ~ 0], (record_crossings, [c2 => :v], [], [], cr2); - rootfind = SciMLBase.RightRootFind) - @named trigsys = ODESystem(eqs, t; continuous_events = [evt1, evt2]) - trigsys_ss = structural_simplify(trigsys) - prob = ODEProblem(trigsys_ss, [], (0.0, 2π)) - sol = solve(prob, Tsit5(); dtmax = 0.01) - required_crossings_c1 = [π / 2, 3 * π / 2] - required_crossings_c2 = [π / 6, π / 2, 5 * π / 6, 7 * π / 6, 3 * π / 2, 11 * π / 6] - @test maximum(abs.(first.(cr1) .- required_crossings_c1)) < 1e-4 - @test maximum(abs.(first.(cr2) .- required_crossings_c2)) < 1e-4 - @test sign.(cos.(required_crossings_c1 .+ 1e-6)) == sign.(last.(cr1)) - @test sign.(cos.(3 * (required_crossings_c2 .+ 1e-6))) == sign.(last.(cr2)) - - # baseline affect w/ mixed rootfind (pos + neg + right root find) - cr1 = [] - cr2 = [] - evt1 = ModelingToolkit.SymbolicContinuousCallback( - [c1 ~ 0], (record_crossings, [c1 => :v], [], [], cr1); - rootfind = SciMLBase.LeftRootFind) - evt2 = ModelingToolkit.SymbolicContinuousCallback( - [c2 ~ 0], (record_crossings, [c2 => :v], [], [], cr2); - rootfind = SciMLBase.RightRootFind) - @named trigsys = ODESystem(eqs, t; continuous_events = [evt1, evt2]) - trigsys_ss = structural_simplify(trigsys) - prob = ODEProblem(trigsys_ss, [], (0.0, 2π)) - sol = solve(prob, Tsit5()) - @test maximum(abs.(first.(cr1) .- required_crossings_c1)) < 1e-4 - @test maximum(abs.(first.(cr2) .- required_crossings_c2)) < 1e-4 - @test sign.(cos.(required_crossings_c1 .- 1e-6)) == sign.(last.(cr1)) - @test sign.(cos.(3 * (required_crossings_c2 .+ 1e-6))) == sign.(last.(cr2)) - - #flip order and ensure results are okay - cr1 = [] - cr2 = [] - evt1 = ModelingToolkit.SymbolicContinuousCallback( - [c1 ~ 0], (record_crossings, [c1 => :v], [], [], cr1); - rootfind = SciMLBase.LeftRootFind) - evt2 = ModelingToolkit.SymbolicContinuousCallback( - [c2 ~ 0], (record_crossings, [c2 => :v], [], [], cr2); - rootfind = SciMLBase.RightRootFind) - @named trigsys = ODESystem(eqs, t; continuous_events = [evt2, evt1]) - trigsys_ss = structural_simplify(trigsys) - prob = ODEProblem(trigsys_ss, [], (0.0, 2π)) - sol = solve(prob, Tsit5()) - @test maximum(abs.(first.(cr1) .- required_crossings_c1)) < 1e-4 - @test maximum(abs.(first.(cr2) .- required_crossings_c2)) < 1e-4 - @test sign.(cos.(required_crossings_c1 .- 1e-6)) == sign.(last.(cr1)) - @test sign.(cos.(3 * (required_crossings_c2 .+ 1e-6))) == sign.(last.(cr2)) -end +#@testset "Additional SymbolicContinuousCallback options" begin +# # baseline affect (pos + neg + left root find) +# @variables c1(t)=1.0 c2(t)=1.0 # c1 = cos(t), c2 = cos(3t) +# eqs = [D(c1) ~ -sin(t); D(c2) ~ -3 * sin(3 * t)] +# record_crossings(i, u, _, c) = push!(c, i.t => i.u[u.v]) +# cr1 = [] +# cr2 = [] +# evt1 = ModelingToolkit.SymbolicContinuousCallback( +# [c1 ~ 0], (record_crossings, [c1 => :v], [], [], cr1)) +# evt2 = ModelingToolkit.SymbolicContinuousCallback( +# [c2 ~ 0], (record_crossings, [c2 => :v], [], [], cr2)) +# @named trigsys = ODESystem(eqs, t; continuous_events = [evt1, evt2]) +# trigsys_ss = structural_simplify(trigsys) +# prob = ODEProblem(trigsys_ss, [], (0.0, 2π)) +# sol = solve(prob, Tsit5()) +# required_crossings_c1 = [π / 2, 3 * π / 2] +# required_crossings_c2 = [π / 6, π / 2, 5 * π / 6, 7 * π / 6, 3 * π / 2, 11 * π / 6] +# @test maximum(abs.(first.(cr1) .- required_crossings_c1)) < 1e-4 +# @test maximum(abs.(first.(cr2) .- required_crossings_c2)) < 1e-4 +# @test sign.(cos.(required_crossings_c1 .- 1e-6)) == sign.(last.(cr1)) +# @test sign.(cos.(3 * (required_crossings_c2 .- 1e-6))) == sign.(last.(cr2)) +# +# # with neg affect (pos * neg + left root find) +# cr1p = [] +# cr2p = [] +# cr1n = [] +# cr2n = [] +# evt1 = ModelingToolkit.SymbolicContinuousCallback( +# [c1 ~ 0], (record_crossings, [c1 => :v], [], [], cr1p); +# affect_neg = (record_crossings, [c1 => :v], [], [], cr1n)) +# evt2 = ModelingToolkit.SymbolicContinuousCallback( +# [c2 ~ 0], (record_crossings, [c2 => :v], [], [], cr2p); +# affect_neg = (record_crossings, [c2 => :v], [], [], cr2n)) +# @named trigsys = ODESystem(eqs, t; continuous_events = [evt1, evt2]) +# trigsys_ss = structural_simplify(trigsys) +# prob = ODEProblem(trigsys_ss, [], (0.0, 2π)) +# sol = solve(prob, Tsit5(); dtmax = 0.01) +# c1_pc = filter((<=)(0) ∘ sin, required_crossings_c1) +# c1_nc = filter((>=)(0) ∘ sin, required_crossings_c1) +# c2_pc = filter(c -> -sin(3c) > 0, required_crossings_c2) +# c2_nc = filter(c -> -sin(3c) < 0, required_crossings_c2) +# @test maximum(abs.(c1_pc .- first.(cr1p))) < 1e-5 +# @test maximum(abs.(c1_nc .- first.(cr1n))) < 1e-5 +# @test maximum(abs.(c2_pc .- first.(cr2p))) < 1e-5 +# @test maximum(abs.(c2_nc .- first.(cr2n))) < 1e-5 +# @test sign.(cos.(c1_pc .- 1e-6)) == sign.(last.(cr1p)) +# @test sign.(cos.(c1_nc .- 1e-6)) == sign.(last.(cr1n)) +# @test sign.(cos.(3 * (c2_pc .- 1e-6))) == sign.(last.(cr2p)) +# @test sign.(cos.(3 * (c2_nc .- 1e-6))) == sign.(last.(cr2n)) +# +# # with nothing neg affect (pos * neg + left root find) +# cr1p = [] +# cr2p = [] +# evt1 = ModelingToolkit.SymbolicContinuousCallback( +# [c1 ~ 0], (record_crossings, [c1 => :v], [], [], cr1p); affect_neg = nothing) +# evt2 = ModelingToolkit.SymbolicContinuousCallback( +# [c2 ~ 0], (record_crossings, [c2 => :v], [], [], cr2p); affect_neg = nothing) +# @named trigsys = ODESystem(eqs, t; continuous_events = [evt1, evt2]) +# trigsys_ss = structural_simplify(trigsys) +# prob = ODEProblem(trigsys_ss, [], (0.0, 2π)) +# sol = solve(prob, Tsit5(); dtmax = 0.01) +# @test maximum(abs.(c1_pc .- first.(cr1p))) < 1e-5 +# @test maximum(abs.(c2_pc .- first.(cr2p))) < 1e-5 +# @test sign.(cos.(c1_pc .- 1e-6)) == sign.(last.(cr1p)) +# @test sign.(cos.(3 * (c2_pc .- 1e-6))) == sign.(last.(cr2p)) +# +# #mixed +# cr1p = [] +# cr2p = [] +# cr1n = [] +# cr2n = [] +# evt1 = ModelingToolkit.SymbolicContinuousCallback( +# [c1 ~ 0], (record_crossings, [c1 => :v], [], [], cr1p); affect_neg = nothing) +# evt2 = ModelingToolkit.SymbolicContinuousCallback( +# [c2 ~ 0], (record_crossings, [c2 => :v], [], [], cr2p); +# affect_neg = (record_crossings, [c2 => :v], [], [], cr2n)) +# @named trigsys = ODESystem(eqs, t; continuous_events = [evt1, evt2]) +# trigsys_ss = structural_simplify(trigsys) +# prob = ODEProblem(trigsys_ss, [], (0.0, 2π)) +# sol = solve(prob, Tsit5(); dtmax = 0.01) +# c1_pc = filter((<=)(0) ∘ sin, required_crossings_c1) +# c2_pc = filter(c -> -sin(3c) > 0, required_crossings_c2) +# c2_nc = filter(c -> -sin(3c) < 0, required_crossings_c2) +# @test maximum(abs.(c1_pc .- first.(cr1p))) < 1e-5 +# @test maximum(abs.(c2_pc .- first.(cr2p))) < 1e-5 +# @test maximum(abs.(c2_nc .- first.(cr2n))) < 1e-5 +# @test sign.(cos.(c1_pc .- 1e-6)) == sign.(last.(cr1p)) +# @test sign.(cos.(3 * (c2_pc .- 1e-6))) == sign.(last.(cr2p)) +# @test sign.(cos.(3 * (c2_nc .- 1e-6))) == sign.(last.(cr2n)) +# +# # baseline affect w/ right rootfind (pos + neg + right root find) +# @variables c1(t)=1.0 c2(t)=1.0 # c1 = cos(t), c2 = cos(3t) +# cr1 = [] +# cr2 = [] +# evt1 = ModelingToolkit.SymbolicContinuousCallback( +# [c1 ~ 0], (record_crossings, [c1 => :v], [], [], cr1); +# rootfind = SciMLBase.RightRootFind) +# evt2 = ModelingToolkit.SymbolicContinuousCallback( +# [c2 ~ 0], (record_crossings, [c2 => :v], [], [], cr2); +# rootfind = SciMLBase.RightRootFind) +# @named trigsys = ODESystem(eqs, t; continuous_events = [evt1, evt2]) +# trigsys_ss = structural_simplify(trigsys) +# prob = ODEProblem(trigsys_ss, [], (0.0, 2π)) +# sol = solve(prob, Tsit5(); dtmax = 0.01) +# required_crossings_c1 = [π / 2, 3 * π / 2] +# required_crossings_c2 = [π / 6, π / 2, 5 * π / 6, 7 * π / 6, 3 * π / 2, 11 * π / 6] +# @test maximum(abs.(first.(cr1) .- required_crossings_c1)) < 1e-4 +# @test maximum(abs.(first.(cr2) .- required_crossings_c2)) < 1e-4 +# @test sign.(cos.(required_crossings_c1 .+ 1e-6)) == sign.(last.(cr1)) +# @test sign.(cos.(3 * (required_crossings_c2 .+ 1e-6))) == sign.(last.(cr2)) +# +# # baseline affect w/ mixed rootfind (pos + neg + right root find) +# cr1 = [] +# cr2 = [] +# evt1 = ModelingToolkit.SymbolicContinuousCallback( +# [c1 ~ 0], (record_crossings, [c1 => :v], [], [], cr1); +# rootfind = SciMLBase.LeftRootFind) +# evt2 = ModelingToolkit.SymbolicContinuousCallback( +# [c2 ~ 0], (record_crossings, [c2 => :v], [], [], cr2); +# rootfind = SciMLBase.RightRootFind) +# @named trigsys = ODESystem(eqs, t; continuous_events = [evt1, evt2]) +# trigsys_ss = structural_simplify(trigsys) +# prob = ODEProblem(trigsys_ss, [], (0.0, 2π)) +# sol = solve(prob, Tsit5()) +# @test maximum(abs.(first.(cr1) .- required_crossings_c1)) < 1e-4 +# @test maximum(abs.(first.(cr2) .- required_crossings_c2)) < 1e-4 +# @test sign.(cos.(required_crossings_c1 .- 1e-6)) == sign.(last.(cr1)) +# @test sign.(cos.(3 * (required_crossings_c2 .+ 1e-6))) == sign.(last.(cr2)) +# +# #flip order and ensure results are okay +# cr1 = [] +# cr2 = [] +# evt1 = ModelingToolkit.SymbolicContinuousCallback( +# [c1 ~ 0], (record_crossings, [c1 => :v], [], [], cr1); +# rootfind = SciMLBase.LeftRootFind) +# evt2 = ModelingToolkit.SymbolicContinuousCallback( +# [c2 ~ 0], (record_crossings, [c2 => :v], [], [], cr2); +# rootfind = SciMLBase.RightRootFind) +# @named trigsys = ODESystem(eqs, t; continuous_events = [evt2, evt1]) +# trigsys_ss = structural_simplify(trigsys) +# prob = ODEProblem(trigsys_ss, [], (0.0, 2π)) +# sol = solve(prob, Tsit5()) +# @test maximum(abs.(first.(cr1) .- required_crossings_c1)) < 1e-4 +# @test maximum(abs.(first.(cr2) .- required_crossings_c2)) < 1e-4 +# @test sign.(cos.(required_crossings_c1 .- 1e-6)) == sign.(last.(cr1)) +# @test sign.(cos.(3 * (required_crossings_c2 .+ 1e-6))) == sign.(last.(cr2)) +#end @testset "Discrete event reinitialization (#3142)" begin @connector LiquidPort begin @@ -961,7 +961,7 @@ end @testset "Discrete variable timeseries" begin @variables x(t) @parameters a(t) b(t) c(t) - cb1 = [x ~ 1.0] => [a ~ -a] + cb1 = [x ~ 1.0] => [a ~ -Pre(a)] function save_affect!(integ, u, p, ctx) integ.ps[p.b] = 5.0 end From ab39e35741788f6e5c297b953d16ff662e920eab Mon Sep 17 00:00:00 2001 From: vyudu Date: Thu, 20 Mar 2025 14:55:28 -0400 Subject: [PATCH 17/52] fix: fix initialization and finalization affects --- .../bipartite_tearing/modia_tearing.jl | 6 +- src/structural_transformation/utils.jl | 1 + src/systems/callbacks.jl | 134 +++++++++++------- .../implicit_discrete_system.jl | 5 +- src/systems/imperative_affect.jl | 18 +-- src/systems/systemstructure.jl | 1 + 6 files changed, 93 insertions(+), 72 deletions(-) diff --git a/src/structural_transformation/bipartite_tearing/modia_tearing.jl b/src/structural_transformation/bipartite_tearing/modia_tearing.jl index 5da873afdf..59b32abd56 100644 --- a/src/structural_transformation/bipartite_tearing/modia_tearing.jl +++ b/src/structural_transformation/bipartite_tearing/modia_tearing.jl @@ -96,7 +96,7 @@ function tear_graph_modia(structure::SystemStructure, isder::F = nothing, ieqs = Int[] filtered_vars = BitSet() free_eqs = free_equations(graph, var_sccs, var_eq_matching, varfilter) - is_overdetemined = !isempty(free_eqs) + is_overdetermined = !isempty(free_eqs) for vars in var_sccs for var in vars if varfilter(var) @@ -112,7 +112,7 @@ function tear_graph_modia(structure::SystemStructure, isder::F = nothing, filtered_vars, isder) # If the systems is overdetemined, we cannot assume the free equations # will not form algebraic loops with equations in the sccs. - if !is_overdetemined + if !is_overdetermined vargraph.ne = 0 for var in vars vargraph.matching[var] = unassigned @@ -121,7 +121,7 @@ function tear_graph_modia(structure::SystemStructure, isder::F = nothing, empty!(ieqs) empty!(filtered_vars) end - if is_overdetemined + if is_overdetermined free_vars = findall(x -> !(x isa Int), var_eq_matching) tear_graph_block_modia!(var_eq_matching, ict, solvable_graph, free_eqs, BitSet(free_vars), isder) diff --git a/src/structural_transformation/utils.jl b/src/structural_transformation/utils.jl index 14628f2958..ebcb834bb1 100644 --- a/src/structural_transformation/utils.jl +++ b/src/structural_transformation/utils.jl @@ -218,6 +218,7 @@ function find_eq_solvables!(state::TearingState, ieq, to_rm = Int[], coeffs = no all_int_vars = true coeffs === nothing || empty!(coeffs) empty!(to_rm) + for j in 𝑠neighbors(graph, ieq) var = fullvars[j] isirreducible(var) && (all_int_vars = false; continue) diff --git a/src/systems/callbacks.jl b/src/systems/callbacks.jl index 80556566a2..054a1032b4 100644 --- a/src/systems/callbacks.jl +++ b/src/systems/callbacks.jl @@ -217,7 +217,7 @@ struct SymbolicContinuousCallback <: AbstractCallback end # Default affect to nothing end -SymbolicContinuousCallback(p::Pair, args...; kwargs...) = SymbolicContinuousCallback(p[1], p[2]) +SymbolicContinuousCallback(p::Pair, args...; kwargs...) = SymbolicContinuousCallback(p[1], p[2], args...; kwargs...) SymbolicContinuousCallback(cb::SymbolicContinuousCallback, args...; kwargs...) = cb make_affect(affect::Nothing; kwargs...) = nothing @@ -395,7 +395,7 @@ Arguments: - algeeqs: Algebraic equations of the system that must be satisfied after the callback occurs. """ struct SymbolicDiscreteCallback <: AbstractCallback - conditions::Union{Real, Vector{<:Real}, Vector{Equation}} + conditions::Any affect::Union{Affect, Nothing} initialize::Union{Affect, Nothing} finalize::Union{Affect, Nothing} @@ -410,7 +410,7 @@ struct SymbolicDiscreteCallback <: AbstractCallback end # Default affect to nothing end -SymbolicDiscreteCallback(p::Pair, args...; kwargs...) = SymbolicDiscreteCallback(p[1], p[2]) +SymbolicDiscreteCallback(p::Pair, args...; kwargs...) = SymbolicDiscreteCallback(p[1], p[2], args...; kwargs...) SymbolicDiscreteCallback(cb::SymbolicDiscreteCallback, args...; kwargs...) = cb """ @@ -630,7 +630,7 @@ end """ Compile user-defined functional affect. """ -function compile_functional_affect(affect::FunctionalAffect, cb, sys; kwargs...) +function compile_functional_affect(affect::FunctionalAffect, sys; kwargs...) dvs = unknowns(sys) ps = parameters(sys) dvs_ind = Dict(reverse(en) for en in enumerate(dvs)) @@ -639,11 +639,9 @@ function compile_functional_affect(affect::FunctionalAffect, cb, sys; kwargs...) if has_index_cache(sys) && (ic = get_index_cache(sys)) !== nothing p_inds = [(pind = parameter_index(sys, sym)) === nothing ? sym : pind for sym in parameters(affect)] - save_idxs = get(ic.callback_to_clocks, cb, Int[]) else ps_ind = Dict(reverse(en) for en in enumerate(ps)) p_inds = map(sym -> get(ps_ind, sym, sym), parameters(affect)) - save_idxs = Int[] end # HACK: filter out eliminated symbols. Not clear this is the right thing to do # (MTK should keep these symbols) @@ -652,13 +650,9 @@ function compile_functional_affect(affect::FunctionalAffect, cb, sys; kwargs...) p = filter(x -> !isnothing(x[2]), collect(zip(parameters_syms(affect), p_inds))) |> NamedTuple - let u = u, p = p, user_affect = func(affect), ctx = context(affect), - save_idxs = save_idxs - function (integ) + let u = u, p = p, user_affect = func(affect), ctx = context(affect) + (integ) -> begin user_affect(integ, u, p, ctx) - for idx in save_idxs - SciMLBase.save_discretes!(integ, idx) - end end end end @@ -670,6 +664,8 @@ function generate_continuous_callbacks(sys::AbstractSystem, dvs = unknowns(sys), cbs = continuous_events(sys) isempty(cbs) && return nothing cb_classes = Dict{SciMLBase.RootfindOpt, Vector{SymbolicContinuousCallback}}() + + # Sort the callbacks by their rootfinding method for cb in cbs _cbs = get!(() -> SymbolicContinuousCallback[], cb_classes, cb.rootfind) push!(_cbs, cb) @@ -709,12 +705,12 @@ function generate_callback(cbs::Vector{SymbolicContinuousCallback}, sys; kwargs. inits = [] finals = [] for cb in cbs - affect = compile_affect(cb.affect, cb, sys, default = (args...) -> ()) + affect = compile_affect(cb.affect, cb, sys, default = nothing) push!(affects, affect) - affect_neg = (cb.affect_neg === cb.affect) ? affect : compile_affect(cb.affect_neg, cb, sys, default = (args...) -> ()) + affect_neg = (cb.affect_neg == cb.affect) ? affect : compile_affect(cb.affect_neg, cb, sys, default = nothing) push!(affect_negs, affect_neg) - push!(inits, compile_affect(cb.initialize, cb, sys, default = nothing)) - push!(finals, compile_affect(cb.finalize, cb, sys, default = nothing)) + push!(inits, compile_affect(cb.initialize, cb, sys; default = nothing, is_init = true)) + push!(finals, compile_affect(cb.finalize, cb, sys; default = nothing)) end # Since there may be different number of conditions and affects, @@ -746,10 +742,16 @@ function generate_callback(cb, sys; kwargs...) trigger = is_timed ? conditions(cb) : compile_condition(cb, sys, dvs, ps; kwargs...) affect = compile_affect(cb.affect, cb, sys, default = (args...) -> ()) - affect_neg = hasfield(typeof(cb), :affect_neg) ? - compile_affect(cb.affect_neg, cb, sys, default = affect) : nothing - initialize = compile_affect(cb.initialize, cb, sys, default = SciMLBase.INITIALIZE_DEFAULT) - finalize = compile_affect(cb.finalize, cb, sys, default = SciMLBase.FINALIZE_DEFAULT) + affect_neg = if is_discrete(cb) + nothing + else + (cb.affect == cb.affect_neg) ? affect : compile_affect(cb.affect_neg, cb, sys, default = nothing) + end + init = compile_affect(cb.initialize, cb, sys, default = SciMLBase.INITIALIZE_DEFAULT, is_init = true) + final = compile_affect(cb.finalize, cb, sys, default = SciMLBase.FINALIZE_DEFAULT) + + initialize = isnothing(cb.initialize) ? init : ((c, u, t, i) -> init(i)) + finalize = isnothing(cb.finalize) ? final : ((c, u, t, i) -> final(i)) if is_discrete(cb) if is_timed && conditions(cb) isa AbstractVector @@ -784,24 +786,73 @@ Notes - `kwargs` are passed through to `Symbolics.build_function`. """ function compile_affect( - aff::Union{Nothing, Affect}, cb::AbstractCallback, sys::AbstractSystem; default = nothing, kwargs...) - isnothing(aff) && return default - + aff::Union{Nothing, Affect}, cb::AbstractCallback, sys::AbstractSystem; default = nothing, is_init = false, kwargs...) save_idxs = if !(has_index_cache(sys) && (ic = get_index_cache(sys)) !== nothing) Int[] else get(ic.callback_to_clocks, cb, Int[]) end - if aff isa AffectSystem - affsys = system(aff) - aff_map = aff_to_sys(aff) - sys_map = Dict([v => k for (k, v) in aff_map]) - reinit = has_alg_eqs(sys) - ps_to_modify = discretes(aff) - dvs_to_modify = setdiff(unknowns(aff), getfield.(observed(sys), :lhs)) + f = if isnothing(aff) + default + elseif aff isa AffectSystem + compile_equational_affect(aff, sys) + elseif aff isa FunctionalAffect || aff isa ImperativeAffect + compile_functional_affect(aff, sys; kwargs...) + end + wrap_save_discretes(f, save_idxs; is_init) +end + +# Init can be: user defined function, nothing, or INITIALIZE_DEFAULT +function wrap_save_discretes(f, save_idxs; is_init = false) + if isempty(save_idxs) || f === SciMLBase.FINALIZE_DEFAULT || (isnothing(f) && !is_init) + return f + elseif f === SciMLBase.INITIALIZE_DEFAULT + let save_idxs = save_idxs + (c, u, t, i) -> begin + f(c, u, t, i) + for idx in save_idxs + SciMLBase.save_discretes!(i, idx) + end + end + end + else + let save_idxs = save_idxs + (i) -> begin + isnothing(f) || f(i) + for idx in save_idxs + SciMLBase.save_discretes!(i, idx) + end + end + end + end +end + +""" +Initialize and Finalize for VectorContinuousCallback. +""" +function compile_vector_optional_affect(funs, default) + all(isnothing, funs) && return default + return let funs = funs + function (cb, u, t, integ) + for func in funs + isnothing(func) ? continue : func(integ) + end + end + end +end + +function compile_equational_affect(aff::AffectSystem, sys; kwargs...) + affsys = system(aff) + aff_map = aff_to_sys(aff) + sys_map = Dict([v => k for (k, v) in aff_map]) + ps_to_modify = discretes(aff) + dvs_to_modify = setdiff(unknowns(aff), getfield.(observed(sys), :lhs)) + #TODO: Add an optimization for systems without algebraic equations - function affect!(integrator) + return let dvs_to_modify = dvs_to_modify, aff_map = aff_map, sys_map = sys_map, affsys = affsys, ps_to_modify = ps_to_modify + + @inline function affect!(integrator) pmap = Pair[] for pre_p in parameters(affsys) p = only(arguments(unwrap(pre_p))) @@ -809,7 +860,7 @@ function compile_affect( push!(pmap, pre_p => pval) end guesses = Pair[u => integrator[aff_map[u]] for u in unknowns(affsys)] - affprob = ImplicitDiscreteProblem(affsys, Pair[], (0, 1), pmap; guesses, build_initializeprob = reinit) + affprob = ImplicitDiscreteProblem(affsys, Pair[], (0, 1), pmap; guesses, build_initializeprob = false) affsol = init(affprob, SimpleIDSolve()) for u in dvs_to_modify @@ -818,28 +869,9 @@ function compile_affect( for p in ps_to_modify integrator.ps[p] = affsol[sys_map[p]] end - for idx in save_idxs - SciMLBase.save_discretes!(integrator, idx) - end sys isa JumpSystem && reset_aggregated_jumps!(integrator) end - elseif aff isa FunctionalAffect || aff isa ImperativeAffect - compile_functional_affect(aff, cb, sys; kwargs...) - end -end - -""" -Initialize and Finalize for VectorContinuousCallback. -""" -function compile_vector_optional_affect(funs, default) - all(isnothing, funs) && return default - return let funs = funs - function (cb, u, t, integ) - for func in funs - isnothing(func) ? continue : func(integ) - end - end end end diff --git a/src/systems/discrete_system/implicit_discrete_system.jl b/src/systems/discrete_system/implicit_discrete_system.jl index 60ee09cf4d..c0ac00734b 100644 --- a/src/systems/discrete_system/implicit_discrete_system.jl +++ b/src/systems/discrete_system/implicit_discrete_system.jl @@ -270,7 +270,7 @@ function flatten(sys::ImplicitDiscreteSystem, noeqs = false) end function generate_function( - sys::ImplicitDiscreteSystem, dvs = unknowns(sys), ps = parameters(sys); wrap_code = identity, kwargs...) + sys::ImplicitDiscreteSystem, dvs = unknowns(sys), ps = parameters(sys); wrap_code = identity, cachesyms::Tuple = (), kwargs...) iv = get_iv(sys) # Algebraic equations get shifted forward 1, to match with differential equations exprs = map(equations(sys)) do eq @@ -286,8 +286,9 @@ function generate_function( u_next = map(Shift(iv, 1), dvs) u = dvs + p = (reorder_parameters(sys, unwrap.(ps))..., cachesyms...) build_function_wrapper( - sys, exprs, u_next, u, ps..., iv; p_start = 3, extra_assignments, kwargs...) + sys, exprs, u_next, u, p..., iv; p_start = 3, extra_assignments, kwargs...) end function shift_u0map_forward(sys::ImplicitDiscreteSystem, u0map, defs) diff --git a/src/systems/imperative_affect.jl b/src/systems/imperative_affect.jl index c36b250fbf..81e4cf724f 100644 --- a/src/systems/imperative_affect.jl +++ b/src/systems/imperative_affect.jl @@ -109,10 +109,6 @@ function namespace_affect(affect::ImperativeAffect, s) affect.skip_checks) end -function compile_affect(affect::ImperativeAffect, cb, sys, dvs, ps; kwargs...) - compile_functional_affect(affect, cb, sys, dvs, ps; kwargs...) -end - function invalid_variables(sys, expr) filter(x -> !any(isequal(x), all_symbols(sys)), reduce(vcat, vars(expr); init = [])) end @@ -155,7 +151,7 @@ function check_assignable(sys, sym) end end -function compile_functional_affect(affect::ImperativeAffect, cb, sys; kwargs...) +function compile_functional_affect(affect::ImperativeAffect, sys; kwargs...) #= Implementation sketch: generate observed function (oop), should save to a component array under obs_syms @@ -235,14 +231,8 @@ function compile_functional_affect(affect::ImperativeAffect, cb, sys; kwargs...) upd_funs = NamedTuple{mod_names}((setu.((sys,), first.(mod_pairs))...,)) - if has_index_cache(sys) && (ic = get_index_cache(sys)) !== nothing - save_idxs = get(ic.callback_to_clocks, cb, Int[]) - else - save_idxs = Int[] - end - let user_affect = func(affect), ctx = context(affect) - function (integ) + @inline function (integ) # update the to-be-mutated values; this ensures that if you do a no-op then nothing happens modvals = mod_og_val_fun(integ.u, integ.p, integ.t) upd_component_array = NamedTuple{mod_names}(modvals) @@ -256,10 +246,6 @@ function compile_functional_affect(affect::ImperativeAffect, cb, sys; kwargs...) # write the new values back to the integrator _generated_writeback(integ, upd_funs, upd_vals) - - for idx in save_idxs - SciMLBase.save_discretes!(integ, idx) - end end end end diff --git a/src/systems/systemstructure.jl b/src/systems/systemstructure.jl index c0c4a5ff4d..b1de95d074 100644 --- a/src/systems/systemstructure.jl +++ b/src/systems/systemstructure.jl @@ -688,6 +688,7 @@ function _structural_simplify!(state::TearingState, io; simplify = false, check_consistency = true, fully_determined = true, warn_initialize_determined = false, dummy_derivative = true, kwargs...) + if fully_determined isa Bool check_consistency &= fully_determined else From 0275fbcede9c392cc74a0177138032ea4aed3ff6 Mon Sep 17 00:00:00 2001 From: vyudu Date: Thu, 20 Mar 2025 14:58:09 -0400 Subject: [PATCH 18/52] uncomment tests --- test/symbolic_events.jl | 1722 +++++++++++++++++++-------------------- 1 file changed, 860 insertions(+), 862 deletions(-) diff --git a/test/symbolic_events.jl b/test/symbolic_events.jl index 80a15c2b6c..763dfcbbea 100644 --- a/test/symbolic_events.jl +++ b/test/symbolic_events.jl @@ -18,863 +18,863 @@ eqs = [D(x) ~ 1] affect = [x ~ 0] affect_neg = [x ~ 1] -#@testset "SymbolicContinuousCallback constructors" begin -# e = SymbolicContinuousCallback(eqs[]) -# @test e isa SymbolicContinuousCallback -# @test isequal(equations(e), eqs) -# @test e.affect == nothing -# @test e.affect_neg == nothing -# @test e.rootfind == SciMLBase.LeftRootFind -# -# e = SymbolicContinuousCallback(eqs) -# @test e isa SymbolicContinuousCallback -# @test isequal(equations(e), eqs) -# @test e.affect == nothing -# @test e.affect_neg == nothing -# @test e.rootfind == SciMLBase.LeftRootFind -# -# e = SymbolicContinuousCallback(eqs, nothing) -# @test e isa SymbolicContinuousCallback -# @test isequal(equations(e), eqs) -# @test e.affect == nothing -# @test e.affect_neg == nothing -# @test e.rootfind == SciMLBase.LeftRootFind -# -# e = SymbolicContinuousCallback(eqs[], nothing) -# @test e isa SymbolicContinuousCallback -# @test isequal(equations(e), eqs) -# @test e.affect == nothing -# @test e.affect_neg == nothing -# @test e.rootfind == SciMLBase.LeftRootFind -# -# e = SymbolicContinuousCallback(eqs => nothing) -# @test e isa SymbolicContinuousCallback -# @test isequal(equations(e), eqs) -# @test e.affect == nothing -# @test e.affect_neg == nothing -# @test e.rootfind == SciMLBase.LeftRootFind -# -# e = SymbolicContinuousCallback(eqs[] => nothing) -# @test e isa SymbolicContinuousCallback -# @test isequal(equations(e), eqs) -# @test e.affect == nothing -# @test e.affect_neg == nothing -# @test e.rootfind == SciMLBase.LeftRootFind -# -# ## With affect -# e = SymbolicContinuousCallback(eqs[], affect) -# @test e isa SymbolicContinuousCallback -# @test isequal(equations(e), eqs) -# @test observed(system(affects(e))) == affect -# @test observed(system(affect_negs(e))) == affect -# @test e.rootfind == SciMLBase.LeftRootFind -# -# # with only positive edge affect -# e = SymbolicContinuousCallback(eqs[], affect, affect_neg = nothing) -# @test e isa SymbolicContinuousCallback -# @test isequal(equations(e), eqs) -# @test observed(system(affects(e))) == affect -# @test isnothing(e.affect_neg) -# @test e.rootfind == SciMLBase.LeftRootFind -# -# # with explicit edge affects -# e = SymbolicContinuousCallback(eqs[], affect, affect_neg = affect_neg) -# @test e isa SymbolicContinuousCallback -# @test isequal(equations(e), eqs) -# @test observed(system(affects(e))) == affect -# @test observed(system(affect_negs(e))) == affect_neg -# @test e.rootfind == SciMLBase.LeftRootFind -# -# # with different root finding ops -# e = SymbolicContinuousCallback( -# eqs[], affect, affect_neg = affect_neg, rootfind = SciMLBase.LeftRootFind) -# @test e isa SymbolicContinuousCallback -# @test isequal(equations(e), eqs) -# @test e.rootfind == SciMLBase.LeftRootFind -# -# # test plural constructor -# e = SymbolicContinuousCallbacks(eqs[]) -# @test e isa Vector{SymbolicContinuousCallback} -# @test isequal(equations(e[]), eqs) -# @test e[].affect == nothing -# -# e = SymbolicContinuousCallbacks(eqs) -# @test e isa Vector{SymbolicContinuousCallback} -# @test isequal(equations(e[]), eqs) -# @test e[].affect == nothing -# -# e = SymbolicContinuousCallbacks(eqs[] => affect) -# @test e isa Vector{SymbolicContinuousCallback} -# @test isequal(equations(e[]), eqs) -# @test e[].affect isa AffectSystem -# -# e = SymbolicContinuousCallbacks(eqs => affect) -# @test e isa Vector{SymbolicContinuousCallback} -# @test isequal(equations(e[]), eqs) -# @test e[].affect isa AffectSystem -# -# e = SymbolicContinuousCallbacks([eqs[] => affect]) -# @test e isa Vector{SymbolicContinuousCallback} -# @test isequal(equations(e[]), eqs) -# @test e[].affect isa AffectSystem -# -# e = SymbolicContinuousCallbacks([eqs => affect]) -# @test e isa Vector{SymbolicContinuousCallback} -# @test isequal(equations(e[]), eqs) -# @test e[].affect isa AffectSystem -#end -# -#@testset "ImperativeAffect constructors" begin -# fmfa(o, x, i, c) = nothing -# m = ModelingToolkit.ImperativeAffect(fmfa) -# @test m isa ModelingToolkit.ImperativeAffect -# @test m.f == fmfa -# @test m.obs == [] -# @test m.obs_syms == [] -# @test m.modified == [] -# @test m.mod_syms == [] -# @test m.ctx === nothing -# -# m = ModelingToolkit.ImperativeAffect(fmfa, (;)) -# @test m isa ModelingToolkit.ImperativeAffect -# @test m.f == fmfa -# @test m.obs == [] -# @test m.obs_syms == [] -# @test m.modified == [] -# @test m.mod_syms == [] -# @test m.ctx === nothing -# -# m = ModelingToolkit.ImperativeAffect(fmfa, (; x)) -# @test m isa ModelingToolkit.ImperativeAffect -# @test m.f == fmfa -# @test isequal(m.obs, []) -# @test m.obs_syms == [] -# @test isequal(m.modified, [x]) -# @test m.mod_syms == [:x] -# @test m.ctx === nothing -# -# m = ModelingToolkit.ImperativeAffect(fmfa, (; y = x)) -# @test m isa ModelingToolkit.ImperativeAffect -# @test m.f == fmfa -# @test isequal(m.obs, []) -# @test m.obs_syms == [] -# @test isequal(m.modified, [x]) -# @test m.mod_syms == [:y] -# @test m.ctx === nothing -# -# m = ModelingToolkit.ImperativeAffect(fmfa; observed = (; y = x)) -# @test m isa ModelingToolkit.ImperativeAffect -# @test m.f == fmfa -# @test isequal(m.obs, [x]) -# @test m.obs_syms == [:y] -# @test m.modified == [] -# @test m.mod_syms == [] -# @test m.ctx === nothing -# -# m = ModelingToolkit.ImperativeAffect(fmfa; modified = (; x)) -# @test m isa ModelingToolkit.ImperativeAffect -# @test m.f == fmfa -# @test isequal(m.obs, []) -# @test m.obs_syms == [] -# @test isequal(m.modified, [x]) -# @test m.mod_syms == [:x] -# @test m.ctx === nothing -# -# m = ModelingToolkit.ImperativeAffect(fmfa; modified = (; y = x)) -# @test m isa ModelingToolkit.ImperativeAffect -# @test m.f == fmfa -# @test isequal(m.obs, []) -# @test m.obs_syms == [] -# @test isequal(m.modified, [x]) -# @test m.mod_syms == [:y] -# @test m.ctx === nothing -# -# m = ModelingToolkit.ImperativeAffect(fmfa, (; x), (; x)) -# @test m isa ModelingToolkit.ImperativeAffect -# @test m.f == fmfa -# @test isequal(m.obs, [x]) -# @test m.obs_syms == [:x] -# @test isequal(m.modified, [x]) -# @test m.mod_syms == [:x] -# @test m.ctx === nothing -# -# m = ModelingToolkit.ImperativeAffect(fmfa, (; y = x), (; y = x)) -# @test m isa ModelingToolkit.ImperativeAffect -# @test m.f == fmfa -# @test isequal(m.obs, [x]) -# @test m.obs_syms == [:y] -# @test isequal(m.modified, [x]) -# @test m.mod_syms == [:y] -# @test m.ctx === nothing -# -# m = ModelingToolkit.ImperativeAffect( -# fmfa; modified = (; y = x), observed = (; y = x)) -# @test m isa ModelingToolkit.ImperativeAffect -# @test m.f == fmfa -# @test isequal(m.obs, [x]) -# @test m.obs_syms == [:y] -# @test isequal(m.modified, [x]) -# @test m.mod_syms == [:y] -# @test m.ctx === nothing -# -# m = ModelingToolkit.ImperativeAffect( -# fmfa; modified = (; y = x), observed = (; y = x), ctx = 3) -# @test m isa ModelingToolkit.ImperativeAffect -# @test m.f == fmfa -# @test isequal(m.obs, [x]) -# @test m.obs_syms == [:y] -# @test isequal(m.modified, [x]) -# @test m.mod_syms == [:y] -# @test m.ctx === 3 -# -# m = ModelingToolkit.ImperativeAffect(fmfa, (; x), (; x), 3) -# @test m isa ModelingToolkit.ImperativeAffect -# @test m.f == fmfa -# @test isequal(m.obs, [x]) -# @test m.obs_syms == [:x] -# @test isequal(m.modified, [x]) -# @test m.mod_syms == [:x] -# @test m.ctx === 3 -#end - -#@testset "Condition Compilation" begin -# @named sys = ODESystem(eqs, t, continuous_events = [x ~ 1]) -# @test getfield(sys, :continuous_events)[] == -# SymbolicContinuousCallback(Equation[x ~ 1], nothing) -# @test isequal(equations(getfield(sys, :continuous_events))[], x ~ 1) -# fsys = flatten(sys) -# @test isequal(equations(getfield(fsys, :continuous_events))[], x ~ 1) -# -# @named sys2 = ODESystem([D(x) ~ 1], t, continuous_events = [x ~ 2], systems = [sys]) -# @test getfield(sys2, :continuous_events)[] == -# SymbolicContinuousCallback(Equation[x ~ 2], nothing) -# @test all(ModelingToolkit.continuous_events(sys2) .== [ -# SymbolicContinuousCallback(Equation[x ~ 2], nothing), -# SymbolicContinuousCallback(Equation[sys.x ~ 1], nothing) -# ]) -# -# @test isequal(equations(getfield(sys2, :continuous_events))[1], x ~ 2) -# @test length(ModelingToolkit.continuous_events(sys2)) == 2 -# @test isequal(equations(ModelingToolkit.continuous_events(sys2)[1])[], x ~ 2) -# @test isequal(equations(ModelingToolkit.continuous_events(sys2)[2])[], sys.x ~ 1) -# -# sys = complete(sys) -# sys_nosplit = complete(sys; split = false) -# sys2 = complete(sys2) -# -# # Test proper rootfinding -# prob = ODEProblem(sys, Pair[], (0.0, 2.0)) -# p0 = 0 -# t0 = 0 -# @test get_callback(prob) isa ModelingToolkit.DiffEqCallbacks.ContinuousCallback -# cb = ModelingToolkit.generate_continuous_callbacks(sys) -# cond = cb.condition -# out = [0.0] -# cond.f_iip(out, [0], p0, t0) -# @test out[] ≈ -1 # signature is u,p,t -# cond.f_iip(out, [1], p0, t0) -# @test out[] ≈ 0 # signature is u,p,t -# cond.f_iip(out, [2], p0, t0) -# @test out[] ≈ 1 # signature is u,p,t -# -# prob = ODEProblem(sys, Pair[], (0.0, 2.0)) -# prob_nosplit = ODEProblem(sys_nosplit, Pair[], (0.0, 2.0)) -# sol = solve(prob, Tsit5()) -# sol_nosplit = solve(prob_nosplit, Tsit5()) -# @test minimum(t -> abs(t - 1), sol.t) < 1e-10 # test that the solver stepped at the root -# @test minimum(t -> abs(t - 1), sol_nosplit.t) < 1e-10 # test that the solver stepped at the root -# -# # Test user-provided callback is respected -# test_callback = DiscreteCallback(x -> x, x -> x) -# prob = ODEProblem(sys, Pair[], (0.0, 2.0), callback = test_callback) -# prob_nosplit = ODEProblem(sys_nosplit, Pair[], (0.0, 2.0), callback = test_callback) -# cbs = get_callback(prob) -# cbs_nosplit = get_callback(prob_nosplit) -# @test cbs isa CallbackSet -# @test cbs.discrete_callbacks[1] == test_callback -# @test cbs_nosplit isa CallbackSet -# @test cbs_nosplit.discrete_callbacks[1] == test_callback -# -# prob = ODEProblem(sys2, Pair[], (0.0, 3.0)) -# cb = get_callback(prob) -# @test cb isa ModelingToolkit.DiffEqCallbacks.VectorContinuousCallback -# -# cond = cb.condition -# out = [0.0, 0.0] -# # the root to find is 2 -# cond.f_iip(out, [0, 0], p0, t0) -# @test out[1] ≈ -2 # signature is u,p,t -# cond.f_iip(out, [1, 0], p0, t0) -# @test out[1] ≈ -1 # signature is u,p,t -# cond.f_iip(out, [2, 0], p0, t0) # this should return 0 -# @test out[1] ≈ 0 # signature is u,p,t -# -# # the root to find is 1 -# out = [0.0, 0.0] -# cond.f_iip(out, [0, 0], p0, t0) -# @test out[2] ≈ -1 # signature is u,p,t -# cond.f_iip(out, [0, 1], p0, t0) # this should return 0 -# @test out[2] ≈ 0 # signature is u,p,t -# cond.f_iip(out, [0, 2], p0, t0) -# @test out[2] ≈ 1 # signature is u,p,t -# -# sol = solve(prob, Tsit5()) -# @test minimum(t -> abs(t - 1), sol.t) < 1e-10 # test that the solver stepped at the first root -# @test minimum(t -> abs(t - 2), sol.t) < 1e-10 # test that the solver stepped at the second root -# -# @named sys = ODESystem(eqs, t, continuous_events = [x ~ 1, x ~ 2]) # two root eqs using the same unknown -# sys = complete(sys) -# prob = ODEProblem(sys, Pair[], (0.0, 3.0)) -# @test get_callback(prob) isa ModelingToolkit.DiffEqCallbacks.VectorContinuousCallback -# sol = solve(prob, Tsit5()) -# @test minimum(t -> abs(t - 1), sol.t) < 1e-10 # test that the solver stepped at the first root -# @test minimum(t -> abs(t - 2), sol.t) < 1e-10 # test that the solver stepped at the second root -#end - -#@testset "Bouncing Ball" begin -# ###### 1D Bounce -# @variables x(t)=1 v(t)=0 -# -# root_eqs = [x ~ 0] -# affect = [v ~ -Pre(v)] -# -# @named ball = ODESystem( -# [D(x) ~ v -# D(v) ~ -9.8], t, continuous_events = root_eqs => affect) -# -# @test only(continuous_events(ball)) == -# SymbolicContinuousCallback(Equation[x ~ 0], Equation[v ~ -Pre(v)]) -# ball = structural_simplify(ball) -# -# @test length(ModelingToolkit.continuous_events(ball)) == 1 -# -# tspan = (0.0, 5.0) -# prob = ODEProblem(ball, Pair[], tspan) -# sol = solve(prob, Tsit5()) -# @test 0 <= minimum(sol[x]) <= 1e-10 # the ball never went through the floor but got very close -# -# ###### 2D bouncing ball -# @variables x(t)=1 y(t)=0 vx(t)=0 vy(t)=1 -# -# events = [[x ~ 0] => [vx ~ -Pre(vx)] -# [y ~ -1.5, y ~ 1.5] => [vy ~ -Pre(vy)]] -# -# @named ball = ODESystem( -# [D(x) ~ vx -# D(y) ~ vy -# D(vx) ~ -9.8 -# D(vy) ~ -0.01vy], t; continuous_events = events) -# -# _ball = ball -# ball = structural_simplify(_ball) -# ball_nosplit = structural_simplify(_ball; split = false) -# -# tspan = (0.0, 5.0) -# prob = ODEProblem(ball, Pair[], tspan) -# prob_nosplit = ODEProblem(ball_nosplit, Pair[], tspan) -# -# cb = get_callback(prob) -# @test cb isa ModelingToolkit.DiffEqCallbacks.VectorContinuousCallback -# @test getfield(ball, :continuous_events)[1] == -# SymbolicContinuousCallback(Equation[x ~ 0], Equation[vx ~ -Pre(vx)]) -# @test getfield(ball, :continuous_events)[2] == -# SymbolicContinuousCallback(Equation[y ~ -1.5, y ~ 1.5], Equation[vy ~ -Pre(vy)]) -# cond = cb.condition -# out = [0.0, 0.0, 0.0] -# p0 = 0. -# t0 = 0. -# cond.f_iip(out, [0, 0, 0, 0], p0, t0) -# @test out ≈ [0, 1.5, -1.5] -# -# sol = solve(prob, Tsit5()) -# sol_nosplit = solve(prob_nosplit, Tsit5()) -# @test 0 <= minimum(sol[x]) <= 1e-10 # the ball never went through the floor but got very close -# @test minimum(sol[y]) ≈ -1.5 # check wall conditions -# @test maximum(sol[y]) ≈ 1.5 # check wall conditions -# @test 0 <= minimum(sol_nosplit[x]) <= 1e-10 # the ball never went through the floor but got very close -# @test minimum(sol_nosplit[y]) ≈ -1.5 # check wall conditions -# @test maximum(sol_nosplit[y]) ≈ 1.5 # check wall conditions -# -# ## Test multi-variable affect -# # in this test, there are two variables affected by a single event. -# events = [[x ~ 0] => [vx ~ -Pre(vx), vy ~ -Pre(vy)]] -# -# @named ball = ODESystem([D(x) ~ vx -# D(y) ~ vy -# D(vx) ~ -1 -# D(vy) ~ 0], t; continuous_events = events) -# -# ball_nosplit = structural_simplify(ball) -# ball = structural_simplify(ball) -# -# tspan = (0.0, 5.0) -# prob = ODEProblem(ball, Pair[], tspan) -# prob_nosplit = ODEProblem(ball_nosplit, Pair[], tspan) -# sol = solve(prob, Tsit5()) -# sol_nosplit = solve(prob_nosplit, Tsit5()) -# @test 0 <= minimum(sol[x]) <= 1e-10 # the ball never went through the floor but got very close -# @test -minimum(sol[y]) ≈ maximum(sol[y]) ≈ sqrt(2) # the ball will never go further than √2 in either direction (gravity was changed to 1 to get this particular number) -# @test 0 <= minimum(sol_nosplit[x]) <= 1e-10 # the ball never went through the floor but got very close -# @test -minimum(sol_nosplit[y]) ≈ maximum(sol_nosplit[y]) ≈ sqrt(2) # the ball will never go further than √2 in either direction (gravity was changed to 1 to get this particular number) -#end -# -## issue https://github.com/SciML/ModelingToolkit.jl/issues/1386 -## tests that it works for ODAESystem -#@testset "ODAESystem" begin -# @variables vs(t) v(t) vmeasured(t) -# eq = [vs ~ sin(2pi * t) -# D(v) ~ vs - v -# D(vmeasured) ~ 0.0] -# ev = [sin(20pi * t) ~ 0.0] => [vmeasured ~ Pre(v)] -# @named sys = ODESystem(eq, t, continuous_events = ev) -# sys = structural_simplify(sys) -# prob = ODEProblem(sys, zeros(2), (0.0, 5.1)) -# sol = solve(prob, Tsit5()) -# @test all(minimum((0:0.1:5) .- sol.t', dims = 2) .< 0.0001) # test that the solver stepped every 0.1s as dictated by event -# @test sol([0.25])[vmeasured][] == sol([0.23])[vmeasured][] # test the hold property -#end -# -### https://github.com/SciML/ModelingToolkit.jl/issues/1528 -#@testset "Handle Empty Events" begin -# Dₜ = D -# -# @parameters u(t) [input = true] # Indicate that this is a controlled input -# @parameters y(t) [output = true] # Indicate that this is a measured output -# -# function Mass(; name, m = 1.0, p = 0, v = 0) -# ps = @parameters m = m -# sts = @variables pos(t)=p vel(t)=v -# eqs = Dₜ(pos) ~ vel -# ODESystem(eqs, t, [pos, vel], ps; name) -# end -# function Spring(; name, k = 1e4) -# ps = @parameters k = k -# @variables x(t) = 0 # Spring deflection -# ODESystem(Equation[], t, [x], ps; name) -# end -# function Damper(; name, c = 10) -# ps = @parameters c = c -# @variables vel(t) = 0 -# ODESystem(Equation[], t, [vel], ps; name) -# end -# function SpringDamper(; name, k = false, c = false) -# spring = Spring(; name = :spring, k) -# damper = Damper(; name = :damper, c) -# compose(ODESystem(Equation[], t; name), -# spring, damper) -# end -# connect_sd(sd, m1, m2) = [ -# sd.spring.x ~ m1.pos - m2.pos, sd.damper.vel ~ m1.vel - m2.vel] -# sd_force(sd) = -sd.spring.k * sd.spring.x - sd.damper.c * sd.damper.vel -# @named mass1 = Mass(; m = 1) -# @named mass2 = Mass(; m = 1) -# @named sd = SpringDamper(; k = 1000, c = 10) -# function Model(u, d = 0) -# eqs = [connect_sd(sd, mass1, mass2) -# Dₜ(mass1.vel) ~ (sd_force(sd) + u) / mass1.m -# Dₜ(mass2.vel) ~ (-sd_force(sd) + d) / mass2.m] -# @named _model = ODESystem(eqs, t; observed = [y ~ mass2.pos]) -# @named model = compose(_model, mass1, mass2, sd) -# end -# model = Model(sin(30t)) -# sys = structural_simplify(model) -# @test isempty(ModelingToolkit.continuous_events(sys)) -#end -# -#@testset "ODESystem Discrete Callbacks" begin -# function testsol(osys, u0, p, tspan; tstops = Float64[], paramtotest = nothing, -# kwargs...) -# oprob = ODEProblem(complete(osys), u0, tspan, p; kwargs...) -# sol = solve(oprob, Tsit5(); tstops = tstops, abstol = 1e-10, reltol = 1e-10) -# @test isapprox(sol(1.0000000001)[1] - sol(0.999999999)[1], 1.0; rtol = 1e-6) -# paramtotest === nothing || (@test sol.ps[paramtotest] == 1.0) -# @test isapprox(sol(4.0)[1], 2 * exp(-2.0)) -# sol -# end -# -# @parameters k t1 t2 -# @variables A(t) B(t) -# -# cond1 = (t == t1) -# affect1 = [A ~ Pre(A) + 1] -# cb1 = cond1 => affect1 -# cond2 = (t == t2) -# affect2 = [k ~ 1.0] -# cb2 = cond2 => affect2 -# -# ∂ₜ = D -# eqs = [∂ₜ(A) ~ -k * A] -# @named osys = ODESystem(eqs, t, [A], [k, t1, t2], discrete_events = [cb1, cb2]) -# u0 = [A => 1.0] -# p = [k => 0.0, t1 => 1.0, t2 => 2.0] -# tspan = (0.0, 4.0) -# testsol(osys, u0, p, tspan; tstops = [1.0, 2.0], paramtotest = k) -# -# cond1a = (t == t1) -# affect1a = [A ~ Pre(A) + 1, B ~ A] -# cb1a = cond1a => affect1a -# @named osys1 = ODESystem(eqs, t, [A, B], [k, t1, t2], discrete_events = [cb1a, cb2]) -# u0′ = [A => 1.0, B => 0.0] -# sol = testsol( -# osys1, u0′, p, tspan; tstops = [1.0, 2.0], check_length = false, paramtotest = k) -# @test sol(1.0000001, idxs = B) == 2.0 -# -# # same as above - but with set-time event syntax -# cb1‵ = [1.0] => affect1 # needs to be a Vector for the event to happen only once -# cb2‵ = [2.0] => affect2 -# @named osys‵ = ODESystem(eqs, t, [A], [k], discrete_events = [cb1‵, cb2‵]) -# testsol(osys‵, u0, p, tspan; paramtotest = k) -# -# # mixing discrete affects -# @named osys3 = ODESystem(eqs, t, [A], [k, t1, t2], discrete_events = [cb1, cb2‵]) -# testsol(osys3, u0, p, tspan; tstops = [1.0], paramtotest = k) -# -# # mixing with a func affect -# function affect!(integrator, u, p, ctx) -# integrator.ps[p.k] = 1.0 -# nothing -# end -# cb2‵‵ = [2.0] => (affect!, [], [k], [k], nothing) -# @named osys4 = ODESystem(eqs, t, [A], [k, t1], discrete_events = [cb1, cb2‵‵]) -# oprob4 = ODEProblem(complete(osys4), u0, tspan, p) -# testsol(osys4, u0, p, tspan; tstops = [1.0], paramtotest = k) -# -# # mixing with symbolic condition in the func affect -# cb2‵‵‵ = (t == t2) => (affect!, [], [k], [k], nothing) -# @named osys5 = ODESystem(eqs, t, [A], [k, t1, t2], discrete_events = [cb1, cb2‵‵‵]) -# testsol(osys5, u0, p, tspan; tstops = [1.0, 2.0]) -# @named osys6 = ODESystem(eqs, t, [A], [k, t1, t2], discrete_events = [cb2‵‵‵, cb1]) -# testsol(osys6, u0, p, tspan; tstops = [1.0, 2.0], paramtotest = k) -# -# # mix a continuous event too -# cond3 = A ~ 0.1 -# affect3 = [k ~ 0.0] -# cb3 = cond3 => affect3 -# @named osys7 = ODESystem(eqs, t, [A], [k, t1, t2], discrete_events = [cb1, cb2‵‵‵], -# continuous_events = [cb3]) -# sol = testsol(osys7, u0, p, (0.0, 10.0); tstops = [1.0, 2.0]) -# @test isapprox(sol(10.0)[1], 0.1; atol = 1e-10, rtol = 1e-10) -#end -# -#@testset "SDESystem Discrete Callbacks" begin -# function testsol(ssys, u0, p, tspan; tstops = Float64[], paramtotest = nothing, -# kwargs...) -# sprob = SDEProblem(complete(ssys), u0, tspan, p; kwargs...) -# sol = solve(sprob, RI5(); tstops = tstops, abstol = 1e-10, reltol = 1e-10) -# @test isapprox(sol(1.0000000001)[1] - sol(0.999999999)[1], 1.0; rtol = 1e-4) -# paramtotest === nothing || (@test sol.ps[paramtotest] == 1.0) -# @test isapprox(sol(4.0)[1], 2 * exp(-2.0), atol = 1e-4) -# sol -# end -# -# @parameters k t1 t2 -# @variables A(t) B(t) -# -# cond1 = (t == t1) -# affect1 = [A ~ Pre(A) + 1] -# cb1 = cond1 => affect1 -# cond2 = (t == t2) -# affect2 = [k ~ 1.0] -# cb2 = cond2 => affect2 -# -# ∂ₜ = D -# eqs = [∂ₜ(A) ~ -k * A] -# @named ssys = SDESystem(eqs, [0.0], t, [A], [k, t1, t2], -# discrete_events = [cb1, cb2]) -# u0 = [A => 1.0] -# p = [k => 0.0, t1 => 1.0, t2 => 2.0] -# tspan = (0.0, 4.0) -# testsol(ssys, u0, p, tspan; tstops = [1.0, 2.0], paramtotest = k) -# -# cond1a = (t == t1) -# affect1a = [A ~ Pre(A) + 1, B ~ A] -# cb1a = cond1a => affect1a -# @named ssys1 = SDESystem(eqs, [0.0], t, [A, B], [k, t1, t2], -# discrete_events = [cb1a, cb2]) -# u0′ = [A => 1.0, B => 0.0] -# sol = testsol( -# ssys1, u0′, p, tspan; tstops = [1.0, 2.0], check_length = false, paramtotest = k) -# @test sol(1.0000001, idxs = 2) == 2.0 -# -# # same as above - but with set-time event syntax -# cb1‵ = [1.0] => affect1 # needs to be a Vector for the event to happen only once -# cb2‵ = [2.0] => affect2 -# @named ssys‵ = SDESystem(eqs, [0.0], t, [A], [k], discrete_events = [cb1‵, cb2‵]) -# testsol(ssys‵, u0, p, tspan; paramtotest = k) -# -# # mixing discrete affects -# @named ssys3 = SDESystem(eqs, [0.0], t, [A], [k, t1, t2], -# discrete_events = [cb1, cb2‵]) -# testsol(ssys3, u0, p, tspan; tstops = [1.0], paramtotest = k) -# -# # mixing with a func affect -# function affect!(integrator, u, p, ctx) -# setp(integrator, p.k)(integrator, 1.0) -# nothing -# end -# cb2‵‵ = [2.0] => (affect!, [], [k], [k], nothing) -# @named ssys4 = SDESystem(eqs, [0.0], t, [A], [k, t1], -# discrete_events = [cb1, cb2‵‵]) -# testsol(ssys4, u0, p, tspan; tstops = [1.0], paramtotest = k) -# -# # mixing with symbolic condition in the func affect -# cb2‵‵‵ = (t == t2) => (affect!, [], [k], [k], nothing) -# @named ssys5 = SDESystem(eqs, [0.0], t, [A], [k, t1, t2], -# discrete_events = [cb1, cb2‵‵‵]) -# testsol(ssys5, u0, p, tspan; tstops = [1.0, 2.0], paramtotest = k) -# @named ssys6 = SDESystem(eqs, [0.0], t, [A], [k, t1, t2], -# discrete_events = [cb2‵‵‵, cb1]) -# testsol(ssys6, u0, p, tspan; tstops = [1.0, 2.0], paramtotest = k) -# -# # mix a continuous event too -# cond3 = A ~ 0.1 -# affect3 = [k ~ 0.0] -# cb3 = cond3 => affect3 -# @named ssys7 = SDESystem(eqs, [0.0], t, [A], [k, t1, t2], -# discrete_events = [cb1, cb2‵‵‵], -# continuous_events = [cb3]) -# sol = testsol(ssys7, u0, p, (0.0, 10.0); tstops = [1.0, 2.0]) -# @test isapprox(sol(10.0)[1], 0.1; atol = 1e-10, rtol = 1e-10) -#end - -#@testset "JumpSystem Discrete Callbacks" begin -# function testsol(jsys, u0, p, tspan; tstops = Float64[], paramtotest = nothing, -# N = 40000, kwargs...) -# jsys = complete(jsys) -# dprob = DiscreteProblem(jsys, u0, tspan, p) -# jprob = JumpProblem(jsys, dprob, Direct(); kwargs...) -# sol = solve(jprob, SSAStepper(); tstops = tstops) -# @show sol -# @test (sol(1.000000000001)[1] - sol(0.99999999999)[1]) == 1 -# paramtotest === nothing || (@test sol.ps[paramtotest] == 1.0) -# @test sol(40.0)[1] == 0 -# sol -# end -# -# @parameters k t1 t2 -# @variables A(t) B(t) -# -# cond1 = (t == t1) -# affect1 = [A ~ Pre(A) + 1] -# cb1 = cond1 => affect1 -# cond2 = (t == t2) -# affect2 = [k ~ 1.0] -# cb2 = cond2 => affect2 -# -# eqs = [MassActionJump(k, [A => 1], [A => -1])] -# @named jsys = JumpSystem(eqs, t, [A], [k, t1, t2], discrete_events = [cb1, cb2]) -# u0 = [A => 1] -# p = [k => 0.0, t1 => 1.0, t2 => 2.0] -# tspan = (0.0, 40.0) -# testsol(jsys, u0, p, tspan; tstops = [1.0, 2.0], rng, paramtotest = k) -# -# cond1a = (t == t1) -# affect1a = [A ~ Pre(A) + 1, B ~ A] -# cb1a = cond1a => affect1a -# @named jsys1 = JumpSystem(eqs, t, [A, B], [k, t1, t2], discrete_events = [cb1a, cb2]) -# u0′ = [A => 1, B => 0] -# sol = testsol(jsys1, u0′, p, tspan; tstops = [1.0, 2.0], -# check_length = false, rng, paramtotest = k) -# @test sol(1.000000001, idxs = B) == 2 -# -# # same as above - but with set-time event syntax -# cb1‵ = [1.0] => affect1 # needs to be a Vector for the event to happen only once -# cb2‵ = [2.0] => affect2 -# @named jsys‵ = JumpSystem(eqs, t, [A], [k], discrete_events = [cb1‵, cb2‵]) -# testsol(jsys‵, u0, [p[1]], tspan; rng, paramtotest = k) -# -# # mixing discrete affects -# @named jsys3 = JumpSystem(eqs, t, [A], [k, t1, t2], discrete_events = [cb1, cb2‵]) -# testsol(jsys3, u0, p, tspan; tstops = [1.0], rng, paramtotest = k) -# -# # mixing with a func affect -# function affect!(integrator, u, p, ctx) -# integrator.ps[p.k] = 1.0 -# reset_aggregated_jumps!(integrator) -# nothing -# end -# cb2‵‵ = [2.0] => (affect!, [], [k], [k], nothing) -# @named jsys4 = JumpSystem(eqs, t, [A], [k, t1], discrete_events = [cb1, cb2‵‵]) -# testsol(jsys4, u0, p, tspan; tstops = [1.0], rng, paramtotest = k) -# -# # mixing with symbolic condition in the func affect -# cb2‵‵‵ = (t == t2) => (affect!, [], [k], [k], nothing) -# @named jsys5 = JumpSystem(eqs, t, [A], [k, t1, t2], discrete_events = [cb1, cb2‵‵‵]) -# testsol(jsys5, u0, p, tspan; tstops = [1.0, 2.0], rng, paramtotest = k) -# @named jsys6 = JumpSystem(eqs, t, [A], [k, t1, t2], discrete_events = [cb2‵‵‵, cb1]) -# testsol(jsys6, u0, p, tspan; tstops = [1.0, 2.0], rng, paramtotest = k) -#end -# -#@testset "Namespacing" begin -# function oscillator_ce(k = 1.0; name) -# sts = @variables x(t)=1.0 v(t)=0.0 F(t) -# ps = @parameters k=k Θ=0.5 -# eqs = [D(x) ~ v, D(v) ~ -k * x + F] -# ev = [x ~ Θ] => [x ~ 1.0, v ~ 0.0] -# ODESystem(eqs, t, sts, ps, continuous_events = [ev]; name) -# end -# -# @named oscce = oscillator_ce() -# eqs = [oscce.F ~ 0] -# @named eqs_sys = ODESystem(eqs, t) -# @named oneosc_ce = compose(eqs_sys, oscce) -# oneosc_ce_simpl = structural_simplify(oneosc_ce) -# -# prob = ODEProblem(oneosc_ce_simpl, [], (0.0, 2.0), []) -# sol = solve(prob, Tsit5(), saveat = 0.1) -# -# @test typeof(oneosc_ce_simpl) == ODESystem -# @test sol[1, 6] < 1.0 # test whether x(t) decreases over time -# @test sol[1, 18] > 0.5 # test whether event happened -#end - -#@testset "Additional SymbolicContinuousCallback options" begin -# # baseline affect (pos + neg + left root find) -# @variables c1(t)=1.0 c2(t)=1.0 # c1 = cos(t), c2 = cos(3t) -# eqs = [D(c1) ~ -sin(t); D(c2) ~ -3 * sin(3 * t)] -# record_crossings(i, u, _, c) = push!(c, i.t => i.u[u.v]) -# cr1 = [] -# cr2 = [] -# evt1 = ModelingToolkit.SymbolicContinuousCallback( -# [c1 ~ 0], (record_crossings, [c1 => :v], [], [], cr1)) -# evt2 = ModelingToolkit.SymbolicContinuousCallback( -# [c2 ~ 0], (record_crossings, [c2 => :v], [], [], cr2)) -# @named trigsys = ODESystem(eqs, t; continuous_events = [evt1, evt2]) -# trigsys_ss = structural_simplify(trigsys) -# prob = ODEProblem(trigsys_ss, [], (0.0, 2π)) -# sol = solve(prob, Tsit5()) -# required_crossings_c1 = [π / 2, 3 * π / 2] -# required_crossings_c2 = [π / 6, π / 2, 5 * π / 6, 7 * π / 6, 3 * π / 2, 11 * π / 6] -# @test maximum(abs.(first.(cr1) .- required_crossings_c1)) < 1e-4 -# @test maximum(abs.(first.(cr2) .- required_crossings_c2)) < 1e-4 -# @test sign.(cos.(required_crossings_c1 .- 1e-6)) == sign.(last.(cr1)) -# @test sign.(cos.(3 * (required_crossings_c2 .- 1e-6))) == sign.(last.(cr2)) -# -# # with neg affect (pos * neg + left root find) -# cr1p = [] -# cr2p = [] -# cr1n = [] -# cr2n = [] -# evt1 = ModelingToolkit.SymbolicContinuousCallback( -# [c1 ~ 0], (record_crossings, [c1 => :v], [], [], cr1p); -# affect_neg = (record_crossings, [c1 => :v], [], [], cr1n)) -# evt2 = ModelingToolkit.SymbolicContinuousCallback( -# [c2 ~ 0], (record_crossings, [c2 => :v], [], [], cr2p); -# affect_neg = (record_crossings, [c2 => :v], [], [], cr2n)) -# @named trigsys = ODESystem(eqs, t; continuous_events = [evt1, evt2]) -# trigsys_ss = structural_simplify(trigsys) -# prob = ODEProblem(trigsys_ss, [], (0.0, 2π)) -# sol = solve(prob, Tsit5(); dtmax = 0.01) -# c1_pc = filter((<=)(0) ∘ sin, required_crossings_c1) -# c1_nc = filter((>=)(0) ∘ sin, required_crossings_c1) -# c2_pc = filter(c -> -sin(3c) > 0, required_crossings_c2) -# c2_nc = filter(c -> -sin(3c) < 0, required_crossings_c2) -# @test maximum(abs.(c1_pc .- first.(cr1p))) < 1e-5 -# @test maximum(abs.(c1_nc .- first.(cr1n))) < 1e-5 -# @test maximum(abs.(c2_pc .- first.(cr2p))) < 1e-5 -# @test maximum(abs.(c2_nc .- first.(cr2n))) < 1e-5 -# @test sign.(cos.(c1_pc .- 1e-6)) == sign.(last.(cr1p)) -# @test sign.(cos.(c1_nc .- 1e-6)) == sign.(last.(cr1n)) -# @test sign.(cos.(3 * (c2_pc .- 1e-6))) == sign.(last.(cr2p)) -# @test sign.(cos.(3 * (c2_nc .- 1e-6))) == sign.(last.(cr2n)) -# -# # with nothing neg affect (pos * neg + left root find) -# cr1p = [] -# cr2p = [] -# evt1 = ModelingToolkit.SymbolicContinuousCallback( -# [c1 ~ 0], (record_crossings, [c1 => :v], [], [], cr1p); affect_neg = nothing) -# evt2 = ModelingToolkit.SymbolicContinuousCallback( -# [c2 ~ 0], (record_crossings, [c2 => :v], [], [], cr2p); affect_neg = nothing) -# @named trigsys = ODESystem(eqs, t; continuous_events = [evt1, evt2]) -# trigsys_ss = structural_simplify(trigsys) -# prob = ODEProblem(trigsys_ss, [], (0.0, 2π)) -# sol = solve(prob, Tsit5(); dtmax = 0.01) -# @test maximum(abs.(c1_pc .- first.(cr1p))) < 1e-5 -# @test maximum(abs.(c2_pc .- first.(cr2p))) < 1e-5 -# @test sign.(cos.(c1_pc .- 1e-6)) == sign.(last.(cr1p)) -# @test sign.(cos.(3 * (c2_pc .- 1e-6))) == sign.(last.(cr2p)) -# -# #mixed -# cr1p = [] -# cr2p = [] -# cr1n = [] -# cr2n = [] -# evt1 = ModelingToolkit.SymbolicContinuousCallback( -# [c1 ~ 0], (record_crossings, [c1 => :v], [], [], cr1p); affect_neg = nothing) -# evt2 = ModelingToolkit.SymbolicContinuousCallback( -# [c2 ~ 0], (record_crossings, [c2 => :v], [], [], cr2p); -# affect_neg = (record_crossings, [c2 => :v], [], [], cr2n)) -# @named trigsys = ODESystem(eqs, t; continuous_events = [evt1, evt2]) -# trigsys_ss = structural_simplify(trigsys) -# prob = ODEProblem(trigsys_ss, [], (0.0, 2π)) -# sol = solve(prob, Tsit5(); dtmax = 0.01) -# c1_pc = filter((<=)(0) ∘ sin, required_crossings_c1) -# c2_pc = filter(c -> -sin(3c) > 0, required_crossings_c2) -# c2_nc = filter(c -> -sin(3c) < 0, required_crossings_c2) -# @test maximum(abs.(c1_pc .- first.(cr1p))) < 1e-5 -# @test maximum(abs.(c2_pc .- first.(cr2p))) < 1e-5 -# @test maximum(abs.(c2_nc .- first.(cr2n))) < 1e-5 -# @test sign.(cos.(c1_pc .- 1e-6)) == sign.(last.(cr1p)) -# @test sign.(cos.(3 * (c2_pc .- 1e-6))) == sign.(last.(cr2p)) -# @test sign.(cos.(3 * (c2_nc .- 1e-6))) == sign.(last.(cr2n)) -# -# # baseline affect w/ right rootfind (pos + neg + right root find) -# @variables c1(t)=1.0 c2(t)=1.0 # c1 = cos(t), c2 = cos(3t) -# cr1 = [] -# cr2 = [] -# evt1 = ModelingToolkit.SymbolicContinuousCallback( -# [c1 ~ 0], (record_crossings, [c1 => :v], [], [], cr1); -# rootfind = SciMLBase.RightRootFind) -# evt2 = ModelingToolkit.SymbolicContinuousCallback( -# [c2 ~ 0], (record_crossings, [c2 => :v], [], [], cr2); -# rootfind = SciMLBase.RightRootFind) -# @named trigsys = ODESystem(eqs, t; continuous_events = [evt1, evt2]) -# trigsys_ss = structural_simplify(trigsys) -# prob = ODEProblem(trigsys_ss, [], (0.0, 2π)) -# sol = solve(prob, Tsit5(); dtmax = 0.01) -# required_crossings_c1 = [π / 2, 3 * π / 2] -# required_crossings_c2 = [π / 6, π / 2, 5 * π / 6, 7 * π / 6, 3 * π / 2, 11 * π / 6] -# @test maximum(abs.(first.(cr1) .- required_crossings_c1)) < 1e-4 -# @test maximum(abs.(first.(cr2) .- required_crossings_c2)) < 1e-4 -# @test sign.(cos.(required_crossings_c1 .+ 1e-6)) == sign.(last.(cr1)) -# @test sign.(cos.(3 * (required_crossings_c2 .+ 1e-6))) == sign.(last.(cr2)) -# -# # baseline affect w/ mixed rootfind (pos + neg + right root find) -# cr1 = [] -# cr2 = [] -# evt1 = ModelingToolkit.SymbolicContinuousCallback( -# [c1 ~ 0], (record_crossings, [c1 => :v], [], [], cr1); -# rootfind = SciMLBase.LeftRootFind) -# evt2 = ModelingToolkit.SymbolicContinuousCallback( -# [c2 ~ 0], (record_crossings, [c2 => :v], [], [], cr2); -# rootfind = SciMLBase.RightRootFind) -# @named trigsys = ODESystem(eqs, t; continuous_events = [evt1, evt2]) -# trigsys_ss = structural_simplify(trigsys) -# prob = ODEProblem(trigsys_ss, [], (0.0, 2π)) -# sol = solve(prob, Tsit5()) -# @test maximum(abs.(first.(cr1) .- required_crossings_c1)) < 1e-4 -# @test maximum(abs.(first.(cr2) .- required_crossings_c2)) < 1e-4 -# @test sign.(cos.(required_crossings_c1 .- 1e-6)) == sign.(last.(cr1)) -# @test sign.(cos.(3 * (required_crossings_c2 .+ 1e-6))) == sign.(last.(cr2)) -# -# #flip order and ensure results are okay -# cr1 = [] -# cr2 = [] -# evt1 = ModelingToolkit.SymbolicContinuousCallback( -# [c1 ~ 0], (record_crossings, [c1 => :v], [], [], cr1); -# rootfind = SciMLBase.LeftRootFind) -# evt2 = ModelingToolkit.SymbolicContinuousCallback( -# [c2 ~ 0], (record_crossings, [c2 => :v], [], [], cr2); -# rootfind = SciMLBase.RightRootFind) -# @named trigsys = ODESystem(eqs, t; continuous_events = [evt2, evt1]) -# trigsys_ss = structural_simplify(trigsys) -# prob = ODEProblem(trigsys_ss, [], (0.0, 2π)) -# sol = solve(prob, Tsit5()) -# @test maximum(abs.(first.(cr1) .- required_crossings_c1)) < 1e-4 -# @test maximum(abs.(first.(cr2) .- required_crossings_c2)) < 1e-4 -# @test sign.(cos.(required_crossings_c1 .- 1e-6)) == sign.(last.(cr1)) -# @test sign.(cos.(3 * (required_crossings_c2 .+ 1e-6))) == sign.(last.(cr2)) -#end +@testset "SymbolicContinuousCallback constructors" begin + e = SymbolicContinuousCallback(eqs[]) + @test e isa SymbolicContinuousCallback + @test isequal(equations(e), eqs) + @test e.affect == nothing + @test e.affect_neg == nothing + @test e.rootfind == SciMLBase.LeftRootFind + + e = SymbolicContinuousCallback(eqs) + @test e isa SymbolicContinuousCallback + @test isequal(equations(e), eqs) + @test e.affect == nothing + @test e.affect_neg == nothing + @test e.rootfind == SciMLBase.LeftRootFind + + e = SymbolicContinuousCallback(eqs, nothing) + @test e isa SymbolicContinuousCallback + @test isequal(equations(e), eqs) + @test e.affect == nothing + @test e.affect_neg == nothing + @test e.rootfind == SciMLBase.LeftRootFind + + e = SymbolicContinuousCallback(eqs[], nothing) + @test e isa SymbolicContinuousCallback + @test isequal(equations(e), eqs) + @test e.affect == nothing + @test e.affect_neg == nothing + @test e.rootfind == SciMLBase.LeftRootFind + + e = SymbolicContinuousCallback(eqs => nothing) + @test e isa SymbolicContinuousCallback + @test isequal(equations(e), eqs) + @test e.affect == nothing + @test e.affect_neg == nothing + @test e.rootfind == SciMLBase.LeftRootFind + + e = SymbolicContinuousCallback(eqs[] => nothing) + @test e isa SymbolicContinuousCallback + @test isequal(equations(e), eqs) + @test e.affect == nothing + @test e.affect_neg == nothing + @test e.rootfind == SciMLBase.LeftRootFind + + ## With affect + e = SymbolicContinuousCallback(eqs[], affect) + @test e isa SymbolicContinuousCallback + @test isequal(equations(e), eqs) + @test observed(system(affects(e))) == affect + @test observed(system(affect_negs(e))) == affect + @test e.rootfind == SciMLBase.LeftRootFind + + # with only positive edge affect + e = SymbolicContinuousCallback(eqs[], affect, affect_neg = nothing) + @test e isa SymbolicContinuousCallback + @test isequal(equations(e), eqs) + @test observed(system(affects(e))) == affect + @test isnothing(e.affect_neg) + @test e.rootfind == SciMLBase.LeftRootFind + + # with explicit edge affects + e = SymbolicContinuousCallback(eqs[], affect, affect_neg = affect_neg) + @test e isa SymbolicContinuousCallback + @test isequal(equations(e), eqs) + @test observed(system(affects(e))) == affect + @test observed(system(affect_negs(e))) == affect_neg + @test e.rootfind == SciMLBase.LeftRootFind + + # with different root finding ops + e = SymbolicContinuousCallback( + eqs[], affect, affect_neg = affect_neg, rootfind = SciMLBase.LeftRootFind) + @test e isa SymbolicContinuousCallback + @test isequal(equations(e), eqs) + @test e.rootfind == SciMLBase.LeftRootFind + + # test plural constructor + e = SymbolicContinuousCallbacks(eqs[]) + @test e isa Vector{SymbolicContinuousCallback} + @test isequal(equations(e[]), eqs) + @test e[].affect == nothing + + e = SymbolicContinuousCallbacks(eqs) + @test e isa Vector{SymbolicContinuousCallback} + @test isequal(equations(e[]), eqs) + @test e[].affect == nothing + + e = SymbolicContinuousCallbacks(eqs[] => affect) + @test e isa Vector{SymbolicContinuousCallback} + @test isequal(equations(e[]), eqs) + @test e[].affect isa AffectSystem + + e = SymbolicContinuousCallbacks(eqs => affect) + @test e isa Vector{SymbolicContinuousCallback} + @test isequal(equations(e[]), eqs) + @test e[].affect isa AffectSystem + + e = SymbolicContinuousCallbacks([eqs[] => affect]) + @test e isa Vector{SymbolicContinuousCallback} + @test isequal(equations(e[]), eqs) + @test e[].affect isa AffectSystem + + e = SymbolicContinuousCallbacks([eqs => affect]) + @test e isa Vector{SymbolicContinuousCallback} + @test isequal(equations(e[]), eqs) + @test e[].affect isa AffectSystem +end + +@testset "ImperativeAffect constructors" begin + fmfa(o, x, i, c) = nothing + m = ModelingToolkit.ImperativeAffect(fmfa) + @test m isa ModelingToolkit.ImperativeAffect + @test m.f == fmfa + @test m.obs == [] + @test m.obs_syms == [] + @test m.modified == [] + @test m.mod_syms == [] + @test m.ctx === nothing + + m = ModelingToolkit.ImperativeAffect(fmfa, (;)) + @test m isa ModelingToolkit.ImperativeAffect + @test m.f == fmfa + @test m.obs == [] + @test m.obs_syms == [] + @test m.modified == [] + @test m.mod_syms == [] + @test m.ctx === nothing + + m = ModelingToolkit.ImperativeAffect(fmfa, (; x)) + @test m isa ModelingToolkit.ImperativeAffect + @test m.f == fmfa + @test isequal(m.obs, []) + @test m.obs_syms == [] + @test isequal(m.modified, [x]) + @test m.mod_syms == [:x] + @test m.ctx === nothing + + m = ModelingToolkit.ImperativeAffect(fmfa, (; y = x)) + @test m isa ModelingToolkit.ImperativeAffect + @test m.f == fmfa + @test isequal(m.obs, []) + @test m.obs_syms == [] + @test isequal(m.modified, [x]) + @test m.mod_syms == [:y] + @test m.ctx === nothing + + m = ModelingToolkit.ImperativeAffect(fmfa; observed = (; y = x)) + @test m isa ModelingToolkit.ImperativeAffect + @test m.f == fmfa + @test isequal(m.obs, [x]) + @test m.obs_syms == [:y] + @test m.modified == [] + @test m.mod_syms == [] + @test m.ctx === nothing + + m = ModelingToolkit.ImperativeAffect(fmfa; modified = (; x)) + @test m isa ModelingToolkit.ImperativeAffect + @test m.f == fmfa + @test isequal(m.obs, []) + @test m.obs_syms == [] + @test isequal(m.modified, [x]) + @test m.mod_syms == [:x] + @test m.ctx === nothing + + m = ModelingToolkit.ImperativeAffect(fmfa; modified = (; y = x)) + @test m isa ModelingToolkit.ImperativeAffect + @test m.f == fmfa + @test isequal(m.obs, []) + @test m.obs_syms == [] + @test isequal(m.modified, [x]) + @test m.mod_syms == [:y] + @test m.ctx === nothing + + m = ModelingToolkit.ImperativeAffect(fmfa, (; x), (; x)) + @test m isa ModelingToolkit.ImperativeAffect + @test m.f == fmfa + @test isequal(m.obs, [x]) + @test m.obs_syms == [:x] + @test isequal(m.modified, [x]) + @test m.mod_syms == [:x] + @test m.ctx === nothing + + m = ModelingToolkit.ImperativeAffect(fmfa, (; y = x), (; y = x)) + @test m isa ModelingToolkit.ImperativeAffect + @test m.f == fmfa + @test isequal(m.obs, [x]) + @test m.obs_syms == [:y] + @test isequal(m.modified, [x]) + @test m.mod_syms == [:y] + @test m.ctx === nothing + + m = ModelingToolkit.ImperativeAffect( + fmfa; modified = (; y = x), observed = (; y = x)) + @test m isa ModelingToolkit.ImperativeAffect + @test m.f == fmfa + @test isequal(m.obs, [x]) + @test m.obs_syms == [:y] + @test isequal(m.modified, [x]) + @test m.mod_syms == [:y] + @test m.ctx === nothing + + m = ModelingToolkit.ImperativeAffect( + fmfa; modified = (; y = x), observed = (; y = x), ctx = 3) + @test m isa ModelingToolkit.ImperativeAffect + @test m.f == fmfa + @test isequal(m.obs, [x]) + @test m.obs_syms == [:y] + @test isequal(m.modified, [x]) + @test m.mod_syms == [:y] + @test m.ctx === 3 + + m = ModelingToolkit.ImperativeAffect(fmfa, (; x), (; x), 3) + @test m isa ModelingToolkit.ImperativeAffect + @test m.f == fmfa + @test isequal(m.obs, [x]) + @test m.obs_syms == [:x] + @test isequal(m.modified, [x]) + @test m.mod_syms == [:x] + @test m.ctx === 3 +end + +@testset "Condition Compilation" begin + @named sys = ODESystem(eqs, t, continuous_events = [x ~ 1]) + @test getfield(sys, :continuous_events)[] == + SymbolicContinuousCallback(Equation[x ~ 1], nothing) + @test isequal(equations(getfield(sys, :continuous_events))[], x ~ 1) + fsys = flatten(sys) + @test isequal(equations(getfield(fsys, :continuous_events))[], x ~ 1) + + @named sys2 = ODESystem([D(x) ~ 1], t, continuous_events = [x ~ 2], systems = [sys]) + @test getfield(sys2, :continuous_events)[] == + SymbolicContinuousCallback(Equation[x ~ 2], nothing) + @test all(ModelingToolkit.continuous_events(sys2) .== [ + SymbolicContinuousCallback(Equation[x ~ 2], nothing), + SymbolicContinuousCallback(Equation[sys.x ~ 1], nothing) + ]) + + @test isequal(equations(getfield(sys2, :continuous_events))[1], x ~ 2) + @test length(ModelingToolkit.continuous_events(sys2)) == 2 + @test isequal(equations(ModelingToolkit.continuous_events(sys2)[1])[], x ~ 2) + @test isequal(equations(ModelingToolkit.continuous_events(sys2)[2])[], sys.x ~ 1) + + sys = complete(sys) + sys_nosplit = complete(sys; split = false) + sys2 = complete(sys2) + + # Test proper rootfinding + prob = ODEProblem(sys, Pair[], (0.0, 2.0)) + p0 = 0 + t0 = 0 + @test get_callback(prob) isa ModelingToolkit.DiffEqCallbacks.ContinuousCallback + cb = ModelingToolkit.generate_continuous_callbacks(sys) + cond = cb.condition + out = [0.0] + cond.f_iip(out, [0], p0, t0) + @test out[] ≈ -1 # signature is u,p,t + cond.f_iip(out, [1], p0, t0) + @test out[] ≈ 0 # signature is u,p,t + cond.f_iip(out, [2], p0, t0) + @test out[] ≈ 1 # signature is u,p,t + + prob = ODEProblem(sys, Pair[], (0.0, 2.0)) + prob_nosplit = ODEProblem(sys_nosplit, Pair[], (0.0, 2.0)) + sol = solve(prob, Tsit5()) + sol_nosplit = solve(prob_nosplit, Tsit5()) + @test minimum(t -> abs(t - 1), sol.t) < 1e-10 # test that the solver stepped at the root + @test minimum(t -> abs(t - 1), sol_nosplit.t) < 1e-10 # test that the solver stepped at the root + + # Test user-provided callback is respected + test_callback = DiscreteCallback(x -> x, x -> x) + prob = ODEProblem(sys, Pair[], (0.0, 2.0), callback = test_callback) + prob_nosplit = ODEProblem(sys_nosplit, Pair[], (0.0, 2.0), callback = test_callback) + cbs = get_callback(prob) + cbs_nosplit = get_callback(prob_nosplit) + @test cbs isa CallbackSet + @test cbs.discrete_callbacks[1] == test_callback + @test cbs_nosplit isa CallbackSet + @test cbs_nosplit.discrete_callbacks[1] == test_callback + + prob = ODEProblem(sys2, Pair[], (0.0, 3.0)) + cb = get_callback(prob) + @test cb isa ModelingToolkit.DiffEqCallbacks.VectorContinuousCallback + + cond = cb.condition + out = [0.0, 0.0] + # the root to find is 2 + cond.f_iip(out, [0, 0], p0, t0) + @test out[1] ≈ -2 # signature is u,p,t + cond.f_iip(out, [1, 0], p0, t0) + @test out[1] ≈ -1 # signature is u,p,t + cond.f_iip(out, [2, 0], p0, t0) # this should return 0 + @test out[1] ≈ 0 # signature is u,p,t + + # the root to find is 1 + out = [0.0, 0.0] + cond.f_iip(out, [0, 0], p0, t0) + @test out[2] ≈ -1 # signature is u,p,t + cond.f_iip(out, [0, 1], p0, t0) # this should return 0 + @test out[2] ≈ 0 # signature is u,p,t + cond.f_iip(out, [0, 2], p0, t0) + @test out[2] ≈ 1 # signature is u,p,t + + sol = solve(prob, Tsit5()) + @test minimum(t -> abs(t - 1), sol.t) < 1e-10 # test that the solver stepped at the first root + @test minimum(t -> abs(t - 2), sol.t) < 1e-10 # test that the solver stepped at the second root + + @named sys = ODESystem(eqs, t, continuous_events = [x ~ 1, x ~ 2]) # two root eqs using the same unknown + sys = complete(sys) + prob = ODEProblem(sys, Pair[], (0.0, 3.0)) + @test get_callback(prob) isa ModelingToolkit.DiffEqCallbacks.VectorContinuousCallback + sol = solve(prob, Tsit5()) + @test minimum(t -> abs(t - 1), sol.t) < 1e-10 # test that the solver stepped at the first root + @test minimum(t -> abs(t - 2), sol.t) < 1e-10 # test that the solver stepped at the second root +end + +@testset "Bouncing Ball" begin + ###### 1D Bounce + @variables x(t)=1 v(t)=0 + + root_eqs = [x ~ 0] + affect = [v ~ -Pre(v)] + + @named ball = ODESystem( + [D(x) ~ v + D(v) ~ -9.8], t, continuous_events = root_eqs => affect) + + @test only(continuous_events(ball)) == + SymbolicContinuousCallback(Equation[x ~ 0], Equation[v ~ -Pre(v)]) + ball = structural_simplify(ball) + + @test length(ModelingToolkit.continuous_events(ball)) == 1 + + tspan = (0.0, 5.0) + prob = ODEProblem(ball, Pair[], tspan) + sol = solve(prob, Tsit5()) + @test 0 <= minimum(sol[x]) <= 1e-10 # the ball never went through the floor but got very close + + ###### 2D bouncing ball + @variables x(t)=1 y(t)=0 vx(t)=0 vy(t)=1 + + events = [[x ~ 0] => [vx ~ -Pre(vx)] + [y ~ -1.5, y ~ 1.5] => [vy ~ -Pre(vy)]] + + @named ball = ODESystem( + [D(x) ~ vx + D(y) ~ vy + D(vx) ~ -9.8 + D(vy) ~ -0.01vy], t; continuous_events = events) + + _ball = ball + ball = structural_simplify(_ball) + ball_nosplit = structural_simplify(_ball; split = false) + + tspan = (0.0, 5.0) + prob = ODEProblem(ball, Pair[], tspan) + prob_nosplit = ODEProblem(ball_nosplit, Pair[], tspan) + + cb = get_callback(prob) + @test cb isa ModelingToolkit.DiffEqCallbacks.VectorContinuousCallback + @test getfield(ball, :continuous_events)[1] == + SymbolicContinuousCallback(Equation[x ~ 0], Equation[vx ~ -Pre(vx)]) + @test getfield(ball, :continuous_events)[2] == + SymbolicContinuousCallback(Equation[y ~ -1.5, y ~ 1.5], Equation[vy ~ -Pre(vy)]) + cond = cb.condition + out = [0.0, 0.0, 0.0] + p0 = 0. + t0 = 0. + cond.f_iip(out, [0, 0, 0, 0], p0, t0) + @test out ≈ [0, 1.5, -1.5] + + sol = solve(prob, Tsit5()) + sol_nosplit = solve(prob_nosplit, Tsit5()) + @test 0 <= minimum(sol[x]) <= 1e-10 # the ball never went through the floor but got very close + @test minimum(sol[y]) ≈ -1.5 # check wall conditions + @test maximum(sol[y]) ≈ 1.5 # check wall conditions + @test 0 <= minimum(sol_nosplit[x]) <= 1e-10 # the ball never went through the floor but got very close + @test minimum(sol_nosplit[y]) ≈ -1.5 # check wall conditions + @test maximum(sol_nosplit[y]) ≈ 1.5 # check wall conditions + + ## Test multi-variable affect + # in this test, there are two variables affected by a single event. + events = [[x ~ 0] => [vx ~ -Pre(vx), vy ~ -Pre(vy)]] + + @named ball = ODESystem([D(x) ~ vx + D(y) ~ vy + D(vx) ~ -1 + D(vy) ~ 0], t; continuous_events = events) + + ball_nosplit = structural_simplify(ball) + ball = structural_simplify(ball) + + tspan = (0.0, 5.0) + prob = ODEProblem(ball, Pair[], tspan) + prob_nosplit = ODEProblem(ball_nosplit, Pair[], tspan) + sol = solve(prob, Tsit5()) + sol_nosplit = solve(prob_nosplit, Tsit5()) + @test 0 <= minimum(sol[x]) <= 1e-10 # the ball never went through the floor but got very close + @test -minimum(sol[y]) ≈ maximum(sol[y]) ≈ sqrt(2) # the ball will never go further than √2 in either direction (gravity was changed to 1 to get this particular number) + @test 0 <= minimum(sol_nosplit[x]) <= 1e-10 # the ball never went through the floor but got very close + @test -minimum(sol_nosplit[y]) ≈ maximum(sol_nosplit[y]) ≈ sqrt(2) # the ball will never go further than √2 in either direction (gravity was changed to 1 to get this particular number) +end + +# issue https://github.com/SciML/ModelingToolkit.jl/issues/1386 +# tests that it works for ODAESystem +@testset "ODAESystem" begin + @variables vs(t) v(t) vmeasured(t) + eq = [vs ~ sin(2pi * t) + D(v) ~ vs - v + D(vmeasured) ~ 0.0] + ev = [sin(20pi * t) ~ 0.0] => [vmeasured ~ Pre(v)] + @named sys = ODESystem(eq, t, continuous_events = ev) + sys = structural_simplify(sys) + prob = ODEProblem(sys, zeros(2), (0.0, 5.1)) + sol = solve(prob, Tsit5()) + @test all(minimum((0:0.1:5) .- sol.t', dims = 2) .< 0.0001) # test that the solver stepped every 0.1s as dictated by event + @test sol([0.25])[vmeasured][] == sol([0.23])[vmeasured][] # test the hold property +end + +## https://github.com/SciML/ModelingToolkit.jl/issues/1528 +@testset "Handle Empty Events" begin + Dₜ = D + + @parameters u(t) [input = true] # Indicate that this is a controlled input + @parameters y(t) [output = true] # Indicate that this is a measured output + + function Mass(; name, m = 1.0, p = 0, v = 0) + ps = @parameters m = m + sts = @variables pos(t)=p vel(t)=v + eqs = Dₜ(pos) ~ vel + ODESystem(eqs, t, [pos, vel], ps; name) + end + function Spring(; name, k = 1e4) + ps = @parameters k = k + @variables x(t) = 0 # Spring deflection + ODESystem(Equation[], t, [x], ps; name) + end + function Damper(; name, c = 10) + ps = @parameters c = c + @variables vel(t) = 0 + ODESystem(Equation[], t, [vel], ps; name) + end + function SpringDamper(; name, k = false, c = false) + spring = Spring(; name = :spring, k) + damper = Damper(; name = :damper, c) + compose(ODESystem(Equation[], t; name), + spring, damper) + end + connect_sd(sd, m1, m2) = [ + sd.spring.x ~ m1.pos - m2.pos, sd.damper.vel ~ m1.vel - m2.vel] + sd_force(sd) = -sd.spring.k * sd.spring.x - sd.damper.c * sd.damper.vel + @named mass1 = Mass(; m = 1) + @named mass2 = Mass(; m = 1) + @named sd = SpringDamper(; k = 1000, c = 10) + function Model(u, d = 0) + eqs = [connect_sd(sd, mass1, mass2) + Dₜ(mass1.vel) ~ (sd_force(sd) + u) / mass1.m + Dₜ(mass2.vel) ~ (-sd_force(sd) + d) / mass2.m] + @named _model = ODESystem(eqs, t; observed = [y ~ mass2.pos]) + @named model = compose(_model, mass1, mass2, sd) + end + model = Model(sin(30t)) + sys = structural_simplify(model) + @test isempty(ModelingToolkit.continuous_events(sys)) +end + +@testset "ODESystem Discrete Callbacks" begin + function testsol(osys, u0, p, tspan; tstops = Float64[], paramtotest = nothing, + kwargs...) + oprob = ODEProblem(complete(osys), u0, tspan, p; kwargs...) + sol = solve(oprob, Tsit5(); tstops = tstops, abstol = 1e-10, reltol = 1e-10) + @test isapprox(sol(1.0000000001)[1] - sol(0.999999999)[1], 1.0; rtol = 1e-6) + paramtotest === nothing || (@test sol.ps[paramtotest] == 1.0) + @test isapprox(sol(4.0)[1], 2 * exp(-2.0)) + sol + end + + @parameters k t1 t2 + @variables A(t) B(t) + + cond1 = (t == t1) + affect1 = [A ~ Pre(A) + 1] + cb1 = cond1 => affect1 + cond2 = (t == t2) + affect2 = [k ~ 1.0] + cb2 = cond2 => affect2 + + ∂ₜ = D + eqs = [∂ₜ(A) ~ -k * A] + @named osys = ODESystem(eqs, t, [A], [k, t1, t2], discrete_events = [cb1, cb2]) + u0 = [A => 1.0] + p = [k => 0.0, t1 => 1.0, t2 => 2.0] + tspan = (0.0, 4.0) + testsol(osys, u0, p, tspan; tstops = [1.0, 2.0], paramtotest = k) + + cond1a = (t == t1) + affect1a = [A ~ Pre(A) + 1, B ~ A] + cb1a = cond1a => affect1a + @named osys1 = ODESystem(eqs, t, [A, B], [k, t1, t2], discrete_events = [cb1a, cb2]) + u0′ = [A => 1.0, B => 0.0] + sol = testsol( + osys1, u0′, p, tspan; tstops = [1.0, 2.0], check_length = false, paramtotest = k) + @test sol(1.0000001, idxs = B) == 2.0 + + # same as above - but with set-time event syntax + cb1‵ = [1.0] => affect1 # needs to be a Vector for the event to happen only once + cb2‵ = [2.0] => affect2 + @named osys‵ = ODESystem(eqs, t, [A], [k], discrete_events = [cb1‵, cb2‵]) + testsol(osys‵, u0, p, tspan; paramtotest = k) + + # mixing discrete affects + @named osys3 = ODESystem(eqs, t, [A], [k, t1, t2], discrete_events = [cb1, cb2‵]) + testsol(osys3, u0, p, tspan; tstops = [1.0], paramtotest = k) + + # mixing with a func affect + function affect!(integrator, u, p, ctx) + integrator.ps[p.k] = 1.0 + nothing + end + cb2‵‵ = [2.0] => (affect!, [], [k], [k], nothing) + @named osys4 = ODESystem(eqs, t, [A], [k, t1], discrete_events = [cb1, cb2‵‵]) + oprob4 = ODEProblem(complete(osys4), u0, tspan, p) + testsol(osys4, u0, p, tspan; tstops = [1.0], paramtotest = k) + + # mixing with symbolic condition in the func affect + cb2‵‵‵ = (t == t2) => (affect!, [], [k], [k], nothing) + @named osys5 = ODESystem(eqs, t, [A], [k, t1, t2], discrete_events = [cb1, cb2‵‵‵]) + testsol(osys5, u0, p, tspan; tstops = [1.0, 2.0]) + @named osys6 = ODESystem(eqs, t, [A], [k, t1, t2], discrete_events = [cb2‵‵‵, cb1]) + testsol(osys6, u0, p, tspan; tstops = [1.0, 2.0], paramtotest = k) + + # mix a continuous event too + cond3 = A ~ 0.1 + affect3 = [k ~ 0.0] + cb3 = cond3 => affect3 + @named osys7 = ODESystem(eqs, t, [A], [k, t1, t2], discrete_events = [cb1, cb2‵‵‵], + continuous_events = [cb3]) + sol = testsol(osys7, u0, p, (0.0, 10.0); tstops = [1.0, 2.0]) + @test isapprox(sol(10.0)[1], 0.1; atol = 1e-10, rtol = 1e-10) +end + +@testset "SDESystem Discrete Callbacks" begin + function testsol(ssys, u0, p, tspan; tstops = Float64[], paramtotest = nothing, + kwargs...) + sprob = SDEProblem(complete(ssys), u0, tspan, p; kwargs...) + sol = solve(sprob, RI5(); tstops = tstops, abstol = 1e-10, reltol = 1e-10) + @test isapprox(sol(1.0000000001)[1] - sol(0.999999999)[1], 1.0; rtol = 1e-4) + paramtotest === nothing || (@test sol.ps[paramtotest] == 1.0) + @test isapprox(sol(4.0)[1], 2 * exp(-2.0), atol = 1e-4) + sol + end + + @parameters k t1 t2 + @variables A(t) B(t) + + cond1 = (t == t1) + affect1 = [A ~ Pre(A) + 1] + cb1 = cond1 => affect1 + cond2 = (t == t2) + affect2 = [k ~ 1.0] + cb2 = cond2 => affect2 + + ∂ₜ = D + eqs = [∂ₜ(A) ~ -k * A] + @named ssys = SDESystem(eqs, [0.0], t, [A], [k, t1, t2], + discrete_events = [cb1, cb2]) + u0 = [A => 1.0] + p = [k => 0.0, t1 => 1.0, t2 => 2.0] + tspan = (0.0, 4.0) + testsol(ssys, u0, p, tspan; tstops = [1.0, 2.0], paramtotest = k) + + cond1a = (t == t1) + affect1a = [A ~ Pre(A) + 1, B ~ A] + cb1a = cond1a => affect1a + @named ssys1 = SDESystem(eqs, [0.0], t, [A, B], [k, t1, t2], + discrete_events = [cb1a, cb2]) + u0′ = [A => 1.0, B => 0.0] + sol = testsol( + ssys1, u0′, p, tspan; tstops = [1.0, 2.0], check_length = false, paramtotest = k) + @test sol(1.0000001, idxs = 2) == 2.0 + + # same as above - but with set-time event syntax + cb1‵ = [1.0] => affect1 # needs to be a Vector for the event to happen only once + cb2‵ = [2.0] => affect2 + @named ssys‵ = SDESystem(eqs, [0.0], t, [A], [k], discrete_events = [cb1‵, cb2‵]) + testsol(ssys‵, u0, p, tspan; paramtotest = k) + + # mixing discrete affects + @named ssys3 = SDESystem(eqs, [0.0], t, [A], [k, t1, t2], + discrete_events = [cb1, cb2‵]) + testsol(ssys3, u0, p, tspan; tstops = [1.0], paramtotest = k) + + # mixing with a func affect + function affect!(integrator, u, p, ctx) + setp(integrator, p.k)(integrator, 1.0) + nothing + end + cb2‵‵ = [2.0] => (affect!, [], [k], [k], nothing) + @named ssys4 = SDESystem(eqs, [0.0], t, [A], [k, t1], + discrete_events = [cb1, cb2‵‵]) + testsol(ssys4, u0, p, tspan; tstops = [1.0], paramtotest = k) + + # mixing with symbolic condition in the func affect + cb2‵‵‵ = (t == t2) => (affect!, [], [k], [k], nothing) + @named ssys5 = SDESystem(eqs, [0.0], t, [A], [k, t1, t2], + discrete_events = [cb1, cb2‵‵‵]) + testsol(ssys5, u0, p, tspan; tstops = [1.0, 2.0], paramtotest = k) + @named ssys6 = SDESystem(eqs, [0.0], t, [A], [k, t1, t2], + discrete_events = [cb2‵‵‵, cb1]) + testsol(ssys6, u0, p, tspan; tstops = [1.0, 2.0], paramtotest = k) + + # mix a continuous event too + cond3 = A ~ 0.1 + affect3 = [k ~ 0.0] + cb3 = cond3 => affect3 + @named ssys7 = SDESystem(eqs, [0.0], t, [A], [k, t1, t2], + discrete_events = [cb1, cb2‵‵‵], + continuous_events = [cb3]) + sol = testsol(ssys7, u0, p, (0.0, 10.0); tstops = [1.0, 2.0]) + @test isapprox(sol(10.0)[1], 0.1; atol = 1e-10, rtol = 1e-10) +end + +@testset "JumpSystem Discrete Callbacks" begin + function testsol(jsys, u0, p, tspan; tstops = Float64[], paramtotest = nothing, + N = 40000, kwargs...) + jsys = complete(jsys) + dprob = DiscreteProblem(jsys, u0, tspan, p) + jprob = JumpProblem(jsys, dprob, Direct(); kwargs...) + sol = solve(jprob, SSAStepper(); tstops = tstops) + @show sol + @test (sol(1.000000000001)[1] - sol(0.99999999999)[1]) == 1 + paramtotest === nothing || (@test sol.ps[paramtotest] == 1.0) + @test sol(40.0)[1] == 0 + sol + end + + @parameters k t1 t2 + @variables A(t) B(t) + + cond1 = (t == t1) + affect1 = [A ~ Pre(A) + 1] + cb1 = cond1 => affect1 + cond2 = (t == t2) + affect2 = [k ~ 1.0] + cb2 = cond2 => affect2 + + eqs = [MassActionJump(k, [A => 1], [A => -1])] + @named jsys = JumpSystem(eqs, t, [A], [k, t1, t2], discrete_events = [cb1, cb2]) + u0 = [A => 1] + p = [k => 0.0, t1 => 1.0, t2 => 2.0] + tspan = (0.0, 40.0) + testsol(jsys, u0, p, tspan; tstops = [1.0, 2.0], rng, paramtotest = k) + + cond1a = (t == t1) + affect1a = [A ~ Pre(A) + 1, B ~ A] + cb1a = cond1a => affect1a + @named jsys1 = JumpSystem(eqs, t, [A, B], [k, t1, t2], discrete_events = [cb1a, cb2]) + u0′ = [A => 1, B => 0] + sol = testsol(jsys1, u0′, p, tspan; tstops = [1.0, 2.0], + check_length = false, rng, paramtotest = k) + @test sol(1.000000001, idxs = B) == 2 + + # same as above - but with set-time event syntax + cb1‵ = [1.0] => affect1 # needs to be a Vector for the event to happen only once + cb2‵ = [2.0] => affect2 + @named jsys‵ = JumpSystem(eqs, t, [A], [k], discrete_events = [cb1‵, cb2‵]) + testsol(jsys‵, u0, [p[1]], tspan; rng, paramtotest = k) + + # mixing discrete affects + @named jsys3 = JumpSystem(eqs, t, [A], [k, t1, t2], discrete_events = [cb1, cb2‵]) + testsol(jsys3, u0, p, tspan; tstops = [1.0], rng, paramtotest = k) + + # mixing with a func affect + function affect!(integrator, u, p, ctx) + integrator.ps[p.k] = 1.0 + reset_aggregated_jumps!(integrator) + nothing + end + cb2‵‵ = [2.0] => (affect!, [], [k], [k], nothing) + @named jsys4 = JumpSystem(eqs, t, [A], [k, t1], discrete_events = [cb1, cb2‵‵]) + testsol(jsys4, u0, p, tspan; tstops = [1.0], rng, paramtotest = k) + + # mixing with symbolic condition in the func affect + cb2‵‵‵ = (t == t2) => (affect!, [], [k], [k], nothing) + @named jsys5 = JumpSystem(eqs, t, [A], [k, t1, t2], discrete_events = [cb1, cb2‵‵‵]) + testsol(jsys5, u0, p, tspan; tstops = [1.0, 2.0], rng, paramtotest = k) + @named jsys6 = JumpSystem(eqs, t, [A], [k, t1, t2], discrete_events = [cb2‵‵‵, cb1]) + testsol(jsys6, u0, p, tspan; tstops = [1.0, 2.0], rng, paramtotest = k) +end + +@testset "Namespacing" begin + function oscillator_ce(k = 1.0; name) + sts = @variables x(t)=1.0 v(t)=0.0 F(t) + ps = @parameters k=k Θ=0.5 + eqs = [D(x) ~ v, D(v) ~ -k * x + F] + ev = [x ~ Θ] => [x ~ 1.0, v ~ 0.0] + ODESystem(eqs, t, sts, ps, continuous_events = [ev]; name) + end + + @named oscce = oscillator_ce() + eqs = [oscce.F ~ 0] + @named eqs_sys = ODESystem(eqs, t) + @named oneosc_ce = compose(eqs_sys, oscce) + oneosc_ce_simpl = structural_simplify(oneosc_ce) + + prob = ODEProblem(oneosc_ce_simpl, [], (0.0, 2.0), []) + sol = solve(prob, Tsit5(), saveat = 0.1) + + @test typeof(oneosc_ce_simpl) == ODESystem + @test sol[1, 6] < 1.0 # test whether x(t) decreases over time + @test sol[1, 18] > 0.5 # test whether event happened +end + +@testset "Additional SymbolicContinuousCallback options" begin + # baseline affect (pos + neg + left root find) + @variables c1(t)=1.0 c2(t)=1.0 # c1 = cos(t), c2 = cos(3t) + eqs = [D(c1) ~ -sin(t); D(c2) ~ -3 * sin(3 * t)] + record_crossings(i, u, _, c) = push!(c, i.t => i.u[u.v]) + cr1 = [] + cr2 = [] + evt1 = ModelingToolkit.SymbolicContinuousCallback( + [c1 ~ 0], (record_crossings, [c1 => :v], [], [], cr1)) + evt2 = ModelingToolkit.SymbolicContinuousCallback( + [c2 ~ 0], (record_crossings, [c2 => :v], [], [], cr2)) + @named trigsys = ODESystem(eqs, t; continuous_events = [evt1, evt2]) + trigsys_ss = structural_simplify(trigsys) + prob = ODEProblem(trigsys_ss, [], (0.0, 2π)) + sol = solve(prob, Tsit5()) + required_crossings_c1 = [π / 2, 3 * π / 2] + required_crossings_c2 = [π / 6, π / 2, 5 * π / 6, 7 * π / 6, 3 * π / 2, 11 * π / 6] + @test maximum(abs.(first.(cr1) .- required_crossings_c1)) < 1e-4 + @test maximum(abs.(first.(cr2) .- required_crossings_c2)) < 1e-4 + @test sign.(cos.(required_crossings_c1 .- 1e-6)) == sign.(last.(cr1)) + @test sign.(cos.(3 * (required_crossings_c2 .- 1e-6))) == sign.(last.(cr2)) + + # with neg affect (pos * neg + left root find) + cr1p = [] + cr2p = [] + cr1n = [] + cr2n = [] + evt1 = ModelingToolkit.SymbolicContinuousCallback( + [c1 ~ 0], (record_crossings, [c1 => :v], [], [], cr1p); + affect_neg = (record_crossings, [c1 => :v], [], [], cr1n)) + evt2 = ModelingToolkit.SymbolicContinuousCallback( + [c2 ~ 0], (record_crossings, [c2 => :v], [], [], cr2p); + affect_neg = (record_crossings, [c2 => :v], [], [], cr2n)) + @named trigsys = ODESystem(eqs, t; continuous_events = [evt1, evt2]) + trigsys_ss = structural_simplify(trigsys) + prob = ODEProblem(trigsys_ss, [], (0.0, 2π)) + sol = solve(prob, Tsit5(); dtmax = 0.01) + c1_pc = filter((<=)(0) ∘ sin, required_crossings_c1) + c1_nc = filter((>=)(0) ∘ sin, required_crossings_c1) + c2_pc = filter(c -> -sin(3c) > 0, required_crossings_c2) + c2_nc = filter(c -> -sin(3c) < 0, required_crossings_c2) + @test maximum(abs.(c1_pc .- first.(cr1p))) < 1e-5 + @test maximum(abs.(c1_nc .- first.(cr1n))) < 1e-5 + @test maximum(abs.(c2_pc .- first.(cr2p))) < 1e-5 + @test maximum(abs.(c2_nc .- first.(cr2n))) < 1e-5 + @test sign.(cos.(c1_pc .- 1e-6)) == sign.(last.(cr1p)) + @test sign.(cos.(c1_nc .- 1e-6)) == sign.(last.(cr1n)) + @test sign.(cos.(3 * (c2_pc .- 1e-6))) == sign.(last.(cr2p)) + @test sign.(cos.(3 * (c2_nc .- 1e-6))) == sign.(last.(cr2n)) + + # with nothing neg affect (pos * neg + left root find) + cr1p = [] + cr2p = [] + evt1 = ModelingToolkit.SymbolicContinuousCallback( + [c1 ~ 0], (record_crossings, [c1 => :v], [], [], cr1p); affect_neg = nothing) + evt2 = ModelingToolkit.SymbolicContinuousCallback( + [c2 ~ 0], (record_crossings, [c2 => :v], [], [], cr2p); affect_neg = nothing) + @named trigsys = ODESystem(eqs, t; continuous_events = [evt1, evt2]) + trigsys_ss = structural_simplify(trigsys) + prob = ODEProblem(trigsys_ss, [], (0.0, 2π)) + sol = solve(prob, Tsit5(); dtmax = 0.01) + @test maximum(abs.(c1_pc .- first.(cr1p))) < 1e-5 + @test maximum(abs.(c2_pc .- first.(cr2p))) < 1e-5 + @test sign.(cos.(c1_pc .- 1e-6)) == sign.(last.(cr1p)) + @test sign.(cos.(3 * (c2_pc .- 1e-6))) == sign.(last.(cr2p)) + + #mixed + cr1p = [] + cr2p = [] + cr1n = [] + cr2n = [] + evt1 = ModelingToolkit.SymbolicContinuousCallback( + [c1 ~ 0], (record_crossings, [c1 => :v], [], [], cr1p); affect_neg = nothing) + evt2 = ModelingToolkit.SymbolicContinuousCallback( + [c2 ~ 0], (record_crossings, [c2 => :v], [], [], cr2p); + affect_neg = (record_crossings, [c2 => :v], [], [], cr2n)) + @named trigsys = ODESystem(eqs, t; continuous_events = [evt1, evt2]) + trigsys_ss = structural_simplify(trigsys) + prob = ODEProblem(trigsys_ss, [], (0.0, 2π)) + sol = solve(prob, Tsit5(); dtmax = 0.01) + c1_pc = filter((<=)(0) ∘ sin, required_crossings_c1) + c2_pc = filter(c -> -sin(3c) > 0, required_crossings_c2) + c2_nc = filter(c -> -sin(3c) < 0, required_crossings_c2) + @test maximum(abs.(c1_pc .- first.(cr1p))) < 1e-5 + @test maximum(abs.(c2_pc .- first.(cr2p))) < 1e-5 + @test maximum(abs.(c2_nc .- first.(cr2n))) < 1e-5 + @test sign.(cos.(c1_pc .- 1e-6)) == sign.(last.(cr1p)) + @test sign.(cos.(3 * (c2_pc .- 1e-6))) == sign.(last.(cr2p)) + @test sign.(cos.(3 * (c2_nc .- 1e-6))) == sign.(last.(cr2n)) + + # baseline affect w/ right rootfind (pos + neg + right root find) + @variables c1(t)=1.0 c2(t)=1.0 # c1 = cos(t), c2 = cos(3t) + cr1 = [] + cr2 = [] + evt1 = ModelingToolkit.SymbolicContinuousCallback( + [c1 ~ 0], (record_crossings, [c1 => :v], [], [], cr1); + rootfind = SciMLBase.RightRootFind) + evt2 = ModelingToolkit.SymbolicContinuousCallback( + [c2 ~ 0], (record_crossings, [c2 => :v], [], [], cr2); + rootfind = SciMLBase.RightRootFind) + @named trigsys = ODESystem(eqs, t; continuous_events = [evt1, evt2]) + trigsys_ss = structural_simplify(trigsys) + prob = ODEProblem(trigsys_ss, [], (0.0, 2π)) + sol = solve(prob, Tsit5(); dtmax = 0.01) + required_crossings_c1 = [π / 2, 3 * π / 2] + required_crossings_c2 = [π / 6, π / 2, 5 * π / 6, 7 * π / 6, 3 * π / 2, 11 * π / 6] + @test maximum(abs.(first.(cr1) .- required_crossings_c1)) < 1e-4 + @test maximum(abs.(first.(cr2) .- required_crossings_c2)) < 1e-4 + @test sign.(cos.(required_crossings_c1 .+ 1e-6)) == sign.(last.(cr1)) + @test sign.(cos.(3 * (required_crossings_c2 .+ 1e-6))) == sign.(last.(cr2)) + + # baseline affect w/ mixed rootfind (pos + neg + right root find) + cr1 = [] + cr2 = [] + evt1 = ModelingToolkit.SymbolicContinuousCallback( + [c1 ~ 0], (record_crossings, [c1 => :v], [], [], cr1); + rootfind = SciMLBase.LeftRootFind) + evt2 = ModelingToolkit.SymbolicContinuousCallback( + [c2 ~ 0], (record_crossings, [c2 => :v], [], [], cr2); + rootfind = SciMLBase.RightRootFind) + @named trigsys = ODESystem(eqs, t; continuous_events = [evt1, evt2]) + trigsys_ss = structural_simplify(trigsys) + prob = ODEProblem(trigsys_ss, [], (0.0, 2π)) + sol = solve(prob, Tsit5()) + @test maximum(abs.(first.(cr1) .- required_crossings_c1)) < 1e-4 + @test maximum(abs.(first.(cr2) .- required_crossings_c2)) < 1e-4 + @test sign.(cos.(required_crossings_c1 .- 1e-6)) == sign.(last.(cr1)) + @test sign.(cos.(3 * (required_crossings_c2 .+ 1e-6))) == sign.(last.(cr2)) + + #flip order and ensure results are okay + cr1 = [] + cr2 = [] + evt1 = ModelingToolkit.SymbolicContinuousCallback( + [c1 ~ 0], (record_crossings, [c1 => :v], [], [], cr1); + rootfind = SciMLBase.LeftRootFind) + evt2 = ModelingToolkit.SymbolicContinuousCallback( + [c2 ~ 0], (record_crossings, [c2 => :v], [], [], cr2); + rootfind = SciMLBase.RightRootFind) + @named trigsys = ODESystem(eqs, t; continuous_events = [evt2, evt1]) + trigsys_ss = structural_simplify(trigsys) + prob = ODEProblem(trigsys_ss, [], (0.0, 2π)) + sol = solve(prob, Tsit5()) + @test maximum(abs.(first.(cr1) .- required_crossings_c1)) < 1e-4 + @test maximum(abs.(first.(cr2) .- required_crossings_c2)) < 1e-4 + @test sign.(cos.(required_crossings_c1 .- 1e-6)) == sign.(last.(cr1)) + @test sign.(cos.(3 * (required_crossings_c2 .+ 1e-6))) == sign.(last.(cr2)) +end @testset "Discrete event reinitialization (#3142)" begin @connector LiquidPort begin @@ -1153,7 +1153,7 @@ end f = ModelingToolkit.FunctionalAffect( f = (i, u, p, c) -> seen = true, sts = [], pars = [], discretes = []) cb1 = ModelingToolkit.SymbolicContinuousCallback( - [x ~ 0], nothing, initialize = [x ~ 1.5], finalize = f) + [x ~ 0], Equation[], initialize = [x ~ 1.5], finalize = f) @mtkbuild sys = ODESystem(D(x) ~ -1, t, [x], []; continuous_events = [cb1]) prob = ODEProblem(sys, [x => 1.0], (0.0, 2), []) sol = solve(prob, Tsit5(); dtmax = 0.01) @@ -1238,9 +1238,7 @@ end @variables x(t) [irreducible = true] y(t) [irreducible = true] eqs = [x ~ y, D(x) ~ -1] cb = [x ~ 0.0] => [x ~ 0, y ~ 1] - @mtkbuild pend = ODESystem(eqs, t; continuous_events = [cb]) - prob = ODEProblem(pend, [x => 1], (0.0, 3.0), guesses = [y => x]) - @test_throws "DAE initialization failed" solve(prob, Rodas5()) + @test_throws ErrorException @mtkbuild pend = ODESystem(eqs, t; continuous_events = [cb]) cb = [x ~ 0.0] => [y ~ 1] @mtkbuild pend = ODESystem(eqs, t; continuous_events = [cb]) @@ -1331,6 +1329,6 @@ end @test 100.0 ∈ sol2[sys2.wd2.θ] end -# TO teste: +# TODO: test: # - Functional affects reinitialize correctly # - explicit equation of t in a functional affect From 377f279ce8b96a90a6834d0bd337d2f023fa38ce Mon Sep 17 00:00:00 2001 From: vyudu Date: Thu, 20 Mar 2025 17:24:23 -0400 Subject: [PATCH 19/52] fix: most tests passing --- src/systems/callbacks.jl | 12 +++++++----- test/symbolic_events.jl | 9 ++++----- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/systems/callbacks.jl b/src/systems/callbacks.jl index 054a1032b4..f11698638f 100644 --- a/src/systems/callbacks.jl +++ b/src/systems/callbacks.jl @@ -685,6 +685,8 @@ function generate_discrete_callbacks(sys::AbstractSystem, dvs = unknowns(sys), p [generate_callback(db, sys; kwargs...) for db in dbs] end +const EMPTY_FUNCTION = (args...) -> () + """ Codegen a DifferentialEquations callback. A (set of) continuous callback with multiple equations becomes a VectorContinuousCallback. Continuous callbacks with only one equation will become a ContinuousCallback. @@ -705,9 +707,9 @@ function generate_callback(cbs::Vector{SymbolicContinuousCallback}, sys; kwargs. inits = [] finals = [] for cb in cbs - affect = compile_affect(cb.affect, cb, sys, default = nothing) + affect = compile_affect(cb.affect, cb, sys, default = EMPTY_FUNCTION) push!(affects, affect) - affect_neg = (cb.affect_neg == cb.affect) ? affect : compile_affect(cb.affect_neg, cb, sys, default = nothing) + affect_neg = (cb.affect_neg === cb.affect) ? affect : compile_affect(cb.affect_neg, cb, sys, default = EMPTY_FUNCTION) push!(affect_negs, affect_neg) push!(inits, compile_affect(cb.initialize, cb, sys; default = nothing, is_init = true)) push!(finals, compile_affect(cb.finalize, cb, sys; default = nothing)) @@ -741,11 +743,11 @@ function generate_callback(cb, sys; kwargs...) ps = parameters(sys; initial_parameters = true) trigger = is_timed ? conditions(cb) : compile_condition(cb, sys, dvs, ps; kwargs...) - affect = compile_affect(cb.affect, cb, sys, default = (args...) -> ()) + affect = compile_affect(cb.affect, cb, sys, default = EMPTY_FUNCTION) affect_neg = if is_discrete(cb) nothing else - (cb.affect == cb.affect_neg) ? affect : compile_affect(cb.affect_neg, cb, sys, default = nothing) + (cb.affect === cb.affect_neg) ? affect : compile_affect(cb.affect_neg, cb, sys, default = EMPTY_FUNCTION) end init = compile_affect(cb.initialize, cb, sys, default = SciMLBase.INITIALIZE_DEFAULT, is_init = true) final = compile_affect(cb.finalize, cb, sys, default = SciMLBase.FINALIZE_DEFAULT) @@ -860,7 +862,7 @@ function compile_equational_affect(aff::AffectSystem, sys; kwargs...) push!(pmap, pre_p => pval) end guesses = Pair[u => integrator[aff_map[u]] for u in unknowns(affsys)] - affprob = ImplicitDiscreteProblem(affsys, Pair[], (0, 1), pmap; guesses, build_initializeprob = false) + affprob = ImplicitDiscreteProblem(affsys, Pair[], (integrator.t, integrator.t), pmap; guesses, build_initializeprob = false) affsol = init(affprob, SimpleIDSolve()) for u in dvs_to_modify diff --git a/test/symbolic_events.jl b/test/symbolic_events.jl index 763dfcbbea..4a7e8e90c0 100644 --- a/test/symbolic_events.jl +++ b/test/symbolic_events.jl @@ -644,7 +644,6 @@ end dprob = DiscreteProblem(jsys, u0, tspan, p) jprob = JumpProblem(jsys, dprob, Direct(); kwargs...) sol = solve(jprob, SSAStepper(); tstops = tstops) - @show sol @test (sol(1.000000000001)[1] - sol(0.99999999999)[1]) == 1 paramtotest === nothing || (@test sol.ps[paramtotest] == 1.0) @test sol(40.0)[1] == 0 @@ -1153,7 +1152,7 @@ end f = ModelingToolkit.FunctionalAffect( f = (i, u, p, c) -> seen = true, sts = [], pars = [], discretes = []) cb1 = ModelingToolkit.SymbolicContinuousCallback( - [x ~ 0], Equation[], initialize = [x ~ 1.5], finalize = f) + [x ~ 0], nothing, initialize = [x ~ 1.5], finalize = f) @mtkbuild sys = ODESystem(D(x) ~ -1, t, [x], []; continuous_events = [cb1]) prob = ODEProblem(sys, [x => 1.0], (0.0, 2), []) sol = solve(prob, Tsit5(); dtmax = 0.01) @@ -1166,7 +1165,7 @@ end f = ModelingToolkit.FunctionalAffect( f = (i, u, p, c) -> seen = true, sts = [], pars = [], discretes = []) cb1 = ModelingToolkit.SymbolicContinuousCallback( - [x ~ 0], Equation[], initialize = [x ~ 1.5], finalize = f) + [x ~ 0], nothing, initialize = [x ~ 1.5], finalize = f) inited = false finaled = false a = ModelingToolkit.FunctionalAffect( @@ -1174,7 +1173,7 @@ end b = ModelingToolkit.FunctionalAffect( f = (i, u, p, c) -> finaled = true, sts = [], pars = [], discretes = []) cb2 = ModelingToolkit.SymbolicContinuousCallback( - [x ~ 0.1], Equation[], initialize = a, finalize = b) + [x ~ 0.1], nothing, initialize = a, finalize = b) @mtkbuild sys = ODESystem(D(x) ~ -1, t, [x], []; continuous_events = [cb1, cb2]) prob = ODEProblem(sys, [x => 1.0], (0.0, 2), []) sol = solve(prob, Tsit5()) @@ -1238,7 +1237,7 @@ end @variables x(t) [irreducible = true] y(t) [irreducible = true] eqs = [x ~ y, D(x) ~ -1] cb = [x ~ 0.0] => [x ~ 0, y ~ 1] - @test_throws ErrorException @mtkbuild pend = ODESystem(eqs, t; continuous_events = [cb]) + @test_throws Exception @mtkbuild pend = ODESystem(eqs, t; continuous_events = [cb]) cb = [x ~ 0.0] => [y ~ 1] @mtkbuild pend = ODESystem(eqs, t; continuous_events = [cb]) From 58a355fe3e17b7aa48cc7ee23dae5bd67f434ab9 Mon Sep 17 00:00:00 2001 From: vyudu Date: Sat, 22 Mar 2025 02:38:40 -0400 Subject: [PATCH 20/52] feat: add optimization for explicit affects --- src/systems/callbacks.jl | 185 +++++++++++++++++++------------ src/systems/imperative_affect.jl | 2 +- src/systems/jumps/jumpsystem.jl | 20 ++-- test/accessor_functions.jl | 4 +- test/symbolic_events.jl | 1 + 5 files changed, 124 insertions(+), 88 deletions(-) diff --git a/src/systems/callbacks.jl b/src/systems/callbacks.jl index f11698638f..f26405ea58 100644 --- a/src/systems/callbacks.jl +++ b/src/systems/callbacks.jl @@ -69,6 +69,7 @@ struct AffectSystem discretes::Vector """Maps the symbols of unknowns/observed in the ImplicitDiscreteSystem to its corresponding unknown/parameter in the parent system.""" aff_to_sys::Dict + explicit::Bool end system(a::AffectSystem) = a.system @@ -77,6 +78,7 @@ unknowns(a::AffectSystem) = a.unknowns parameters(a::AffectSystem) = a.parameters aff_to_sys(a::AffectSystem) = a.aff_to_sys previous_vals(a::AffectSystem) = parameters(system(a)) +is_explicit(a::AffectSystem) = a.explicit function Base.show(iio::IO, aff::AffectSystem) eqs = vcat(equations(system(aff)), observed(system(aff))) @@ -105,6 +107,8 @@ Base.nameof(::Pre) = :Pre Base.show(io::IO, x::Pre) = print(io, "Pre") input_timedomain(::Pre, _ = nothing) = ContinuousClock() output_timedomain(::Pre, _ = nothing) = ContinuousClock() +unPre(x::Num) = unPre(unwrap(x)) +unPre(x::BasicSymbolic) = operation(x) isa Pre ? only(arguments(x)) : x function (p::Pre)(x) iw = Symbolics.iswrapped(x) @@ -229,24 +233,28 @@ function make_affect(affect::Vector{Equation}; iv = nothing, algeeqs = Equation[ isempty(affect) && return nothing isempty(algeeqs) && @warn "No algebraic equations were found. If the system has no algebraic equations, this can be disregarded. Otherwise pass in `algeeqs` to the SymbolicContinuousCallback constructor." + explicit = true affect = scalarize(affect) dvs = OrderedSet() params = OrderedSet() + params = OrderedSet() for eq in affect if !haspre(eq) && !(symbolic_type(eq.rhs) === NotSymbolic()) @warn "Affect equation $eq has no `Pre` operator. As such it will be interpreted as an algebraic equation to be satisfied after the callback. If you intended to use the value of a variable x before the affect, use Pre(x)." + explicit = false end collect_vars!(dvs, params, eq, iv; op = Pre) end for eq in algeeqs collect_vars!(dvs, params, eq, iv) + expilcit = false end if isnothing(iv) iv = isempty(dvs) ? iv : only(arguments(dvs[1])) isnothing(iv) && @warn "No independent variable specified and could not be inferred. If the iv appears in an affect equation explicitly, like x ~ t + 1, then it must be specified as an argument to the SymbolicContinuousCallback or SymbolicDiscreteCallback constructor. Otherwise this warning can be disregarded." end - # System parameters should become unknowns in the ImplicitDiscreteSystem. + # Parameters in affect equations should become unknowns in the ImplicitDiscreteSystem. cb_params = Any[] discretes = Any[] p_as_dvs = Any[] @@ -268,15 +276,15 @@ function make_affect(affect::Vector{Equation}; iv = nothing, algeeqs = Equation[ aff_map = Dict(zip(p_as_dvs, discretes)) rev_map = Dict([v => k for (k, v) in aff_map]) affect = Symbolics.substitute(affect, rev_map) - @mtkbuild affectsys = ImplicitDiscreteSystem(vcat(affect, algeeqs), iv, collect(union(dvs, p_as_dvs)), cb_params) + @named affectsys = ImplicitDiscreteSystem(vcat(affect, algeeqs), iv, collect(union(dvs, p_as_dvs)), cb_params) # get accessed parameters p from Pre(p) in the callback parameters - params = filter(isparameter, map(x -> only(arguments(unwrap(x))), cb_params)) + params = filter(isparameter, map(x -> unPre(x), cb_params)) # add unknowns to the map for u in dvs aff_map[u] = u end - return AffectSystem(affectsys, collect(dvs), params, discretes, aff_map) + return AffectSystem(affectsys, collect(dvs), params, discretes, aff_map, explicit) end function make_affect(affect; kwargs...) @@ -468,7 +476,7 @@ end ########## Namespacing Utilities ########### ############################################ -function namespace_affect(affect::FunctionalAffect, s) +function namespace_affects(affect::FunctionalAffect, s) FunctionalAffect(func(affect), renamespace.((s,), unknowns(affect)), unknowns_syms(affect), @@ -478,35 +486,35 @@ function namespace_affect(affect::FunctionalAffect, s) context(affect)) end -function namespace_affect(affect::AffectSystem, s) +function namespace_affects(affect::AffectSystem, s) AffectSystem(renamespace(s, system(affect)), renamespace.((s,), unknowns(affect)), renamespace.((s,), parameters(affect)), renamespace.((s,), discretes(affect)), - Dict([k => renamespace(s, v) for (k, v) in aff_to_sys(affect)])) + Dict([k => renamespace(s, v) for (k, v) in aff_to_sys(affect)]), is_explicit(affect)) end -namespace_affect(af::Nothing, s) = nothing +namespace_affects(af::Nothing, s) = nothing function namespace_callback(cb::SymbolicContinuousCallback, s)::SymbolicContinuousCallback SymbolicContinuousCallback( namespace_equation.(equations(cb), (s,)), - namespace_affect(affects(cb), s), - affect_neg = namespace_affect(affect_negs(cb), s), - initialize = namespace_affect(initialize_affects(cb), s), - finalize = namespace_affect(finalize_affects(cb), s), + namespace_affects(affects(cb), s), + affect_neg = namespace_affects(affect_negs(cb), s), + initialize = namespace_affects(initialize_affects(cb), s), + finalize = namespace_affects(finalize_affects(cb), s), rootfind = cb.rootfind) end -function namespace_condition(condition, s) +function namespace_conditions(condition, s) is_timed_condition(condition) ? condition : namespace_expr(condition, s) end function namespace_callback(cb::SymbolicDiscreteCallback, s)::SymbolicDiscreteCallback SymbolicDiscreteCallback( - namespace_condition(condition(cb), s), + namespace_conditions(conditions(cb), s), namespace_affects(affects(cb), s), - namespace_affects(initialize_affects(cb), s), - namespace_affects(finalize_affects(cb), s)) + initialize = namespace_affects(initialize_affects(cb), s), + finalize = namespace_affects(finalize_affects(cb), s)) end function Base.hash(cb::SymbolicContinuousCallback, s::UInt) @@ -623,8 +631,6 @@ function compile_condition(cbs::Union{AbstractCallback, Vector{<:AbstractCallbac end end end - - cond end """ @@ -707,12 +713,12 @@ function generate_callback(cbs::Vector{SymbolicContinuousCallback}, sys; kwargs. inits = [] finals = [] for cb in cbs - affect = compile_affect(cb.affect, cb, sys, default = EMPTY_FUNCTION) + affect = compile_affect(cb.affect, cb, sys, default = EMPTY_FUNCTION, kwargs...) push!(affects, affect) - affect_neg = (cb.affect_neg === cb.affect) ? affect : compile_affect(cb.affect_neg, cb, sys, default = EMPTY_FUNCTION) + affect_neg = (cb.affect_neg === cb.affect) ? affect : compile_affect(cb.affect_neg, cb, sys, default = EMPTY_FUNCTION, kwargs...) push!(affect_negs, affect_neg) - push!(inits, compile_affect(cb.initialize, cb, sys; default = nothing, is_init = true)) - push!(finals, compile_affect(cb.finalize, cb, sys; default = nothing)) + push!(inits, compile_affect(cb.initialize, cb, sys; default = nothing, is_init = true), kwargs...) + push!(finals, compile_affect(cb.finalize, cb, sys; default = nothing), kwargs...) end # Since there may be different number of conditions and affects, @@ -729,8 +735,8 @@ function generate_callback(cbs::Vector{SymbolicContinuousCallback}, sys; kwargs. isnothing(f) && return f(integ) end - initialize = compile_vector_optional_affect(inits, SciMLBase.INITIALIZE_DEFAULT) - finalize = compile_vector_optional_affect(finals, SciMLBase.FINALIZE_DEFAULT) + initialize = wrap_vector_optional_affect(inits, SciMLBase.INITIALIZE_DEFAULT) + finalize = wrap_vector_optional_affect(finals, SciMLBase.FINALIZE_DEFAULT) return VectorContinuousCallback( trigger, affect, affect_neg, length(eqs); initialize, finalize, @@ -743,14 +749,14 @@ function generate_callback(cb, sys; kwargs...) ps = parameters(sys; initial_parameters = true) trigger = is_timed ? conditions(cb) : compile_condition(cb, sys, dvs, ps; kwargs...) - affect = compile_affect(cb.affect, cb, sys, default = EMPTY_FUNCTION) + affect = compile_affect(cb.affect, cb, sys, default = EMPTY_FUNCTION, kwargs...) affect_neg = if is_discrete(cb) nothing else - (cb.affect === cb.affect_neg) ? affect : compile_affect(cb.affect_neg, cb, sys, default = EMPTY_FUNCTION) + (cb.affect === cb.affect_neg) ? affect : compile_affect(cb.affect_neg, cb, sys, default = EMPTY_FUNCTION, kwargs...) end - init = compile_affect(cb.initialize, cb, sys, default = SciMLBase.INITIALIZE_DEFAULT, is_init = true) - final = compile_affect(cb.finalize, cb, sys, default = SciMLBase.FINALIZE_DEFAULT) + init = compile_affect(cb.initialize, cb, sys, default = SciMLBase.INITIALIZE_DEFAULT, is_init = true, kwargs...) + final = compile_affect(cb.finalize, cb, sys, default = SciMLBase.FINALIZE_DEFAULT, kwargs...) initialize = isnothing(cb.initialize) ? init : ((c, u, t, i) -> init(i)) finalize = isnothing(cb.finalize) ? final : ((c, u, t, i) -> final(i)) @@ -795,32 +801,29 @@ function compile_affect( get(ic.callback_to_clocks, cb, Int[]) end - f = if isnothing(aff) - default + if isnothing(aff) + full_args = is_init && (default === SciMLBase.INITIALIZE_DEFAULT) + is_init ? wrap_save_discretes(f, save_idxs; full_args) : default elseif aff isa AffectSystem - compile_equational_affect(aff, sys) + f = compile_equational_affect(aff, sys; kwargs...) + wrap_save_discretes(f, save_idxs) elseif aff isa FunctionalAffect || aff isa ImperativeAffect - compile_functional_affect(aff, sys; kwargs...) + f = compile_functional_affect(aff, sys; kwargs...) + wrap_save_discretes(f, save_idxs; full_args = true) end - wrap_save_discretes(f, save_idxs; is_init) end -# Init can be: user defined function, nothing, or INITIALIZE_DEFAULT -function wrap_save_discretes(f, save_idxs; is_init = false) - if isempty(save_idxs) || f === SciMLBase.FINALIZE_DEFAULT || (isnothing(f) && !is_init) - return f - elseif f === SciMLBase.INITIALIZE_DEFAULT - let save_idxs = save_idxs - (c, u, t, i) -> begin - f(c, u, t, i) +function wrap_save_discretes(f, save_idxs; full_args = false) + let save_idxs = save_idxs + if full_args + return (c, u, t, i) -> begin + isnothing(f) || f(c, u, t, i) for idx in save_idxs SciMLBase.save_discretes!(i, idx) end end - end - else - let save_idxs = save_idxs - (i) -> begin + else + return (i) -> begin isnothing(f) || f(i) for idx in save_idxs SciMLBase.save_discretes!(i, idx) @@ -831,9 +834,9 @@ function wrap_save_discretes(f, save_idxs; is_init = false) end """ -Initialize and Finalize for VectorContinuousCallback. +Initialize and finalize for VectorContinuousCallback. """ -function compile_vector_optional_affect(funs, default) +function wrap_vector_optional_affect(funs, default) all(isnothing, funs) && return default return let funs = funs function (cb, u, t, integ) @@ -844,35 +847,71 @@ function compile_vector_optional_affect(funs, default) end end -function compile_equational_affect(aff::AffectSystem, sys; kwargs...) +function add_integrator_header( + sys::AbstractSystem, integrator = gensym(:MTKIntegrator), out = :u) + expr -> Func([DestructuredArgs(expr.args, integrator, inds = [:u, :p, :t])], [], + expr.body), + expr -> Func( + [DestructuredArgs(expr.args, integrator, inds = [out, :u, :p, :t])], [], + expr.body) +end + +""" +Compile an affect defined by a set of equations. Systems with algebraic equations will solve implicit discrete problems to obtain their next state. Systems without will generate functions that perform explicit updates. +""" +function compile_equational_affect(aff::AffectSystem, sys; reset_jumps = false, kwargs...) affsys = system(aff) - aff_map = aff_to_sys(aff) - sys_map = Dict([v => k for (k, v) in aff_map]) - ps_to_modify = discretes(aff) - dvs_to_modify = setdiff(unknowns(aff), getfield.(observed(sys), :lhs)) - #TODO: Add an optimization for systems without algebraic equations - - return let dvs_to_modify = dvs_to_modify, aff_map = aff_map, sys_map = sys_map, affsys = affsys, ps_to_modify = ps_to_modify - - @inline function affect!(integrator) - pmap = Pair[] - for pre_p in parameters(affsys) - p = only(arguments(unwrap(pre_p))) - pval = isparameter(p) ? integrator.ps[p] : integrator[p] - push!(pmap, pre_p => pval) - end - guesses = Pair[u => integrator[aff_map[u]] for u in unknowns(affsys)] - affprob = ImplicitDiscreteProblem(affsys, Pair[], (integrator.t, integrator.t), pmap; guesses, build_initializeprob = false) + reinit = has_alg_equations(sys) || has_alg_equations(affsys) + ps_to_update = discretes(aff) + dvs_to_update = setdiff(unknowns(aff), getfield.(observed(sys), :lhs)) + + if is_explicit(aff) + update_eqs = equations(affsys) + update_eqs = Symbolics.fast_substitute(equations, Dict([p => unPre(p) for p in parameters(affsys)])) + rhss = map(x -> x.rhs, update_eqs) + lhss = map(x -> x.lhs, update_eqs) + is_p = [lhs ∈ ps_to_update for lhs in lhss] + + dvs = unknowns(sys) + ps = parameters(sys) + t = get_iv(sys) + + u_idxs = indexin((@view lhss[.!is_p]), dvs) + p_idxs = indexin((@view lhss[is_p]), ps) + _ps = reorder_parameters(sys, ps) + integ = gensym(:MTKIntegrator) + + u_up, u_up! = build_function_wrapper(sys, (@view rhss[.!is_p]), dvs, _ps..., t; wrap_code = add_integrator_header(sys, integ, :u), expression = Val{false}, outputidxs = u_idxs) + p_up, p_up! = build_function_wrapper(sys, (@view rhss[is_p]), dvs, _ps..., t; wrap_code = add_integrator_header(sys, integ, :p), expression = Val{false}, outputidxs = p_idxs) + + return (integ) -> begin + u_up!(integ) + p_up!(integ) + reset_jumps && reset_aggregated_jumps!(integ) + end + else + aff_map = aff_to_sys(aff) + sys_map = Dict([v => k for (k, v) in aff_map]) + + return let dvs_to_update = dvs_to_update, aff_map = aff_map, sys_map = sys_map, affsys = affsys, ps_to_update = ps_to_update + (integ) -> begin + pmap = Pair[] + for pre_p in parameters(affsys) + p = unPre(pre_p) + pval = isparameter(p) ? integ.ps[p] : integ[p] + push!(pmap, pre_p => pval) + end + guesses = Pair[u => integ[aff_map[u]] for u in unknowns(affsys)] + affprob = ImplicitDiscreteProblem(affsys, Pair[], (integ.t, integ.t), pmap; guesses, build_initializeprob = false) - affsol = init(affprob, SimpleIDSolve()) - for u in dvs_to_modify - integrator[u] = affsol[sys_map[u]] - end - for p in ps_to_modify - integrator.ps[p] = affsol[sys_map[p]] + affsol = init(affprob, SimpleIDSolve()) + for u in dvs_to_update + integ[u] = affsol[sys_map[u]] + end + for p in ps_to_update + integ.ps[p] = affsol[sys_map[p]] + end end - - sys isa JumpSystem && reset_aggregated_jumps!(integrator) end end end diff --git a/src/systems/imperative_affect.jl b/src/systems/imperative_affect.jl index 81e4cf724f..f01682deb1 100644 --- a/src/systems/imperative_affect.jl +++ b/src/systems/imperative_affect.jl @@ -99,7 +99,7 @@ function Base.hash(a::ImperativeAffect, s::UInt) hash(a.ctx, s) end -function namespace_affect(affect::ImperativeAffect, s) +function namespace_affects(affect::ImperativeAffect, s) ImperativeAffect(func(affect), namespace_expr.(observed(affect), (s,)), observed_syms(affect), diff --git a/src/systems/jumps/jumpsystem.jl b/src/systems/jumps/jumpsystem.jl index 3ace018377..a768eeb60d 100644 --- a/src/systems/jumps/jumpsystem.jl +++ b/src/systems/jumps/jumpsystem.jl @@ -282,15 +282,14 @@ function generate_rate_function(js::JumpSystem, rate) expression = Val{true}) end -function generate_affect_function(js::JumpSystem, affect, outputidxs) +function generate_affect_function(js::JumpSystem, affect) consts = collect_constants(affect) if !isempty(consts) # The SymbolicUtils._build_function method of this case doesn't support postprocess_fbody csubs = Dict(c => getdefault(c) for c in consts) affect = substitute(affect, csubs) end - compile_affect( - affect, nothing, js, unknowns(js), parameters(js); outputidxs = outputidxs, - expression = Val{true}, checkvars = false) + compile_equational_affect( + affect, js; expression = Val{true}, checkvars = false) end function assemble_vrj( @@ -299,8 +298,7 @@ function assemble_vrj( rate = GeneratedFunctionWrapper{(2, 3, is_split(js))}(rate, nothing) outputvars = (value(affect.lhs) for affect in vrj.affect!) outputidxs = [unknowntoid[var] for var in outputvars] - affect = eval_or_rgf(generate_affect_function(js, vrj.affect!, outputidxs); - eval_expression, eval_module) + affect = eval_or_rgf(generate_affect_function(js, vrj.affect!); eval_expression, eval_module) VariableRateJump(rate, affect; save_positions = vrj.save_positions) end @@ -308,7 +306,7 @@ function assemble_vrj_expr(js, vrj, unknowntoid) rate = generate_rate_function(js, vrj.rate) outputvars = (value(affect.lhs) for affect in vrj.affect!) outputidxs = ((unknowntoid[var] for var in outputvars)...,) - affect = generate_affect_function(js, vrj.affect!, outputidxs) + affect = generate_affect_function(js, vrj.affect!) quote rate = $rate @@ -323,8 +321,7 @@ function assemble_crj( rate = GeneratedFunctionWrapper{(2, 3, is_split(js))}(rate, nothing) outputvars = (value(affect.lhs) for affect in crj.affect!) outputidxs = [unknowntoid[var] for var in outputvars] - affect = eval_or_rgf(generate_affect_function(js, crj.affect!, outputidxs); - eval_expression, eval_module) + affect = eval_or_rgf(generate_affect_function(js, crj.affect!); eval_expression, eval_module) ConstantRateJump(rate, affect) end @@ -332,7 +329,7 @@ function assemble_crj_expr(js, crj, unknowntoid) rate = generate_rate_function(js, crj.rate) outputvars = (value(affect.lhs) for affect in crj.affect!) outputidxs = ((unknowntoid[var] for var in outputvars)...,) - affect = generate_affect_function(js, crj.affect!, outputidxs) + affect = generate_affect_function(js, crj.affect!) quote rate = $rate @@ -573,8 +570,7 @@ function JumpProcesses.JumpProblem(js::JumpSystem, prob, end # handle events, making sure to reset aggregators in the generated affect functions - cbs = process_events(js; callback, eval_expression, eval_module, - postprocess_affect_expr! = _reset_aggregator!) + cbs = process_events(js; callback, eval_expression, eval_module, reset_jumps = true) JumpProblem(prob, aggregator, jset; dep_graph = jtoj, vartojumps_map = vtoj, jumptovars_map = jtov, scale_rates = false, nocopy = true, diff --git a/test/accessor_functions.jl b/test/accessor_functions.jl index 7ce477155b..a9efde3a98 100644 --- a/test/accessor_functions.jl +++ b/test/accessor_functions.jl @@ -54,8 +54,8 @@ let 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]] + cevs = [[t ~ 1.0] => [Y ~ Pre(Y) + 2.0]] + devs = [(t == 2.0) => [Y ~ Pre(Y) + 2.0]] @named sys_bot = ODESystem( eqs_bot, t; systems = [], continuous_events = cevs, discrete_events = devs) @named sys_mid2 = ODESystem( diff --git a/test/symbolic_events.jl b/test/symbolic_events.jl index 4a7e8e90c0..da0a2d0d98 100644 --- a/test/symbolic_events.jl +++ b/test/symbolic_events.jl @@ -1331,3 +1331,4 @@ end # TODO: test: # - Functional affects reinitialize correctly # - explicit equation of t in a functional affect +# - modifying both u and p in an affect From d2ce3ffd2e3cf860ced5ed58416cb7544a7be307 Mon Sep 17 00:00:00 2001 From: vyudu Date: Sat, 22 Mar 2025 07:08:56 -0400 Subject: [PATCH 21/52] fix: fix FMI tests and parameter dependency tests --- ext/MTKFMIExt.jl | 7 +- src/systems/callbacks.jl | 205 +++++++++++++++++--------------- src/systems/jumps/jumpsystem.jl | 11 +- test/accessor_functions.jl | 17 ++- test/funcaffect.jl | 5 +- test/parameter_dependencies.jl | 6 +- test/symbolic_events.jl | 2 + 7 files changed, 131 insertions(+), 122 deletions(-) diff --git a/ext/MTKFMIExt.jl b/ext/MTKFMIExt.jl index 5cfe9a82ef..912799c4f8 100644 --- a/ext/MTKFMIExt.jl +++ b/ext/MTKFMIExt.jl @@ -93,7 +93,7 @@ with the name `namespace__variable`. - `name`: The name of the system. """ function MTK.FMIComponent(::Val{Ver}; fmu = nothing, tolerance = 1e-6, - communication_step_size = nothing, reinitializealg = SciMLBase.NoInit(), type, name) where {Ver} + communication_step_size = nothing, type, name) where {Ver} if Ver != 2 && Ver != 3 throw(ArgumentError("FMI Version must be `2` or `3`")) end @@ -238,7 +238,7 @@ function MTK.FMIComponent(::Val{Ver}; fmu = nothing, tolerance = 1e-6, finalize_affect = MTK.FunctionalAffect(fmiFinalize!, [], [wrapper], []) step_affect = MTK.FunctionalAffect(Returns(nothing), [], [], []) instance_management_callback = MTK.SymbolicDiscreteCallback( - (t != t - 1), step_affect; finalize = finalize_affect, reinitializealg = reinitializealg) + (t != t - 1), step_affect; finalize = finalize_affect) push!(params, wrapper) append!(observed, der_observed) @@ -279,8 +279,7 @@ function MTK.FMIComponent(::Val{Ver}; fmu = nothing, tolerance = 1e-6, fmiCSStep!; observed = cb_observed, modified = cb_modified, ctx = _functor) instance_management_callback = MTK.SymbolicDiscreteCallback( communication_step_size, step_affect; initialize = initialize_affect, - finalize = finalize_affect, reinitializealg = reinitializealg - ) + finalize = finalize_affect) # guarded in case there are no outputs/states and the variable is `[]`. symbolic_type(__mtk_internal_o) == NotSymbolic() || push!(params, __mtk_internal_o) diff --git a/src/systems/callbacks.jl b/src/systems/callbacks.jl index f26405ea58..47498580da 100644 --- a/src/systems/callbacks.jl +++ b/src/systems/callbacks.jl @@ -55,13 +55,6 @@ function has_functional_affect(cb) (affects(cb) isa FunctionalAffect || affects(cb) isa ImperativeAffect) end -function vars!(vars, aff::FunctionalAffect; op = Differential) - for var in Iterators.flatten((unknowns(aff), parameters(aff), discretes(aff))) - vars!(vars, var) - end - return vars -end - struct AffectSystem system::ImplicitDiscreteSystem unknowns::Vector @@ -81,6 +74,7 @@ previous_vals(a::AffectSystem) = parameters(system(a)) is_explicit(a::AffectSystem) = a.explicit function Base.show(iio::IO, aff::AffectSystem) + println(iio, "Affect system defined by equations:") eqs = vcat(equations(system(aff)), observed(system(aff))) show(iio, eqs) end @@ -90,7 +84,24 @@ function Base.:(==)(a1::AffectSystem, a2::AffectSystem) isequal(discretes(a1), discretes(a2)) && isequal(unknowns(a1), unknowns(a2)) && isequal(parameters(a1), parameters(a2)) && - isequal(aff_to_sys(a1), aff_to_sys(a2)) + isequal(aff_to_sys(a1), aff_to_sys(a2)) && + isequal(is_explicit(a1), is_explicit(a2)) +end + +function Base.hash(a::AffectSystem, s::UInt) + s = hash(system(a), s) + s = hash(unknowns(a), s) + s = hash(parameters(a), s) + s = hash(discretes(a), s) + s = hash(aff_to_sys(a), s) + hash(is_explicit(a), s) +end + +function vars!(vars, aff::Union{FunctionalAffect, AffectSystem}; op = Differential) + for var in Iterators.flatten((unknowns(aff), parameters(aff), discretes(aff))) + vars!(vars, var) + end + vars end """ @@ -233,11 +244,11 @@ function make_affect(affect::Vector{Equation}; iv = nothing, algeeqs = Equation[ isempty(affect) && return nothing isempty(algeeqs) && @warn "No algebraic equations were found. If the system has no algebraic equations, this can be disregarded. Otherwise pass in `algeeqs` to the SymbolicContinuousCallback constructor." + @show affect explicit = true affect = scalarize(affect) dvs = OrderedSet() params = OrderedSet() - params = OrderedSet() for eq in affect if !haspre(eq) && !(symbolic_type(eq.rhs) === NotSymbolic()) @warn "Affect equation $eq has no `Pre` operator. As such it will be interpreted as an algebraic equation to be satisfied after the callback. If you intended to use the value of a variable x before the affect, use Pre(x)." @@ -247,7 +258,7 @@ function make_affect(affect::Vector{Equation}; iv = nothing, algeeqs = Equation[ end for eq in algeeqs collect_vars!(dvs, params, eq, iv) - expilcit = false + explicit = false end if isnothing(iv) iv = isempty(dvs) ? iv : only(arguments(dvs[1])) @@ -366,19 +377,20 @@ function Base.show(io::IO, mime::MIME"text/plain", cb::AbstractCallback) end end -function vars!(vars, cb::SymbolicContinuousCallback; op = Differential) - for eq in equations(cb) - vars!(vars, eq; op) - end - for aff in (affects(cb), affect_negs(cb), initialize_affects(cb), finalize_affects(cb)) - if aff isa AffectSystem - for eq in vcat(observed(system(aff)), equations(system(aff))) +function vars!(vars, cb::AbstractCallback; op = Differential) + if symbolic_type(conditions(cb)) == NotSymbolic + if conditions(cb) isa AbstractArray + for eq in conditions(cb) vars!(vars, eq; op) end - elseif aff !== nothing - vars!(vars, aff; op) end + else + vars!(vars, conditions(cb); op) end + for aff in (affects(cb), initialize_affects(cb), finalize_affects(cb)) + isnothing(aff) || vars!(vars, aff; op) + end + !is_discrete(cb) && vars!(vars, affect_negs(cb); op) return vars end @@ -450,28 +462,6 @@ function is_timed_condition(condition::T) where {T} end end -function vars!(vars, cb::SymbolicDiscreteCallback; op = Differential) - if symbolic_type(conditions(cb)) == NotSymbolic - if conditions(cb) isa AbstractArray - for eq in conditions(cb) - vars!(vars, eq; op) - end - end - else - vars!(vars, conditions(cb); op) - end - for aff in (affects(cb), initialize_affects(cb), finalize_affects(cb)) - if aff isa AffectSystem - for eq in vcat(observed(system(aff)), equations(system(aff))) - vars!(vars, eq; op) - end - elseif aff !== nothing - vars!(vars, aff; op) - end - end - return vars -end - ############################################ ########## Namespacing Utilities ########### ############################################ @@ -517,20 +507,13 @@ function namespace_callback(cb::SymbolicDiscreteCallback, s)::SymbolicDiscreteCa finalize = namespace_affects(finalize_affects(cb), s)) end -function Base.hash(cb::SymbolicContinuousCallback, s::UInt) - s = foldr(hash, cb.conditions, init = s) - s = hash(cb.affect, s) - s = hash(cb.affect_neg, s) - s = hash(cb.initialize, s) - s = hash(cb.finalize, s) - hash(cb.rootfind, s) -end - -function Base.hash(cb::SymbolicDiscreteCallback, s::UInt) - s = foldr(hash, cb.conditions, init = s) - s = hash(cb.affect, s) - s = hash(cb.initialize, s) - hash(cb.finalize, s) +function Base.hash(cb::AbstractCallback, s::UInt) + s = conditions(cb) isa AbstractVector ? foldr(hash, conditions(cb), init = s) : hash(conditions(cb), s) + s = hash(affects(cb), s) + !is_discrete(cb) && (s = hash(affect_negs(cb), s)) + s = hash(initialize_affects(cb), s) + s = hash(finalize_affects(cb), s) + !is_discrete(cb) ? hash(cb.rootfind, s) : s end ########################### @@ -564,15 +547,11 @@ function finalize_affects(cbs::Vector{<:AbstractCallback}) reduce(finalize_affects, vcat, cbs; init = []) end -function Base.:(==)(e1::SymbolicDiscreteCallback, e2::SymbolicDiscreteCallback) - isequal(e1.conditions, e2.conditions) && isequal(e1.affects, e2.affects) && - isequal(e1.initialize, e2.initialize) && isequal(e1.finalize, e2.finalize) -end - -function Base.:(==)(e1::SymbolicContinuousCallback, e2::SymbolicContinuousCallback) - isequal(e1.conditions, e2.conditions) && isequal(e1.affect, e2.affect) && - isequal(e1.initialize, e2.initialize) && isequal(e1.finalize, e2.finalize) && - isequal(e1.affect_neg, e2.affect_neg) && isequal(e1.rootfind, e2.rootfind) +function Base.:(==)(e1::AbstractCallback, e2::AbstractCallback) + (is_discrete(e1) === is_discrete(e2)) || return false + (isequal(e1.conditions, e2.conditions) && isequal(e1.affect, e2.affect) && + isequal(e1.initialize, e2.initialize) && isequal(e1.finalize, e2.finalize)) || return false + is_discrete(e1) || (isequal(e1.affect_neg, e2.affect_neg) && isequal(e1.rootfind, e2.rootfind)) end Base.isempty(cb::AbstractCallback) = isempty(cb.conditions) @@ -600,7 +579,7 @@ function compile_condition(cbs::Union{AbstractCallback, Vector{<:AbstractCallbac cs = collect_constants(condit) if !isempty(cs) cmap = map(x -> x => getdefault(x), cs) - condit = substitute(condit, cmap) + condit = substitute(condit, Dict(cmap)) end if !is_discrete(cbs) @@ -691,7 +670,7 @@ function generate_discrete_callbacks(sys::AbstractSystem, dvs = unknowns(sys), p [generate_callback(db, sys; kwargs...) for db in dbs] end -const EMPTY_FUNCTION = (args...) -> () +EMPTY_AFFECT(args...) = nothing """ Codegen a DifferentialEquations callback. A (set of) continuous callback with multiple equations becomes a VectorContinuousCallback. @@ -713,12 +692,12 @@ function generate_callback(cbs::Vector{SymbolicContinuousCallback}, sys; kwargs. inits = [] finals = [] for cb in cbs - affect = compile_affect(cb.affect, cb, sys, default = EMPTY_FUNCTION, kwargs...) + affect = compile_affect(cb.affect, cb, sys; default = EMPTY_AFFECT, kwargs...) push!(affects, affect) - affect_neg = (cb.affect_neg === cb.affect) ? affect : compile_affect(cb.affect_neg, cb, sys, default = EMPTY_FUNCTION, kwargs...) + affect_neg = (cb.affect_neg === cb.affect) ? affect : compile_affect(cb.affect_neg, cb, sys; default = EMPTY_AFFECT, kwargs...) push!(affect_negs, affect_neg) - push!(inits, compile_affect(cb.initialize, cb, sys; default = nothing, is_init = true), kwargs...) - push!(finals, compile_affect(cb.finalize, cb, sys; default = nothing), kwargs...) + push!(inits, compile_affect(cb.initialize, cb, sys; default = nothing, is_init = true, kwargs...)) + push!(finals, compile_affect(cb.finalize, cb, sys; default = nothing, kwargs...)) end # Since there may be different number of conditions and affects, @@ -749,14 +728,14 @@ function generate_callback(cb, sys; kwargs...) ps = parameters(sys; initial_parameters = true) trigger = is_timed ? conditions(cb) : compile_condition(cb, sys, dvs, ps; kwargs...) - affect = compile_affect(cb.affect, cb, sys, default = EMPTY_FUNCTION, kwargs...) + affect = compile_affect(cb.affect, cb, sys; default = EMPTY_AFFECT, kwargs...) affect_neg = if is_discrete(cb) nothing else - (cb.affect === cb.affect_neg) ? affect : compile_affect(cb.affect_neg, cb, sys, default = EMPTY_FUNCTION, kwargs...) + (cb.affect === cb.affect_neg) ? affect : compile_affect(cb.affect_neg, cb, sys; default = EMPTY_AFFECT, kwargs...) end - init = compile_affect(cb.initialize, cb, sys, default = SciMLBase.INITIALIZE_DEFAULT, is_init = true, kwargs...) - final = compile_affect(cb.finalize, cb, sys, default = SciMLBase.FINALIZE_DEFAULT, kwargs...) + init = compile_affect(cb.initialize, cb, sys; default = SciMLBase.INITIALIZE_DEFAULT, is_init = true, kwargs...) + final = compile_affect(cb.finalize, cb, sys; default = SciMLBase.FINALIZE_DEFAULT, kwargs...) initialize = isnothing(cb.initialize) ? init : ((c, u, t, i) -> init(i)) finalize = isnothing(cb.finalize) ? final : ((c, u, t, i) -> final(i)) @@ -802,28 +781,27 @@ function compile_affect( end if isnothing(aff) - full_args = is_init && (default === SciMLBase.INITIALIZE_DEFAULT) - is_init ? wrap_save_discretes(f, save_idxs; full_args) : default + is_init ? wrap_save_discretes(default, save_idxs) : default elseif aff isa AffectSystem f = compile_equational_affect(aff, sys; kwargs...) wrap_save_discretes(f, save_idxs) elseif aff isa FunctionalAffect || aff isa ImperativeAffect f = compile_functional_affect(aff, sys; kwargs...) - wrap_save_discretes(f, save_idxs; full_args = true) + wrap_save_discretes(f, save_idxs) end end -function wrap_save_discretes(f, save_idxs; full_args = false) +function wrap_save_discretes(f, save_idxs) let save_idxs = save_idxs - if full_args - return (c, u, t, i) -> begin + if f === SciMLBase.INITIALIZE_DEFAULT + (c, u, t, i) -> begin isnothing(f) || f(c, u, t, i) for idx in save_idxs SciMLBase.save_discretes!(i, idx) end end else - return (i) -> begin + (i) -> begin isnothing(f) || f(i) for idx in save_idxs SciMLBase.save_discretes!(i, idx) @@ -859,42 +837,46 @@ end """ Compile an affect defined by a set of equations. Systems with algebraic equations will solve implicit discrete problems to obtain their next state. Systems without will generate functions that perform explicit updates. """ -function compile_equational_affect(aff::AffectSystem, sys; reset_jumps = false, kwargs...) +function compile_equational_affect(aff::Union{AffectSystem, Vector{Equation}}, sys; reset_jumps = false, kwargs...) + aff isa AbstractVector && (aff = make_affect(aff, iv = get_iv(sys))) affsys = system(aff) - reinit = has_alg_equations(sys) || has_alg_equations(affsys) ps_to_update = discretes(aff) dvs_to_update = setdiff(unknowns(aff), getfield.(observed(sys), :lhs)) + aff_map = aff_to_sys(aff) + sys_map = Dict([v => k for (k, v) in aff_map]) if is_explicit(aff) - update_eqs = equations(affsys) - update_eqs = Symbolics.fast_substitute(equations, Dict([p => unPre(p) for p in parameters(affsys)])) + affsys = structural_simplify(affsys) + @assert isempty(equations(affsys)) + update_eqs = Symbolics.fast_substitute(observed(affsys), Dict([p => unPre(p) for p in parameters(affsys)])) rhss = map(x -> x.rhs, update_eqs) - lhss = map(x -> x.lhs, update_eqs) - is_p = [lhs ∈ ps_to_update for lhs in lhss] + lhss = map(x -> aff_map[x.lhs], update_eqs) + is_p = [lhs ∈ Set(ps_to_update) for lhs in lhss] dvs = unknowns(sys) ps = parameters(sys) t = get_iv(sys) u_idxs = indexin((@view lhss[.!is_p]), dvs) - p_idxs = indexin((@view lhss[is_p]), ps) + p_idxs = if has_index_cache(sys) && (get_index_cache(sys) !== nothing) + [parameter_index(sys, p) for p in lhss[is_p]] + else + indexin((@view lhss[is_p]), ps) + end _ps = reorder_parameters(sys, ps) integ = gensym(:MTKIntegrator) - u_up, u_up! = build_function_wrapper(sys, (@view rhss[.!is_p]), dvs, _ps..., t; wrap_code = add_integrator_header(sys, integ, :u), expression = Val{false}, outputidxs = u_idxs) - p_up, p_up! = build_function_wrapper(sys, (@view rhss[is_p]), dvs, _ps..., t; wrap_code = add_integrator_header(sys, integ, :p), expression = Val{false}, outputidxs = p_idxs) + u_up, u_up! = build_function_wrapper(sys, (@view rhss[.!is_p]), dvs, _ps..., t; wrap_code = add_integrator_header(sys, integ, :u), expression = Val{false}, outputidxs = u_idxs, wrap_mtkparameters = false) + p_up, p_up! = build_function_wrapper(sys, (@view rhss[is_p]), dvs, _ps..., t; wrap_code = add_integrator_header(sys, integ, :p), expression = Val{false}, outputidxs = p_idxs, wrap_mtkparameters = false) - return (integ) -> begin + return function explicit_affect!(integ) u_up!(integ) p_up!(integ) reset_jumps && reset_aggregated_jumps!(integ) end else - aff_map = aff_to_sys(aff) - sys_map = Dict([v => k for (k, v) in aff_map]) - return let dvs_to_update = dvs_to_update, aff_map = aff_map, sys_map = sys_map, affsys = affsys, ps_to_update = ps_to_update - (integ) -> begin + function implicit_affect!(integ) pmap = Pair[] for pre_p in parameters(affsys) p = unPre(pre_p) @@ -946,7 +928,7 @@ function discrete_events(sys::AbstractSystem) systems = get_systems(sys) cbs = [obs; 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 @@ -957,6 +939,22 @@ function get_discrete_events(sys::AbstractSystem) getfield(sys, :discrete_events) 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 + + """ continuous_events(sys::AbstractSystem)::Vector{SymbolicContinuousCallback} @@ -984,3 +982,18 @@ function get_continuous_events(sys::AbstractSystem) has_continuous_events(sys) || return SymbolicContinuousCallback[] getfield(sys, :continuous_events) 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 diff --git a/src/systems/jumps/jumpsystem.jl b/src/systems/jumps/jumpsystem.jl index a768eeb60d..51d56d2779 100644 --- a/src/systems/jumps/jumpsystem.jl +++ b/src/systems/jumps/jumpsystem.jl @@ -288,8 +288,8 @@ function generate_affect_function(js::JumpSystem, affect) csubs = Dict(c => getdefault(c) for c in consts) affect = substitute(affect, csubs) end - compile_equational_affect( - affect, js; expression = Val{true}, checkvars = false) + @show dump(affect[1]) + compile_equational_affect(affect, js; expression = Val{true}, checkvars = false) end function assemble_vrj( @@ -298,7 +298,7 @@ function assemble_vrj( rate = GeneratedFunctionWrapper{(2, 3, is_split(js))}(rate, nothing) outputvars = (value(affect.lhs) for affect in vrj.affect!) outputidxs = [unknowntoid[var] for var in outputvars] - affect = eval_or_rgf(generate_affect_function(js, vrj.affect!); eval_expression, eval_module) + affect = generate_affect_function(js, vrj.affect!) VariableRateJump(rate, affect; save_positions = vrj.save_positions) end @@ -309,7 +309,6 @@ function assemble_vrj_expr(js, vrj, unknowntoid) affect = generate_affect_function(js, vrj.affect!) quote rate = $rate - affect = $affect VariableRateJump(rate, affect) end @@ -321,7 +320,7 @@ function assemble_crj( rate = GeneratedFunctionWrapper{(2, 3, is_split(js))}(rate, nothing) outputvars = (value(affect.lhs) for affect in crj.affect!) outputidxs = [unknowntoid[var] for var in outputvars] - affect = eval_or_rgf(generate_affect_function(js, crj.affect!); eval_expression, eval_module) + affect = generate_affect_function(js, crj.affect!) ConstantRateJump(rate, affect) end @@ -332,7 +331,6 @@ function assemble_crj_expr(js, crj, unknowntoid) affect = generate_affect_function(js, crj.affect!) quote rate = $rate - affect = $affect ConstantRateJump(rate, affect) end @@ -542,6 +540,7 @@ function JumpProcesses.JumpProblem(js::JumpSystem, prob, majpmapper = JumpSysMajParamMapper(js, p; jseqs = eqs, rateconsttype = invttype) majs = isempty(eqs.x[1]) ? nothing : assemble_maj(eqs.x[1], unknowntoid, majpmapper) + @show eqs.x[2] crjs = ConstantRateJump[assemble_crj(js, j, unknowntoid; eval_expression, eval_module) for j in eqs.x[2]] vrjs = VariableRateJump[assemble_vrj(js, j, unknowntoid; eval_expression, eval_module) diff --git a/test/accessor_functions.jl b/test/accessor_functions.jl index a9efde3a98..24fb245fed 100644 --- a/test/accessor_functions.jl +++ b/test/accessor_functions.jl @@ -149,20 +149,17 @@ let 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 + # as I stored the same single 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] + bot_cev = ModelingToolkit.SymbolicContinuousCallback(cevs[1], algeeqs = [O ~ (d + p_bot) * X_bot + Y]) + mid_dev = ModelingToolkit.SymbolicDiscreteCallback(devs[1], algeeqs = [O ~ (d + p_mid1) * X_mid1 + Y]) @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]) + continuous_events_toplevel.([sys_bot, sys_bot_comp, sys_bot_ss])..., + [bot_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]) + [sys_mid1, sys_mid1_comp, sys_mid1_ss])..., + [mid_dev]) @test all(sym_issubset( continuous_events_toplevel(sys), get_continuous_events(sys)) for sys in [sys_bot, sys_mid2, sys_mid1, sys_top]) diff --git a/test/funcaffect.jl b/test/funcaffect.jl index 3004044d61..6e699d1838 100644 --- a/test/funcaffect.jl +++ b/test/funcaffect.jl @@ -24,8 +24,7 @@ cb1 = ModelingToolkit.SymbolicDiscreteCallback(t == zr, (affect1!, [], [], [], [ @test cb == cb1 @test ModelingToolkit.SymbolicDiscreteCallback(cb) === cb # passthrough @test hash(cb) == hash(cb1) -ModelingToolkit.generate_discrete_callback(cb, sys, ModelingToolkit.get_variables(sys), - ModelingToolkit.get_ps(sys)); +ModelingToolkit.generate_callback(cb, sys); cb = ModelingToolkit.SymbolicContinuousCallback([t ~ zr], (f = affect1!, sts = [], pars = [], discretes = [], @@ -46,7 +45,7 @@ sys1 = ODESystem(eqs, t, [u], [], name = :sys, de = ModelingToolkit.get_discrete_events(sys1) @test length(de) == 1 de = de[1] -@test ModelingToolkit.condition(de) == [4.0] +@test ModelingToolkit.conditions(de) == [4.0] @test ModelingToolkit.has_functional_affect(de) sys2 = ODESystem(eqs, t, [u], [], name = :sys, diff --git a/test/parameter_dependencies.jl b/test/parameter_dependencies.jl index 31881e1ca8..cc2f137392 100644 --- a/test/parameter_dependencies.jl +++ b/test/parameter_dependencies.jl @@ -287,13 +287,13 @@ end @constants h = 1 @variables S(t) I(t) R(t) rate₁ = β * S * I * h - affect₁ = [S ~ S - 1 * h, I ~ I + 1] + affect₁ = [S ~ Pre(S) - 1 * h, I ~ Pre(I) + 1] rate₃ = γ * I * h - affect₃ = [I ~ I * h - 1, R ~ R + 1] + affect₃ = [I ~ Pre(I) * h - 1, R ~ Pre(R) + 1] j₁ = ConstantRateJump(rate₁, affect₁) j₃ = ConstantRateJump(rate₃, affect₃) @named js2 = JumpSystem( - [j₁, j₃], t, [S, I, R], [γ]; parameter_dependencies = [β => 0.01γ]) + [j₃], t, [S, I, R], [γ]; parameter_dependencies = [β => 0.01γ]) @test isequal(only(parameters(js2)), γ) @test Set(full_parameters(js2)) == Set([γ, β]) js2 = complete(js2) diff --git a/test/symbolic_events.jl b/test/symbolic_events.jl index da0a2d0d98..d0f9f29c32 100644 --- a/test/symbolic_events.jl +++ b/test/symbolic_events.jl @@ -1332,3 +1332,5 @@ end # - Functional affects reinitialize correctly # - explicit equation of t in a functional affect # - modifying both u and p in an affect +# - affects that have Pre but are also algebraic in nature +# - reinitialization after affects From 28d52107a0ba54ef63d37d69e6245dfd2bec0875 Mon Sep 17 00:00:00 2001 From: vyudu Date: Sat, 22 Mar 2025 07:34:02 -0400 Subject: [PATCH 22/52] more test fixes --- src/systems/callbacks.jl | 2 +- test/symbolic_events.jl | 5 ----- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/src/systems/callbacks.jl b/src/systems/callbacks.jl index 47498580da..d9c7bbfc90 100644 --- a/src/systems/callbacks.jl +++ b/src/systems/callbacks.jl @@ -244,7 +244,6 @@ function make_affect(affect::Vector{Equation}; iv = nothing, algeeqs = Equation[ isempty(affect) && return nothing isempty(algeeqs) && @warn "No algebraic equations were found. If the system has no algebraic equations, this can be disregarded. Otherwise pass in `algeeqs` to the SymbolicContinuousCallback constructor." - @show affect explicit = true affect = scalarize(affect) dvs = OrderedSet() @@ -288,6 +287,7 @@ function make_affect(affect::Vector{Equation}; iv = nothing, algeeqs = Equation[ rev_map = Dict([v => k for (k, v) in aff_map]) affect = Symbolics.substitute(affect, rev_map) @named affectsys = ImplicitDiscreteSystem(vcat(affect, algeeqs), iv, collect(union(dvs, p_as_dvs)), cb_params) + affectsys = complete(affectsys) # get accessed parameters p from Pre(p) in the callback parameters params = filter(isparameter, map(x -> unPre(x), cb_params)) # add unknowns to the map diff --git a/test/symbolic_events.jl b/test/symbolic_events.jl index d0f9f29c32..ada6844f90 100644 --- a/test/symbolic_events.jl +++ b/test/symbolic_events.jl @@ -65,15 +65,12 @@ affect_neg = [x ~ 1] e = SymbolicContinuousCallback(eqs[], affect) @test e isa SymbolicContinuousCallback @test isequal(equations(e), eqs) - @test observed(system(affects(e))) == affect - @test observed(system(affect_negs(e))) == affect @test e.rootfind == SciMLBase.LeftRootFind # with only positive edge affect e = SymbolicContinuousCallback(eqs[], affect, affect_neg = nothing) @test e isa SymbolicContinuousCallback @test isequal(equations(e), eqs) - @test observed(system(affects(e))) == affect @test isnothing(e.affect_neg) @test e.rootfind == SciMLBase.LeftRootFind @@ -81,8 +78,6 @@ affect_neg = [x ~ 1] e = SymbolicContinuousCallback(eqs[], affect, affect_neg = affect_neg) @test e isa SymbolicContinuousCallback @test isequal(equations(e), eqs) - @test observed(system(affects(e))) == affect - @test observed(system(affect_negs(e))) == affect_neg @test e.rootfind == SciMLBase.LeftRootFind # with different root finding ops From 9cc68271509f10a1c2d9658d0e5b9b796549213b Mon Sep 17 00:00:00 2001 From: vyudu Date: Mon, 24 Mar 2025 12:38:46 -0400 Subject: [PATCH 23/52] fix: more tests passing --- src/structural_transformation/utils.jl | 1 + src/systems/callbacks.jl | 24 ++++++++++++------- .../implicit_discrete_system.jl | 2 +- src/systems/jumps/jumpsystem.jl | 2 -- test/symbolic_events.jl | 4 +++- 5 files changed, 21 insertions(+), 12 deletions(-) diff --git a/src/structural_transformation/utils.jl b/src/structural_transformation/utils.jl index ebcb834bb1..7834e33b61 100644 --- a/src/structural_transformation/utils.jl +++ b/src/structural_transformation/utils.jl @@ -551,6 +551,7 @@ end function _distribute_shift(expr, shift) if iscall(expr) op = operation(expr) + (op isa Pre || op isa Initial) && return expr args = arguments(expr) if ModelingToolkit.isvariable(expr) diff --git a/src/systems/callbacks.jl b/src/systems/callbacks.jl index d9c7bbfc90..2cf6e8d907 100644 --- a/src/systems/callbacks.jl +++ b/src/systems/callbacks.jl @@ -242,7 +242,7 @@ make_affect(affect::Affect; kwargs...) = affect function make_affect(affect::Vector{Equation}; iv = nothing, algeeqs = Equation[]) isempty(affect) && return nothing - isempty(algeeqs) && @warn "No algebraic equations were found. If the system has no algebraic equations, this can be disregarded. Otherwise pass in `algeeqs` to the SymbolicContinuousCallback constructor." + isempty(algeeqs) && @warn "No algebraic equations were found for the callback defined by $(join(affect, ", ")). If the system has no algebraic equations, this can be disregarded. Otherwise pass in `algeeqs` to the SymbolicContinuousCallback constructor." explicit = true affect = scalarize(affect) @@ -259,6 +259,8 @@ function make_affect(affect::Vector{Equation}; iv = nothing, algeeqs = Equation[ collect_vars!(dvs, params, eq, iv) explicit = false end + any(isirreducible, dvs) && (explicit = false) + if isnothing(iv) iv = isempty(dvs) ? iv : only(arguments(dvs[1])) isnothing(iv) && @warn "No independent variable specified and could not be inferred. If the iv appears in an affect equation explicitly, like x ~ t + 1, then it must be specified as an argument to the SymbolicContinuousCallback or SymbolicDiscreteCallback constructor. Otherwise this warning can be disregarded." @@ -858,16 +860,19 @@ function compile_equational_affect(aff::Union{AffectSystem, Vector{Equation}}, s t = get_iv(sys) u_idxs = indexin((@view lhss[.!is_p]), dvs) - p_idxs = if has_index_cache(sys) && (get_index_cache(sys) !== nothing) - [parameter_index(sys, p) for p in lhss[is_p]] + + wrap_mtkparameters = has_index_cache(sys) && (get_index_cache(sys) !== nothing) + p_idxs = if wrap_mtkparameters + [parameter_index(sys, p) for (i, p) in enumerate(lhss) + if is_p[i]] else indexin((@view lhss[is_p]), ps) end _ps = reorder_parameters(sys, ps) integ = gensym(:MTKIntegrator) - u_up, u_up! = build_function_wrapper(sys, (@view rhss[.!is_p]), dvs, _ps..., t; wrap_code = add_integrator_header(sys, integ, :u), expression = Val{false}, outputidxs = u_idxs, wrap_mtkparameters = false) - p_up, p_up! = build_function_wrapper(sys, (@view rhss[is_p]), dvs, _ps..., t; wrap_code = add_integrator_header(sys, integ, :p), expression = Val{false}, outputidxs = p_idxs, wrap_mtkparameters = false) + u_up, u_up! = build_function_wrapper(sys, (@view rhss[.!is_p]), dvs, _ps..., t; wrap_code = add_integrator_header(sys, integ, :u), expression = Val{false}, outputidxs = u_idxs, wrap_mtkparameters) + p_up, p_up! = build_function_wrapper(sys, (@view rhss[is_p]), dvs, _ps..., t; wrap_code = add_integrator_header(sys, integ, :p), expression = Val{false}, outputidxs = p_idxs, wrap_mtkparameters) return function explicit_affect!(integ) u_up!(integ) @@ -883,9 +888,12 @@ function compile_equational_affect(aff::Union{AffectSystem, Vector{Equation}}, s pval = isparameter(p) ? integ.ps[p] : integ[p] push!(pmap, pre_p => pval) end - guesses = Pair[u => integ[aff_map[u]] for u in unknowns(affsys)] - affprob = ImplicitDiscreteProblem(affsys, Pair[], (integ.t, integ.t), pmap; guesses, build_initializeprob = false) - + u0 = Pair[] + for u in unknowns(affsys) + uval = isparameter(aff_map[u]) ? integ.ps[u] : integ[u] + push!(u0, u => uval) + end + affprob = ImplicitDiscreteProblem(affsys, u0, (integ.t, integ.t), pmap; build_initializeprob = false, check_length = false) affsol = init(affprob, SimpleIDSolve()) for u in dvs_to_update integ[u] = affsol[sys_map[u]] diff --git a/src/systems/discrete_system/implicit_discrete_system.jl b/src/systems/discrete_system/implicit_discrete_system.jl index c0ac00734b..fb83f8fd78 100644 --- a/src/systems/discrete_system/implicit_discrete_system.jl +++ b/src/systems/discrete_system/implicit_discrete_system.jl @@ -298,7 +298,7 @@ function shift_u0map_forward(sys::ImplicitDiscreteSystem, u0map, defs) v = u0map[k] if !((op = operation(k)) isa Shift) isnothing(getunshifted(k)) && - error("Initial conditions must be for the past state of the unknowns. Instead of providing the condition for $k, provide the condition for $(Shift(iv, -1)(k)).") + @warn "Initial condition given in term of current state of the unknown. If `build_initializeprob = false, this may be overriden by the implicit discrete solver." updated[k] = v elseif op.steps > 0 diff --git a/src/systems/jumps/jumpsystem.jl b/src/systems/jumps/jumpsystem.jl index 51d56d2779..8723cc28a4 100644 --- a/src/systems/jumps/jumpsystem.jl +++ b/src/systems/jumps/jumpsystem.jl @@ -288,7 +288,6 @@ function generate_affect_function(js::JumpSystem, affect) csubs = Dict(c => getdefault(c) for c in consts) affect = substitute(affect, csubs) end - @show dump(affect[1]) compile_equational_affect(affect, js; expression = Val{true}, checkvars = false) end @@ -540,7 +539,6 @@ function JumpProcesses.JumpProblem(js::JumpSystem, prob, majpmapper = JumpSysMajParamMapper(js, p; jseqs = eqs, rateconsttype = invttype) majs = isempty(eqs.x[1]) ? nothing : assemble_maj(eqs.x[1], unknowntoid, majpmapper) - @show eqs.x[2] crjs = ConstantRateJump[assemble_crj(js, j, unknowntoid; eval_expression, eval_module) for j in eqs.x[2]] vrjs = VariableRateJump[assemble_vrj(js, j, unknowntoid; eval_expression, eval_module) diff --git a/test/symbolic_events.jl b/test/symbolic_events.jl index ada6844f90..c1b631b6bc 100644 --- a/test/symbolic_events.jl +++ b/test/symbolic_events.jl @@ -1232,7 +1232,9 @@ end @variables x(t) [irreducible = true] y(t) [irreducible = true] eqs = [x ~ y, D(x) ~ -1] cb = [x ~ 0.0] => [x ~ 0, y ~ 1] - @test_throws Exception @mtkbuild pend = ODESystem(eqs, t; continuous_events = [cb]) + @mtkbuild pend = ODESystem(eqs, t; continuous_events = [cb]) + prob = ODEProblem(pend, [x => 1], (0.0, 3.0), guesses = [y => x]) + @test_broken !SciMLBase.successful_retcode(solve(prob, Rodas5())) cb = [x ~ 0.0] => [y ~ 1] @mtkbuild pend = ODESystem(eqs, t; continuous_events = [cb]) From 6670e18d1b313cf6b4803b54aecbe16a479ba78a Mon Sep 17 00:00:00 2001 From: vyudu Date: Mon, 24 Mar 2025 15:46:15 -0400 Subject: [PATCH 24/52] fix: more test fixes --- src/systems/callbacks.jl | 11 +++++++---- src/systems/model_parsing.jl | 18 +++++++----------- test/discrete_system.jl | 1 - test/fmi/fmi.jl | 6 ++---- test/jumpsystem.jl | 6 +++--- test/model_parsing.jl | 2 +- 6 files changed, 20 insertions(+), 24 deletions(-) diff --git a/src/systems/callbacks.jl b/src/systems/callbacks.jl index 2cf6e8d907..e274b6efdd 100644 --- a/src/systems/callbacks.jl +++ b/src/systems/callbacks.jl @@ -240,12 +240,11 @@ make_affect(affect::Tuple; kwargs...) = FunctionalAffect(affect...) make_affect(affect::NamedTuple; kwargs...) = FunctionalAffect(; affect...) make_affect(affect::Affect; kwargs...) = affect -function make_affect(affect::Vector{Equation}; iv = nothing, algeeqs = Equation[]) +function make_affect(affect::Vector{Equation}; iv = nothing, algeeqs::Vector{Equation} = Equation[]) isempty(affect) && return nothing isempty(algeeqs) && @warn "No algebraic equations were found for the callback defined by $(join(affect, ", ")). If the system has no algebraic equations, this can be disregarded. Otherwise pass in `algeeqs` to the SymbolicContinuousCallback constructor." explicit = true - affect = scalarize(affect) dvs = OrderedSet() params = OrderedSet() for eq in affect @@ -296,8 +295,9 @@ function make_affect(affect::Vector{Equation}; iv = nothing, algeeqs = Equation[ for u in dvs aff_map[u] = u end + @show explicit - return AffectSystem(affectsys, collect(dvs), params, discretes, aff_map, explicit) + AffectSystem(affectsys, collect(dvs), params, discretes, aff_map, explicit) end function make_affect(affect; kwargs...) @@ -840,7 +840,10 @@ end Compile an affect defined by a set of equations. Systems with algebraic equations will solve implicit discrete problems to obtain their next state. Systems without will generate functions that perform explicit updates. """ function compile_equational_affect(aff::Union{AffectSystem, Vector{Equation}}, sys; reset_jumps = false, kwargs...) - aff isa AbstractVector && (aff = make_affect(aff, iv = get_iv(sys))) + if aff isa AbstractVector + aff = make_affect(aff; iv = get_iv(sys)) + @show is_explicit(aff) + end affsys = system(aff) ps_to_update = discretes(aff) dvs_to_update = setdiff(unknowns(aff), getfield.(observed(sys), :lhs)) diff --git a/src/systems/model_parsing.jl b/src/systems/model_parsing.jl index 4632c1b889..9090182e44 100644 --- a/src/systems/model_parsing.jl +++ b/src/systems/model_parsing.jl @@ -64,6 +64,8 @@ function _model_macro(mod, name, expr, isconnector) push!(exprs.args, :(systems = ODESystem[])) push!(exprs.args, :(equations = Union{Equation, Vector{Equation}}[])) push!(exprs.args, :(defaults = Dict{Num, Union{Number, Symbol, Function}}())) + push!(exprs.args, :(disc_events = [])) + push!(exprs.args, :(cont_events = [])) Base.remove_linenums!(expr) for arg in expr.args @@ -105,6 +107,8 @@ function _model_macro(mod, name, expr, isconnector) push!(exprs.args, :(push!(parameters, $(ps...)))) push!(exprs.args, :(push!(systems, $(comps...)))) push!(exprs.args, :(push!(variables, $(vs...)))) + push!(exprs.args, :(push!(disc_events, $(d_evts...)))) + push!(exprs.args, :(push!(cont_events, $(c_evts...)))) gui_metadata = isassigned(icon) > 0 ? GUIMetadata(GlobalRef(mod, name), icon[]) : GUIMetadata(GlobalRef(mod, name)) @@ -115,7 +119,7 @@ function _model_macro(mod, name, expr, isconnector) Ref(dict), [:constants, :defaults, :kwargs, :structural_parameters]) sys = :($ODESystem($(flatten_equations)(equations), $iv, variables, parameters; - name, description = $description, systems, gui_metadata = $gui_metadata, defaults)) + name, description = $description, systems, gui_metadata = $gui_metadata, defaults, continuous_events = cont_events, discrete_events = disc_events)) if length(ext) == 0 push!(exprs.args, :(var"#___sys___" = $sys)) @@ -126,16 +130,6 @@ function _model_macro(mod, name, expr, isconnector) isconnector && push!(exprs.args, :($Setfield.@set!(var"#___sys___".connector_type=$connector_type(var"#___sys___")))) - !isempty(c_evts) && push!(exprs.args, - :($Setfield.@set!(var"#___sys___".continuous_events=$SymbolicContinuousCallback.([ - $(c_evts...) - ])))) - - !isempty(d_evts) && push!(exprs.args, - :($Setfield.@set!(var"#___sys___".discrete_events=$SymbolicDiscreteCallback.([ - $(d_evts...) - ])))) - f = if length(where_types) == 0 :($(Symbol(:__, name, :__))(; name, $(kwargs...)) = $exprs) else @@ -1121,6 +1115,7 @@ function parse_equations!(exprs, eqs, dict, body) end function parse_continuous_events!(c_evts, dict, body) + @show body dict[:continuous_events] = [] Base.remove_linenums!(body) for arg in body.args @@ -1130,6 +1125,7 @@ function parse_continuous_events!(c_evts, dict, body) end function parse_discrete_events!(d_evts, dict, body) + @show body dict[:discrete_events] = [] Base.remove_linenums!(body) for arg in body.args diff --git a/test/discrete_system.jl b/test/discrete_system.jl index 874d045aa9..ebc800c680 100644 --- a/test/discrete_system.jl +++ b/test/discrete_system.jl @@ -257,7 +257,6 @@ end @variables x(t) y(t) k = ShiftIndex(t) @named sys = DiscreteSystem([x ~ x^2 + y^2, y ~ x(k - 1) + y(k - 1)], t) -@test_throws ["algebraic equations", "ImplicitDiscreteSystem"] structural_simplify(sys) @testset "Passing `nothing` to `u0`" begin @variables x(t) = 1 diff --git a/test/fmi/fmi.jl b/test/fmi/fmi.jl index 98c93398ff..0d10f3204a 100644 --- a/test/fmi/fmi.jl +++ b/test/fmi/fmi.jl @@ -157,8 +157,7 @@ end @testset "v2, CS" begin fmu = loadFMU(joinpath(FMU_DIR, "SimpleAdder.fmu"); type = :CS) @named adder = MTK.FMIComponent( - Val(2); fmu, type = :CS, communication_step_size = 1e-6, - reinitializealg = BrownFullBasicInit()) + Val(2); fmu, type = :CS, communication_step_size = 1e-6) @test MTK.isinput(adder.a) @test MTK.isinput(adder.b) @test MTK.isoutput(adder.out) @@ -210,8 +209,7 @@ end @testset "v3, CS" begin fmu = loadFMU(joinpath(FMU_DIR, "StateSpace.fmu"); type = :CS) @named sspace = MTK.FMIComponent( - Val(3); fmu, communication_step_size = 1e-6, type = :CS, - reinitializealg = BrownFullBasicInit()) + Val(3); fmu, communication_step_size = 1e-6, type = :CS) @test MTK.isinput(sspace.u) @test MTK.isoutput(sspace.y) @test !MTK.isinput(sspace.x) && !MTK.isoutput(sspace.x) diff --git a/test/jumpsystem.jl b/test/jumpsystem.jl index d77e37f516..89fc828205 100644 --- a/test/jumpsystem.jl +++ b/test/jumpsystem.jl @@ -11,9 +11,9 @@ rng = StableRNG(12345) @constants h = 1 @variables S(t) I(t) R(t) rate₁ = β * S * I * h -affect₁ = [S ~ S - 1 * h, I ~ I + 1] +affect₁ = [S ~ Pre(S) - 1 * h, I ~ Pre(I) + 1] rate₂ = γ * I + t -affect₂ = [I ~ I - 1, R ~ R + 1] +affect₂ = [I ~ Pre(I) - 1, R ~ Pre(R) + 1] j₁ = ConstantRateJump(rate₁, affect₁) j₂ = VariableRateJump(rate₂, affect₂) @named js = JumpSystem([j₁, j₂], t, [S, I, R], [β, γ]) @@ -59,7 +59,7 @@ jump2.affect!(integrator) # test MT can make and solve a jump problem rate₃ = γ * I * h -affect₃ = [I ~ I * h - 1, R ~ R + 1] +affect₃ = [I ~ Pre(I) * h - 1, R ~ Pre(R) + 1] j₃ = ConstantRateJump(rate₃, affect₃) @named js2 = JumpSystem([j₁, j₃], t, [S, I, R], [β, γ]) js2 = complete(js2) diff --git a/test/model_parsing.jl b/test/model_parsing.jl index 62c19d2055..afd1e32ac9 100644 --- a/test/model_parsing.jl +++ b/test/model_parsing.jl @@ -484,7 +484,7 @@ using ModelingToolkit: D_nounits [x ~ 1.5] => [x ~ 5, y ~ 1] end @discrete_events begin - (t == 1.5) => [x ~ x + 5, z ~ 2] + (t == 1.5) => [x ~ Pre(x) + 5, z ~ 2] end end From 57ca008b41b727534b64f341232ae321ba998f19 Mon Sep 17 00:00:00 2001 From: vyudu Date: Mon, 24 Mar 2025 19:28:21 -0400 Subject: [PATCH 25/52] fix: use is_diff_equation instead of isdiffeq when finding algeeqs --- src/systems/callbacks.jl | 2 -- src/systems/diffeqs/odesystem.jl | 2 +- src/systems/diffeqs/sdesystem.jl | 2 +- src/systems/model_parsing.jl | 2 -- test/initializationsystem.jl | 4 ++-- 5 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/systems/callbacks.jl b/src/systems/callbacks.jl index e274b6efdd..eee74360fa 100644 --- a/src/systems/callbacks.jl +++ b/src/systems/callbacks.jl @@ -295,7 +295,6 @@ function make_affect(affect::Vector{Equation}; iv = nothing, algeeqs::Vector{Equ for u in dvs aff_map[u] = u end - @show explicit AffectSystem(affectsys, collect(dvs), params, discretes, aff_map, explicit) end @@ -842,7 +841,6 @@ Compile an affect defined by a set of equations. Systems with algebraic equation function compile_equational_affect(aff::Union{AffectSystem, Vector{Equation}}, sys; reset_jumps = false, kwargs...) if aff isa AbstractVector aff = make_affect(aff; iv = get_iv(sys)) - @show is_explicit(aff) end affsys = system(aff) ps_to_update = discretes(aff) diff --git a/src/systems/diffeqs/odesystem.jl b/src/systems/diffeqs/odesystem.jl index f10c62f94d..127ced51d1 100644 --- a/src/systems/diffeqs/odesystem.jl +++ b/src/systems/diffeqs/odesystem.jl @@ -311,7 +311,7 @@ function ODESystem(deqs::AbstractVector{<:Equation}, iv, dvs, ps; throw(ArgumentError("System names must be unique.")) end - algeeqs = filter(eq -> eq.lhs isa Union{Symbolic, Number} && !isdiffeq(eq), deqs) + algeeqs = filter(eq -> eq.lhs isa Union{Symbolic, Number} && !is_diff_equation(eq), deqs) cont_callbacks = SymbolicContinuousCallbacks(continuous_events; algeeqs, iv) disc_callbacks = SymbolicDiscreteCallbacks(discrete_events; algeeqs, iv) diff --git a/src/systems/diffeqs/sdesystem.jl b/src/systems/diffeqs/sdesystem.jl index 5a4c966ad1..22cc9b9131 100644 --- a/src/systems/diffeqs/sdesystem.jl +++ b/src/systems/diffeqs/sdesystem.jl @@ -268,7 +268,7 @@ function SDESystem(deqs::AbstractVector{<:Equation}, neqs::AbstractArray, iv, dv Wfact = RefValue(EMPTY_JAC) Wfact_t = RefValue(EMPTY_JAC) - algeeqs = filter(eq -> eq.lhs isa Union{Symbolic, Number} && !isdiffeq(eq), deqs) + algeeqs = filter(eq -> eq.lhs isa Union{Symbolic, Number} && !is_diff_equation(eq), deqs) cont_callbacks = SymbolicContinuousCallbacks(continuous_events; algeeqs, iv) disc_callbacks = SymbolicDiscreteCallbacks(discrete_events; algeeqs, iv) if is_dde === nothing diff --git a/src/systems/model_parsing.jl b/src/systems/model_parsing.jl index 9090182e44..3243824dc0 100644 --- a/src/systems/model_parsing.jl +++ b/src/systems/model_parsing.jl @@ -1115,7 +1115,6 @@ function parse_equations!(exprs, eqs, dict, body) end function parse_continuous_events!(c_evts, dict, body) - @show body dict[:continuous_events] = [] Base.remove_linenums!(body) for arg in body.args @@ -1125,7 +1124,6 @@ function parse_continuous_events!(c_evts, dict, body) end function parse_discrete_events!(d_evts, dict, body) - @show body dict[:discrete_events] = [] Base.remove_linenums!(body) for arg in body.args diff --git a/test/initializationsystem.jl b/test/initializationsystem.jl index 54b847c64a..9f300cb898 100644 --- a/test/initializationsystem.jl +++ b/test/initializationsystem.jl @@ -1280,9 +1280,9 @@ end @parameters β γ S0 @variables S(t)=S0 I(t) R(t) rate₁ = β * S * I - affect₁ = [S ~ S - 1, I ~ I + 1] + affect₁ = [S ~ Pre(S) - 1, I ~ Pre(I) + 1] rate₂ = γ * I - affect₂ = [I ~ I - 1, R ~ R + 1] + affect₂ = [I ~ Pre(I) - 1, R ~ Pre(R) + 1] j₁ = ConstantRateJump(rate₁, affect₁) j₂ = ConstantRateJump(rate₂, affect₂) j₃ = MassActionJump(2 * β + γ, [R => 1], [S => 1, R => -1]) From 4e7a4298de0fb14da1a45d6e5894c8d9dec9040a Mon Sep 17 00:00:00 2001 From: vyudu Date: Tue, 25 Mar 2025 17:26:29 -0400 Subject: [PATCH 26/52] feat: specify discrete_parameters --- src/systems/callbacks.jl | 62 +- .../implicit_discrete_system.jl | 8 + test/jumpsystem.jl | 3 +- test/symbolic_events.jl | 2473 +++++++++-------- 4 files changed, 1303 insertions(+), 1243 deletions(-) diff --git a/src/systems/callbacks.jl b/src/systems/callbacks.jl index eee74360fa..5d34644297 100644 --- a/src/systems/callbacks.jl +++ b/src/systems/callbacks.jl @@ -219,6 +219,7 @@ struct SymbolicContinuousCallback <: AbstractCallback function SymbolicContinuousCallback( conditions::Union{Equation, Vector{Equation}}, affect = nothing; + discrete_parameters = Any[], affect_neg = affect, initialize = nothing, finalize = nothing, @@ -227,8 +228,8 @@ struct SymbolicContinuousCallback <: AbstractCallback algeeqs = Equation[]) conditions = (conditions isa AbstractVector) ? conditions : [conditions] - new(conditions, make_affect(affect; iv, algeeqs), make_affect(affect_neg; iv, algeeqs), - make_affect(initialize; iv, algeeqs), make_affect(finalize; iv, algeeqs), rootfind) + new(conditions, make_affect(affect; iv, algeeqs, discrete_parameters), make_affect(affect_neg; iv, algeeqs, discrete_parameters), + make_affect(initialize; iv, algeeqs, discrete_parameters), make_affect(finalize; iv, algeeqs, discrete_parameters), rootfind) end # Default affect to nothing end @@ -240,10 +241,15 @@ make_affect(affect::Tuple; kwargs...) = FunctionalAffect(affect...) make_affect(affect::NamedTuple; kwargs...) = FunctionalAffect(; affect...) make_affect(affect::Affect; kwargs...) = affect -function make_affect(affect::Vector{Equation}; iv = nothing, algeeqs::Vector{Equation} = Equation[]) +function make_affect(affect::Vector{Equation}; discrete_parameters = Any[], iv = nothing, algeeqs::Vector{Equation} = Equation[]) isempty(affect) && return nothing isempty(algeeqs) && @warn "No algebraic equations were found for the callback defined by $(join(affect, ", ")). If the system has no algebraic equations, this can be disregarded. Otherwise pass in `algeeqs` to the SymbolicContinuousCallback constructor." + for p in discretes + # Check if p is time-dependent + false && error("Non-time dependent parameter $p passed in as a discrete. Must be declared as $p(t).") + end + explicit = true dvs = OrderedSet() params = OrderedSet() @@ -265,38 +271,21 @@ function make_affect(affect::Vector{Equation}; iv = nothing, algeeqs::Vector{Equ isnothing(iv) && @warn "No independent variable specified and could not be inferred. If the iv appears in an affect equation explicitly, like x ~ t + 1, then it must be specified as an argument to the SymbolicContinuousCallback or SymbolicDiscreteCallback constructor. Otherwise this warning can be disregarded." end - # Parameters in affect equations should become unknowns in the ImplicitDiscreteSystem. - cb_params = Any[] - discretes = Any[] - p_as_dvs = Any[] - for p in params - if iscall(p) && (operation(p) isa Pre) - push!(cb_params, p) - elseif iscall(p) && length(arguments(p)) == 1 && - isequal(only(arguments(p)), iv) - push!(discretes, p) - push!(p_as_dvs, tovar(p)) - else - push!(discretes, p) - name = iscall(p) ? nameof(operation(p)) : nameof(p) - p = wrap(Sym{FnType{Tuple{symtype(iv)}, Real}}(name)(iv)) - p = setmetadata(p, Symbolics.VariableSource, (:variables, name)) - push!(p_as_dvs, p) - end - end - aff_map = Dict(zip(p_as_dvs, discretes)) - rev_map = Dict([v => k for (k, v) in aff_map]) - affect = Symbolics.substitute(affect, rev_map) - @named affectsys = ImplicitDiscreteSystem(vcat(affect, algeeqs), iv, collect(union(dvs, p_as_dvs)), cb_params) + pre_params = filter(haspre ∘ value, params) + sys_params = setdiff(params, union(discrete_parameters, pre_params)) + discretes = map(tovar, discrete_parameters) + aff_map = Dict(zip(discretes, discrete_parameters)) + @named affectsys = ImplicitDiscreteSystem(vcat(affect, algeeqs), iv, collect(union(dvs, discretes)), collect(union(pre_params, sys_params))) affectsys = complete(affectsys) # get accessed parameters p from Pre(p) in the callback parameters - params = filter(isparameter, map(x -> unPre(x), cb_params)) + accessed_params = filter(isparameter, map(x -> unPre(x), cb_params)) + union!(accessed_params, sys_params) # add unknowns to the map for u in dvs aff_map[u] = u end - AffectSystem(affectsys, collect(dvs), params, discretes, aff_map, explicit) + AffectSystem(affectsys, collect(dvs), collect(accessed_params), collect(discrete_parameters), aff_map, explicit) end function make_affect(affect; kwargs...) @@ -876,8 +865,8 @@ function compile_equational_affect(aff::Union{AffectSystem, Vector{Equation}}, s p_up, p_up! = build_function_wrapper(sys, (@view rhss[is_p]), dvs, _ps..., t; wrap_code = add_integrator_header(sys, integ, :p), expression = Val{false}, outputidxs = p_idxs, wrap_mtkparameters) return function explicit_affect!(integ) - u_up!(integ) - p_up!(integ) + isempty(dvs_to_update) || u_up!(integ) + isempty(ps_to_update) || p_up!(integ) reset_jumps && reset_aggregated_jumps!(integ) end else @@ -891,11 +880,12 @@ function compile_equational_affect(aff::Union{AffectSystem, Vector{Equation}}, s end u0 = Pair[] for u in unknowns(affsys) - uval = isparameter(aff_map[u]) ? integ.ps[u] : integ[u] + uval = isparameter(aff_map[u]) ? integ.ps[aff_map[u]] : integ[u] push!(u0, u => uval) end affprob = ImplicitDiscreteProblem(affsys, u0, (integ.t, integ.t), pmap; build_initializeprob = false, check_length = false) - affsol = init(affprob, SimpleIDSolve()) + affsol = init(affprob, IDSolve()) + check_error(affsol) && throw(UnsolvableCallbackError(equations(affsys))) for u in dvs_to_update integ[u] = affsol[sys_map[u]] end @@ -907,6 +897,14 @@ function compile_equational_affect(aff::Union{AffectSystem, Vector{Equation}}, s end end +struct UnsolvableCallbackError + eqs::Vector{Equation} +end + +function Base.showerror(io, err::UnsolvableCallbackError) + println(io, "The callback defined by the equations, $(join(err.eqs, "\n")), with discrete parameters is not solvable. Please check the algebraic equations, affect equations, and declared discrete parameters.") +end + merge_cb(::Nothing, ::Nothing) = nothing merge_cb(::Nothing, x) = merge_cb(x, nothing) merge_cb(x, ::Nothing) = x diff --git a/src/systems/discrete_system/implicit_discrete_system.jl b/src/systems/discrete_system/implicit_discrete_system.jl index fb83f8fd78..0e0327f3ae 100644 --- a/src/systems/discrete_system/implicit_discrete_system.jl +++ b/src/systems/discrete_system/implicit_discrete_system.jl @@ -287,6 +287,7 @@ function generate_function( u_next = map(Shift(iv, 1), dvs) u = dvs p = (reorder_parameters(sys, unwrap.(ps))..., cachesyms...) + @show exprs build_function_wrapper( sys, exprs, u_next, u, p..., iv; p_start = 3, extra_assignments, kwargs...) end @@ -381,6 +382,12 @@ function SciMLBase.ImplicitDiscreteFunction{iip, specialize}( f(u_next, u, p, t) = f_oop(u_next, u, p, t) f(resid, u_next, u, p, t) = f_iip(resid, u_next, u, p, t) + if length(dvs) == length(equations(sys)) + resid_prototype = nothing + else + resid_prototype = calculate_resid_prototype(length(equations(sys)), u0, p) + end + if specialize === SciMLBase.FunctionWrapperSpecialize && iip if u0 === nothing || p === nothing || t === nothing error("u0, p, and t must be specified for FunctionWrapperSpecialize on ImplicitDiscreteFunction.") @@ -395,6 +402,7 @@ function SciMLBase.ImplicitDiscreteFunction{iip, specialize}( sys = sys, observed = observedfun, analytic = analytic, + resid_prototype = resid_prototype, kwargs...) end diff --git a/test/jumpsystem.jl b/test/jumpsystem.jl index 89fc828205..2fe989bd6a 100644 --- a/test/jumpsystem.jl +++ b/test/jumpsystem.jl @@ -2,6 +2,7 @@ using ModelingToolkit, DiffEqBase, JumpProcesses, Test, LinearAlgebra using Random, StableRNGs, NonlinearSolve using OrdinaryDiffEq using ModelingToolkit: t_nounits as t, D_nounits as D +using BenchmarkTools MT = ModelingToolkit rng = StableRNG(12345) @@ -79,7 +80,7 @@ function getmean(jprob, Nsims; use_stepper = true) end m / Nsims end -m = getmean(jprob, Nsims) +@btime m = $getmean($jprob, $Nsims) # test auto-alg selection works jprobb = JumpProblem(js2, dprob; save_positions = (false, false), rng) diff --git a/test/symbolic_events.jl b/test/symbolic_events.jl index c1b631b6bc..f3f0d51199 100644 --- a/test/symbolic_events.jl +++ b/test/symbolic_events.jl @@ -18,1215 +18,1215 @@ eqs = [D(x) ~ 1] affect = [x ~ 0] affect_neg = [x ~ 1] -@testset "SymbolicContinuousCallback constructors" begin - e = SymbolicContinuousCallback(eqs[]) - @test e isa SymbolicContinuousCallback - @test isequal(equations(e), eqs) - @test e.affect == nothing - @test e.affect_neg == nothing - @test e.rootfind == SciMLBase.LeftRootFind - - e = SymbolicContinuousCallback(eqs) - @test e isa SymbolicContinuousCallback - @test isequal(equations(e), eqs) - @test e.affect == nothing - @test e.affect_neg == nothing - @test e.rootfind == SciMLBase.LeftRootFind - - e = SymbolicContinuousCallback(eqs, nothing) - @test e isa SymbolicContinuousCallback - @test isequal(equations(e), eqs) - @test e.affect == nothing - @test e.affect_neg == nothing - @test e.rootfind == SciMLBase.LeftRootFind - - e = SymbolicContinuousCallback(eqs[], nothing) - @test e isa SymbolicContinuousCallback - @test isequal(equations(e), eqs) - @test e.affect == nothing - @test e.affect_neg == nothing - @test e.rootfind == SciMLBase.LeftRootFind - - e = SymbolicContinuousCallback(eqs => nothing) - @test e isa SymbolicContinuousCallback - @test isequal(equations(e), eqs) - @test e.affect == nothing - @test e.affect_neg == nothing - @test e.rootfind == SciMLBase.LeftRootFind - - e = SymbolicContinuousCallback(eqs[] => nothing) - @test e isa SymbolicContinuousCallback - @test isequal(equations(e), eqs) - @test e.affect == nothing - @test e.affect_neg == nothing - @test e.rootfind == SciMLBase.LeftRootFind - - ## With affect - e = SymbolicContinuousCallback(eqs[], affect) - @test e isa SymbolicContinuousCallback - @test isequal(equations(e), eqs) - @test e.rootfind == SciMLBase.LeftRootFind - - # with only positive edge affect - e = SymbolicContinuousCallback(eqs[], affect, affect_neg = nothing) - @test e isa SymbolicContinuousCallback - @test isequal(equations(e), eqs) - @test isnothing(e.affect_neg) - @test e.rootfind == SciMLBase.LeftRootFind - - # with explicit edge affects - e = SymbolicContinuousCallback(eqs[], affect, affect_neg = affect_neg) - @test e isa SymbolicContinuousCallback - @test isequal(equations(e), eqs) - @test e.rootfind == SciMLBase.LeftRootFind - - # with different root finding ops - e = SymbolicContinuousCallback( - eqs[], affect, affect_neg = affect_neg, rootfind = SciMLBase.LeftRootFind) - @test e isa SymbolicContinuousCallback - @test isequal(equations(e), eqs) - @test e.rootfind == SciMLBase.LeftRootFind - - # test plural constructor - e = SymbolicContinuousCallbacks(eqs[]) - @test e isa Vector{SymbolicContinuousCallback} - @test isequal(equations(e[]), eqs) - @test e[].affect == nothing - - e = SymbolicContinuousCallbacks(eqs) - @test e isa Vector{SymbolicContinuousCallback} - @test isequal(equations(e[]), eqs) - @test e[].affect == nothing - - e = SymbolicContinuousCallbacks(eqs[] => affect) - @test e isa Vector{SymbolicContinuousCallback} - @test isequal(equations(e[]), eqs) - @test e[].affect isa AffectSystem - - e = SymbolicContinuousCallbacks(eqs => affect) - @test e isa Vector{SymbolicContinuousCallback} - @test isequal(equations(e[]), eqs) - @test e[].affect isa AffectSystem - - e = SymbolicContinuousCallbacks([eqs[] => affect]) - @test e isa Vector{SymbolicContinuousCallback} - @test isequal(equations(e[]), eqs) - @test e[].affect isa AffectSystem - - e = SymbolicContinuousCallbacks([eqs => affect]) - @test e isa Vector{SymbolicContinuousCallback} - @test isequal(equations(e[]), eqs) - @test e[].affect isa AffectSystem -end - -@testset "ImperativeAffect constructors" begin - fmfa(o, x, i, c) = nothing - m = ModelingToolkit.ImperativeAffect(fmfa) - @test m isa ModelingToolkit.ImperativeAffect - @test m.f == fmfa - @test m.obs == [] - @test m.obs_syms == [] - @test m.modified == [] - @test m.mod_syms == [] - @test m.ctx === nothing - - m = ModelingToolkit.ImperativeAffect(fmfa, (;)) - @test m isa ModelingToolkit.ImperativeAffect - @test m.f == fmfa - @test m.obs == [] - @test m.obs_syms == [] - @test m.modified == [] - @test m.mod_syms == [] - @test m.ctx === nothing - - m = ModelingToolkit.ImperativeAffect(fmfa, (; x)) - @test m isa ModelingToolkit.ImperativeAffect - @test m.f == fmfa - @test isequal(m.obs, []) - @test m.obs_syms == [] - @test isequal(m.modified, [x]) - @test m.mod_syms == [:x] - @test m.ctx === nothing - - m = ModelingToolkit.ImperativeAffect(fmfa, (; y = x)) - @test m isa ModelingToolkit.ImperativeAffect - @test m.f == fmfa - @test isequal(m.obs, []) - @test m.obs_syms == [] - @test isequal(m.modified, [x]) - @test m.mod_syms == [:y] - @test m.ctx === nothing - - m = ModelingToolkit.ImperativeAffect(fmfa; observed = (; y = x)) - @test m isa ModelingToolkit.ImperativeAffect - @test m.f == fmfa - @test isequal(m.obs, [x]) - @test m.obs_syms == [:y] - @test m.modified == [] - @test m.mod_syms == [] - @test m.ctx === nothing - - m = ModelingToolkit.ImperativeAffect(fmfa; modified = (; x)) - @test m isa ModelingToolkit.ImperativeAffect - @test m.f == fmfa - @test isequal(m.obs, []) - @test m.obs_syms == [] - @test isequal(m.modified, [x]) - @test m.mod_syms == [:x] - @test m.ctx === nothing - - m = ModelingToolkit.ImperativeAffect(fmfa; modified = (; y = x)) - @test m isa ModelingToolkit.ImperativeAffect - @test m.f == fmfa - @test isequal(m.obs, []) - @test m.obs_syms == [] - @test isequal(m.modified, [x]) - @test m.mod_syms == [:y] - @test m.ctx === nothing - - m = ModelingToolkit.ImperativeAffect(fmfa, (; x), (; x)) - @test m isa ModelingToolkit.ImperativeAffect - @test m.f == fmfa - @test isequal(m.obs, [x]) - @test m.obs_syms == [:x] - @test isequal(m.modified, [x]) - @test m.mod_syms == [:x] - @test m.ctx === nothing - - m = ModelingToolkit.ImperativeAffect(fmfa, (; y = x), (; y = x)) - @test m isa ModelingToolkit.ImperativeAffect - @test m.f == fmfa - @test isequal(m.obs, [x]) - @test m.obs_syms == [:y] - @test isequal(m.modified, [x]) - @test m.mod_syms == [:y] - @test m.ctx === nothing - - m = ModelingToolkit.ImperativeAffect( - fmfa; modified = (; y = x), observed = (; y = x)) - @test m isa ModelingToolkit.ImperativeAffect - @test m.f == fmfa - @test isequal(m.obs, [x]) - @test m.obs_syms == [:y] - @test isequal(m.modified, [x]) - @test m.mod_syms == [:y] - @test m.ctx === nothing - - m = ModelingToolkit.ImperativeAffect( - fmfa; modified = (; y = x), observed = (; y = x), ctx = 3) - @test m isa ModelingToolkit.ImperativeAffect - @test m.f == fmfa - @test isequal(m.obs, [x]) - @test m.obs_syms == [:y] - @test isequal(m.modified, [x]) - @test m.mod_syms == [:y] - @test m.ctx === 3 - - m = ModelingToolkit.ImperativeAffect(fmfa, (; x), (; x), 3) - @test m isa ModelingToolkit.ImperativeAffect - @test m.f == fmfa - @test isequal(m.obs, [x]) - @test m.obs_syms == [:x] - @test isequal(m.modified, [x]) - @test m.mod_syms == [:x] - @test m.ctx === 3 -end - -@testset "Condition Compilation" begin - @named sys = ODESystem(eqs, t, continuous_events = [x ~ 1]) - @test getfield(sys, :continuous_events)[] == - SymbolicContinuousCallback(Equation[x ~ 1], nothing) - @test isequal(equations(getfield(sys, :continuous_events))[], x ~ 1) - fsys = flatten(sys) - @test isequal(equations(getfield(fsys, :continuous_events))[], x ~ 1) - - @named sys2 = ODESystem([D(x) ~ 1], t, continuous_events = [x ~ 2], systems = [sys]) - @test getfield(sys2, :continuous_events)[] == - SymbolicContinuousCallback(Equation[x ~ 2], nothing) - @test all(ModelingToolkit.continuous_events(sys2) .== [ - SymbolicContinuousCallback(Equation[x ~ 2], nothing), - SymbolicContinuousCallback(Equation[sys.x ~ 1], nothing) - ]) - - @test isequal(equations(getfield(sys2, :continuous_events))[1], x ~ 2) - @test length(ModelingToolkit.continuous_events(sys2)) == 2 - @test isequal(equations(ModelingToolkit.continuous_events(sys2)[1])[], x ~ 2) - @test isequal(equations(ModelingToolkit.continuous_events(sys2)[2])[], sys.x ~ 1) - - sys = complete(sys) - sys_nosplit = complete(sys; split = false) - sys2 = complete(sys2) - - # Test proper rootfinding - prob = ODEProblem(sys, Pair[], (0.0, 2.0)) - p0 = 0 - t0 = 0 - @test get_callback(prob) isa ModelingToolkit.DiffEqCallbacks.ContinuousCallback - cb = ModelingToolkit.generate_continuous_callbacks(sys) - cond = cb.condition - out = [0.0] - cond.f_iip(out, [0], p0, t0) - @test out[] ≈ -1 # signature is u,p,t - cond.f_iip(out, [1], p0, t0) - @test out[] ≈ 0 # signature is u,p,t - cond.f_iip(out, [2], p0, t0) - @test out[] ≈ 1 # signature is u,p,t - - prob = ODEProblem(sys, Pair[], (0.0, 2.0)) - prob_nosplit = ODEProblem(sys_nosplit, Pair[], (0.0, 2.0)) - sol = solve(prob, Tsit5()) - sol_nosplit = solve(prob_nosplit, Tsit5()) - @test minimum(t -> abs(t - 1), sol.t) < 1e-10 # test that the solver stepped at the root - @test minimum(t -> abs(t - 1), sol_nosplit.t) < 1e-10 # test that the solver stepped at the root - - # Test user-provided callback is respected - test_callback = DiscreteCallback(x -> x, x -> x) - prob = ODEProblem(sys, Pair[], (0.0, 2.0), callback = test_callback) - prob_nosplit = ODEProblem(sys_nosplit, Pair[], (0.0, 2.0), callback = test_callback) - cbs = get_callback(prob) - cbs_nosplit = get_callback(prob_nosplit) - @test cbs isa CallbackSet - @test cbs.discrete_callbacks[1] == test_callback - @test cbs_nosplit isa CallbackSet - @test cbs_nosplit.discrete_callbacks[1] == test_callback - - prob = ODEProblem(sys2, Pair[], (0.0, 3.0)) - cb = get_callback(prob) - @test cb isa ModelingToolkit.DiffEqCallbacks.VectorContinuousCallback - - cond = cb.condition - out = [0.0, 0.0] - # the root to find is 2 - cond.f_iip(out, [0, 0], p0, t0) - @test out[1] ≈ -2 # signature is u,p,t - cond.f_iip(out, [1, 0], p0, t0) - @test out[1] ≈ -1 # signature is u,p,t - cond.f_iip(out, [2, 0], p0, t0) # this should return 0 - @test out[1] ≈ 0 # signature is u,p,t - - # the root to find is 1 - out = [0.0, 0.0] - cond.f_iip(out, [0, 0], p0, t0) - @test out[2] ≈ -1 # signature is u,p,t - cond.f_iip(out, [0, 1], p0, t0) # this should return 0 - @test out[2] ≈ 0 # signature is u,p,t - cond.f_iip(out, [0, 2], p0, t0) - @test out[2] ≈ 1 # signature is u,p,t - - sol = solve(prob, Tsit5()) - @test minimum(t -> abs(t - 1), sol.t) < 1e-10 # test that the solver stepped at the first root - @test minimum(t -> abs(t - 2), sol.t) < 1e-10 # test that the solver stepped at the second root - - @named sys = ODESystem(eqs, t, continuous_events = [x ~ 1, x ~ 2]) # two root eqs using the same unknown - sys = complete(sys) - prob = ODEProblem(sys, Pair[], (0.0, 3.0)) - @test get_callback(prob) isa ModelingToolkit.DiffEqCallbacks.VectorContinuousCallback - sol = solve(prob, Tsit5()) - @test minimum(t -> abs(t - 1), sol.t) < 1e-10 # test that the solver stepped at the first root - @test minimum(t -> abs(t - 2), sol.t) < 1e-10 # test that the solver stepped at the second root -end - -@testset "Bouncing Ball" begin - ###### 1D Bounce - @variables x(t)=1 v(t)=0 - - root_eqs = [x ~ 0] - affect = [v ~ -Pre(v)] - - @named ball = ODESystem( - [D(x) ~ v - D(v) ~ -9.8], t, continuous_events = root_eqs => affect) - - @test only(continuous_events(ball)) == - SymbolicContinuousCallback(Equation[x ~ 0], Equation[v ~ -Pre(v)]) - ball = structural_simplify(ball) - - @test length(ModelingToolkit.continuous_events(ball)) == 1 - - tspan = (0.0, 5.0) - prob = ODEProblem(ball, Pair[], tspan) - sol = solve(prob, Tsit5()) - @test 0 <= minimum(sol[x]) <= 1e-10 # the ball never went through the floor but got very close - - ###### 2D bouncing ball - @variables x(t)=1 y(t)=0 vx(t)=0 vy(t)=1 - - events = [[x ~ 0] => [vx ~ -Pre(vx)] - [y ~ -1.5, y ~ 1.5] => [vy ~ -Pre(vy)]] - - @named ball = ODESystem( - [D(x) ~ vx - D(y) ~ vy - D(vx) ~ -9.8 - D(vy) ~ -0.01vy], t; continuous_events = events) - - _ball = ball - ball = structural_simplify(_ball) - ball_nosplit = structural_simplify(_ball; split = false) - - tspan = (0.0, 5.0) - prob = ODEProblem(ball, Pair[], tspan) - prob_nosplit = ODEProblem(ball_nosplit, Pair[], tspan) - - cb = get_callback(prob) - @test cb isa ModelingToolkit.DiffEqCallbacks.VectorContinuousCallback - @test getfield(ball, :continuous_events)[1] == - SymbolicContinuousCallback(Equation[x ~ 0], Equation[vx ~ -Pre(vx)]) - @test getfield(ball, :continuous_events)[2] == - SymbolicContinuousCallback(Equation[y ~ -1.5, y ~ 1.5], Equation[vy ~ -Pre(vy)]) - cond = cb.condition - out = [0.0, 0.0, 0.0] - p0 = 0. - t0 = 0. - cond.f_iip(out, [0, 0, 0, 0], p0, t0) - @test out ≈ [0, 1.5, -1.5] - - sol = solve(prob, Tsit5()) - sol_nosplit = solve(prob_nosplit, Tsit5()) - @test 0 <= minimum(sol[x]) <= 1e-10 # the ball never went through the floor but got very close - @test minimum(sol[y]) ≈ -1.5 # check wall conditions - @test maximum(sol[y]) ≈ 1.5 # check wall conditions - @test 0 <= minimum(sol_nosplit[x]) <= 1e-10 # the ball never went through the floor but got very close - @test minimum(sol_nosplit[y]) ≈ -1.5 # check wall conditions - @test maximum(sol_nosplit[y]) ≈ 1.5 # check wall conditions - - ## Test multi-variable affect - # in this test, there are two variables affected by a single event. - events = [[x ~ 0] => [vx ~ -Pre(vx), vy ~ -Pre(vy)]] - - @named ball = ODESystem([D(x) ~ vx - D(y) ~ vy - D(vx) ~ -1 - D(vy) ~ 0], t; continuous_events = events) - - ball_nosplit = structural_simplify(ball) - ball = structural_simplify(ball) - - tspan = (0.0, 5.0) - prob = ODEProblem(ball, Pair[], tspan) - prob_nosplit = ODEProblem(ball_nosplit, Pair[], tspan) - sol = solve(prob, Tsit5()) - sol_nosplit = solve(prob_nosplit, Tsit5()) - @test 0 <= minimum(sol[x]) <= 1e-10 # the ball never went through the floor but got very close - @test -minimum(sol[y]) ≈ maximum(sol[y]) ≈ sqrt(2) # the ball will never go further than √2 in either direction (gravity was changed to 1 to get this particular number) - @test 0 <= minimum(sol_nosplit[x]) <= 1e-10 # the ball never went through the floor but got very close - @test -minimum(sol_nosplit[y]) ≈ maximum(sol_nosplit[y]) ≈ sqrt(2) # the ball will never go further than √2 in either direction (gravity was changed to 1 to get this particular number) -end - -# issue https://github.com/SciML/ModelingToolkit.jl/issues/1386 -# tests that it works for ODAESystem -@testset "ODAESystem" begin - @variables vs(t) v(t) vmeasured(t) - eq = [vs ~ sin(2pi * t) - D(v) ~ vs - v - D(vmeasured) ~ 0.0] - ev = [sin(20pi * t) ~ 0.0] => [vmeasured ~ Pre(v)] - @named sys = ODESystem(eq, t, continuous_events = ev) - sys = structural_simplify(sys) - prob = ODEProblem(sys, zeros(2), (0.0, 5.1)) - sol = solve(prob, Tsit5()) - @test all(minimum((0:0.1:5) .- sol.t', dims = 2) .< 0.0001) # test that the solver stepped every 0.1s as dictated by event - @test sol([0.25])[vmeasured][] == sol([0.23])[vmeasured][] # test the hold property -end - -## https://github.com/SciML/ModelingToolkit.jl/issues/1528 -@testset "Handle Empty Events" begin - Dₜ = D - - @parameters u(t) [input = true] # Indicate that this is a controlled input - @parameters y(t) [output = true] # Indicate that this is a measured output - - function Mass(; name, m = 1.0, p = 0, v = 0) - ps = @parameters m = m - sts = @variables pos(t)=p vel(t)=v - eqs = Dₜ(pos) ~ vel - ODESystem(eqs, t, [pos, vel], ps; name) - end - function Spring(; name, k = 1e4) - ps = @parameters k = k - @variables x(t) = 0 # Spring deflection - ODESystem(Equation[], t, [x], ps; name) - end - function Damper(; name, c = 10) - ps = @parameters c = c - @variables vel(t) = 0 - ODESystem(Equation[], t, [vel], ps; name) - end - function SpringDamper(; name, k = false, c = false) - spring = Spring(; name = :spring, k) - damper = Damper(; name = :damper, c) - compose(ODESystem(Equation[], t; name), - spring, damper) - end - connect_sd(sd, m1, m2) = [ - sd.spring.x ~ m1.pos - m2.pos, sd.damper.vel ~ m1.vel - m2.vel] - sd_force(sd) = -sd.spring.k * sd.spring.x - sd.damper.c * sd.damper.vel - @named mass1 = Mass(; m = 1) - @named mass2 = Mass(; m = 1) - @named sd = SpringDamper(; k = 1000, c = 10) - function Model(u, d = 0) - eqs = [connect_sd(sd, mass1, mass2) - Dₜ(mass1.vel) ~ (sd_force(sd) + u) / mass1.m - Dₜ(mass2.vel) ~ (-sd_force(sd) + d) / mass2.m] - @named _model = ODESystem(eqs, t; observed = [y ~ mass2.pos]) - @named model = compose(_model, mass1, mass2, sd) - end - model = Model(sin(30t)) - sys = structural_simplify(model) - @test isempty(ModelingToolkit.continuous_events(sys)) -end - -@testset "ODESystem Discrete Callbacks" begin - function testsol(osys, u0, p, tspan; tstops = Float64[], paramtotest = nothing, - kwargs...) - oprob = ODEProblem(complete(osys), u0, tspan, p; kwargs...) - sol = solve(oprob, Tsit5(); tstops = tstops, abstol = 1e-10, reltol = 1e-10) - @test isapprox(sol(1.0000000001)[1] - sol(0.999999999)[1], 1.0; rtol = 1e-6) - paramtotest === nothing || (@test sol.ps[paramtotest] == 1.0) - @test isapprox(sol(4.0)[1], 2 * exp(-2.0)) - sol - end - - @parameters k t1 t2 - @variables A(t) B(t) - - cond1 = (t == t1) - affect1 = [A ~ Pre(A) + 1] - cb1 = cond1 => affect1 - cond2 = (t == t2) - affect2 = [k ~ 1.0] - cb2 = cond2 => affect2 - - ∂ₜ = D - eqs = [∂ₜ(A) ~ -k * A] - @named osys = ODESystem(eqs, t, [A], [k, t1, t2], discrete_events = [cb1, cb2]) - u0 = [A => 1.0] - p = [k => 0.0, t1 => 1.0, t2 => 2.0] - tspan = (0.0, 4.0) - testsol(osys, u0, p, tspan; tstops = [1.0, 2.0], paramtotest = k) - - cond1a = (t == t1) - affect1a = [A ~ Pre(A) + 1, B ~ A] - cb1a = cond1a => affect1a - @named osys1 = ODESystem(eqs, t, [A, B], [k, t1, t2], discrete_events = [cb1a, cb2]) - u0′ = [A => 1.0, B => 0.0] - sol = testsol( - osys1, u0′, p, tspan; tstops = [1.0, 2.0], check_length = false, paramtotest = k) - @test sol(1.0000001, idxs = B) == 2.0 - - # same as above - but with set-time event syntax - cb1‵ = [1.0] => affect1 # needs to be a Vector for the event to happen only once - cb2‵ = [2.0] => affect2 - @named osys‵ = ODESystem(eqs, t, [A], [k], discrete_events = [cb1‵, cb2‵]) - testsol(osys‵, u0, p, tspan; paramtotest = k) - - # mixing discrete affects - @named osys3 = ODESystem(eqs, t, [A], [k, t1, t2], discrete_events = [cb1, cb2‵]) - testsol(osys3, u0, p, tspan; tstops = [1.0], paramtotest = k) - - # mixing with a func affect - function affect!(integrator, u, p, ctx) - integrator.ps[p.k] = 1.0 - nothing - end - cb2‵‵ = [2.0] => (affect!, [], [k], [k], nothing) - @named osys4 = ODESystem(eqs, t, [A], [k, t1], discrete_events = [cb1, cb2‵‵]) - oprob4 = ODEProblem(complete(osys4), u0, tspan, p) - testsol(osys4, u0, p, tspan; tstops = [1.0], paramtotest = k) - - # mixing with symbolic condition in the func affect - cb2‵‵‵ = (t == t2) => (affect!, [], [k], [k], nothing) - @named osys5 = ODESystem(eqs, t, [A], [k, t1, t2], discrete_events = [cb1, cb2‵‵‵]) - testsol(osys5, u0, p, tspan; tstops = [1.0, 2.0]) - @named osys6 = ODESystem(eqs, t, [A], [k, t1, t2], discrete_events = [cb2‵‵‵, cb1]) - testsol(osys6, u0, p, tspan; tstops = [1.0, 2.0], paramtotest = k) - - # mix a continuous event too - cond3 = A ~ 0.1 - affect3 = [k ~ 0.0] - cb3 = cond3 => affect3 - @named osys7 = ODESystem(eqs, t, [A], [k, t1, t2], discrete_events = [cb1, cb2‵‵‵], - continuous_events = [cb3]) - sol = testsol(osys7, u0, p, (0.0, 10.0); tstops = [1.0, 2.0]) - @test isapprox(sol(10.0)[1], 0.1; atol = 1e-10, rtol = 1e-10) -end - -@testset "SDESystem Discrete Callbacks" begin - function testsol(ssys, u0, p, tspan; tstops = Float64[], paramtotest = nothing, - kwargs...) - sprob = SDEProblem(complete(ssys), u0, tspan, p; kwargs...) - sol = solve(sprob, RI5(); tstops = tstops, abstol = 1e-10, reltol = 1e-10) - @test isapprox(sol(1.0000000001)[1] - sol(0.999999999)[1], 1.0; rtol = 1e-4) - paramtotest === nothing || (@test sol.ps[paramtotest] == 1.0) - @test isapprox(sol(4.0)[1], 2 * exp(-2.0), atol = 1e-4) - sol - end - - @parameters k t1 t2 - @variables A(t) B(t) - - cond1 = (t == t1) - affect1 = [A ~ Pre(A) + 1] - cb1 = cond1 => affect1 - cond2 = (t == t2) - affect2 = [k ~ 1.0] - cb2 = cond2 => affect2 - - ∂ₜ = D - eqs = [∂ₜ(A) ~ -k * A] - @named ssys = SDESystem(eqs, [0.0], t, [A], [k, t1, t2], - discrete_events = [cb1, cb2]) - u0 = [A => 1.0] - p = [k => 0.0, t1 => 1.0, t2 => 2.0] - tspan = (0.0, 4.0) - testsol(ssys, u0, p, tspan; tstops = [1.0, 2.0], paramtotest = k) - - cond1a = (t == t1) - affect1a = [A ~ Pre(A) + 1, B ~ A] - cb1a = cond1a => affect1a - @named ssys1 = SDESystem(eqs, [0.0], t, [A, B], [k, t1, t2], - discrete_events = [cb1a, cb2]) - u0′ = [A => 1.0, B => 0.0] - sol = testsol( - ssys1, u0′, p, tspan; tstops = [1.0, 2.0], check_length = false, paramtotest = k) - @test sol(1.0000001, idxs = 2) == 2.0 - - # same as above - but with set-time event syntax - cb1‵ = [1.0] => affect1 # needs to be a Vector for the event to happen only once - cb2‵ = [2.0] => affect2 - @named ssys‵ = SDESystem(eqs, [0.0], t, [A], [k], discrete_events = [cb1‵, cb2‵]) - testsol(ssys‵, u0, p, tspan; paramtotest = k) - - # mixing discrete affects - @named ssys3 = SDESystem(eqs, [0.0], t, [A], [k, t1, t2], - discrete_events = [cb1, cb2‵]) - testsol(ssys3, u0, p, tspan; tstops = [1.0], paramtotest = k) - - # mixing with a func affect - function affect!(integrator, u, p, ctx) - setp(integrator, p.k)(integrator, 1.0) - nothing - end - cb2‵‵ = [2.0] => (affect!, [], [k], [k], nothing) - @named ssys4 = SDESystem(eqs, [0.0], t, [A], [k, t1], - discrete_events = [cb1, cb2‵‵]) - testsol(ssys4, u0, p, tspan; tstops = [1.0], paramtotest = k) - - # mixing with symbolic condition in the func affect - cb2‵‵‵ = (t == t2) => (affect!, [], [k], [k], nothing) - @named ssys5 = SDESystem(eqs, [0.0], t, [A], [k, t1, t2], - discrete_events = [cb1, cb2‵‵‵]) - testsol(ssys5, u0, p, tspan; tstops = [1.0, 2.0], paramtotest = k) - @named ssys6 = SDESystem(eqs, [0.0], t, [A], [k, t1, t2], - discrete_events = [cb2‵‵‵, cb1]) - testsol(ssys6, u0, p, tspan; tstops = [1.0, 2.0], paramtotest = k) - - # mix a continuous event too - cond3 = A ~ 0.1 - affect3 = [k ~ 0.0] - cb3 = cond3 => affect3 - @named ssys7 = SDESystem(eqs, [0.0], t, [A], [k, t1, t2], - discrete_events = [cb1, cb2‵‵‵], - continuous_events = [cb3]) - sol = testsol(ssys7, u0, p, (0.0, 10.0); tstops = [1.0, 2.0]) - @test isapprox(sol(10.0)[1], 0.1; atol = 1e-10, rtol = 1e-10) -end - -@testset "JumpSystem Discrete Callbacks" begin - function testsol(jsys, u0, p, tspan; tstops = Float64[], paramtotest = nothing, - N = 40000, kwargs...) - jsys = complete(jsys) - dprob = DiscreteProblem(jsys, u0, tspan, p) - jprob = JumpProblem(jsys, dprob, Direct(); kwargs...) - sol = solve(jprob, SSAStepper(); tstops = tstops) - @test (sol(1.000000000001)[1] - sol(0.99999999999)[1]) == 1 - paramtotest === nothing || (@test sol.ps[paramtotest] == 1.0) - @test sol(40.0)[1] == 0 - sol - end - - @parameters k t1 t2 - @variables A(t) B(t) - - cond1 = (t == t1) - affect1 = [A ~ Pre(A) + 1] - cb1 = cond1 => affect1 - cond2 = (t == t2) - affect2 = [k ~ 1.0] - cb2 = cond2 => affect2 - - eqs = [MassActionJump(k, [A => 1], [A => -1])] - @named jsys = JumpSystem(eqs, t, [A], [k, t1, t2], discrete_events = [cb1, cb2]) - u0 = [A => 1] - p = [k => 0.0, t1 => 1.0, t2 => 2.0] - tspan = (0.0, 40.0) - testsol(jsys, u0, p, tspan; tstops = [1.0, 2.0], rng, paramtotest = k) - - cond1a = (t == t1) - affect1a = [A ~ Pre(A) + 1, B ~ A] - cb1a = cond1a => affect1a - @named jsys1 = JumpSystem(eqs, t, [A, B], [k, t1, t2], discrete_events = [cb1a, cb2]) - u0′ = [A => 1, B => 0] - sol = testsol(jsys1, u0′, p, tspan; tstops = [1.0, 2.0], - check_length = false, rng, paramtotest = k) - @test sol(1.000000001, idxs = B) == 2 - - # same as above - but with set-time event syntax - cb1‵ = [1.0] => affect1 # needs to be a Vector for the event to happen only once - cb2‵ = [2.0] => affect2 - @named jsys‵ = JumpSystem(eqs, t, [A], [k], discrete_events = [cb1‵, cb2‵]) - testsol(jsys‵, u0, [p[1]], tspan; rng, paramtotest = k) - - # mixing discrete affects - @named jsys3 = JumpSystem(eqs, t, [A], [k, t1, t2], discrete_events = [cb1, cb2‵]) - testsol(jsys3, u0, p, tspan; tstops = [1.0], rng, paramtotest = k) - - # mixing with a func affect - function affect!(integrator, u, p, ctx) - integrator.ps[p.k] = 1.0 - reset_aggregated_jumps!(integrator) - nothing - end - cb2‵‵ = [2.0] => (affect!, [], [k], [k], nothing) - @named jsys4 = JumpSystem(eqs, t, [A], [k, t1], discrete_events = [cb1, cb2‵‵]) - testsol(jsys4, u0, p, tspan; tstops = [1.0], rng, paramtotest = k) - - # mixing with symbolic condition in the func affect - cb2‵‵‵ = (t == t2) => (affect!, [], [k], [k], nothing) - @named jsys5 = JumpSystem(eqs, t, [A], [k, t1, t2], discrete_events = [cb1, cb2‵‵‵]) - testsol(jsys5, u0, p, tspan; tstops = [1.0, 2.0], rng, paramtotest = k) - @named jsys6 = JumpSystem(eqs, t, [A], [k, t1, t2], discrete_events = [cb2‵‵‵, cb1]) - testsol(jsys6, u0, p, tspan; tstops = [1.0, 2.0], rng, paramtotest = k) -end - -@testset "Namespacing" begin - function oscillator_ce(k = 1.0; name) - sts = @variables x(t)=1.0 v(t)=0.0 F(t) - ps = @parameters k=k Θ=0.5 - eqs = [D(x) ~ v, D(v) ~ -k * x + F] - ev = [x ~ Θ] => [x ~ 1.0, v ~ 0.0] - ODESystem(eqs, t, sts, ps, continuous_events = [ev]; name) - end - - @named oscce = oscillator_ce() - eqs = [oscce.F ~ 0] - @named eqs_sys = ODESystem(eqs, t) - @named oneosc_ce = compose(eqs_sys, oscce) - oneosc_ce_simpl = structural_simplify(oneosc_ce) - - prob = ODEProblem(oneosc_ce_simpl, [], (0.0, 2.0), []) - sol = solve(prob, Tsit5(), saveat = 0.1) - - @test typeof(oneosc_ce_simpl) == ODESystem - @test sol[1, 6] < 1.0 # test whether x(t) decreases over time - @test sol[1, 18] > 0.5 # test whether event happened -end - -@testset "Additional SymbolicContinuousCallback options" begin - # baseline affect (pos + neg + left root find) - @variables c1(t)=1.0 c2(t)=1.0 # c1 = cos(t), c2 = cos(3t) - eqs = [D(c1) ~ -sin(t); D(c2) ~ -3 * sin(3 * t)] - record_crossings(i, u, _, c) = push!(c, i.t => i.u[u.v]) - cr1 = [] - cr2 = [] - evt1 = ModelingToolkit.SymbolicContinuousCallback( - [c1 ~ 0], (record_crossings, [c1 => :v], [], [], cr1)) - evt2 = ModelingToolkit.SymbolicContinuousCallback( - [c2 ~ 0], (record_crossings, [c2 => :v], [], [], cr2)) - @named trigsys = ODESystem(eqs, t; continuous_events = [evt1, evt2]) - trigsys_ss = structural_simplify(trigsys) - prob = ODEProblem(trigsys_ss, [], (0.0, 2π)) - sol = solve(prob, Tsit5()) - required_crossings_c1 = [π / 2, 3 * π / 2] - required_crossings_c2 = [π / 6, π / 2, 5 * π / 6, 7 * π / 6, 3 * π / 2, 11 * π / 6] - @test maximum(abs.(first.(cr1) .- required_crossings_c1)) < 1e-4 - @test maximum(abs.(first.(cr2) .- required_crossings_c2)) < 1e-4 - @test sign.(cos.(required_crossings_c1 .- 1e-6)) == sign.(last.(cr1)) - @test sign.(cos.(3 * (required_crossings_c2 .- 1e-6))) == sign.(last.(cr2)) - - # with neg affect (pos * neg + left root find) - cr1p = [] - cr2p = [] - cr1n = [] - cr2n = [] - evt1 = ModelingToolkit.SymbolicContinuousCallback( - [c1 ~ 0], (record_crossings, [c1 => :v], [], [], cr1p); - affect_neg = (record_crossings, [c1 => :v], [], [], cr1n)) - evt2 = ModelingToolkit.SymbolicContinuousCallback( - [c2 ~ 0], (record_crossings, [c2 => :v], [], [], cr2p); - affect_neg = (record_crossings, [c2 => :v], [], [], cr2n)) - @named trigsys = ODESystem(eqs, t; continuous_events = [evt1, evt2]) - trigsys_ss = structural_simplify(trigsys) - prob = ODEProblem(trigsys_ss, [], (0.0, 2π)) - sol = solve(prob, Tsit5(); dtmax = 0.01) - c1_pc = filter((<=)(0) ∘ sin, required_crossings_c1) - c1_nc = filter((>=)(0) ∘ sin, required_crossings_c1) - c2_pc = filter(c -> -sin(3c) > 0, required_crossings_c2) - c2_nc = filter(c -> -sin(3c) < 0, required_crossings_c2) - @test maximum(abs.(c1_pc .- first.(cr1p))) < 1e-5 - @test maximum(abs.(c1_nc .- first.(cr1n))) < 1e-5 - @test maximum(abs.(c2_pc .- first.(cr2p))) < 1e-5 - @test maximum(abs.(c2_nc .- first.(cr2n))) < 1e-5 - @test sign.(cos.(c1_pc .- 1e-6)) == sign.(last.(cr1p)) - @test sign.(cos.(c1_nc .- 1e-6)) == sign.(last.(cr1n)) - @test sign.(cos.(3 * (c2_pc .- 1e-6))) == sign.(last.(cr2p)) - @test sign.(cos.(3 * (c2_nc .- 1e-6))) == sign.(last.(cr2n)) - - # with nothing neg affect (pos * neg + left root find) - cr1p = [] - cr2p = [] - evt1 = ModelingToolkit.SymbolicContinuousCallback( - [c1 ~ 0], (record_crossings, [c1 => :v], [], [], cr1p); affect_neg = nothing) - evt2 = ModelingToolkit.SymbolicContinuousCallback( - [c2 ~ 0], (record_crossings, [c2 => :v], [], [], cr2p); affect_neg = nothing) - @named trigsys = ODESystem(eqs, t; continuous_events = [evt1, evt2]) - trigsys_ss = structural_simplify(trigsys) - prob = ODEProblem(trigsys_ss, [], (0.0, 2π)) - sol = solve(prob, Tsit5(); dtmax = 0.01) - @test maximum(abs.(c1_pc .- first.(cr1p))) < 1e-5 - @test maximum(abs.(c2_pc .- first.(cr2p))) < 1e-5 - @test sign.(cos.(c1_pc .- 1e-6)) == sign.(last.(cr1p)) - @test sign.(cos.(3 * (c2_pc .- 1e-6))) == sign.(last.(cr2p)) - - #mixed - cr1p = [] - cr2p = [] - cr1n = [] - cr2n = [] - evt1 = ModelingToolkit.SymbolicContinuousCallback( - [c1 ~ 0], (record_crossings, [c1 => :v], [], [], cr1p); affect_neg = nothing) - evt2 = ModelingToolkit.SymbolicContinuousCallback( - [c2 ~ 0], (record_crossings, [c2 => :v], [], [], cr2p); - affect_neg = (record_crossings, [c2 => :v], [], [], cr2n)) - @named trigsys = ODESystem(eqs, t; continuous_events = [evt1, evt2]) - trigsys_ss = structural_simplify(trigsys) - prob = ODEProblem(trigsys_ss, [], (0.0, 2π)) - sol = solve(prob, Tsit5(); dtmax = 0.01) - c1_pc = filter((<=)(0) ∘ sin, required_crossings_c1) - c2_pc = filter(c -> -sin(3c) > 0, required_crossings_c2) - c2_nc = filter(c -> -sin(3c) < 0, required_crossings_c2) - @test maximum(abs.(c1_pc .- first.(cr1p))) < 1e-5 - @test maximum(abs.(c2_pc .- first.(cr2p))) < 1e-5 - @test maximum(abs.(c2_nc .- first.(cr2n))) < 1e-5 - @test sign.(cos.(c1_pc .- 1e-6)) == sign.(last.(cr1p)) - @test sign.(cos.(3 * (c2_pc .- 1e-6))) == sign.(last.(cr2p)) - @test sign.(cos.(3 * (c2_nc .- 1e-6))) == sign.(last.(cr2n)) - - # baseline affect w/ right rootfind (pos + neg + right root find) - @variables c1(t)=1.0 c2(t)=1.0 # c1 = cos(t), c2 = cos(3t) - cr1 = [] - cr2 = [] - evt1 = ModelingToolkit.SymbolicContinuousCallback( - [c1 ~ 0], (record_crossings, [c1 => :v], [], [], cr1); - rootfind = SciMLBase.RightRootFind) - evt2 = ModelingToolkit.SymbolicContinuousCallback( - [c2 ~ 0], (record_crossings, [c2 => :v], [], [], cr2); - rootfind = SciMLBase.RightRootFind) - @named trigsys = ODESystem(eqs, t; continuous_events = [evt1, evt2]) - trigsys_ss = structural_simplify(trigsys) - prob = ODEProblem(trigsys_ss, [], (0.0, 2π)) - sol = solve(prob, Tsit5(); dtmax = 0.01) - required_crossings_c1 = [π / 2, 3 * π / 2] - required_crossings_c2 = [π / 6, π / 2, 5 * π / 6, 7 * π / 6, 3 * π / 2, 11 * π / 6] - @test maximum(abs.(first.(cr1) .- required_crossings_c1)) < 1e-4 - @test maximum(abs.(first.(cr2) .- required_crossings_c2)) < 1e-4 - @test sign.(cos.(required_crossings_c1 .+ 1e-6)) == sign.(last.(cr1)) - @test sign.(cos.(3 * (required_crossings_c2 .+ 1e-6))) == sign.(last.(cr2)) - - # baseline affect w/ mixed rootfind (pos + neg + right root find) - cr1 = [] - cr2 = [] - evt1 = ModelingToolkit.SymbolicContinuousCallback( - [c1 ~ 0], (record_crossings, [c1 => :v], [], [], cr1); - rootfind = SciMLBase.LeftRootFind) - evt2 = ModelingToolkit.SymbolicContinuousCallback( - [c2 ~ 0], (record_crossings, [c2 => :v], [], [], cr2); - rootfind = SciMLBase.RightRootFind) - @named trigsys = ODESystem(eqs, t; continuous_events = [evt1, evt2]) - trigsys_ss = structural_simplify(trigsys) - prob = ODEProblem(trigsys_ss, [], (0.0, 2π)) - sol = solve(prob, Tsit5()) - @test maximum(abs.(first.(cr1) .- required_crossings_c1)) < 1e-4 - @test maximum(abs.(first.(cr2) .- required_crossings_c2)) < 1e-4 - @test sign.(cos.(required_crossings_c1 .- 1e-6)) == sign.(last.(cr1)) - @test sign.(cos.(3 * (required_crossings_c2 .+ 1e-6))) == sign.(last.(cr2)) - - #flip order and ensure results are okay - cr1 = [] - cr2 = [] - evt1 = ModelingToolkit.SymbolicContinuousCallback( - [c1 ~ 0], (record_crossings, [c1 => :v], [], [], cr1); - rootfind = SciMLBase.LeftRootFind) - evt2 = ModelingToolkit.SymbolicContinuousCallback( - [c2 ~ 0], (record_crossings, [c2 => :v], [], [], cr2); - rootfind = SciMLBase.RightRootFind) - @named trigsys = ODESystem(eqs, t; continuous_events = [evt2, evt1]) - trigsys_ss = structural_simplify(trigsys) - prob = ODEProblem(trigsys_ss, [], (0.0, 2π)) - sol = solve(prob, Tsit5()) - @test maximum(abs.(first.(cr1) .- required_crossings_c1)) < 1e-4 - @test maximum(abs.(first.(cr2) .- required_crossings_c2)) < 1e-4 - @test sign.(cos.(required_crossings_c1 .- 1e-6)) == sign.(last.(cr1)) - @test sign.(cos.(3 * (required_crossings_c2 .+ 1e-6))) == sign.(last.(cr2)) -end - -@testset "Discrete event reinitialization (#3142)" begin - @connector LiquidPort begin - p(t)::Float64, [description = "Set pressure in bar", - guess = 1.01325] - Vdot(t)::Float64, - [description = "Volume flow rate in L/min", - guess = 0.0, - connect = Flow] - end - - @mtkmodel PressureSource begin - @components begin - port = LiquidPort() - end - @parameters begin - p_set::Float64 = 1.01325, [description = "Set pressure in bar"] - end - @equations begin - port.p ~ p_set - end - end - - @mtkmodel BinaryValve begin - @constants begin - p_ref::Float64 = 1.0, [description = "Reference pressure drop in bar"] - ρ_ref::Float64 = 1000.0, [description = "Reference density in kg/m^3"] - end - @components begin - port_in = LiquidPort() - port_out = LiquidPort() - end - @parameters begin - k_V::Float64 = 1.0, [description = "Valve coefficient in L/min/bar"] - k_leakage::Float64 = 1e-08, [description = "Leakage coefficient in L/min/bar"] - ρ::Float64 = 1000.0, [description = "Density in kg/m^3"] - end - @variables begin - S(t)::Float64, [description = "Valve state", guess = 1.0, irreducible = true] - Δp(t)::Float64, [description = "Pressure difference in bar", guess = 1.0] - Vdot(t)::Float64, [description = "Volume flow rate in L/min", guess = 1.0] - end - @equations begin - # Port handling - port_in.Vdot ~ -Vdot - port_out.Vdot ~ Vdot - Δp ~ port_in.p - port_out.p - # System behavior - D(S) ~ 0.0 - Vdot ~ S * k_V * sign(Δp) * sqrt(abs(Δp) / p_ref * ρ_ref / ρ) + k_leakage * Δp # softplus alpha function to avoid negative values under the sqrt - end - end - - # Test System - @mtkmodel TestSystem begin - @components begin - pressure_source_1 = PressureSource(p_set = 2.0) - binary_valve_1 = BinaryValve(S = 1.0, k_leakage = 0.0) - binary_valve_2 = BinaryValve(S = 1.0, k_leakage = 0.0) - pressure_source_2 = PressureSource(p_set = 1.0) - end - @equations begin - connect(pressure_source_1.port, binary_valve_1.port_in) - connect(binary_valve_1.port_out, binary_valve_2.port_in) - connect(binary_valve_2.port_out, pressure_source_2.port) - end - @discrete_events begin - [30] => [binary_valve_1.S ~ 0.0, binary_valve_2.Δp ~ 0.0] - [60] => [ - binary_valve_1.S ~ 1.0, binary_valve_2.S ~ 0.0, binary_valve_2.Δp ~ 1.0] - [120] => [binary_valve_1.S ~ 0.0, binary_valve_2.Δp ~ 0.0] - end - end - - # Test Simulation - @mtkbuild sys = TestSystem() - - # Test Simulation - prob = ODEProblem(sys, [], (0.0, 150.0)) - sol = solve(prob) - @test sol[end] == [0.0, 0.0, 0.0] -end - -@testset "Discrete variable timeseries" begin - @variables x(t) - @parameters a(t) b(t) c(t) - cb1 = [x ~ 1.0] => [a ~ -Pre(a)] - function save_affect!(integ, u, p, ctx) - integ.ps[p.b] = 5.0 - end - cb2 = [x ~ 0.5] => (save_affect!, [], [b], [b], nothing) - cb3 = 1.0 => [c ~ t] - - @mtkbuild sys = ODESystem(D(x) ~ cos(t), t, [x], [a, b, c]; - continuous_events = [cb1, cb2], discrete_events = [cb3]) - prob = ODEProblem(sys, [x => 1.0], (0.0, 2pi), [a => 1.0, b => 2.0, c => 0.0]) - @test sort(canonicalize(Discrete(), prob.p)[1]) == [0.0, 1.0, 2.0] - sol = solve(prob, Tsit5()) - - @test sol[a] == [1.0, -1.0] - @test sol[b] == [2.0, 5.0, 5.0] - @test sol[c] == [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0] -end - -@testset "Heater" begin - @variables temp(t) - params = @parameters furnace_on_threshold=0.5 furnace_off_threshold=0.7 furnace_power=1.0 leakage=0.1 furnace_on::Bool=false - eqs = [ - D(temp) ~ furnace_on * furnace_power - temp^2 * leakage - ] - - furnace_off = ModelingToolkit.SymbolicContinuousCallback( - [temp ~ furnace_off_threshold], - ModelingToolkit.ImperativeAffect(modified = (; furnace_on)) do x, o, i, c - @set! x.furnace_on = false - end) - furnace_enable = ModelingToolkit.SymbolicContinuousCallback( - [temp ~ furnace_on_threshold], - ModelingToolkit.ImperativeAffect(modified = (; furnace_on)) do x, o, i, c - @set! x.furnace_on = true - end) - @named sys = ODESystem( - eqs, t, [temp], params; continuous_events = [furnace_off, furnace_enable]) - ss = structural_simplify(sys) - prob = ODEProblem(ss, [temp => 0.0, furnace_on => true], (0.0, 100.0)) - sol = solve(prob, Tsit5(); dtmax = 0.01) - @test all(sol[temp][sol.t .> 1.0] .<= 0.79) && all(sol[temp][sol.t .> 1.0] .>= 0.49) - - furnace_off = ModelingToolkit.SymbolicContinuousCallback( - [temp ~ furnace_off_threshold], - ModelingToolkit.ImperativeAffect(modified = (; furnace_on)) do x, o, c, i - @set! x.furnace_on = false - end; initialize = ModelingToolkit.ImperativeAffect(modified = (; - temp)) do x, o, c, i - @set! x.temp = 0.2 - end) - furnace_enable = ModelingToolkit.SymbolicContinuousCallback( - [temp ~ furnace_on_threshold], - ModelingToolkit.ImperativeAffect(modified = (; furnace_on)) do x, o, c, i - @set! x.furnace_on = true - end) - @named sys = ODESystem( - eqs, t, [temp], params; continuous_events = [furnace_off, furnace_enable]) - ss = structural_simplify(sys) - prob = ODEProblem(ss, [temp => 0.0, furnace_on => true], (0.0, 100.0)) - sol = solve(prob, Tsit5(); dtmax = 0.01) - @test all(sol[temp][sol.t .> 1.0] .<= 0.79) && all(sol[temp][sol.t .> 1.0] .>= 0.49) - @test all(sol[temp][sol.t .!= 0.0] .<= 0.79) && all(sol[temp][sol.t .!= 0.0] .>= 0.2) -end - -@testset "ImperativeAffect errors and warnings" begin - @variables temp(t) - params = @parameters furnace_on_threshold=0.5 furnace_off_threshold=0.7 furnace_power=1.0 leakage=0.1 furnace_on::Bool=false - eqs = [ - D(temp) ~ furnace_on * furnace_power - temp^2 * leakage - ] - - furnace_off = ModelingToolkit.SymbolicContinuousCallback( - [temp ~ furnace_off_threshold], - ModelingToolkit.ImperativeAffect( - modified = (; furnace_on), observed = (; furnace_on)) do x, o, c, i - @set! x.furnace_on = false - end) - @named sys = ODESystem(eqs, t, [temp], params; continuous_events = [furnace_off]) - ss = structural_simplify(sys) - @test_logs (:warn, - "The symbols Any[:furnace_on] are declared as both observed and modified; this is a code smell because it becomes easy to confuse them and assign/not assign a value.") prob=ODEProblem( - ss, [temp => 0.0, furnace_on => true], (0.0, 100.0)) - - @variables tempsq(t) # trivially eliminated - eqs = [tempsq ~ temp^2 - D(temp) ~ furnace_on * furnace_power - temp^2 * leakage] - - furnace_off = ModelingToolkit.SymbolicContinuousCallback( - [temp ~ furnace_off_threshold], - ModelingToolkit.ImperativeAffect( - modified = (; furnace_on, tempsq), observed = (; furnace_on)) do x, o, c, i - @set! x.furnace_on = false - end) - @named sys = ODESystem( - eqs, t, [temp, tempsq], params; continuous_events = [furnace_off]) - ss = structural_simplify(sys) - @test_throws "refers to missing variable(s)" prob=ODEProblem( - ss, [temp => 0.0, furnace_on => true], (0.0, 100.0)) - - @parameters not_actually_here - furnace_off = ModelingToolkit.SymbolicContinuousCallback( - [temp ~ furnace_off_threshold], - ModelingToolkit.ImperativeAffect(modified = (; furnace_on), - observed = (; furnace_on, not_actually_here)) do x, o, c, i - @set! x.furnace_on = false - end) - @named sys = ODESystem( - eqs, t, [temp, tempsq], params; continuous_events = [furnace_off]) - ss = structural_simplify(sys) - @test_throws "refers to missing variable(s)" prob=ODEProblem( - ss, [temp => 0.0, furnace_on => true], (0.0, 100.0)) - - furnace_off = ModelingToolkit.SymbolicContinuousCallback( - [temp ~ furnace_off_threshold], - ModelingToolkit.ImperativeAffect(modified = (; furnace_on), - observed = (; furnace_on)) do x, o, c, i - return (; fictional2 = false) - end) - @named sys = ODESystem( - eqs, t, [temp, tempsq], params; continuous_events = [furnace_off]) - ss = structural_simplify(sys) - prob = ODEProblem( - ss, [temp => 0.0, furnace_on => true], (0.0, 100.0)) - @test_throws "Tried to write back to" solve(prob, Tsit5()) -end - -@testset "Quadrature" begin - @variables theta(t) omega(t) - params = @parameters qA=0 qB=0 hA=0 hB=0 cnt::Int=0 - eqs = [D(theta) ~ omega - omega ~ 1.0] - function decoder(oldA, oldB, newA, newB) - state = (oldA, oldB, newA, newB) - if state == (0, 0, 1, 0) || state == (1, 0, 1, 1) || state == (1, 1, 0, 1) || - state == (0, 1, 0, 0) - return 1 - elseif state == (0, 0, 0, 1) || state == (0, 1, 1, 1) || state == (1, 1, 1, 0) || - state == (1, 0, 0, 0) - return -1 - elseif state == (0, 0, 0, 0) || state == (0, 1, 0, 1) || state == (1, 0, 1, 0) || - state == (1, 1, 1, 1) - return 0 - else - return 0 # err is interpreted as no movement - end - end - qAevt = ModelingToolkit.SymbolicContinuousCallback([cos(100 * theta) ~ 0], - ModelingToolkit.ImperativeAffect((; qA, hA, hB, cnt), (; qB)) do x, o, c, i - @set! x.hA = x.qA - @set! x.hB = o.qB - @set! x.qA = 1 - @set! x.cnt += decoder(x.hA, x.hB, x.qA, o.qB) - x - end, - affect_neg = ModelingToolkit.ImperativeAffect( - (; qA, hA, hB, cnt), (; qB)) do x, o, c, i - @set! x.hA = x.qA - @set! x.hB = o.qB - @set! x.qA = 0 - @set! x.cnt += decoder(x.hA, x.hB, x.qA, o.qB) - x - end; rootfind = SciMLBase.RightRootFind) - qBevt = ModelingToolkit.SymbolicContinuousCallback([cos(100 * theta - π / 2) ~ 0], - ModelingToolkit.ImperativeAffect((; qB, hA, hB, cnt), (; qA)) do x, o, c, i - @set! x.hA = o.qA - @set! x.hB = x.qB - @set! x.qB = 1 - @set! x.cnt += decoder(x.hA, x.hB, o.qA, x.qB) - x - end, - affect_neg = ModelingToolkit.ImperativeAffect( - (; qB, hA, hB, cnt), (; qA)) do x, o, c, i - @set! x.hA = o.qA - @set! x.hB = x.qB - @set! x.qB = 0 - @set! x.cnt += decoder(x.hA, x.hB, o.qA, x.qB) - x - end; rootfind = SciMLBase.RightRootFind) - @named sys = ODESystem( - eqs, t, [theta, omega], params; continuous_events = [qAevt, qBevt]) - ss = structural_simplify(sys) - prob = ODEProblem(ss, [theta => 1e-5], (0.0, pi)) - sol = solve(prob, Tsit5(); dtmax = 0.01) - @test getp(sol, cnt)(sol) == 198 # we get 2 pulses per phase cycle (cos 0 crossing) and we go to 100 cycles; we miss a few due to the initial state -end - -@testset "Initialization" begin - @variables x(t) - seen = false - f = ModelingToolkit.FunctionalAffect( - f = (i, u, p, c) -> seen = true, sts = [], pars = [], discretes = []) - cb1 = ModelingToolkit.SymbolicContinuousCallback( - [x ~ 0], nothing, initialize = [x ~ 1.5], finalize = f) - @mtkbuild sys = ODESystem(D(x) ~ -1, t, [x], []; continuous_events = [cb1]) - prob = ODEProblem(sys, [x => 1.0], (0.0, 2), []) - sol = solve(prob, Tsit5(); dtmax = 0.01) - @test sol[x][1] ≈ 1.0 - @test sol[x][2] ≈ 1.5 # the initialize affect has been applied - @test seen == true - - @variables x(t) - seen = false - f = ModelingToolkit.FunctionalAffect( - f = (i, u, p, c) -> seen = true, sts = [], pars = [], discretes = []) - cb1 = ModelingToolkit.SymbolicContinuousCallback( - [x ~ 0], nothing, initialize = [x ~ 1.5], finalize = f) - inited = false - finaled = false - a = ModelingToolkit.FunctionalAffect( - f = (i, u, p, c) -> inited = true, sts = [], pars = [], discretes = []) - b = ModelingToolkit.FunctionalAffect( - f = (i, u, p, c) -> finaled = true, sts = [], pars = [], discretes = []) - cb2 = ModelingToolkit.SymbolicContinuousCallback( - [x ~ 0.1], nothing, initialize = a, finalize = b) - @mtkbuild sys = ODESystem(D(x) ~ -1, t, [x], []; continuous_events = [cb1, cb2]) - prob = ODEProblem(sys, [x => 1.0], (0.0, 2), []) - sol = solve(prob, Tsit5()) - @test sol[x][1] ≈ 1.0 - @test sol[x][2] ≈ 1.5 # the initialize affect has been applied - @test seen == true - @test inited == true - @test finaled == true - - #periodic - inited = false - finaled = false - cb3 = ModelingToolkit.SymbolicDiscreteCallback( - 1.0, [x ~ 2], initialize = a, finalize = b) - @mtkbuild sys = ODESystem(D(x) ~ -1, t, [x], []; discrete_events = [cb3]) - prob = ODEProblem(sys, [x => 1.0], (0.0, 2), []) - sol = solve(prob, Tsit5()) - @test inited == true - @test finaled == true - @test isapprox(sol[x][3], 0.0, atol = 1e-9) - @test sol[x][4] ≈ 2.0 - @test sol[x][5] ≈ 1.0 - - seen = false - inited = false - finaled = false - cb3 = ModelingToolkit.SymbolicDiscreteCallback(1.0, f, initialize = a, finalize = b) - @mtkbuild sys = ODESystem(D(x) ~ -1, t, [x], []; discrete_events = [cb3]) - prob = ODEProblem(sys, [x => 1.0], (0.0, 2), []) - sol = solve(prob, Tsit5()) - @test seen == true - @test inited == true - - #preset - seen = false - inited = false - finaled = false - cb3 = ModelingToolkit.SymbolicDiscreteCallback([1.0], f, initialize = a, finalize = b) - @mtkbuild sys = ODESystem(D(x) ~ -1, t, [x], []; discrete_events = [cb3]) - prob = ODEProblem(sys, [x => 1.0], (0.0, 2), []) - sol = solve(prob, Tsit5()) - @test seen == true - @test inited == true - @test finaled == true - - #equational - seen = false - inited = false - finaled = false - cb3 = ModelingToolkit.SymbolicDiscreteCallback( - t == 1.0, f, initialize = a, finalize = b) - @mtkbuild sys = ODESystem(D(x) ~ -1, t, [x], []; discrete_events = [cb3]) - prob = ODEProblem(sys, [x => 1.0], (0.0, 2), []) - sol = solve(prob, Tsit5(); tstops = 1.0) - @test seen == true - @test inited == true - @test finaled == true -end +#@testset "SymbolicContinuousCallback constructors" begin +# e = SymbolicContinuousCallback(eqs[]) +# @test e isa SymbolicContinuousCallback +# @test isequal(equations(e), eqs) +# @test e.affect == nothing +# @test e.affect_neg == nothing +# @test e.rootfind == SciMLBase.LeftRootFind +# +# e = SymbolicContinuousCallback(eqs) +# @test e isa SymbolicContinuousCallback +# @test isequal(equations(e), eqs) +# @test e.affect == nothing +# @test e.affect_neg == nothing +# @test e.rootfind == SciMLBase.LeftRootFind +# +# e = SymbolicContinuousCallback(eqs, nothing) +# @test e isa SymbolicContinuousCallback +# @test isequal(equations(e), eqs) +# @test e.affect == nothing +# @test e.affect_neg == nothing +# @test e.rootfind == SciMLBase.LeftRootFind +# +# e = SymbolicContinuousCallback(eqs[], nothing) +# @test e isa SymbolicContinuousCallback +# @test isequal(equations(e), eqs) +# @test e.affect == nothing +# @test e.affect_neg == nothing +# @test e.rootfind == SciMLBase.LeftRootFind +# +# e = SymbolicContinuousCallback(eqs => nothing) +# @test e isa SymbolicContinuousCallback +# @test isequal(equations(e), eqs) +# @test e.affect == nothing +# @test e.affect_neg == nothing +# @test e.rootfind == SciMLBase.LeftRootFind +# +# e = SymbolicContinuousCallback(eqs[] => nothing) +# @test e isa SymbolicContinuousCallback +# @test isequal(equations(e), eqs) +# @test e.affect == nothing +# @test e.affect_neg == nothing +# @test e.rootfind == SciMLBase.LeftRootFind +# +# ## With affect +# e = SymbolicContinuousCallback(eqs[], affect) +# @test e isa SymbolicContinuousCallback +# @test isequal(equations(e), eqs) +# @test e.rootfind == SciMLBase.LeftRootFind +# +# # with only positive edge affect +# e = SymbolicContinuousCallback(eqs[], affect, affect_neg = nothing) +# @test e isa SymbolicContinuousCallback +# @test isequal(equations(e), eqs) +# @test isnothing(e.affect_neg) +# @test e.rootfind == SciMLBase.LeftRootFind +# +# # with explicit edge affects +# e = SymbolicContinuousCallback(eqs[], affect, affect_neg = affect_neg) +# @test e isa SymbolicContinuousCallback +# @test isequal(equations(e), eqs) +# @test e.rootfind == SciMLBase.LeftRootFind +# +# # with different root finding ops +# e = SymbolicContinuousCallback( +# eqs[], affect, affect_neg = affect_neg, rootfind = SciMLBase.LeftRootFind) +# @test e isa SymbolicContinuousCallback +# @test isequal(equations(e), eqs) +# @test e.rootfind == SciMLBase.LeftRootFind +# +# # test plural constructor +# e = SymbolicContinuousCallbacks(eqs[]) +# @test e isa Vector{SymbolicContinuousCallback} +# @test isequal(equations(e[]), eqs) +# @test e[].affect == nothing +# +# e = SymbolicContinuousCallbacks(eqs) +# @test e isa Vector{SymbolicContinuousCallback} +# @test isequal(equations(e[]), eqs) +# @test e[].affect == nothing +# +# e = SymbolicContinuousCallbacks(eqs[] => affect) +# @test e isa Vector{SymbolicContinuousCallback} +# @test isequal(equations(e[]), eqs) +# @test e[].affect isa AffectSystem +# +# e = SymbolicContinuousCallbacks(eqs => affect) +# @test e isa Vector{SymbolicContinuousCallback} +# @test isequal(equations(e[]), eqs) +# @test e[].affect isa AffectSystem +# +# e = SymbolicContinuousCallbacks([eqs[] => affect]) +# @test e isa Vector{SymbolicContinuousCallback} +# @test isequal(equations(e[]), eqs) +# @test e[].affect isa AffectSystem +# +# e = SymbolicContinuousCallbacks([eqs => affect]) +# @test e isa Vector{SymbolicContinuousCallback} +# @test isequal(equations(e[]), eqs) +# @test e[].affect isa AffectSystem +#end +# +#@testset "ImperativeAffect constructors" begin +# fmfa(o, x, i, c) = nothing +# m = ModelingToolkit.ImperativeAffect(fmfa) +# @test m isa ModelingToolkit.ImperativeAffect +# @test m.f == fmfa +# @test m.obs == [] +# @test m.obs_syms == [] +# @test m.modified == [] +# @test m.mod_syms == [] +# @test m.ctx === nothing +# +# m = ModelingToolkit.ImperativeAffect(fmfa, (;)) +# @test m isa ModelingToolkit.ImperativeAffect +# @test m.f == fmfa +# @test m.obs == [] +# @test m.obs_syms == [] +# @test m.modified == [] +# @test m.mod_syms == [] +# @test m.ctx === nothing +# +# m = ModelingToolkit.ImperativeAffect(fmfa, (; x)) +# @test m isa ModelingToolkit.ImperativeAffect +# @test m.f == fmfa +# @test isequal(m.obs, []) +# @test m.obs_syms == [] +# @test isequal(m.modified, [x]) +# @test m.mod_syms == [:x] +# @test m.ctx === nothing +# +# m = ModelingToolkit.ImperativeAffect(fmfa, (; y = x)) +# @test m isa ModelingToolkit.ImperativeAffect +# @test m.f == fmfa +# @test isequal(m.obs, []) +# @test m.obs_syms == [] +# @test isequal(m.modified, [x]) +# @test m.mod_syms == [:y] +# @test m.ctx === nothing +# +# m = ModelingToolkit.ImperativeAffect(fmfa; observed = (; y = x)) +# @test m isa ModelingToolkit.ImperativeAffect +# @test m.f == fmfa +# @test isequal(m.obs, [x]) +# @test m.obs_syms == [:y] +# @test m.modified == [] +# @test m.mod_syms == [] +# @test m.ctx === nothing +# +# m = ModelingToolkit.ImperativeAffect(fmfa; modified = (; x)) +# @test m isa ModelingToolkit.ImperativeAffect +# @test m.f == fmfa +# @test isequal(m.obs, []) +# @test m.obs_syms == [] +# @test isequal(m.modified, [x]) +# @test m.mod_syms == [:x] +# @test m.ctx === nothing +# +# m = ModelingToolkit.ImperativeAffect(fmfa; modified = (; y = x)) +# @test m isa ModelingToolkit.ImperativeAffect +# @test m.f == fmfa +# @test isequal(m.obs, []) +# @test m.obs_syms == [] +# @test isequal(m.modified, [x]) +# @test m.mod_syms == [:y] +# @test m.ctx === nothing +# +# m = ModelingToolkit.ImperativeAffect(fmfa, (; x), (; x)) +# @test m isa ModelingToolkit.ImperativeAffect +# @test m.f == fmfa +# @test isequal(m.obs, [x]) +# @test m.obs_syms == [:x] +# @test isequal(m.modified, [x]) +# @test m.mod_syms == [:x] +# @test m.ctx === nothing +# +# m = ModelingToolkit.ImperativeAffect(fmfa, (; y = x), (; y = x)) +# @test m isa ModelingToolkit.ImperativeAffect +# @test m.f == fmfa +# @test isequal(m.obs, [x]) +# @test m.obs_syms == [:y] +# @test isequal(m.modified, [x]) +# @test m.mod_syms == [:y] +# @test m.ctx === nothing +# +# m = ModelingToolkit.ImperativeAffect( +# fmfa; modified = (; y = x), observed = (; y = x)) +# @test m isa ModelingToolkit.ImperativeAffect +# @test m.f == fmfa +# @test isequal(m.obs, [x]) +# @test m.obs_syms == [:y] +# @test isequal(m.modified, [x]) +# @test m.mod_syms == [:y] +# @test m.ctx === nothing +# +# m = ModelingToolkit.ImperativeAffect( +# fmfa; modified = (; y = x), observed = (; y = x), ctx = 3) +# @test m isa ModelingToolkit.ImperativeAffect +# @test m.f == fmfa +# @test isequal(m.obs, [x]) +# @test m.obs_syms == [:y] +# @test isequal(m.modified, [x]) +# @test m.mod_syms == [:y] +# @test m.ctx === 3 +# +# m = ModelingToolkit.ImperativeAffect(fmfa, (; x), (; x), 3) +# @test m isa ModelingToolkit.ImperativeAffect +# @test m.f == fmfa +# @test isequal(m.obs, [x]) +# @test m.obs_syms == [:x] +# @test isequal(m.modified, [x]) +# @test m.mod_syms == [:x] +# @test m.ctx === 3 +#end +# +#@testset "Condition Compilation" begin +# @named sys = ODESystem(eqs, t, continuous_events = [x ~ 1]) +# @test getfield(sys, :continuous_events)[] == +# SymbolicContinuousCallback(Equation[x ~ 1], nothing) +# @test isequal(equations(getfield(sys, :continuous_events))[], x ~ 1) +# fsys = flatten(sys) +# @test isequal(equations(getfield(fsys, :continuous_events))[], x ~ 1) +# +# @named sys2 = ODESystem([D(x) ~ 1], t, continuous_events = [x ~ 2], systems = [sys]) +# @test getfield(sys2, :continuous_events)[] == +# SymbolicContinuousCallback(Equation[x ~ 2], nothing) +# @test all(ModelingToolkit.continuous_events(sys2) .== [ +# SymbolicContinuousCallback(Equation[x ~ 2], nothing), +# SymbolicContinuousCallback(Equation[sys.x ~ 1], nothing) +# ]) +# +# @test isequal(equations(getfield(sys2, :continuous_events))[1], x ~ 2) +# @test length(ModelingToolkit.continuous_events(sys2)) == 2 +# @test isequal(equations(ModelingToolkit.continuous_events(sys2)[1])[], x ~ 2) +# @test isequal(equations(ModelingToolkit.continuous_events(sys2)[2])[], sys.x ~ 1) +# +# sys = complete(sys) +# sys_nosplit = complete(sys; split = false) +# sys2 = complete(sys2) +# +# # Test proper rootfinding +# prob = ODEProblem(sys, Pair[], (0.0, 2.0)) +# p0 = 0 +# t0 = 0 +# @test get_callback(prob) isa ModelingToolkit.DiffEqCallbacks.ContinuousCallback +# cb = ModelingToolkit.generate_continuous_callbacks(sys) +# cond = cb.condition +# out = [0.0] +# cond.f_iip(out, [0], p0, t0) +# @test out[] ≈ -1 # signature is u,p,t +# cond.f_iip(out, [1], p0, t0) +# @test out[] ≈ 0 # signature is u,p,t +# cond.f_iip(out, [2], p0, t0) +# @test out[] ≈ 1 # signature is u,p,t +# +# prob = ODEProblem(sys, Pair[], (0.0, 2.0)) +# prob_nosplit = ODEProblem(sys_nosplit, Pair[], (0.0, 2.0)) +# sol = solve(prob, Tsit5()) +# sol_nosplit = solve(prob_nosplit, Tsit5()) +# @test minimum(t -> abs(t - 1), sol.t) < 1e-10 # test that the solver stepped at the root +# @test minimum(t -> abs(t - 1), sol_nosplit.t) < 1e-10 # test that the solver stepped at the root +# +# # Test user-provided callback is respected +# test_callback = DiscreteCallback(x -> x, x -> x) +# prob = ODEProblem(sys, Pair[], (0.0, 2.0), callback = test_callback) +# prob_nosplit = ODEProblem(sys_nosplit, Pair[], (0.0, 2.0), callback = test_callback) +# cbs = get_callback(prob) +# cbs_nosplit = get_callback(prob_nosplit) +# @test cbs isa CallbackSet +# @test cbs.discrete_callbacks[1] == test_callback +# @test cbs_nosplit isa CallbackSet +# @test cbs_nosplit.discrete_callbacks[1] == test_callback +# +# prob = ODEProblem(sys2, Pair[], (0.0, 3.0)) +# cb = get_callback(prob) +# @test cb isa ModelingToolkit.DiffEqCallbacks.VectorContinuousCallback +# +# cond = cb.condition +# out = [0.0, 0.0] +# # the root to find is 2 +# cond.f_iip(out, [0, 0], p0, t0) +# @test out[1] ≈ -2 # signature is u,p,t +# cond.f_iip(out, [1, 0], p0, t0) +# @test out[1] ≈ -1 # signature is u,p,t +# cond.f_iip(out, [2, 0], p0, t0) # this should return 0 +# @test out[1] ≈ 0 # signature is u,p,t +# +# # the root to find is 1 +# out = [0.0, 0.0] +# cond.f_iip(out, [0, 0], p0, t0) +# @test out[2] ≈ -1 # signature is u,p,t +# cond.f_iip(out, [0, 1], p0, t0) # this should return 0 +# @test out[2] ≈ 0 # signature is u,p,t +# cond.f_iip(out, [0, 2], p0, t0) +# @test out[2] ≈ 1 # signature is u,p,t +# +# sol = solve(prob, Tsit5()) +# @test minimum(t -> abs(t - 1), sol.t) < 1e-10 # test that the solver stepped at the first root +# @test minimum(t -> abs(t - 2), sol.t) < 1e-10 # test that the solver stepped at the second root +# +# @named sys = ODESystem(eqs, t, continuous_events = [x ~ 1, x ~ 2]) # two root eqs using the same unknown +# sys = complete(sys) +# prob = ODEProblem(sys, Pair[], (0.0, 3.0)) +# @test get_callback(prob) isa ModelingToolkit.DiffEqCallbacks.VectorContinuousCallback +# sol = solve(prob, Tsit5()) +# @test minimum(t -> abs(t - 1), sol.t) < 1e-10 # test that the solver stepped at the first root +# @test minimum(t -> abs(t - 2), sol.t) < 1e-10 # test that the solver stepped at the second root +#end +# +#@testset "Bouncing Ball" begin +# ###### 1D Bounce +# @variables x(t)=1 v(t)=0 +# +# root_eqs = [x ~ 0] +# affect = [v ~ -Pre(v)] +# +# @named ball = ODESystem( +# [D(x) ~ v +# D(v) ~ -9.8], t, continuous_events = root_eqs => affect) +# +# @test only(continuous_events(ball)) == +# SymbolicContinuousCallback(Equation[x ~ 0], Equation[v ~ -Pre(v)]) +# ball = structural_simplify(ball) +# +# @test length(ModelingToolkit.continuous_events(ball)) == 1 +# +# tspan = (0.0, 5.0) +# prob = ODEProblem(ball, Pair[], tspan) +# sol = solve(prob, Tsit5()) +# @test 0 <= minimum(sol[x]) <= 1e-10 # the ball never went through the floor but got very close +# +# ###### 2D bouncing ball +# @variables x(t)=1 y(t)=0 vx(t)=0 vy(t)=1 +# +# events = [[x ~ 0] => [vx ~ -Pre(vx)] +# [y ~ -1.5, y ~ 1.5] => [vy ~ -Pre(vy)]] +# +# @named ball = ODESystem( +# [D(x) ~ vx +# D(y) ~ vy +# D(vx) ~ -9.8 +# D(vy) ~ -0.01vy], t; continuous_events = events) +# +# _ball = ball +# ball = structural_simplify(_ball) +# ball_nosplit = structural_simplify(_ball; split = false) +# +# tspan = (0.0, 5.0) +# prob = ODEProblem(ball, Pair[], tspan) +# prob_nosplit = ODEProblem(ball_nosplit, Pair[], tspan) +# +# cb = get_callback(prob) +# @test cb isa ModelingToolkit.DiffEqCallbacks.VectorContinuousCallback +# @test getfield(ball, :continuous_events)[1] == +# SymbolicContinuousCallback(Equation[x ~ 0], Equation[vx ~ -Pre(vx)]) +# @test getfield(ball, :continuous_events)[2] == +# SymbolicContinuousCallback(Equation[y ~ -1.5, y ~ 1.5], Equation[vy ~ -Pre(vy)]) +# cond = cb.condition +# out = [0.0, 0.0, 0.0] +# p0 = 0. +# t0 = 0. +# cond.f_iip(out, [0, 0, 0, 0], p0, t0) +# @test out ≈ [0, 1.5, -1.5] +# +# sol = solve(prob, Tsit5()) +# sol_nosplit = solve(prob_nosplit, Tsit5()) +# @test 0 <= minimum(sol[x]) <= 1e-10 # the ball never went through the floor but got very close +# @test minimum(sol[y]) ≈ -1.5 # check wall conditions +# @test maximum(sol[y]) ≈ 1.5 # check wall conditions +# @test 0 <= minimum(sol_nosplit[x]) <= 1e-10 # the ball never went through the floor but got very close +# @test minimum(sol_nosplit[y]) ≈ -1.5 # check wall conditions +# @test maximum(sol_nosplit[y]) ≈ 1.5 # check wall conditions +# +# ## Test multi-variable affect +# # in this test, there are two variables affected by a single event. +# events = [[x ~ 0] => [vx ~ -Pre(vx), vy ~ -Pre(vy)]] +# +# @named ball = ODESystem([D(x) ~ vx +# D(y) ~ vy +# D(vx) ~ -1 +# D(vy) ~ 0], t; continuous_events = events) +# +# ball_nosplit = structural_simplify(ball) +# ball = structural_simplify(ball) +# +# tspan = (0.0, 5.0) +# prob = ODEProblem(ball, Pair[], tspan) +# prob_nosplit = ODEProblem(ball_nosplit, Pair[], tspan) +# sol = solve(prob, Tsit5()) +# sol_nosplit = solve(prob_nosplit, Tsit5()) +# @test 0 <= minimum(sol[x]) <= 1e-10 # the ball never went through the floor but got very close +# @test -minimum(sol[y]) ≈ maximum(sol[y]) ≈ sqrt(2) # the ball will never go further than √2 in either direction (gravity was changed to 1 to get this particular number) +# @test 0 <= minimum(sol_nosplit[x]) <= 1e-10 # the ball never went through the floor but got very close +# @test -minimum(sol_nosplit[y]) ≈ maximum(sol_nosplit[y]) ≈ sqrt(2) # the ball will never go further than √2 in either direction (gravity was changed to 1 to get this particular number) +#end +# +## issue https://github.com/SciML/ModelingToolkit.jl/issues/1386 +## tests that it works for ODAESystem +#@testset "ODAESystem" begin +# @variables vs(t) v(t) vmeasured(t) +# eq = [vs ~ sin(2pi * t) +# D(v) ~ vs - v +# D(vmeasured) ~ 0.0] +# ev = [sin(20pi * t) ~ 0.0] => [vmeasured ~ Pre(v)] +# @named sys = ODESystem(eq, t, continuous_events = ev) +# sys = structural_simplify(sys) +# prob = ODEProblem(sys, zeros(2), (0.0, 5.1)) +# sol = solve(prob, Tsit5()) +# @test all(minimum((0:0.1:5) .- sol.t', dims = 2) .< 0.0001) # test that the solver stepped every 0.1s as dictated by event +# @test sol([0.25])[vmeasured][] == sol([0.23])[vmeasured][] # test the hold property +#end +# +### https://github.com/SciML/ModelingToolkit.jl/issues/1528 +#@testset "Handle Empty Events" begin +# Dₜ = D +# +# @parameters u(t) [input = true] # Indicate that this is a controlled input +# @parameters y(t) [output = true] # Indicate that this is a measured output +# +# function Mass(; name, m = 1.0, p = 0, v = 0) +# ps = @parameters m = m +# sts = @variables pos(t)=p vel(t)=v +# eqs = Dₜ(pos) ~ vel +# ODESystem(eqs, t, [pos, vel], ps; name) +# end +# function Spring(; name, k = 1e4) +# ps = @parameters k = k +# @variables x(t) = 0 # Spring deflection +# ODESystem(Equation[], t, [x], ps; name) +# end +# function Damper(; name, c = 10) +# ps = @parameters c = c +# @variables vel(t) = 0 +# ODESystem(Equation[], t, [vel], ps; name) +# end +# function SpringDamper(; name, k = false, c = false) +# spring = Spring(; name = :spring, k) +# damper = Damper(; name = :damper, c) +# compose(ODESystem(Equation[], t; name), +# spring, damper) +# end +# connect_sd(sd, m1, m2) = [ +# sd.spring.x ~ m1.pos - m2.pos, sd.damper.vel ~ m1.vel - m2.vel] +# sd_force(sd) = -sd.spring.k * sd.spring.x - sd.damper.c * sd.damper.vel +# @named mass1 = Mass(; m = 1) +# @named mass2 = Mass(; m = 1) +# @named sd = SpringDamper(; k = 1000, c = 10) +# function Model(u, d = 0) +# eqs = [connect_sd(sd, mass1, mass2) +# Dₜ(mass1.vel) ~ (sd_force(sd) + u) / mass1.m +# Dₜ(mass2.vel) ~ (-sd_force(sd) + d) / mass2.m] +# @named _model = ODESystem(eqs, t; observed = [y ~ mass2.pos]) +# @named model = compose(_model, mass1, mass2, sd) +# end +# model = Model(sin(30t)) +# sys = structural_simplify(model) +# @test isempty(ModelingToolkit.continuous_events(sys)) +#end +# +#@testset "ODESystem Discrete Callbacks" begin +# function testsol(osys, u0, p, tspan; tstops = Float64[], paramtotest = nothing, +# kwargs...) +# oprob = ODEProblem(complete(osys), u0, tspan, p; kwargs...) +# sol = solve(oprob, Tsit5(); tstops = tstops, abstol = 1e-10, reltol = 1e-10) +# @test isapprox(sol(1.0000000001)[1] - sol(0.999999999)[1], 1.0; rtol = 1e-6) +# paramtotest === nothing || (@test sol.ps[paramtotest] == 1.0) +# @test isapprox(sol(4.0)[1], 2 * exp(-2.0)) +# sol +# end +# +# @parameters k t1 t2 +# @variables A(t) B(t) +# +# cond1 = (t == t1) +# affect1 = [A ~ Pre(A) + 1] +# cb1 = cond1 => affect1 +# cond2 = (t == t2) +# affect2 = [k ~ 1.0] +# cb2 = cond2 => affect2 +# +# ∂ₜ = D +# eqs = [∂ₜ(A) ~ -k * A] +# @named osys = ODESystem(eqs, t, [A], [k, t1, t2], discrete_events = [cb1, cb2]) +# u0 = [A => 1.0] +# p = [k => 0.0, t1 => 1.0, t2 => 2.0] +# tspan = (0.0, 4.0) +# testsol(osys, u0, p, tspan; tstops = [1.0, 2.0], paramtotest = k) +# +# cond1a = (t == t1) +# affect1a = [A ~ Pre(A) + 1, B ~ A] +# cb1a = cond1a => affect1a +# @named osys1 = ODESystem(eqs, t, [A, B], [k, t1, t2], discrete_events = [cb1a, cb2]) +# u0′ = [A => 1.0, B => 0.0] +# sol = testsol( +# osys1, u0′, p, tspan; tstops = [1.0, 2.0], check_length = false, paramtotest = k) +# @test sol(1.0000001, idxs = B) == 2.0 +# +# # same as above - but with set-time event syntax +# cb1‵ = [1.0] => affect1 # needs to be a Vector for the event to happen only once +# cb2‵ = [2.0] => affect2 +# @named osys‵ = ODESystem(eqs, t, [A], [k], discrete_events = [cb1‵, cb2‵]) +# testsol(osys‵, u0, p, tspan; paramtotest = k) +# +# # mixing discrete affects +# @named osys3 = ODESystem(eqs, t, [A], [k, t1, t2], discrete_events = [cb1, cb2‵]) +# testsol(osys3, u0, p, tspan; tstops = [1.0], paramtotest = k) +# +# # mixing with a func affect +# function affect!(integrator, u, p, ctx) +# integrator.ps[p.k] = 1.0 +# nothing +# end +# cb2‵‵ = [2.0] => (affect!, [], [k], [k], nothing) +# @named osys4 = ODESystem(eqs, t, [A], [k, t1], discrete_events = [cb1, cb2‵‵]) +# oprob4 = ODEProblem(complete(osys4), u0, tspan, p) +# testsol(osys4, u0, p, tspan; tstops = [1.0], paramtotest = k) +# +# # mixing with symbolic condition in the func affect +# cb2‵‵‵ = (t == t2) => (affect!, [], [k], [k], nothing) +# @named osys5 = ODESystem(eqs, t, [A], [k, t1, t2], discrete_events = [cb1, cb2‵‵‵]) +# testsol(osys5, u0, p, tspan; tstops = [1.0, 2.0]) +# @named osys6 = ODESystem(eqs, t, [A], [k, t1, t2], discrete_events = [cb2‵‵‵, cb1]) +# testsol(osys6, u0, p, tspan; tstops = [1.0, 2.0], paramtotest = k) +# +# # mix a continuous event too +# cond3 = A ~ 0.1 +# affect3 = [k ~ 0.0] +# cb3 = cond3 => affect3 +# @named osys7 = ODESystem(eqs, t, [A], [k, t1, t2], discrete_events = [cb1, cb2‵‵‵], +# continuous_events = [cb3]) +# sol = testsol(osys7, u0, p, (0.0, 10.0); tstops = [1.0, 2.0]) +# @test isapprox(sol(10.0)[1], 0.1; atol = 1e-10, rtol = 1e-10) +#end +# +#@testset "SDESystem Discrete Callbacks" begin +# function testsol(ssys, u0, p, tspan; tstops = Float64[], paramtotest = nothing, +# kwargs...) +# sprob = SDEProblem(complete(ssys), u0, tspan, p; kwargs...) +# sol = solve(sprob, RI5(); tstops = tstops, abstol = 1e-10, reltol = 1e-10) +# @test isapprox(sol(1.0000000001)[1] - sol(0.999999999)[1], 1.0; rtol = 1e-4) +# paramtotest === nothing || (@test sol.ps[paramtotest] == 1.0) +# @test isapprox(sol(4.0)[1], 2 * exp(-2.0), atol = 1e-4) +# sol +# end +# +# @parameters k t1 t2 +# @variables A(t) B(t) +# +# cond1 = (t == t1) +# affect1 = [A ~ Pre(A) + 1] +# cb1 = cond1 => affect1 +# cond2 = (t == t2) +# affect2 = [k ~ 1.0] +# cb2 = cond2 => affect2 +# +# ∂ₜ = D +# eqs = [∂ₜ(A) ~ -k * A] +# @named ssys = SDESystem(eqs, [0.0], t, [A], [k, t1, t2], +# discrete_events = [cb1, cb2]) +# u0 = [A => 1.0] +# p = [k => 0.0, t1 => 1.0, t2 => 2.0] +# tspan = (0.0, 4.0) +# testsol(ssys, u0, p, tspan; tstops = [1.0, 2.0], paramtotest = k) +# +# cond1a = (t == t1) +# affect1a = [A ~ Pre(A) + 1, B ~ A] +# cb1a = cond1a => affect1a +# @named ssys1 = SDESystem(eqs, [0.0], t, [A, B], [k, t1, t2], +# discrete_events = [cb1a, cb2]) +# u0′ = [A => 1.0, B => 0.0] +# sol = testsol( +# ssys1, u0′, p, tspan; tstops = [1.0, 2.0], check_length = false, paramtotest = k) +# @test sol(1.0000001, idxs = 2) == 2.0 +# +# # same as above - but with set-time event syntax +# cb1‵ = [1.0] => affect1 # needs to be a Vector for the event to happen only once +# cb2‵ = [2.0] => affect2 +# @named ssys‵ = SDESystem(eqs, [0.0], t, [A], [k], discrete_events = [cb1‵, cb2‵]) +# testsol(ssys‵, u0, p, tspan; paramtotest = k) +# +# # mixing discrete affects +# @named ssys3 = SDESystem(eqs, [0.0], t, [A], [k, t1, t2], +# discrete_events = [cb1, cb2‵]) +# testsol(ssys3, u0, p, tspan; tstops = [1.0], paramtotest = k) +# +# # mixing with a func affect +# function affect!(integrator, u, p, ctx) +# setp(integrator, p.k)(integrator, 1.0) +# nothing +# end +# cb2‵‵ = [2.0] => (affect!, [], [k], [k], nothing) +# @named ssys4 = SDESystem(eqs, [0.0], t, [A], [k, t1], +# discrete_events = [cb1, cb2‵‵]) +# testsol(ssys4, u0, p, tspan; tstops = [1.0], paramtotest = k) +# +# # mixing with symbolic condition in the func affect +# cb2‵‵‵ = (t == t2) => (affect!, [], [k], [k], nothing) +# @named ssys5 = SDESystem(eqs, [0.0], t, [A], [k, t1, t2], +# discrete_events = [cb1, cb2‵‵‵]) +# testsol(ssys5, u0, p, tspan; tstops = [1.0, 2.0], paramtotest = k) +# @named ssys6 = SDESystem(eqs, [0.0], t, [A], [k, t1, t2], +# discrete_events = [cb2‵‵‵, cb1]) +# testsol(ssys6, u0, p, tspan; tstops = [1.0, 2.0], paramtotest = k) +# +# # mix a continuous event too +# cond3 = A ~ 0.1 +# affect3 = [k ~ 0.0] +# cb3 = cond3 => affect3 +# @named ssys7 = SDESystem(eqs, [0.0], t, [A], [k, t1, t2], +# discrete_events = [cb1, cb2‵‵‵], +# continuous_events = [cb3]) +# sol = testsol(ssys7, u0, p, (0.0, 10.0); tstops = [1.0, 2.0]) +# @test isapprox(sol(10.0)[1], 0.1; atol = 1e-10, rtol = 1e-10) +#end +# +#@testset "JumpSystem Discrete Callbacks" begin +# function testsol(jsys, u0, p, tspan; tstops = Float64[], paramtotest = nothing, +# N = 40000, kwargs...) +# jsys = complete(jsys) +# dprob = DiscreteProblem(jsys, u0, tspan, p) +# jprob = JumpProblem(jsys, dprob, Direct(); kwargs...) +# sol = solve(jprob, SSAStepper(); tstops = tstops) +# @test (sol(1.000000000001)[1] - sol(0.99999999999)[1]) == 1 +# paramtotest === nothing || (@test sol.ps[paramtotest] == 1.0) +# @test sol(40.0)[1] == 0 +# sol +# end +# +# @parameters k t1 t2 +# @variables A(t) B(t) +# +# cond1 = (t == t1) +# affect1 = [A ~ Pre(A) + 1] +# cb1 = cond1 => affect1 +# cond2 = (t == t2) +# affect2 = [k ~ 1.0] +# cb2 = cond2 => affect2 +# +# eqs = [MassActionJump(k, [A => 1], [A => -1])] +# @named jsys = JumpSystem(eqs, t, [A], [k, t1, t2], discrete_events = [cb1, cb2]) +# u0 = [A => 1] +# p = [k => 0.0, t1 => 1.0, t2 => 2.0] +# tspan = (0.0, 40.0) +# testsol(jsys, u0, p, tspan; tstops = [1.0, 2.0], rng, paramtotest = k) +# +# cond1a = (t == t1) +# affect1a = [A ~ Pre(A) + 1, B ~ A] +# cb1a = cond1a => affect1a +# @named jsys1 = JumpSystem(eqs, t, [A, B], [k, t1, t2], discrete_events = [cb1a, cb2]) +# u0′ = [A => 1, B => 0] +# sol = testsol(jsys1, u0′, p, tspan; tstops = [1.0, 2.0], +# check_length = false, rng, paramtotest = k) +# @test sol(1.000000001, idxs = B) == 2 +# +# # same as above - but with set-time event syntax +# cb1‵ = [1.0] => affect1 # needs to be a Vector for the event to happen only once +# cb2‵ = [2.0] => affect2 +# @named jsys‵ = JumpSystem(eqs, t, [A], [k], discrete_events = [cb1‵, cb2‵]) +# testsol(jsys‵, u0, [p[1]], tspan; rng, paramtotest = k) +# +# # mixing discrete affects +# @named jsys3 = JumpSystem(eqs, t, [A], [k, t1, t2], discrete_events = [cb1, cb2‵]) +# testsol(jsys3, u0, p, tspan; tstops = [1.0], rng, paramtotest = k) +# +# # mixing with a func affect +# function affect!(integrator, u, p, ctx) +# integrator.ps[p.k] = 1.0 +# reset_aggregated_jumps!(integrator) +# nothing +# end +# cb2‵‵ = [2.0] => (affect!, [], [k], [k], nothing) +# @named jsys4 = JumpSystem(eqs, t, [A], [k, t1], discrete_events = [cb1, cb2‵‵]) +# testsol(jsys4, u0, p, tspan; tstops = [1.0], rng, paramtotest = k) +# +# # mixing with symbolic condition in the func affect +# cb2‵‵‵ = (t == t2) => (affect!, [], [k], [k], nothing) +# @named jsys5 = JumpSystem(eqs, t, [A], [k, t1, t2], discrete_events = [cb1, cb2‵‵‵]) +# testsol(jsys5, u0, p, tspan; tstops = [1.0, 2.0], rng, paramtotest = k) +# @named jsys6 = JumpSystem(eqs, t, [A], [k, t1, t2], discrete_events = [cb2‵‵‵, cb1]) +# testsol(jsys6, u0, p, tspan; tstops = [1.0, 2.0], rng, paramtotest = k) +#end +# +#@testset "Namespacing" begin +# function oscillator_ce(k = 1.0; name) +# sts = @variables x(t)=1.0 v(t)=0.0 F(t) +# ps = @parameters k=k Θ=0.5 +# eqs = [D(x) ~ v, D(v) ~ -k * x + F] +# ev = [x ~ Θ] => [x ~ 1.0, v ~ 0.0] +# ODESystem(eqs, t, sts, ps, continuous_events = [ev]; name) +# end +# +# @named oscce = oscillator_ce() +# eqs = [oscce.F ~ 0] +# @named eqs_sys = ODESystem(eqs, t) +# @named oneosc_ce = compose(eqs_sys, oscce) +# oneosc_ce_simpl = structural_simplify(oneosc_ce) +# +# prob = ODEProblem(oneosc_ce_simpl, [], (0.0, 2.0), []) +# sol = solve(prob, Tsit5(), saveat = 0.1) +# +# @test typeof(oneosc_ce_simpl) == ODESystem +# @test sol[1, 6] < 1.0 # test whether x(t) decreases over time +# @test sol[1, 18] > 0.5 # test whether event happened +#end +# +#@testset "Additional SymbolicContinuousCallback options" begin +# # baseline affect (pos + neg + left root find) +# @variables c1(t)=1.0 c2(t)=1.0 # c1 = cos(t), c2 = cos(3t) +# eqs = [D(c1) ~ -sin(t); D(c2) ~ -3 * sin(3 * t)] +# record_crossings(i, u, _, c) = push!(c, i.t => i.u[u.v]) +# cr1 = [] +# cr2 = [] +# evt1 = ModelingToolkit.SymbolicContinuousCallback( +# [c1 ~ 0], (record_crossings, [c1 => :v], [], [], cr1)) +# evt2 = ModelingToolkit.SymbolicContinuousCallback( +# [c2 ~ 0], (record_crossings, [c2 => :v], [], [], cr2)) +# @named trigsys = ODESystem(eqs, t; continuous_events = [evt1, evt2]) +# trigsys_ss = structural_simplify(trigsys) +# prob = ODEProblem(trigsys_ss, [], (0.0, 2π)) +# sol = solve(prob, Tsit5()) +# required_crossings_c1 = [π / 2, 3 * π / 2] +# required_crossings_c2 = [π / 6, π / 2, 5 * π / 6, 7 * π / 6, 3 * π / 2, 11 * π / 6] +# @test maximum(abs.(first.(cr1) .- required_crossings_c1)) < 1e-4 +# @test maximum(abs.(first.(cr2) .- required_crossings_c2)) < 1e-4 +# @test sign.(cos.(required_crossings_c1 .- 1e-6)) == sign.(last.(cr1)) +# @test sign.(cos.(3 * (required_crossings_c2 .- 1e-6))) == sign.(last.(cr2)) +# +# # with neg affect (pos * neg + left root find) +# cr1p = [] +# cr2p = [] +# cr1n = [] +# cr2n = [] +# evt1 = ModelingToolkit.SymbolicContinuousCallback( +# [c1 ~ 0], (record_crossings, [c1 => :v], [], [], cr1p); +# affect_neg = (record_crossings, [c1 => :v], [], [], cr1n)) +# evt2 = ModelingToolkit.SymbolicContinuousCallback( +# [c2 ~ 0], (record_crossings, [c2 => :v], [], [], cr2p); +# affect_neg = (record_crossings, [c2 => :v], [], [], cr2n)) +# @named trigsys = ODESystem(eqs, t; continuous_events = [evt1, evt2]) +# trigsys_ss = structural_simplify(trigsys) +# prob = ODEProblem(trigsys_ss, [], (0.0, 2π)) +# sol = solve(prob, Tsit5(); dtmax = 0.01) +# c1_pc = filter((<=)(0) ∘ sin, required_crossings_c1) +# c1_nc = filter((>=)(0) ∘ sin, required_crossings_c1) +# c2_pc = filter(c -> -sin(3c) > 0, required_crossings_c2) +# c2_nc = filter(c -> -sin(3c) < 0, required_crossings_c2) +# @test maximum(abs.(c1_pc .- first.(cr1p))) < 1e-5 +# @test maximum(abs.(c1_nc .- first.(cr1n))) < 1e-5 +# @test maximum(abs.(c2_pc .- first.(cr2p))) < 1e-5 +# @test maximum(abs.(c2_nc .- first.(cr2n))) < 1e-5 +# @test sign.(cos.(c1_pc .- 1e-6)) == sign.(last.(cr1p)) +# @test sign.(cos.(c1_nc .- 1e-6)) == sign.(last.(cr1n)) +# @test sign.(cos.(3 * (c2_pc .- 1e-6))) == sign.(last.(cr2p)) +# @test sign.(cos.(3 * (c2_nc .- 1e-6))) == sign.(last.(cr2n)) +# +# # with nothing neg affect (pos * neg + left root find) +# cr1p = [] +# cr2p = [] +# evt1 = ModelingToolkit.SymbolicContinuousCallback( +# [c1 ~ 0], (record_crossings, [c1 => :v], [], [], cr1p); affect_neg = nothing) +# evt2 = ModelingToolkit.SymbolicContinuousCallback( +# [c2 ~ 0], (record_crossings, [c2 => :v], [], [], cr2p); affect_neg = nothing) +# @named trigsys = ODESystem(eqs, t; continuous_events = [evt1, evt2]) +# trigsys_ss = structural_simplify(trigsys) +# prob = ODEProblem(trigsys_ss, [], (0.0, 2π)) +# sol = solve(prob, Tsit5(); dtmax = 0.01) +# @test maximum(abs.(c1_pc .- first.(cr1p))) < 1e-5 +# @test maximum(abs.(c2_pc .- first.(cr2p))) < 1e-5 +# @test sign.(cos.(c1_pc .- 1e-6)) == sign.(last.(cr1p)) +# @test sign.(cos.(3 * (c2_pc .- 1e-6))) == sign.(last.(cr2p)) +# +# #mixed +# cr1p = [] +# cr2p = [] +# cr1n = [] +# cr2n = [] +# evt1 = ModelingToolkit.SymbolicContinuousCallback( +# [c1 ~ 0], (record_crossings, [c1 => :v], [], [], cr1p); affect_neg = nothing) +# evt2 = ModelingToolkit.SymbolicContinuousCallback( +# [c2 ~ 0], (record_crossings, [c2 => :v], [], [], cr2p); +# affect_neg = (record_crossings, [c2 => :v], [], [], cr2n)) +# @named trigsys = ODESystem(eqs, t; continuous_events = [evt1, evt2]) +# trigsys_ss = structural_simplify(trigsys) +# prob = ODEProblem(trigsys_ss, [], (0.0, 2π)) +# sol = solve(prob, Tsit5(); dtmax = 0.01) +# c1_pc = filter((<=)(0) ∘ sin, required_crossings_c1) +# c2_pc = filter(c -> -sin(3c) > 0, required_crossings_c2) +# c2_nc = filter(c -> -sin(3c) < 0, required_crossings_c2) +# @test maximum(abs.(c1_pc .- first.(cr1p))) < 1e-5 +# @test maximum(abs.(c2_pc .- first.(cr2p))) < 1e-5 +# @test maximum(abs.(c2_nc .- first.(cr2n))) < 1e-5 +# @test sign.(cos.(c1_pc .- 1e-6)) == sign.(last.(cr1p)) +# @test sign.(cos.(3 * (c2_pc .- 1e-6))) == sign.(last.(cr2p)) +# @test sign.(cos.(3 * (c2_nc .- 1e-6))) == sign.(last.(cr2n)) +# +# # baseline affect w/ right rootfind (pos + neg + right root find) +# @variables c1(t)=1.0 c2(t)=1.0 # c1 = cos(t), c2 = cos(3t) +# cr1 = [] +# cr2 = [] +# evt1 = ModelingToolkit.SymbolicContinuousCallback( +# [c1 ~ 0], (record_crossings, [c1 => :v], [], [], cr1); +# rootfind = SciMLBase.RightRootFind) +# evt2 = ModelingToolkit.SymbolicContinuousCallback( +# [c2 ~ 0], (record_crossings, [c2 => :v], [], [], cr2); +# rootfind = SciMLBase.RightRootFind) +# @named trigsys = ODESystem(eqs, t; continuous_events = [evt1, evt2]) +# trigsys_ss = structural_simplify(trigsys) +# prob = ODEProblem(trigsys_ss, [], (0.0, 2π)) +# sol = solve(prob, Tsit5(); dtmax = 0.01) +# required_crossings_c1 = [π / 2, 3 * π / 2] +# required_crossings_c2 = [π / 6, π / 2, 5 * π / 6, 7 * π / 6, 3 * π / 2, 11 * π / 6] +# @test maximum(abs.(first.(cr1) .- required_crossings_c1)) < 1e-4 +# @test maximum(abs.(first.(cr2) .- required_crossings_c2)) < 1e-4 +# @test sign.(cos.(required_crossings_c1 .+ 1e-6)) == sign.(last.(cr1)) +# @test sign.(cos.(3 * (required_crossings_c2 .+ 1e-6))) == sign.(last.(cr2)) +# +# # baseline affect w/ mixed rootfind (pos + neg + right root find) +# cr1 = [] +# cr2 = [] +# evt1 = ModelingToolkit.SymbolicContinuousCallback( +# [c1 ~ 0], (record_crossings, [c1 => :v], [], [], cr1); +# rootfind = SciMLBase.LeftRootFind) +# evt2 = ModelingToolkit.SymbolicContinuousCallback( +# [c2 ~ 0], (record_crossings, [c2 => :v], [], [], cr2); +# rootfind = SciMLBase.RightRootFind) +# @named trigsys = ODESystem(eqs, t; continuous_events = [evt1, evt2]) +# trigsys_ss = structural_simplify(trigsys) +# prob = ODEProblem(trigsys_ss, [], (0.0, 2π)) +# sol = solve(prob, Tsit5()) +# @test maximum(abs.(first.(cr1) .- required_crossings_c1)) < 1e-4 +# @test maximum(abs.(first.(cr2) .- required_crossings_c2)) < 1e-4 +# @test sign.(cos.(required_crossings_c1 .- 1e-6)) == sign.(last.(cr1)) +# @test sign.(cos.(3 * (required_crossings_c2 .+ 1e-6))) == sign.(last.(cr2)) +# +# #flip order and ensure results are okay +# cr1 = [] +# cr2 = [] +# evt1 = ModelingToolkit.SymbolicContinuousCallback( +# [c1 ~ 0], (record_crossings, [c1 => :v], [], [], cr1); +# rootfind = SciMLBase.LeftRootFind) +# evt2 = ModelingToolkit.SymbolicContinuousCallback( +# [c2 ~ 0], (record_crossings, [c2 => :v], [], [], cr2); +# rootfind = SciMLBase.RightRootFind) +# @named trigsys = ODESystem(eqs, t; continuous_events = [evt2, evt1]) +# trigsys_ss = structural_simplify(trigsys) +# prob = ODEProblem(trigsys_ss, [], (0.0, 2π)) +# sol = solve(prob, Tsit5()) +# @test maximum(abs.(first.(cr1) .- required_crossings_c1)) < 1e-4 +# @test maximum(abs.(first.(cr2) .- required_crossings_c2)) < 1e-4 +# @test sign.(cos.(required_crossings_c1 .- 1e-6)) == sign.(last.(cr1)) +# @test sign.(cos.(3 * (required_crossings_c2 .+ 1e-6))) == sign.(last.(cr2)) +#end +# +#@testset "Discrete event reinitialization (#3142)" begin +# @connector LiquidPort begin +# p(t)::Float64, [description = "Set pressure in bar", +# guess = 1.01325] +# Vdot(t)::Float64, +# [description = "Volume flow rate in L/min", +# guess = 0.0, +# connect = Flow] +# end +# +# @mtkmodel PressureSource begin +# @components begin +# port = LiquidPort() +# end +# @parameters begin +# p_set::Float64 = 1.01325, [description = "Set pressure in bar"] +# end +# @equations begin +# port.p ~ p_set +# end +# end +# +# @mtkmodel BinaryValve begin +# @constants begin +# p_ref::Float64 = 1.0, [description = "Reference pressure drop in bar"] +# ρ_ref::Float64 = 1000.0, [description = "Reference density in kg/m^3"] +# end +# @components begin +# port_in = LiquidPort() +# port_out = LiquidPort() +# end +# @parameters begin +# k_V::Float64 = 1.0, [description = "Valve coefficient in L/min/bar"] +# k_leakage::Float64 = 1e-08, [description = "Leakage coefficient in L/min/bar"] +# ρ::Float64 = 1000.0, [description = "Density in kg/m^3"] +# end +# @variables begin +# S(t)::Float64, [description = "Valve state", guess = 1.0, irreducible = true] +# Δp(t)::Float64, [description = "Pressure difference in bar", guess = 1.0] +# Vdot(t)::Float64, [description = "Volume flow rate in L/min", guess = 1.0] +# end +# @equations begin +# # Port handling +# port_in.Vdot ~ -Vdot +# port_out.Vdot ~ Vdot +# Δp ~ port_in.p - port_out.p +# # System behavior +# D(S) ~ 0.0 +# Vdot ~ S * k_V * sign(Δp) * sqrt(abs(Δp) / p_ref * ρ_ref / ρ) + k_leakage * Δp # softplus alpha function to avoid negative values under the sqrt +# end +# end +# +# # Test System +# @mtkmodel TestSystem begin +# @components begin +# pressure_source_1 = PressureSource(p_set = 2.0) +# binary_valve_1 = BinaryValve(S = 1.0, k_leakage = 0.0) +# binary_valve_2 = BinaryValve(S = 1.0, k_leakage = 0.0) +# pressure_source_2 = PressureSource(p_set = 1.0) +# end +# @equations begin +# connect(pressure_source_1.port, binary_valve_1.port_in) +# connect(binary_valve_1.port_out, binary_valve_2.port_in) +# connect(binary_valve_2.port_out, pressure_source_2.port) +# end +# @discrete_events begin +# [30] => [binary_valve_1.S ~ 0.0, binary_valve_2.Δp ~ 0.0] +# [60] => [ +# binary_valve_1.S ~ 1.0, binary_valve_2.S ~ 0.0, binary_valve_2.Δp ~ 1.0] +# [120] => [binary_valve_1.S ~ 0.0, binary_valve_2.Δp ~ 0.0] +# end +# end +# +# # Test Simulation +# @mtkbuild sys = TestSystem() +# +# # Test Simulation +# prob = ODEProblem(sys, [], (0.0, 150.0)) +# sol = solve(prob) +# @test sol[end] == [0.0, 0.0, 0.0] +#end +# +#@testset "Discrete variable timeseries" begin +# @variables x(t) +# @parameters a(t) b(t) c(t) +# cb1 = [x ~ 1.0] => [a ~ -Pre(a)] +# function save_affect!(integ, u, p, ctx) +# integ.ps[p.b] = 5.0 +# end +# cb2 = [x ~ 0.5] => (save_affect!, [], [b], [b], nothing) +# cb3 = 1.0 => [c ~ t] +# +# @mtkbuild sys = ODESystem(D(x) ~ cos(t), t, [x], [a, b, c]; +# continuous_events = [cb1, cb2], discrete_events = [cb3]) +# prob = ODEProblem(sys, [x => 1.0], (0.0, 2pi), [a => 1.0, b => 2.0, c => 0.0]) +# @test sort(canonicalize(Discrete(), prob.p)[1]) == [0.0, 1.0, 2.0] +# sol = solve(prob, Tsit5()) +# +# @test sol[a] == [1.0, -1.0] +# @test sol[b] == [2.0, 5.0, 5.0] +# @test sol[c] == [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0] +#end +# +#@testset "Heater" begin +# @variables temp(t) +# params = @parameters furnace_on_threshold=0.5 furnace_off_threshold=0.7 furnace_power=1.0 leakage=0.1 furnace_on::Bool=false +# eqs = [ +# D(temp) ~ furnace_on * furnace_power - temp^2 * leakage +# ] +# +# furnace_off = ModelingToolkit.SymbolicContinuousCallback( +# [temp ~ furnace_off_threshold], +# ModelingToolkit.ImperativeAffect(modified = (; furnace_on)) do x, o, i, c +# @set! x.furnace_on = false +# end) +# furnace_enable = ModelingToolkit.SymbolicContinuousCallback( +# [temp ~ furnace_on_threshold], +# ModelingToolkit.ImperativeAffect(modified = (; furnace_on)) do x, o, i, c +# @set! x.furnace_on = true +# end) +# @named sys = ODESystem( +# eqs, t, [temp], params; continuous_events = [furnace_off, furnace_enable]) +# ss = structural_simplify(sys) +# prob = ODEProblem(ss, [temp => 0.0, furnace_on => true], (0.0, 100.0)) +# sol = solve(prob, Tsit5(); dtmax = 0.01) +# @test all(sol[temp][sol.t .> 1.0] .<= 0.79) && all(sol[temp][sol.t .> 1.0] .>= 0.49) +# +# furnace_off = ModelingToolkit.SymbolicContinuousCallback( +# [temp ~ furnace_off_threshold], +# ModelingToolkit.ImperativeAffect(modified = (; furnace_on)) do x, o, c, i +# @set! x.furnace_on = false +# end; initialize = ModelingToolkit.ImperativeAffect(modified = (; +# temp)) do x, o, c, i +# @set! x.temp = 0.2 +# end) +# furnace_enable = ModelingToolkit.SymbolicContinuousCallback( +# [temp ~ furnace_on_threshold], +# ModelingToolkit.ImperativeAffect(modified = (; furnace_on)) do x, o, c, i +# @set! x.furnace_on = true +# end) +# @named sys = ODESystem( +# eqs, t, [temp], params; continuous_events = [furnace_off, furnace_enable]) +# ss = structural_simplify(sys) +# prob = ODEProblem(ss, [temp => 0.0, furnace_on => true], (0.0, 100.0)) +# sol = solve(prob, Tsit5(); dtmax = 0.01) +# @test all(sol[temp][sol.t .> 1.0] .<= 0.79) && all(sol[temp][sol.t .> 1.0] .>= 0.49) +# @test all(sol[temp][sol.t .!= 0.0] .<= 0.79) && all(sol[temp][sol.t .!= 0.0] .>= 0.2) +#end +# +#@testset "ImperativeAffect errors and warnings" begin +# @variables temp(t) +# params = @parameters furnace_on_threshold=0.5 furnace_off_threshold=0.7 furnace_power=1.0 leakage=0.1 furnace_on::Bool=false +# eqs = [ +# D(temp) ~ furnace_on * furnace_power - temp^2 * leakage +# ] +# +# furnace_off = ModelingToolkit.SymbolicContinuousCallback( +# [temp ~ furnace_off_threshold], +# ModelingToolkit.ImperativeAffect( +# modified = (; furnace_on), observed = (; furnace_on)) do x, o, c, i +# @set! x.furnace_on = false +# end) +# @named sys = ODESystem(eqs, t, [temp], params; continuous_events = [furnace_off]) +# ss = structural_simplify(sys) +# @test_logs (:warn, +# "The symbols Any[:furnace_on] are declared as both observed and modified; this is a code smell because it becomes easy to confuse them and assign/not assign a value.") prob=ODEProblem( +# ss, [temp => 0.0, furnace_on => true], (0.0, 100.0)) +# +# @variables tempsq(t) # trivially eliminated +# eqs = [tempsq ~ temp^2 +# D(temp) ~ furnace_on * furnace_power - temp^2 * leakage] +# +# furnace_off = ModelingToolkit.SymbolicContinuousCallback( +# [temp ~ furnace_off_threshold], +# ModelingToolkit.ImperativeAffect( +# modified = (; furnace_on, tempsq), observed = (; furnace_on)) do x, o, c, i +# @set! x.furnace_on = false +# end) +# @named sys = ODESystem( +# eqs, t, [temp, tempsq], params; continuous_events = [furnace_off]) +# ss = structural_simplify(sys) +# @test_throws "refers to missing variable(s)" prob=ODEProblem( +# ss, [temp => 0.0, furnace_on => true], (0.0, 100.0)) +# +# @parameters not_actually_here +# furnace_off = ModelingToolkit.SymbolicContinuousCallback( +# [temp ~ furnace_off_threshold], +# ModelingToolkit.ImperativeAffect(modified = (; furnace_on), +# observed = (; furnace_on, not_actually_here)) do x, o, c, i +# @set! x.furnace_on = false +# end) +# @named sys = ODESystem( +# eqs, t, [temp, tempsq], params; continuous_events = [furnace_off]) +# ss = structural_simplify(sys) +# @test_throws "refers to missing variable(s)" prob=ODEProblem( +# ss, [temp => 0.0, furnace_on => true], (0.0, 100.0)) +# +# furnace_off = ModelingToolkit.SymbolicContinuousCallback( +# [temp ~ furnace_off_threshold], +# ModelingToolkit.ImperativeAffect(modified = (; furnace_on), +# observed = (; furnace_on)) do x, o, c, i +# return (; fictional2 = false) +# end) +# @named sys = ODESystem( +# eqs, t, [temp, tempsq], params; continuous_events = [furnace_off]) +# ss = structural_simplify(sys) +# prob = ODEProblem( +# ss, [temp => 0.0, furnace_on => true], (0.0, 100.0)) +# @test_throws "Tried to write back to" solve(prob, Tsit5()) +#end +# +#@testset "Quadrature" begin +# @variables theta(t) omega(t) +# params = @parameters qA=0 qB=0 hA=0 hB=0 cnt::Int=0 +# eqs = [D(theta) ~ omega +# omega ~ 1.0] +# function decoder(oldA, oldB, newA, newB) +# state = (oldA, oldB, newA, newB) +# if state == (0, 0, 1, 0) || state == (1, 0, 1, 1) || state == (1, 1, 0, 1) || +# state == (0, 1, 0, 0) +# return 1 +# elseif state == (0, 0, 0, 1) || state == (0, 1, 1, 1) || state == (1, 1, 1, 0) || +# state == (1, 0, 0, 0) +# return -1 +# elseif state == (0, 0, 0, 0) || state == (0, 1, 0, 1) || state == (1, 0, 1, 0) || +# state == (1, 1, 1, 1) +# return 0 +# else +# return 0 # err is interpreted as no movement +# end +# end +# qAevt = ModelingToolkit.SymbolicContinuousCallback([cos(100 * theta) ~ 0], +# ModelingToolkit.ImperativeAffect((; qA, hA, hB, cnt), (; qB)) do x, o, c, i +# @set! x.hA = x.qA +# @set! x.hB = o.qB +# @set! x.qA = 1 +# @set! x.cnt += decoder(x.hA, x.hB, x.qA, o.qB) +# x +# end, +# affect_neg = ModelingToolkit.ImperativeAffect( +# (; qA, hA, hB, cnt), (; qB)) do x, o, c, i +# @set! x.hA = x.qA +# @set! x.hB = o.qB +# @set! x.qA = 0 +# @set! x.cnt += decoder(x.hA, x.hB, x.qA, o.qB) +# x +# end; rootfind = SciMLBase.RightRootFind) +# qBevt = ModelingToolkit.SymbolicContinuousCallback([cos(100 * theta - π / 2) ~ 0], +# ModelingToolkit.ImperativeAffect((; qB, hA, hB, cnt), (; qA)) do x, o, c, i +# @set! x.hA = o.qA +# @set! x.hB = x.qB +# @set! x.qB = 1 +# @set! x.cnt += decoder(x.hA, x.hB, o.qA, x.qB) +# x +# end, +# affect_neg = ModelingToolkit.ImperativeAffect( +# (; qB, hA, hB, cnt), (; qA)) do x, o, c, i +# @set! x.hA = o.qA +# @set! x.hB = x.qB +# @set! x.qB = 0 +# @set! x.cnt += decoder(x.hA, x.hB, o.qA, x.qB) +# x +# end; rootfind = SciMLBase.RightRootFind) +# @named sys = ODESystem( +# eqs, t, [theta, omega], params; continuous_events = [qAevt, qBevt]) +# ss = structural_simplify(sys) +# prob = ODEProblem(ss, [theta => 1e-5], (0.0, pi)) +# sol = solve(prob, Tsit5(); dtmax = 0.01) +# @test getp(sol, cnt)(sol) == 198 # we get 2 pulses per phase cycle (cos 0 crossing) and we go to 100 cycles; we miss a few due to the initial state +#end +# +#@testset "Initialization" begin +# @variables x(t) +# seen = false +# f = ModelingToolkit.FunctionalAffect( +# f = (i, u, p, c) -> seen = true, sts = [], pars = [], discretes = []) +# cb1 = ModelingToolkit.SymbolicContinuousCallback( +# [x ~ 0], nothing, initialize = [x ~ 1.5], finalize = f) +# @mtkbuild sys = ODESystem(D(x) ~ -1, t, [x], []; continuous_events = [cb1]) +# prob = ODEProblem(sys, [x => 1.0], (0.0, 2), []) +# sol = solve(prob, Tsit5(); dtmax = 0.01) +# @test sol[x][1] ≈ 1.0 +# @test sol[x][2] ≈ 1.5 # the initialize affect has been applied +# @test seen == true +# +# @variables x(t) +# seen = false +# f = ModelingToolkit.FunctionalAffect( +# f = (i, u, p, c) -> seen = true, sts = [], pars = [], discretes = []) +# cb1 = ModelingToolkit.SymbolicContinuousCallback( +# [x ~ 0], nothing, initialize = [x ~ 1.5], finalize = f) +# inited = false +# finaled = false +# a = ModelingToolkit.FunctionalAffect( +# f = (i, u, p, c) -> inited = true, sts = [], pars = [], discretes = []) +# b = ModelingToolkit.FunctionalAffect( +# f = (i, u, p, c) -> finaled = true, sts = [], pars = [], discretes = []) +# cb2 = ModelingToolkit.SymbolicContinuousCallback( +# [x ~ 0.1], nothing, initialize = a, finalize = b) +# @mtkbuild sys = ODESystem(D(x) ~ -1, t, [x], []; continuous_events = [cb1, cb2]) +# prob = ODEProblem(sys, [x => 1.0], (0.0, 2), []) +# sol = solve(prob, Tsit5()) +# @test sol[x][1] ≈ 1.0 +# @test sol[x][2] ≈ 1.5 # the initialize affect has been applied +# @test seen == true +# @test inited == true +# @test finaled == true +# +# #periodic +# inited = false +# finaled = false +# cb3 = ModelingToolkit.SymbolicDiscreteCallback( +# 1.0, [x ~ 2], initialize = a, finalize = b) +# @mtkbuild sys = ODESystem(D(x) ~ -1, t, [x], []; discrete_events = [cb3]) +# prob = ODEProblem(sys, [x => 1.0], (0.0, 2), []) +# sol = solve(prob, Tsit5()) +# @test inited == true +# @test finaled == true +# @test isapprox(sol[x][3], 0.0, atol = 1e-9) +# @test sol[x][4] ≈ 2.0 +# @test sol[x][5] ≈ 1.0 +# +# seen = false +# inited = false +# finaled = false +# cb3 = ModelingToolkit.SymbolicDiscreteCallback(1.0, f, initialize = a, finalize = b) +# @mtkbuild sys = ODESystem(D(x) ~ -1, t, [x], []; discrete_events = [cb3]) +# prob = ODEProblem(sys, [x => 1.0], (0.0, 2), []) +# sol = solve(prob, Tsit5()) +# @test seen == true +# @test inited == true +# +# #preset +# seen = false +# inited = false +# finaled = false +# cb3 = ModelingToolkit.SymbolicDiscreteCallback([1.0], f, initialize = a, finalize = b) +# @mtkbuild sys = ODESystem(D(x) ~ -1, t, [x], []; discrete_events = [cb3]) +# prob = ODEProblem(sys, [x => 1.0], (0.0, 2), []) +# sol = solve(prob, Tsit5()) +# @test seen == true +# @test inited == true +# @test finaled == true +# +# #equational +# seen = false +# inited = false +# finaled = false +# cb3 = ModelingToolkit.SymbolicDiscreteCallback( +# t == 1.0, f, initialize = a, finalize = b) +# @mtkbuild sys = ODESystem(D(x) ~ -1, t, [x], []; discrete_events = [cb3]) +# prob = ODEProblem(sys, [x => 1.0], (0.0, 2), []) +# sol = solve(prob, Tsit5(); tstops = 1.0) +# @test seen == true +# @test inited == true +# @test finaled == true +#end @testset "Bump" begin @variables x(t) [irreducible = true] y(t) [irreducible = true] @@ -1325,9 +1325,62 @@ end @test 100.0 ∈ sol2[sys2.wd2.θ] end +@testset "Implicit affects with Pre" begin + @parameters g + @variables x(t) y(t) λ(t) + eqs = [D(D(x)) ~ λ * x + D(D(y)) ~ λ * y - g + x^2 + y^2 ~ 1] + c_evt = [t ~ 0.5] => [x ~ Pre(x) + 0.1] + @mtkbuild pend = ODESystem(eqs, t, continuous_events = c_evt) + prob = ODEProblem(pend, [x => 1, y => 0], (0., 1.), [g => 1], guesses = [λ => 1]) + sol = solve(prob, Rodas5()) + @test sol(0.5000001)[1] - sol(0.4999999)[1] ≈ 0.1 + @test sol(0.5000001)[1]^2 + sol(0.5000001)[2]^2 ≈ 1 + + # Implicit affect with Pre + c_evt = [t ~ 0.5] => [x ~ Pre(x) + y^2] + @mtkbuild pend = ODESystem(eqs, t, continuous_events = c_evt) + prob = ODEProblem(pend, [x => 1, y => 0], (0., 1.), [g => 1], guesses = [λ => 1]) + sol = solve(prob, Rodas5()) + @test sol(0.5000001)[2]^2 - sol(0.4999999)[1] ≈ sol(0.5000001)[1] + @test sol(0.5000001)[1]^2 + sol(0.5000001)[2]^2 ≈ 1 + + # Impossible affect errors + c_evt = [t ~ 0.5] => [x ~ Pre(x) + 1] + @mtkbuild pend = ODESystem(eqs, t, continuous_events = c_evt) + prob = ODEProblem(pend, [x => 1, y => 0], (0., 1.), [g => 1], guesses = [λ => 1]) + @test_throws Exception sol = solve(prob, Rodas5()) + + # Changing both variables and parameters in the same affect. + c_evt = [t ~ 0.5] => [x ~ Pre(x) + 1, g ~ Pre(g) + 1] + @mtkbuild pend = ODESystem(eqs, t, continuous_events = c_evt) + prob = ODEProblem(pend, [x => 1, y => 0], (0., 1.), [g => 1], guesses = [λ => 1]) + sol = solve(prob, Rodas5()) + @test sol.ps[g] ≈ 2 + @test sol(0.5000001, idxs = x) - sol(0.4999999, idxs = x) ≈ 1 + + # Proper re-initialization after parameter change + eqs = [x ~ g^2 - y, D(x) ~ x] + c_evt = [t ~ 0.5] => [x ~ Pre(x) + 1, g ~ Pre(g) + 1] + @mtkbuild sys = ODESystem(eqs, t, continuous_events = c_evt) + prob = ODEProblem(sys, [x => 0.5], (0., 1.), [g => 2], guesses = [y => 0]) + sol = solve(prob, Rodas5()) + @test sol.ps[g] ≈ 3 + @test ≈(sol(0.5000001)[1] - sol(0.4999999)[1], 1; atol = 1e-6) + @test sol(0.5000001, idxs = y) ≈ 9 - sol(0.5000001, idxs = x) + + # Parameters that don't appear in affects should not be mutated. + c_evt = [t ~ 0.5] => [x ~ Pre(x) + 1] + @mtkbuild sys = ODESystem(eqs, t, continuous_events = c_evt) + prob = ODEProblem(sys, [x => 0.5], (0., 1.), [g => 2], guesses = [y => 0]) + sol = solve(prob, Rodas5()) + @test prob.ps[g] == sol.ps[g] +end + + # TODO: test: # - Functional affects reinitialize correctly # - explicit equation of t in a functional affect -# - modifying both u and p in an affect # - affects that have Pre but are also algebraic in nature # - reinitialization after affects From 6b955e5c6fc4f84272a582b3070412e6f3f67e93 Mon Sep 17 00:00:00 2001 From: vyudu Date: Wed, 26 Mar 2025 14:03:22 -0400 Subject: [PATCH 27/52] up --- src/systems/callbacks.jl | 76 +- .../implicit_discrete_system.jl | 1 - src/systems/index_cache.jl | 1 + test/symbolic_events.jl | 802 ++++++++---------- 4 files changed, 410 insertions(+), 470 deletions(-) diff --git a/src/systems/callbacks.jl b/src/systems/callbacks.jl index 5d34644297..5a97472c6d 100644 --- a/src/systems/callbacks.jl +++ b/src/systems/callbacks.jl @@ -62,7 +62,6 @@ struct AffectSystem discretes::Vector """Maps the symbols of unknowns/observed in the ImplicitDiscreteSystem to its corresponding unknown/parameter in the parent system.""" aff_to_sys::Dict - explicit::Bool end system(a::AffectSystem) = a.system @@ -71,11 +70,11 @@ unknowns(a::AffectSystem) = a.unknowns parameters(a::AffectSystem) = a.parameters aff_to_sys(a::AffectSystem) = a.aff_to_sys previous_vals(a::AffectSystem) = parameters(system(a)) -is_explicit(a::AffectSystem) = a.explicit +all_equations(a::AffectSystem) = vcat(equations(system(a)), observed(system(a))) function Base.show(iio::IO, aff::AffectSystem) println(iio, "Affect system defined by equations:") - eqs = vcat(equations(system(aff)), observed(system(aff))) + eqs = all_equations(aff) show(iio, eqs) end @@ -84,8 +83,7 @@ function Base.:(==)(a1::AffectSystem, a2::AffectSystem) isequal(discretes(a1), discretes(a2)) && isequal(unknowns(a1), unknowns(a2)) && isequal(parameters(a1), parameters(a2)) && - isequal(aff_to_sys(a1), aff_to_sys(a2)) && - isequal(is_explicit(a1), is_explicit(a2)) + isequal(aff_to_sys(a1), aff_to_sys(a2)) end function Base.hash(a::AffectSystem, s::UInt) @@ -93,8 +91,7 @@ function Base.hash(a::AffectSystem, s::UInt) s = hash(unknowns(a), s) s = hash(parameters(a), s) s = hash(discretes(a), s) - s = hash(aff_to_sys(a), s) - hash(is_explicit(a), s) + hash(aff_to_sys(a), s) end function vars!(vars, aff::Union{FunctionalAffect, AffectSystem}; op = Differential) @@ -241,51 +238,44 @@ make_affect(affect::Tuple; kwargs...) = FunctionalAffect(affect...) make_affect(affect::NamedTuple; kwargs...) = FunctionalAffect(; affect...) make_affect(affect::Affect; kwargs...) = affect -function make_affect(affect::Vector{Equation}; discrete_parameters = Any[], iv = nothing, algeeqs::Vector{Equation} = Equation[]) +function make_affect(affect::Vector{Equation}; discrete_parameters::AbstractVector = Any[], iv = nothing, algeeqs::Vector{Equation} = Equation[]) isempty(affect) && return nothing isempty(algeeqs) && @warn "No algebraic equations were found for the callback defined by $(join(affect, ", ")). If the system has no algebraic equations, this can be disregarded. Otherwise pass in `algeeqs` to the SymbolicContinuousCallback constructor." + isnothing(iv) && error("Must specify iv.") - for p in discretes - # Check if p is time-dependent - false && error("Non-time dependent parameter $p passed in as a discrete. Must be declared as $p(t).") + for p in discrete_parameters + occursin(unwrap(iv), unwrap(p)) || error("Non-time dependent parameter $p passed in as a discrete. Must be declared as @parameters $p(t).") end - explicit = true dvs = OrderedSet() params = OrderedSet() for eq in affect - if !haspre(eq) && !(symbolic_type(eq.rhs) === NotSymbolic()) + if !haspre(eq) && !(symbolic_type(eq.rhs) === NotSymbolic() || symbolic_type(eq.lhs) === NotSymbolic()) @warn "Affect equation $eq has no `Pre` operator. As such it will be interpreted as an algebraic equation to be satisfied after the callback. If you intended to use the value of a variable x before the affect, use Pre(x)." - explicit = false end collect_vars!(dvs, params, eq, iv; op = Pre) end for eq in algeeqs collect_vars!(dvs, params, eq, iv) - explicit = false - end - any(isirreducible, dvs) && (explicit = false) - - if isnothing(iv) - iv = isempty(dvs) ? iv : only(arguments(dvs[1])) - isnothing(iv) && @warn "No independent variable specified and could not be inferred. If the iv appears in an affect equation explicitly, like x ~ t + 1, then it must be specified as an argument to the SymbolicContinuousCallback or SymbolicDiscreteCallback constructor. Otherwise this warning can be disregarded." end pre_params = filter(haspre ∘ value, params) - sys_params = setdiff(params, union(discrete_parameters, pre_params)) + sys_params = collect(setdiff(params, union(discrete_parameters, pre_params))) discretes = map(tovar, discrete_parameters) aff_map = Dict(zip(discretes, discrete_parameters)) - @named affectsys = ImplicitDiscreteSystem(vcat(affect, algeeqs), iv, collect(union(dvs, discretes)), collect(union(pre_params, sys_params))) - affectsys = complete(affectsys) + rev_map = Dict(zip(discrete_parameters, discretes)) + affect = Symbolics.fast_substitute(affect, rev_map) + algeeqs = Symbolics.fast_substitute(algeeqs, rev_map) + @mtkbuild affectsys = ImplicitDiscreteSystem(vcat(affect, algeeqs), iv, collect(union(dvs, discretes)), collect(union(pre_params, sys_params))) # get accessed parameters p from Pre(p) in the callback parameters - accessed_params = filter(isparameter, map(x -> unPre(x), cb_params)) + accessed_params = filter(isparameter, map(unPre, collect(pre_params))) union!(accessed_params, sys_params) # add unknowns to the map for u in dvs aff_map[u] = u end - AffectSystem(affectsys, collect(dvs), collect(accessed_params), collect(discrete_parameters), aff_map, explicit) + AffectSystem(affectsys, collect(dvs), collect(accessed_params), collect(discrete_parameters), aff_map) end function make_affect(affect; kwargs...) @@ -295,7 +285,7 @@ end """ Generate continuous callbacks. """ -function SymbolicContinuousCallbacks(events; algeeqs::Vector{Equation} = Equation[], iv = nothing) +function SymbolicContinuousCallbacks(events; discrete_parameters = Any[], algeeqs::Vector{Equation} = Equation[], iv = nothing) callbacks = SymbolicContinuousCallback[] isnothing(events) && return callbacks @@ -304,7 +294,7 @@ function SymbolicContinuousCallbacks(events; algeeqs::Vector{Equation} = Equatio for event in events cond, affs = event isa Pair ? (event[1], event[2]) : (event, nothing) - push!(callbacks, SymbolicContinuousCallback(cond, affs; iv, algeeqs)) + push!(callbacks, SymbolicContinuousCallback(cond, affs; iv, algeeqs, discrete_parameters)) end callbacks end @@ -412,11 +402,11 @@ struct SymbolicDiscreteCallback <: AbstractCallback function SymbolicDiscreteCallback( condition, affect = nothing; - initialize = nothing, finalize = nothing, iv = nothing, algeeqs = Equation[]) + initialize = nothing, finalize = nothing, iv = nothing, algeeqs = Equation[], discrete_parameters = Any[]) c = is_timed_condition(condition) ? condition : value(scalarize(condition)) - new(c, make_affect(affect; iv, algeeqs), make_affect(initialize; iv, algeeqs), - make_affect(finalize; iv, algeeqs)) + new(c, make_affect(affect; iv, algeeqs, discrete_parameters), make_affect(initialize; iv, algeeqs, discrete_parameters), + make_affect(finalize; iv, algeeqs, discrete_parameters)) end # Default affect to nothing end @@ -426,7 +416,7 @@ SymbolicDiscreteCallback(cb::SymbolicDiscreteCallback, args...; kwargs...) = cb """ Generate discrete callbacks. """ -function SymbolicDiscreteCallbacks(events; algeeqs::Vector{Equation} = Equation[], iv = nothing) +function SymbolicDiscreteCallbacks(events; discrete_parameters::Vector = Any[], algeeqs::Vector{Equation} = Equation[], iv = nothing) callbacks = SymbolicDiscreteCallback[] isnothing(events) && return callbacks @@ -435,7 +425,7 @@ function SymbolicDiscreteCallbacks(events; algeeqs::Vector{Equation} = Equation[ for event in events cond, affs = event isa Pair ? (event[1], event[2]) : (event, nothing) - push!(callbacks, SymbolicDiscreteCallback(cond, affs; iv, algeeqs)) + push!(callbacks, SymbolicDiscreteCallback(cond, affs; iv, algeeqs, discrete_parameters)) end callbacks end @@ -471,7 +461,7 @@ function namespace_affects(affect::AffectSystem, s) renamespace.((s,), unknowns(affect)), renamespace.((s,), parameters(affect)), renamespace.((s,), discretes(affect)), - Dict([k => renamespace(s, v) for (k, v) in aff_to_sys(affect)]), is_explicit(affect)) + Dict([k => renamespace(s, v) for (k, v) in aff_to_sys(affect)])) end namespace_affects(af::Nothing, s) = nothing @@ -837,19 +827,17 @@ function compile_equational_affect(aff::Union{AffectSystem, Vector{Equation}}, s aff_map = aff_to_sys(aff) sys_map = Dict([v => k for (k, v) in aff_map]) - if is_explicit(aff) - affsys = structural_simplify(affsys) - @assert isempty(equations(affsys)) + if isempty(equations(affsys)) update_eqs = Symbolics.fast_substitute(observed(affsys), Dict([p => unPre(p) for p in parameters(affsys)])) rhss = map(x -> x.rhs, update_eqs) lhss = map(x -> aff_map[x.lhs], update_eqs) is_p = [lhs ∈ Set(ps_to_update) for lhs in lhss] - + is_u = [lhs ∈ Set(dvs_to_update) for lhs in lhss] dvs = unknowns(sys) ps = parameters(sys) t = get_iv(sys) - u_idxs = indexin((@view lhss[.!is_p]), dvs) + u_idxs = indexin((@view lhss[is_u]), dvs) wrap_mtkparameters = has_index_cache(sys) && (get_index_cache(sys) !== nothing) p_idxs = if wrap_mtkparameters @@ -861,7 +849,7 @@ function compile_equational_affect(aff::Union{AffectSystem, Vector{Equation}}, s _ps = reorder_parameters(sys, ps) integ = gensym(:MTKIntegrator) - u_up, u_up! = build_function_wrapper(sys, (@view rhss[.!is_p]), dvs, _ps..., t; wrap_code = add_integrator_header(sys, integ, :u), expression = Val{false}, outputidxs = u_idxs, wrap_mtkparameters) + u_up, u_up! = build_function_wrapper(sys, (@view rhss[is_u]), dvs, _ps..., t; wrap_code = add_integrator_header(sys, integ, :u), expression = Val{false}, outputidxs = u_idxs, wrap_mtkparameters) p_up, p_up! = build_function_wrapper(sys, (@view rhss[is_p]), dvs, _ps..., t; wrap_code = add_integrator_header(sys, integ, :p), expression = Val{false}, outputidxs = p_idxs, wrap_mtkparameters) return function explicit_affect!(integ) @@ -870,7 +858,7 @@ function compile_equational_affect(aff::Union{AffectSystem, Vector{Equation}}, s reset_jumps && reset_aggregated_jumps!(integ) end else - return let dvs_to_update = dvs_to_update, aff_map = aff_map, sys_map = sys_map, affsys = affsys, ps_to_update = ps_to_update + return let dvs_to_update = dvs_to_update, aff_map = aff_map, sys_map = sys_map, affsys = affsys, ps_to_update = ps_to_update, aff = aff function implicit_affect!(integ) pmap = Pair[] for pre_p in parameters(affsys) @@ -885,7 +873,7 @@ function compile_equational_affect(aff::Union{AffectSystem, Vector{Equation}}, s end affprob = ImplicitDiscreteProblem(affsys, u0, (integ.t, integ.t), pmap; build_initializeprob = false, check_length = false) affsol = init(affprob, IDSolve()) - check_error(affsol) && throw(UnsolvableCallbackError(equations(affsys))) + (check_error(affsol) === ReturnCode.InitialFailure) && throw(UnsolvableCallbackError(all_equations(aff))) for u in dvs_to_update integ[u] = affsol[sys_map[u]] end @@ -901,8 +889,8 @@ struct UnsolvableCallbackError eqs::Vector{Equation} end -function Base.showerror(io, err::UnsolvableCallbackError) - println(io, "The callback defined by the equations, $(join(err.eqs, "\n")), with discrete parameters is not solvable. Please check the algebraic equations, affect equations, and declared discrete parameters.") +function Base.showerror(io::IO, err::UnsolvableCallbackError) + println(io, "The callback defined by the following equations:\n\n$(join(err.eqs, "\n"))\n\nis not solvable. Please check that the algebraic equations and affect equations are correct, and that all parameters intended to be changed are passed in as `discrete_parameters`.") end merge_cb(::Nothing, ::Nothing) = nothing diff --git a/src/systems/discrete_system/implicit_discrete_system.jl b/src/systems/discrete_system/implicit_discrete_system.jl index 0e0327f3ae..9ba8212e74 100644 --- a/src/systems/discrete_system/implicit_discrete_system.jl +++ b/src/systems/discrete_system/implicit_discrete_system.jl @@ -287,7 +287,6 @@ function generate_function( u_next = map(Shift(iv, 1), dvs) u = dvs p = (reorder_parameters(sys, unwrap.(ps))..., cachesyms...) - @show exprs build_function_wrapper( sys, exprs, u_next, u, p..., iv; p_start = 3, extra_assignments, kwargs...) end diff --git a/src/systems/index_cache.jl b/src/systems/index_cache.jl index 5141f71e76..e3812c79a6 100644 --- a/src/systems/index_cache.jl +++ b/src/systems/index_cache.jl @@ -127,6 +127,7 @@ function IndexCache(sys::AbstractSystem) end for sym in discs + @show sym is_parameter(sys, sym) || error("Expected discrete variable $sym in callback to be a parameter") diff --git a/test/symbolic_events.jl b/test/symbolic_events.jl index f3f0d51199..bc58f78900 100644 --- a/test/symbolic_events.jl +++ b/test/symbolic_events.jl @@ -2,6 +2,8 @@ using ModelingToolkit, OrdinaryDiffEq, StochasticDiffEq, JumpProcesses, Test using SciMLStructures: canonicalize, Discrete using ModelingToolkit: SymbolicContinuousCallback, SymbolicContinuousCallbacks, + SymbolicDiscreteCallback, + SymbolicDiscreteCallbacks, get_callback, t_nounits as t, D_nounits as D, @@ -475,400 +477,348 @@ affect_neg = [x ~ 1] # sys = structural_simplify(model) # @test isempty(ModelingToolkit.continuous_events(sys)) #end -# -#@testset "ODESystem Discrete Callbacks" begin -# function testsol(osys, u0, p, tspan; tstops = Float64[], paramtotest = nothing, -# kwargs...) -# oprob = ODEProblem(complete(osys), u0, tspan, p; kwargs...) -# sol = solve(oprob, Tsit5(); tstops = tstops, abstol = 1e-10, reltol = 1e-10) -# @test isapprox(sol(1.0000000001)[1] - sol(0.999999999)[1], 1.0; rtol = 1e-6) -# paramtotest === nothing || (@test sol.ps[paramtotest] == 1.0) -# @test isapprox(sol(4.0)[1], 2 * exp(-2.0)) -# sol -# end -# -# @parameters k t1 t2 -# @variables A(t) B(t) -# -# cond1 = (t == t1) -# affect1 = [A ~ Pre(A) + 1] -# cb1 = cond1 => affect1 -# cond2 = (t == t2) -# affect2 = [k ~ 1.0] -# cb2 = cond2 => affect2 -# -# ∂ₜ = D -# eqs = [∂ₜ(A) ~ -k * A] -# @named osys = ODESystem(eqs, t, [A], [k, t1, t2], discrete_events = [cb1, cb2]) -# u0 = [A => 1.0] -# p = [k => 0.0, t1 => 1.0, t2 => 2.0] -# tspan = (0.0, 4.0) -# testsol(osys, u0, p, tspan; tstops = [1.0, 2.0], paramtotest = k) -# -# cond1a = (t == t1) -# affect1a = [A ~ Pre(A) + 1, B ~ A] -# cb1a = cond1a => affect1a -# @named osys1 = ODESystem(eqs, t, [A, B], [k, t1, t2], discrete_events = [cb1a, cb2]) -# u0′ = [A => 1.0, B => 0.0] -# sol = testsol( -# osys1, u0′, p, tspan; tstops = [1.0, 2.0], check_length = false, paramtotest = k) -# @test sol(1.0000001, idxs = B) == 2.0 -# -# # same as above - but with set-time event syntax -# cb1‵ = [1.0] => affect1 # needs to be a Vector for the event to happen only once -# cb2‵ = [2.0] => affect2 -# @named osys‵ = ODESystem(eqs, t, [A], [k], discrete_events = [cb1‵, cb2‵]) -# testsol(osys‵, u0, p, tspan; paramtotest = k) -# -# # mixing discrete affects -# @named osys3 = ODESystem(eqs, t, [A], [k, t1, t2], discrete_events = [cb1, cb2‵]) -# testsol(osys3, u0, p, tspan; tstops = [1.0], paramtotest = k) -# -# # mixing with a func affect -# function affect!(integrator, u, p, ctx) -# integrator.ps[p.k] = 1.0 -# nothing -# end -# cb2‵‵ = [2.0] => (affect!, [], [k], [k], nothing) -# @named osys4 = ODESystem(eqs, t, [A], [k, t1], discrete_events = [cb1, cb2‵‵]) -# oprob4 = ODEProblem(complete(osys4), u0, tspan, p) -# testsol(osys4, u0, p, tspan; tstops = [1.0], paramtotest = k) -# -# # mixing with symbolic condition in the func affect -# cb2‵‵‵ = (t == t2) => (affect!, [], [k], [k], nothing) -# @named osys5 = ODESystem(eqs, t, [A], [k, t1, t2], discrete_events = [cb1, cb2‵‵‵]) -# testsol(osys5, u0, p, tspan; tstops = [1.0, 2.0]) -# @named osys6 = ODESystem(eqs, t, [A], [k, t1, t2], discrete_events = [cb2‵‵‵, cb1]) -# testsol(osys6, u0, p, tspan; tstops = [1.0, 2.0], paramtotest = k) -# -# # mix a continuous event too -# cond3 = A ~ 0.1 -# affect3 = [k ~ 0.0] -# cb3 = cond3 => affect3 -# @named osys7 = ODESystem(eqs, t, [A], [k, t1, t2], discrete_events = [cb1, cb2‵‵‵], -# continuous_events = [cb3]) -# sol = testsol(osys7, u0, p, (0.0, 10.0); tstops = [1.0, 2.0]) -# @test isapprox(sol(10.0)[1], 0.1; atol = 1e-10, rtol = 1e-10) -#end -# -#@testset "SDESystem Discrete Callbacks" begin -# function testsol(ssys, u0, p, tspan; tstops = Float64[], paramtotest = nothing, -# kwargs...) -# sprob = SDEProblem(complete(ssys), u0, tspan, p; kwargs...) -# sol = solve(sprob, RI5(); tstops = tstops, abstol = 1e-10, reltol = 1e-10) -# @test isapprox(sol(1.0000000001)[1] - sol(0.999999999)[1], 1.0; rtol = 1e-4) -# paramtotest === nothing || (@test sol.ps[paramtotest] == 1.0) -# @test isapprox(sol(4.0)[1], 2 * exp(-2.0), atol = 1e-4) -# sol -# end -# -# @parameters k t1 t2 -# @variables A(t) B(t) -# -# cond1 = (t == t1) -# affect1 = [A ~ Pre(A) + 1] -# cb1 = cond1 => affect1 -# cond2 = (t == t2) -# affect2 = [k ~ 1.0] -# cb2 = cond2 => affect2 -# -# ∂ₜ = D -# eqs = [∂ₜ(A) ~ -k * A] -# @named ssys = SDESystem(eqs, [0.0], t, [A], [k, t1, t2], -# discrete_events = [cb1, cb2]) -# u0 = [A => 1.0] -# p = [k => 0.0, t1 => 1.0, t2 => 2.0] -# tspan = (0.0, 4.0) -# testsol(ssys, u0, p, tspan; tstops = [1.0, 2.0], paramtotest = k) -# -# cond1a = (t == t1) -# affect1a = [A ~ Pre(A) + 1, B ~ A] -# cb1a = cond1a => affect1a -# @named ssys1 = SDESystem(eqs, [0.0], t, [A, B], [k, t1, t2], -# discrete_events = [cb1a, cb2]) -# u0′ = [A => 1.0, B => 0.0] -# sol = testsol( -# ssys1, u0′, p, tspan; tstops = [1.0, 2.0], check_length = false, paramtotest = k) -# @test sol(1.0000001, idxs = 2) == 2.0 -# -# # same as above - but with set-time event syntax -# cb1‵ = [1.0] => affect1 # needs to be a Vector for the event to happen only once -# cb2‵ = [2.0] => affect2 -# @named ssys‵ = SDESystem(eqs, [0.0], t, [A], [k], discrete_events = [cb1‵, cb2‵]) -# testsol(ssys‵, u0, p, tspan; paramtotest = k) -# -# # mixing discrete affects -# @named ssys3 = SDESystem(eqs, [0.0], t, [A], [k, t1, t2], -# discrete_events = [cb1, cb2‵]) -# testsol(ssys3, u0, p, tspan; tstops = [1.0], paramtotest = k) -# -# # mixing with a func affect -# function affect!(integrator, u, p, ctx) -# setp(integrator, p.k)(integrator, 1.0) -# nothing -# end -# cb2‵‵ = [2.0] => (affect!, [], [k], [k], nothing) -# @named ssys4 = SDESystem(eqs, [0.0], t, [A], [k, t1], -# discrete_events = [cb1, cb2‵‵]) -# testsol(ssys4, u0, p, tspan; tstops = [1.0], paramtotest = k) -# -# # mixing with symbolic condition in the func affect -# cb2‵‵‵ = (t == t2) => (affect!, [], [k], [k], nothing) -# @named ssys5 = SDESystem(eqs, [0.0], t, [A], [k, t1, t2], -# discrete_events = [cb1, cb2‵‵‵]) -# testsol(ssys5, u0, p, tspan; tstops = [1.0, 2.0], paramtotest = k) -# @named ssys6 = SDESystem(eqs, [0.0], t, [A], [k, t1, t2], -# discrete_events = [cb2‵‵‵, cb1]) -# testsol(ssys6, u0, p, tspan; tstops = [1.0, 2.0], paramtotest = k) -# -# # mix a continuous event too -# cond3 = A ~ 0.1 -# affect3 = [k ~ 0.0] -# cb3 = cond3 => affect3 -# @named ssys7 = SDESystem(eqs, [0.0], t, [A], [k, t1, t2], -# discrete_events = [cb1, cb2‵‵‵], -# continuous_events = [cb3]) -# sol = testsol(ssys7, u0, p, (0.0, 10.0); tstops = [1.0, 2.0]) -# @test isapprox(sol(10.0)[1], 0.1; atol = 1e-10, rtol = 1e-10) -#end -# -#@testset "JumpSystem Discrete Callbacks" begin -# function testsol(jsys, u0, p, tspan; tstops = Float64[], paramtotest = nothing, -# N = 40000, kwargs...) -# jsys = complete(jsys) -# dprob = DiscreteProblem(jsys, u0, tspan, p) -# jprob = JumpProblem(jsys, dprob, Direct(); kwargs...) -# sol = solve(jprob, SSAStepper(); tstops = tstops) -# @test (sol(1.000000000001)[1] - sol(0.99999999999)[1]) == 1 -# paramtotest === nothing || (@test sol.ps[paramtotest] == 1.0) -# @test sol(40.0)[1] == 0 -# sol -# end -# -# @parameters k t1 t2 -# @variables A(t) B(t) -# -# cond1 = (t == t1) -# affect1 = [A ~ Pre(A) + 1] -# cb1 = cond1 => affect1 -# cond2 = (t == t2) -# affect2 = [k ~ 1.0] -# cb2 = cond2 => affect2 -# -# eqs = [MassActionJump(k, [A => 1], [A => -1])] -# @named jsys = JumpSystem(eqs, t, [A], [k, t1, t2], discrete_events = [cb1, cb2]) -# u0 = [A => 1] -# p = [k => 0.0, t1 => 1.0, t2 => 2.0] -# tspan = (0.0, 40.0) -# testsol(jsys, u0, p, tspan; tstops = [1.0, 2.0], rng, paramtotest = k) -# -# cond1a = (t == t1) -# affect1a = [A ~ Pre(A) + 1, B ~ A] -# cb1a = cond1a => affect1a -# @named jsys1 = JumpSystem(eqs, t, [A, B], [k, t1, t2], discrete_events = [cb1a, cb2]) -# u0′ = [A => 1, B => 0] -# sol = testsol(jsys1, u0′, p, tspan; tstops = [1.0, 2.0], -# check_length = false, rng, paramtotest = k) -# @test sol(1.000000001, idxs = B) == 2 -# -# # same as above - but with set-time event syntax -# cb1‵ = [1.0] => affect1 # needs to be a Vector for the event to happen only once -# cb2‵ = [2.0] => affect2 -# @named jsys‵ = JumpSystem(eqs, t, [A], [k], discrete_events = [cb1‵, cb2‵]) -# testsol(jsys‵, u0, [p[1]], tspan; rng, paramtotest = k) -# -# # mixing discrete affects -# @named jsys3 = JumpSystem(eqs, t, [A], [k, t1, t2], discrete_events = [cb1, cb2‵]) -# testsol(jsys3, u0, p, tspan; tstops = [1.0], rng, paramtotest = k) -# -# # mixing with a func affect -# function affect!(integrator, u, p, ctx) -# integrator.ps[p.k] = 1.0 -# reset_aggregated_jumps!(integrator) -# nothing -# end -# cb2‵‵ = [2.0] => (affect!, [], [k], [k], nothing) -# @named jsys4 = JumpSystem(eqs, t, [A], [k, t1], discrete_events = [cb1, cb2‵‵]) -# testsol(jsys4, u0, p, tspan; tstops = [1.0], rng, paramtotest = k) -# -# # mixing with symbolic condition in the func affect -# cb2‵‵‵ = (t == t2) => (affect!, [], [k], [k], nothing) -# @named jsys5 = JumpSystem(eqs, t, [A], [k, t1, t2], discrete_events = [cb1, cb2‵‵‵]) -# testsol(jsys5, u0, p, tspan; tstops = [1.0, 2.0], rng, paramtotest = k) -# @named jsys6 = JumpSystem(eqs, t, [A], [k, t1, t2], discrete_events = [cb2‵‵‵, cb1]) -# testsol(jsys6, u0, p, tspan; tstops = [1.0, 2.0], rng, paramtotest = k) -#end -# -#@testset "Namespacing" begin -# function oscillator_ce(k = 1.0; name) -# sts = @variables x(t)=1.0 v(t)=0.0 F(t) -# ps = @parameters k=k Θ=0.5 -# eqs = [D(x) ~ v, D(v) ~ -k * x + F] -# ev = [x ~ Θ] => [x ~ 1.0, v ~ 0.0] -# ODESystem(eqs, t, sts, ps, continuous_events = [ev]; name) -# end -# -# @named oscce = oscillator_ce() -# eqs = [oscce.F ~ 0] -# @named eqs_sys = ODESystem(eqs, t) -# @named oneosc_ce = compose(eqs_sys, oscce) -# oneosc_ce_simpl = structural_simplify(oneosc_ce) -# -# prob = ODEProblem(oneosc_ce_simpl, [], (0.0, 2.0), []) -# sol = solve(prob, Tsit5(), saveat = 0.1) -# -# @test typeof(oneosc_ce_simpl) == ODESystem -# @test sol[1, 6] < 1.0 # test whether x(t) decreases over time -# @test sol[1, 18] > 0.5 # test whether event happened -#end -# -#@testset "Additional SymbolicContinuousCallback options" begin -# # baseline affect (pos + neg + left root find) -# @variables c1(t)=1.0 c2(t)=1.0 # c1 = cos(t), c2 = cos(3t) -# eqs = [D(c1) ~ -sin(t); D(c2) ~ -3 * sin(3 * t)] -# record_crossings(i, u, _, c) = push!(c, i.t => i.u[u.v]) -# cr1 = [] -# cr2 = [] -# evt1 = ModelingToolkit.SymbolicContinuousCallback( -# [c1 ~ 0], (record_crossings, [c1 => :v], [], [], cr1)) -# evt2 = ModelingToolkit.SymbolicContinuousCallback( -# [c2 ~ 0], (record_crossings, [c2 => :v], [], [], cr2)) -# @named trigsys = ODESystem(eqs, t; continuous_events = [evt1, evt2]) -# trigsys_ss = structural_simplify(trigsys) -# prob = ODEProblem(trigsys_ss, [], (0.0, 2π)) -# sol = solve(prob, Tsit5()) -# required_crossings_c1 = [π / 2, 3 * π / 2] -# required_crossings_c2 = [π / 6, π / 2, 5 * π / 6, 7 * π / 6, 3 * π / 2, 11 * π / 6] -# @test maximum(abs.(first.(cr1) .- required_crossings_c1)) < 1e-4 -# @test maximum(abs.(first.(cr2) .- required_crossings_c2)) < 1e-4 -# @test sign.(cos.(required_crossings_c1 .- 1e-6)) == sign.(last.(cr1)) -# @test sign.(cos.(3 * (required_crossings_c2 .- 1e-6))) == sign.(last.(cr2)) -# -# # with neg affect (pos * neg + left root find) -# cr1p = [] -# cr2p = [] -# cr1n = [] -# cr2n = [] -# evt1 = ModelingToolkit.SymbolicContinuousCallback( -# [c1 ~ 0], (record_crossings, [c1 => :v], [], [], cr1p); -# affect_neg = (record_crossings, [c1 => :v], [], [], cr1n)) -# evt2 = ModelingToolkit.SymbolicContinuousCallback( -# [c2 ~ 0], (record_crossings, [c2 => :v], [], [], cr2p); -# affect_neg = (record_crossings, [c2 => :v], [], [], cr2n)) -# @named trigsys = ODESystem(eqs, t; continuous_events = [evt1, evt2]) -# trigsys_ss = structural_simplify(trigsys) -# prob = ODEProblem(trigsys_ss, [], (0.0, 2π)) -# sol = solve(prob, Tsit5(); dtmax = 0.01) -# c1_pc = filter((<=)(0) ∘ sin, required_crossings_c1) -# c1_nc = filter((>=)(0) ∘ sin, required_crossings_c1) -# c2_pc = filter(c -> -sin(3c) > 0, required_crossings_c2) -# c2_nc = filter(c -> -sin(3c) < 0, required_crossings_c2) -# @test maximum(abs.(c1_pc .- first.(cr1p))) < 1e-5 -# @test maximum(abs.(c1_nc .- first.(cr1n))) < 1e-5 -# @test maximum(abs.(c2_pc .- first.(cr2p))) < 1e-5 -# @test maximum(abs.(c2_nc .- first.(cr2n))) < 1e-5 -# @test sign.(cos.(c1_pc .- 1e-6)) == sign.(last.(cr1p)) -# @test sign.(cos.(c1_nc .- 1e-6)) == sign.(last.(cr1n)) -# @test sign.(cos.(3 * (c2_pc .- 1e-6))) == sign.(last.(cr2p)) -# @test sign.(cos.(3 * (c2_nc .- 1e-6))) == sign.(last.(cr2n)) -# -# # with nothing neg affect (pos * neg + left root find) -# cr1p = [] -# cr2p = [] -# evt1 = ModelingToolkit.SymbolicContinuousCallback( -# [c1 ~ 0], (record_crossings, [c1 => :v], [], [], cr1p); affect_neg = nothing) -# evt2 = ModelingToolkit.SymbolicContinuousCallback( -# [c2 ~ 0], (record_crossings, [c2 => :v], [], [], cr2p); affect_neg = nothing) -# @named trigsys = ODESystem(eqs, t; continuous_events = [evt1, evt2]) -# trigsys_ss = structural_simplify(trigsys) -# prob = ODEProblem(trigsys_ss, [], (0.0, 2π)) -# sol = solve(prob, Tsit5(); dtmax = 0.01) -# @test maximum(abs.(c1_pc .- first.(cr1p))) < 1e-5 -# @test maximum(abs.(c2_pc .- first.(cr2p))) < 1e-5 -# @test sign.(cos.(c1_pc .- 1e-6)) == sign.(last.(cr1p)) -# @test sign.(cos.(3 * (c2_pc .- 1e-6))) == sign.(last.(cr2p)) -# -# #mixed -# cr1p = [] -# cr2p = [] -# cr1n = [] -# cr2n = [] -# evt1 = ModelingToolkit.SymbolicContinuousCallback( -# [c1 ~ 0], (record_crossings, [c1 => :v], [], [], cr1p); affect_neg = nothing) -# evt2 = ModelingToolkit.SymbolicContinuousCallback( -# [c2 ~ 0], (record_crossings, [c2 => :v], [], [], cr2p); -# affect_neg = (record_crossings, [c2 => :v], [], [], cr2n)) -# @named trigsys = ODESystem(eqs, t; continuous_events = [evt1, evt2]) -# trigsys_ss = structural_simplify(trigsys) -# prob = ODEProblem(trigsys_ss, [], (0.0, 2π)) -# sol = solve(prob, Tsit5(); dtmax = 0.01) -# c1_pc = filter((<=)(0) ∘ sin, required_crossings_c1) -# c2_pc = filter(c -> -sin(3c) > 0, required_crossings_c2) -# c2_nc = filter(c -> -sin(3c) < 0, required_crossings_c2) -# @test maximum(abs.(c1_pc .- first.(cr1p))) < 1e-5 -# @test maximum(abs.(c2_pc .- first.(cr2p))) < 1e-5 -# @test maximum(abs.(c2_nc .- first.(cr2n))) < 1e-5 -# @test sign.(cos.(c1_pc .- 1e-6)) == sign.(last.(cr1p)) -# @test sign.(cos.(3 * (c2_pc .- 1e-6))) == sign.(last.(cr2p)) -# @test sign.(cos.(3 * (c2_nc .- 1e-6))) == sign.(last.(cr2n)) -# -# # baseline affect w/ right rootfind (pos + neg + right root find) -# @variables c1(t)=1.0 c2(t)=1.0 # c1 = cos(t), c2 = cos(3t) -# cr1 = [] -# cr2 = [] -# evt1 = ModelingToolkit.SymbolicContinuousCallback( -# [c1 ~ 0], (record_crossings, [c1 => :v], [], [], cr1); -# rootfind = SciMLBase.RightRootFind) -# evt2 = ModelingToolkit.SymbolicContinuousCallback( -# [c2 ~ 0], (record_crossings, [c2 => :v], [], [], cr2); -# rootfind = SciMLBase.RightRootFind) -# @named trigsys = ODESystem(eqs, t; continuous_events = [evt1, evt2]) -# trigsys_ss = structural_simplify(trigsys) -# prob = ODEProblem(trigsys_ss, [], (0.0, 2π)) -# sol = solve(prob, Tsit5(); dtmax = 0.01) -# required_crossings_c1 = [π / 2, 3 * π / 2] -# required_crossings_c2 = [π / 6, π / 2, 5 * π / 6, 7 * π / 6, 3 * π / 2, 11 * π / 6] -# @test maximum(abs.(first.(cr1) .- required_crossings_c1)) < 1e-4 -# @test maximum(abs.(first.(cr2) .- required_crossings_c2)) < 1e-4 -# @test sign.(cos.(required_crossings_c1 .+ 1e-6)) == sign.(last.(cr1)) -# @test sign.(cos.(3 * (required_crossings_c2 .+ 1e-6))) == sign.(last.(cr2)) -# -# # baseline affect w/ mixed rootfind (pos + neg + right root find) -# cr1 = [] -# cr2 = [] -# evt1 = ModelingToolkit.SymbolicContinuousCallback( -# [c1 ~ 0], (record_crossings, [c1 => :v], [], [], cr1); -# rootfind = SciMLBase.LeftRootFind) -# evt2 = ModelingToolkit.SymbolicContinuousCallback( -# [c2 ~ 0], (record_crossings, [c2 => :v], [], [], cr2); -# rootfind = SciMLBase.RightRootFind) -# @named trigsys = ODESystem(eqs, t; continuous_events = [evt1, evt2]) -# trigsys_ss = structural_simplify(trigsys) -# prob = ODEProblem(trigsys_ss, [], (0.0, 2π)) -# sol = solve(prob, Tsit5()) -# @test maximum(abs.(first.(cr1) .- required_crossings_c1)) < 1e-4 -# @test maximum(abs.(first.(cr2) .- required_crossings_c2)) < 1e-4 -# @test sign.(cos.(required_crossings_c1 .- 1e-6)) == sign.(last.(cr1)) -# @test sign.(cos.(3 * (required_crossings_c2 .+ 1e-6))) == sign.(last.(cr2)) -# -# #flip order and ensure results are okay -# cr1 = [] -# cr2 = [] -# evt1 = ModelingToolkit.SymbolicContinuousCallback( -# [c1 ~ 0], (record_crossings, [c1 => :v], [], [], cr1); -# rootfind = SciMLBase.LeftRootFind) -# evt2 = ModelingToolkit.SymbolicContinuousCallback( -# [c2 ~ 0], (record_crossings, [c2 => :v], [], [], cr2); -# rootfind = SciMLBase.RightRootFind) -# @named trigsys = ODESystem(eqs, t; continuous_events = [evt2, evt1]) -# trigsys_ss = structural_simplify(trigsys) -# prob = ODEProblem(trigsys_ss, [], (0.0, 2π)) -# sol = solve(prob, Tsit5()) -# @test maximum(abs.(first.(cr1) .- required_crossings_c1)) < 1e-4 -# @test maximum(abs.(first.(cr2) .- required_crossings_c2)) < 1e-4 -# @test sign.(cos.(required_crossings_c1 .- 1e-6)) == sign.(last.(cr1)) -# @test sign.(cos.(3 * (required_crossings_c2 .+ 1e-6))) == sign.(last.(cr2)) -#end + +@testset "SDE/ODESystem Discrete Callbacks" begin + function testsol(sys, probtype, solver, u0, p, tspan; tstops = Float64[], paramtotest = nothing, + kwargs...) + prob = probtype(complete(sys), u0, tspan, p; kwargs...) + sol = solve(prob, solver(); tstops = tstops, abstol = 1e-10, reltol = 1e-10) + @test isapprox(sol(1.0000000001)[1] - sol(0.999999999)[1], 1.0; rtol = 1e-6) + paramtotest === nothing || (@test sol.ps[paramtotest] == [0., 1.]) + @test isapprox(sol(4.0)[1], 2 * exp(-2.0); rtol = 1e-6) + sol + end + + @parameters k(t) t1 t2 + @variables A(t) B(t) + + cond1 = (t == t1) + affect1 = [A ~ Pre(A) + 1] + cb1 = cond1 => affect1 + cond2 = (t == t2) + affect2 = [k ~ 1.0] + cb2 = cond2 => affect2 + cb2 = SymbolicDiscreteCallback(cb2, discrete_parameters = [k], iv = t) + + ∂ₜ = D + eqs = [∂ₜ(A) ~ -k * A] + @named osys = ODESystem(eqs, t, [A], [k, t1, t2], discrete_events = [cb1, cb2]) + @named ssys = SDESystem(eqs, [0.0], t, [A], [k, t1, t2], + discrete_events = [cb1, cb2]) + u0 = [A => 1.0] + p = [k => 0.0, t1 => 1.0, t2 => 2.0] + tspan = (0.0, 4.0) + testsol(osys, ODEProblem, Tsit5, u0, p, tspan; tstops = [1.0, 2.0], paramtotest = k) + testsol(ssys, SDEProblem, RI5, u0, p, tspan; tstops = [1.0, 2.0], paramtotest = k) + + cond1a = (t == t1) + affect1a = [A ~ Pre(A) + 1, B ~ A] + cb1a = cond1a => affect1a + @named osys1 = ODESystem(eqs, t, [A, B], [k, t1, t2], discrete_events = [cb1a, cb2]) + @named ssys1 = SDESystem(eqs, [0.0], t, [A, B], [k, t1, t2], + discrete_events = [cb1a, cb2]) + u0′ = [A => 1.0, B => 0.0] + sol = testsol(osys1, ODEProblem, Tsit5, u0′, p, tspan; tstops = [1.0, 2.0], check_length = false, paramtotest = k) + @test sol(1.0000001, idxs = B) == 2.0 + + sol = testsol(ssys1, SDEProblem, RI5, u0′, p, tspan; tstops = [1.0, 2.0], check_length = false, paramtotest = k) + @test sol(1.0000001, idxs = B) == 2.0 + + # same as above - but with set-time event syntax + cb1‵ = [1.0] => affect1 # needs to be a Vector for the event to happen only once + cb2‵ = SymbolicDiscreteCallback([2.0] => affect2, discrete_parameters = [k], iv = t) + @named osys‵ = ODESystem(eqs, t, [A], [k], discrete_events = [cb1‵, cb2‵]) + @named ssys‵ = SDESystem(eqs, [0.0], t, [A], [k], discrete_events = [cb1‵, cb2‵]) + testsol(osys‵, ODEProblem, Tsit5, u0, p, tspan; paramtotest = k) + testsol(ssys‵, SDEProblem, RI5, u0, p, tspan; paramtotest = k) + + # mixing discrete affects + @named osys3 = ODESystem(eqs, t, [A], [k, t1, t2], discrete_events = [cb1, cb2‵]) + @named ssys3 = SDESystem(eqs, [0.0], t, [A], [k, t1, t2], + discrete_events = [cb1, cb2‵]) + testsol(osys3, ODEProblem, Tsit5, u0, p, tspan; tstops = [1.0], paramtotest = k) + testsol(ssys3, SDEProblem, RI5, u0, p, tspan; tstops = [1.0], paramtotest = k) + + # mixing with a func affect + function affect!(integrator, u, p, ctx) + integrator.ps[p.k] = 1.0 + nothing + end + cb2‵‵ = [2.0] => (affect!, [], [k], [k], nothing) + @named osys4 = ODESystem(eqs, t, [A], [k, t1], discrete_events = [cb1, cb2‵‵]) + @named ssys4 = SDESystem(eqs, [0.0], t, [A], [k, t1], + discrete_events = [cb1, cb2‵‵]) + oprob4 = ODEProblem(complete(osys4), u0, tspan, p) + testsol(osys4, ODEProblem, Tsit5, u0, p, tspan; tstops = [1.0], paramtotest = k) + testsol(ssys4, SDEProblem, RI5, u0, p, tspan; tstops = [1.0], paramtotest = k) + + # mixing with symbolic condition in the func affect + cb2‵‵‵ = (t == t2) => (affect!, [], [k], [k], nothing) + @named osys5 = ODESystem(eqs, t, [A], [k, t1, t2], discrete_events = [cb1, cb2‵‵‵]) + @named ssys5 = SDESystem(eqs, [0.0], t, [A], [k, t1, t2], + discrete_events = [cb1, cb2‵‵‵]) + testsol(osys5, ODEProblem, Tsit5, u0, p, tspan; tstops = [1.0, 2.0]) + testsol(ssys5, SDEProblem, RI5, u0, p, tspan; tstops = [1.0, 2.0]) + @named osys6 = ODESystem(eqs, t, [A], [k, t1, t2], discrete_events = [cb2‵‵‵, cb1]) + @named ssys6 = SDESystem(eqs, [0.0], t, [A], [k, t1, t2], + discrete_events = [cb2‵‵‵, cb1]) + testsol(osys6, ODEProblem, Tsit5, u0, p, tspan; tstops = [1.0, 2.0], paramtotest = k) + testsol(ssys6, SDEProblem, RI5, u0, p, tspan; tstops = [1.0, 2.0], paramtotest = k) + + # mix a continuous event too + cond3 = A ~ 0.1 + affect3 = [k ~ 0.0] + cb3 = SymbolicContinuousCallback(cond3 => affect3, discrete_parameters = [k], iv = t) + @named osys7 = ODESystem(eqs, t, [A], [k, t1, t2], discrete_events = [cb1, cb2‵‵‵], + continuous_events = [cb3]) + @named ssys7 = SDESystem(eqs, [0.0], t, [A], [k, t1, t2], + discrete_events = [cb1, cb2‵‵‵], + continuous_events = [cb3]) + + sol = testsol(osys7, ODEProblem, Tsit5, u0, p, (0.0, 10.0); tstops = [1.0, 2.0]) + @test isapprox(sol(10.0)[1], 0.1; atol = 1e-10, rtol = 1e-10) + sol = testsol(ssys7, SDEProblem, RI5, u0, p, (0.0, 10.0); tstops = [1.0, 2.0]) + @test isapprox(sol(10.0)[1], 0.1; atol = 1e-10, rtol = 1e-10) +end + +@testset "JumpSystem Discrete Callbacks" begin + function testsol(jsys, u0, p, tspan; tstops = Float64[], paramtotest = nothing, + N = 40000, kwargs...) + jsys = complete(jsys) + dprob = DiscreteProblem(jsys, u0, tspan, p) + jprob = JumpProblem(jsys, dprob, Direct(); kwargs...) + sol = solve(jprob, SSAStepper(); tstops = tstops) + @test (sol(1.000000000001)[1] - sol(0.99999999999)[1]) == 1 + paramtotest === nothing || (@test sol.ps[paramtotest] == [0., 1.0]) + @test sol(40.0)[1] == 0 + sol + end + + @parameters k(t) t1 t2 + @variables A(t) B(t) + + eqs = [MassActionJump(k, [A => 1], [A => -1])] + cond1 = (t == t1) + affect1 = [A ~ Pre(A) + 1] + cb1 = cond1 => affect1 + cond2 = (t == t2) + affect2 = [k ~ 1.0] + cb2 = cond2 => affect2 + cb2 = SymbolicDiscreteCallback(cb2, discrete_parameters = [k], iv = t) + + @named jsys = JumpSystem(eqs, t, [A], [k, t1, t2], discrete_events = [cb1, cb2]) + u0 = [A => 1] + p = [k => 0.0, t1 => 1.0, t2 => 2.0] + tspan = (0.0, 40.0) + testsol(jsys, u0, p, tspan; tstops = [1.0, 2.0], rng, paramtotest = k) + + cond1a = (t == t1) + affect1a = [A ~ Pre(A) + 1, B ~ A] + cb1a = cond1a => affect1a + @named jsys1 = JumpSystem(eqs, t, [A, B], [k, t1, t2], discrete_events = [cb1a, cb2]) + u0′ = [A => 1, B => 0] + sol = testsol(jsys1, u0′, p, tspan; tstops = [1.0, 2.0], + check_length = false, rng, paramtotest = k) + @test sol(1.000000001, idxs = B) == 2 + + # same as above - but with set-time event syntax + cb1‵ = [1.0] => affect1 # needs to be a Vector for the event to happen only once + cb2‵ = SymbolicDiscreteCallback([2.0] => affect2, discrete_parameters = [k], iv = t) + @named jsys‵ = JumpSystem(eqs, t, [A], [k], discrete_events = [cb1‵, cb2‵]) + testsol(jsys‵, u0, [p[1]], tspan; rng, paramtotest = k) + + # mixing discrete affects + @named jsys3 = JumpSystem(eqs, t, [A], [k, t1, t2], discrete_events = [cb1, cb2‵]) + testsol(jsys3, u0, p, tspan; tstops = [1.0], rng, paramtotest = k) + + # mixing with a func affect + function affect!(integrator, u, p, ctx) + integrator.ps[p.k] = 1.0 + reset_aggregated_jumps!(integrator) + nothing + end + cb2‵‵ = [2.0] => (affect!, [], [k], [k], nothing) + @named jsys4 = JumpSystem(eqs, t, [A], [k, t1], discrete_events = [cb1, cb2‵‵]) + testsol(jsys4, u0, p, tspan; tstops = [1.0], rng, paramtotest = k) + + # mixing with symbolic condition in the func affect + cb2‵‵‵ = (t == t2) => (affect!, [], [k], [k], nothing) + @named jsys5 = JumpSystem(eqs, t, [A], [k, t1, t2], discrete_events = [cb1, cb2‵‵‵]) + testsol(jsys5, u0, p, tspan; tstops = [1.0, 2.0], rng, paramtotest = k) + @named jsys6 = JumpSystem(eqs, t, [A], [k, t1, t2], discrete_events = [cb2‵‵‵, cb1]) + testsol(jsys6, u0, p, tspan; tstops = [1.0, 2.0], rng, paramtotest = k) +end + +@testset "Namespacing" begin + function oscillator_ce(k = 1.0; name) + sts = @variables x(t)=1.0 v(t)=0.0 F(t) + ps = @parameters k=k Θ=0.5 + eqs = [D(x) ~ v, D(v) ~ -k * x + F] + ev = [x ~ Θ] => [x ~ 1.0, v ~ 0.0] + ODESystem(eqs, t, sts, ps, continuous_events = [ev]; name) + end + + @named oscce = oscillator_ce() + eqs = [oscce.F ~ 0] + @named eqs_sys = ODESystem(eqs, t) + @named oneosc_ce = compose(eqs_sys, oscce) + oneosc_ce_simpl = structural_simplify(oneosc_ce) + + prob = ODEProblem(oneosc_ce_simpl, [], (0.0, 2.0), []) + sol = solve(prob, Tsit5(), saveat = 0.1) + + @test typeof(oneosc_ce_simpl) == ODESystem + @test sol[1, 6] < 1.0 # test whether x(t) decreases over time + @test sol[1, 18] > 0.5 # test whether event happened +end + +@testset "Additional SymbolicContinuousCallback options" begin + # baseline affect (pos + neg + left root find) + @variables c1(t)=1.0 c2(t)=1.0 # c1 = cos(t), c2 = cos(3t) + eqs = [D(c1) ~ -sin(t); D(c2) ~ -3 * sin(3 * t)] + record_crossings(i, u, _, c) = push!(c, i.t => i.u[u.v]) + cr1 = [] + cr2 = [] + evt1 = ModelingToolkit.SymbolicContinuousCallback( + [c1 ~ 0], (record_crossings, [c1 => :v], [], [], cr1)) + evt2 = ModelingToolkit.SymbolicContinuousCallback( + [c2 ~ 0], (record_crossings, [c2 => :v], [], [], cr2)) + @named trigsys = ODESystem(eqs, t; continuous_events = [evt1, evt2]) + trigsys_ss = structural_simplify(trigsys) + prob = ODEProblem(trigsys_ss, [], (0.0, 2π)) + sol = solve(prob, Tsit5()) + required_crossings_c1 = [π / 2, 3 * π / 2] + required_crossings_c2 = [π / 6, π / 2, 5 * π / 6, 7 * π / 6, 3 * π / 2, 11 * π / 6] + @test maximum(abs.(first.(cr1) .- required_crossings_c1)) < 1e-4 + @test maximum(abs.(first.(cr2) .- required_crossings_c2)) < 1e-4 + @test sign.(cos.(required_crossings_c1 .- 1e-6)) == sign.(last.(cr1)) + @test sign.(cos.(3 * (required_crossings_c2 .- 1e-6))) == sign.(last.(cr2)) + + # with neg affect (pos * neg + left root find) + cr1p = [] + cr2p = [] + cr1n = [] + cr2n = [] + evt1 = ModelingToolkit.SymbolicContinuousCallback( + [c1 ~ 0], (record_crossings, [c1 => :v], [], [], cr1p); + affect_neg = (record_crossings, [c1 => :v], [], [], cr1n)) + evt2 = ModelingToolkit.SymbolicContinuousCallback( + [c2 ~ 0], (record_crossings, [c2 => :v], [], [], cr2p); + affect_neg = (record_crossings, [c2 => :v], [], [], cr2n)) + @named trigsys = ODESystem(eqs, t; continuous_events = [evt1, evt2]) + trigsys_ss = structural_simplify(trigsys) + prob = ODEProblem(trigsys_ss, [], (0.0, 2π)) + sol = solve(prob, Tsit5(); dtmax = 0.01) + c1_pc = filter((<=)(0) ∘ sin, required_crossings_c1) + c1_nc = filter((>=)(0) ∘ sin, required_crossings_c1) + c2_pc = filter(c -> -sin(3c) > 0, required_crossings_c2) + c2_nc = filter(c -> -sin(3c) < 0, required_crossings_c2) + @test maximum(abs.(c1_pc .- first.(cr1p))) < 1e-5 + @test maximum(abs.(c1_nc .- first.(cr1n))) < 1e-5 + @test maximum(abs.(c2_pc .- first.(cr2p))) < 1e-5 + @test maximum(abs.(c2_nc .- first.(cr2n))) < 1e-5 + @test sign.(cos.(c1_pc .- 1e-6)) == sign.(last.(cr1p)) + @test sign.(cos.(c1_nc .- 1e-6)) == sign.(last.(cr1n)) + @test sign.(cos.(3 * (c2_pc .- 1e-6))) == sign.(last.(cr2p)) + @test sign.(cos.(3 * (c2_nc .- 1e-6))) == sign.(last.(cr2n)) + + # with nothing neg affect (pos * neg + left root find) + cr1p = [] + cr2p = [] + evt1 = ModelingToolkit.SymbolicContinuousCallback( + [c1 ~ 0], (record_crossings, [c1 => :v], [], [], cr1p); affect_neg = nothing) + evt2 = ModelingToolkit.SymbolicContinuousCallback( + [c2 ~ 0], (record_crossings, [c2 => :v], [], [], cr2p); affect_neg = nothing) + @named trigsys = ODESystem(eqs, t; continuous_events = [evt1, evt2]) + trigsys_ss = structural_simplify(trigsys) + prob = ODEProblem(trigsys_ss, [], (0.0, 2π)) + sol = solve(prob, Tsit5(); dtmax = 0.01) + @test maximum(abs.(c1_pc .- first.(cr1p))) < 1e-5 + @test maximum(abs.(c2_pc .- first.(cr2p))) < 1e-5 + @test sign.(cos.(c1_pc .- 1e-6)) == sign.(last.(cr1p)) + @test sign.(cos.(3 * (c2_pc .- 1e-6))) == sign.(last.(cr2p)) + + #mixed + cr1p = [] + cr2p = [] + cr1n = [] + cr2n = [] + evt1 = ModelingToolkit.SymbolicContinuousCallback( + [c1 ~ 0], (record_crossings, [c1 => :v], [], [], cr1p); affect_neg = nothing) + evt2 = ModelingToolkit.SymbolicContinuousCallback( + [c2 ~ 0], (record_crossings, [c2 => :v], [], [], cr2p); + affect_neg = (record_crossings, [c2 => :v], [], [], cr2n)) + @named trigsys = ODESystem(eqs, t; continuous_events = [evt1, evt2]) + trigsys_ss = structural_simplify(trigsys) + prob = ODEProblem(trigsys_ss, [], (0.0, 2π)) + sol = solve(prob, Tsit5(); dtmax = 0.01) + c1_pc = filter((<=)(0) ∘ sin, required_crossings_c1) + c2_pc = filter(c -> -sin(3c) > 0, required_crossings_c2) + c2_nc = filter(c -> -sin(3c) < 0, required_crossings_c2) + @test maximum(abs.(c1_pc .- first.(cr1p))) < 1e-5 + @test maximum(abs.(c2_pc .- first.(cr2p))) < 1e-5 + @test maximum(abs.(c2_nc .- first.(cr2n))) < 1e-5 + @test sign.(cos.(c1_pc .- 1e-6)) == sign.(last.(cr1p)) + @test sign.(cos.(3 * (c2_pc .- 1e-6))) == sign.(last.(cr2p)) + @test sign.(cos.(3 * (c2_nc .- 1e-6))) == sign.(last.(cr2n)) + + # baseline affect w/ right rootfind (pos + neg + right root find) + @variables c1(t)=1.0 c2(t)=1.0 # c1 = cos(t), c2 = cos(3t) + cr1 = [] + cr2 = [] + evt1 = ModelingToolkit.SymbolicContinuousCallback( + [c1 ~ 0], (record_crossings, [c1 => :v], [], [], cr1); + rootfind = SciMLBase.RightRootFind) + evt2 = ModelingToolkit.SymbolicContinuousCallback( + [c2 ~ 0], (record_crossings, [c2 => :v], [], [], cr2); + rootfind = SciMLBase.RightRootFind) + @named trigsys = ODESystem(eqs, t; continuous_events = [evt1, evt2]) + trigsys_ss = structural_simplify(trigsys) + prob = ODEProblem(trigsys_ss, [], (0.0, 2π)) + sol = solve(prob, Tsit5(); dtmax = 0.01) + required_crossings_c1 = [π / 2, 3 * π / 2] + required_crossings_c2 = [π / 6, π / 2, 5 * π / 6, 7 * π / 6, 3 * π / 2, 11 * π / 6] + @test maximum(abs.(first.(cr1) .- required_crossings_c1)) < 1e-4 + @test maximum(abs.(first.(cr2) .- required_crossings_c2)) < 1e-4 + @test sign.(cos.(required_crossings_c1 .+ 1e-6)) == sign.(last.(cr1)) + @test sign.(cos.(3 * (required_crossings_c2 .+ 1e-6))) == sign.(last.(cr2)) + + # baseline affect w/ mixed rootfind (pos + neg + right root find) + cr1 = [] + cr2 = [] + evt1 = ModelingToolkit.SymbolicContinuousCallback( + [c1 ~ 0], (record_crossings, [c1 => :v], [], [], cr1); + rootfind = SciMLBase.LeftRootFind) + evt2 = ModelingToolkit.SymbolicContinuousCallback( + [c2 ~ 0], (record_crossings, [c2 => :v], [], [], cr2); + rootfind = SciMLBase.RightRootFind) + @named trigsys = ODESystem(eqs, t; continuous_events = [evt1, evt2]) + trigsys_ss = structural_simplify(trigsys) + prob = ODEProblem(trigsys_ss, [], (0.0, 2π)) + sol = solve(prob, Tsit5()) + @test maximum(abs.(first.(cr1) .- required_crossings_c1)) < 1e-4 + @test maximum(abs.(first.(cr2) .- required_crossings_c2)) < 1e-4 + @test sign.(cos.(required_crossings_c1 .- 1e-6)) == sign.(last.(cr1)) + @test sign.(cos.(3 * (required_crossings_c2 .+ 1e-6))) == sign.(last.(cr2)) + + #flip order and ensure results are okay + cr1 = [] + cr2 = [] + evt1 = ModelingToolkit.SymbolicContinuousCallback( + [c1 ~ 0], (record_crossings, [c1 => :v], [], [], cr1); + rootfind = SciMLBase.LeftRootFind) + evt2 = ModelingToolkit.SymbolicContinuousCallback( + [c2 ~ 0], (record_crossings, [c2 => :v], [], [], cr2); + rootfind = SciMLBase.RightRootFind) + @named trigsys = ODESystem(eqs, t; continuous_events = [evt2, evt1]) + trigsys_ss = structural_simplify(trigsys) + prob = ODEProblem(trigsys_ss, [], (0.0, 2π)) + sol = solve(prob, Tsit5()) + @test maximum(abs.(first.(cr1) .- required_crossings_c1)) < 1e-4 + @test maximum(abs.(first.(cr2) .- required_crossings_c2)) < 1e-4 + @test sign.(cos.(required_crossings_c1 .- 1e-6)) == sign.(last.(cr1)) + @test sign.(cos.(3 * (required_crossings_c2 .+ 1e-6))) == sign.(last.(cr2)) +end # #@testset "Discrete event reinitialization (#3142)" begin # @connector LiquidPort begin @@ -1326,61 +1276,63 @@ end end @testset "Implicit affects with Pre" begin + using ModelingToolkit: UnsolvableCallbackError @parameters g @variables x(t) y(t) λ(t) eqs = [D(D(x)) ~ λ * x D(D(y)) ~ λ * y - g x^2 + y^2 ~ 1] - c_evt = [t ~ 0.5] => [x ~ Pre(x) + 0.1] + c_evt = [t ~ 5.] => [x ~ Pre(x) + 0.1] @mtkbuild pend = ODESystem(eqs, t, continuous_events = c_evt) - prob = ODEProblem(pend, [x => 1, y => 0], (0., 1.), [g => 1], guesses = [λ => 1]) - sol = solve(prob, Rodas5()) - @test sol(0.5000001)[1] - sol(0.4999999)[1] ≈ 0.1 - @test sol(0.5000001)[1]^2 + sol(0.5000001)[2]^2 ≈ 1 + prob = ODEProblem(pend, [x => -1, y => 0], (0., 10.), [g => 1], guesses = [λ => 1]) + sol = solve(prob, FBDF()) + @test ≈(sol(5.000001, idxs = x) - sol(4.999999, idxs = x), 0.1, rtol = 1e-4) + @test ≈(sol(5.000001, idxs = x)^2 + sol(5.000001, idxs = y)^2, 1, rtol = 1e-4) # Implicit affect with Pre - c_evt = [t ~ 0.5] => [x ~ Pre(x) + y^2] + c_evt = [t ~ 5.] => [x ~ Pre(x) + y^2] @mtkbuild pend = ODESystem(eqs, t, continuous_events = c_evt) - prob = ODEProblem(pend, [x => 1, y => 0], (0., 1.), [g => 1], guesses = [λ => 1]) - sol = solve(prob, Rodas5()) - @test sol(0.5000001)[2]^2 - sol(0.4999999)[1] ≈ sol(0.5000001)[1] - @test sol(0.5000001)[1]^2 + sol(0.5000001)[2]^2 ≈ 1 + prob = ODEProblem(pend, [x => 1, y => 0], (0., 10.), [g => 1], guesses = [λ => 1]) + sol = solve(prob, FBDF()) + @test ≈(sol(5.000001, idxs = y)^2 + sol(4.999999, idxs = x), sol(5.000001, idxs = x), rtol = 1e-4) + @test ≈(sol(5.000001, idxs = x)^2 + sol(5.000001, idxs = y)^2, 1, rtol = 1e-4) # Impossible affect errors - c_evt = [t ~ 0.5] => [x ~ Pre(x) + 1] + c_evt = [t ~ 5.] => [x ~ Pre(x) + 2] @mtkbuild pend = ODESystem(eqs, t, continuous_events = c_evt) - prob = ODEProblem(pend, [x => 1, y => 0], (0., 1.), [g => 1], guesses = [λ => 1]) - @test_throws Exception sol = solve(prob, Rodas5()) + prob = ODEProblem(pend, [x => 1, y => 0], (0., 10.), [g => 1], guesses = [λ => 1]) + @test_throws UnsolvableCallbackError sol = solve(prob, FBDF()) # Changing both variables and parameters in the same affect. - c_evt = [t ~ 0.5] => [x ~ Pre(x) + 1, g ~ Pre(g) + 1] + @parameters g(t) + eqs = [D(D(x)) ~ λ * x + D(D(y)) ~ λ * y - g + x^2 + y^2 ~ 1] + c_evt = SymbolicContinuousCallback([t ~ 5.0], [x ~ Pre(x) + 0.1, g ~ Pre(g) + 1], discrete_parameters = [g], iv = t) @mtkbuild pend = ODESystem(eqs, t, continuous_events = c_evt) - prob = ODEProblem(pend, [x => 1, y => 0], (0., 1.), [g => 1], guesses = [λ => 1]) - sol = solve(prob, Rodas5()) - @test sol.ps[g] ≈ 2 - @test sol(0.5000001, idxs = x) - sol(0.4999999, idxs = x) ≈ 1 + prob = ODEProblem(pend, [x => 1, y => 0], (0., 10.), [g => 1], guesses = [λ => 1]) + sol = solve(prob, FBDF()) + @test sol.ps[g] ≈ [1, 2] + @test ≈(sol(5.0000001, idxs = x) - sol(4.999999, idxs = x), .1, rtol = 1e-4) # Proper re-initialization after parameter change - eqs = [x ~ g^2 - y, D(x) ~ x] - c_evt = [t ~ 0.5] => [x ~ Pre(x) + 1, g ~ Pre(g) + 1] + eqs = [y ~ g^2 - x, D(x) ~ x] + c_evt = SymbolicContinuousCallback([t ~ 5.0], [x ~ Pre(x) + 1, g ~ Pre(g) + 1], discrete_parameters = [g], iv = t) @mtkbuild sys = ODESystem(eqs, t, continuous_events = c_evt) - prob = ODEProblem(sys, [x => 0.5], (0., 1.), [g => 2], guesses = [y => 0]) - sol = solve(prob, Rodas5()) - @test sol.ps[g] ≈ 3 - @test ≈(sol(0.5000001)[1] - sol(0.4999999)[1], 1; atol = 1e-6) - @test sol(0.5000001, idxs = y) ≈ 9 - sol(0.5000001, idxs = x) + prob = ODEProblem(sys, [x => 1.0], (0., 10.), [g => 2], guesses = [y => 0.]) + sol = solve(prob, FBDF()) + @test sol.ps[g] ≈ [2., 3.] + @test ≈(sol(5.00000001, idxs = x) - sol(4.9999999, idxs = x), 1; rtol = 1e-4) + @test ≈(sol(5.00000001, idxs = y), 9 - sol(5.00000001, idxs = x), rtol = 1e-4) # Parameters that don't appear in affects should not be mutated. - c_evt = [t ~ 0.5] => [x ~ Pre(x) + 1] + c_evt = [t ~ 5.0] => [x ~ Pre(x) + 1] @mtkbuild sys = ODESystem(eqs, t, continuous_events = c_evt) - prob = ODEProblem(sys, [x => 0.5], (0., 1.), [g => 2], guesses = [y => 0]) - sol = solve(prob, Rodas5()) + prob = ODEProblem(sys, [x => 0.5], (0., 10.), [g => 2], guesses = [y => 0]) + sol = solve(prob, FBDF()) @test prob.ps[g] == sol.ps[g] end - - # TODO: test: # - Functional affects reinitialize correctly # - explicit equation of t in a functional affect -# - affects that have Pre but are also algebraic in nature # - reinitialization after affects From 54b1d1bf5cef9bc9cfee20064066acd41d37c165 Mon Sep 17 00:00:00 2001 From: vyudu Date: Wed, 26 Mar 2025 14:07:31 -0400 Subject: [PATCH 28/52] feat: add discrete_parameters --- src/systems/callbacks.jl | 5 +- test/symbolic_events.jl | 916 +++++++++++++++++++-------------------- 2 files changed, 462 insertions(+), 459 deletions(-) diff --git a/src/systems/callbacks.jl b/src/systems/callbacks.jl index 5a97472c6d..28a617678d 100644 --- a/src/systems/callbacks.jl +++ b/src/systems/callbacks.jl @@ -241,7 +241,10 @@ make_affect(affect::Affect; kwargs...) = affect function make_affect(affect::Vector{Equation}; discrete_parameters::AbstractVector = Any[], iv = nothing, algeeqs::Vector{Equation} = Equation[]) isempty(affect) && return nothing isempty(algeeqs) && @warn "No algebraic equations were found for the callback defined by $(join(affect, ", ")). If the system has no algebraic equations, this can be disregarded. Otherwise pass in `algeeqs` to the SymbolicContinuousCallback constructor." - isnothing(iv) && error("Must specify iv.") + if isnothing(iv) + iv = t_nounits + @warn "No independent variable specified. Defaulting to t_nounits." + end for p in discrete_parameters occursin(unwrap(iv), unwrap(p)) || error("Non-time dependent parameter $p passed in as a discrete. Must be declared as @parameters $p(t).") diff --git a/test/symbolic_events.jl b/test/symbolic_events.jl index bc58f78900..e6c1df4363 100644 --- a/test/symbolic_events.jl +++ b/test/symbolic_events.jl @@ -20,463 +20,463 @@ eqs = [D(x) ~ 1] affect = [x ~ 0] affect_neg = [x ~ 1] -#@testset "SymbolicContinuousCallback constructors" begin -# e = SymbolicContinuousCallback(eqs[]) -# @test e isa SymbolicContinuousCallback -# @test isequal(equations(e), eqs) -# @test e.affect == nothing -# @test e.affect_neg == nothing -# @test e.rootfind == SciMLBase.LeftRootFind -# -# e = SymbolicContinuousCallback(eqs) -# @test e isa SymbolicContinuousCallback -# @test isequal(equations(e), eqs) -# @test e.affect == nothing -# @test e.affect_neg == nothing -# @test e.rootfind == SciMLBase.LeftRootFind -# -# e = SymbolicContinuousCallback(eqs, nothing) -# @test e isa SymbolicContinuousCallback -# @test isequal(equations(e), eqs) -# @test e.affect == nothing -# @test e.affect_neg == nothing -# @test e.rootfind == SciMLBase.LeftRootFind -# -# e = SymbolicContinuousCallback(eqs[], nothing) -# @test e isa SymbolicContinuousCallback -# @test isequal(equations(e), eqs) -# @test e.affect == nothing -# @test e.affect_neg == nothing -# @test e.rootfind == SciMLBase.LeftRootFind -# -# e = SymbolicContinuousCallback(eqs => nothing) -# @test e isa SymbolicContinuousCallback -# @test isequal(equations(e), eqs) -# @test e.affect == nothing -# @test e.affect_neg == nothing -# @test e.rootfind == SciMLBase.LeftRootFind -# -# e = SymbolicContinuousCallback(eqs[] => nothing) -# @test e isa SymbolicContinuousCallback -# @test isequal(equations(e), eqs) -# @test e.affect == nothing -# @test e.affect_neg == nothing -# @test e.rootfind == SciMLBase.LeftRootFind -# -# ## With affect -# e = SymbolicContinuousCallback(eqs[], affect) -# @test e isa SymbolicContinuousCallback -# @test isequal(equations(e), eqs) -# @test e.rootfind == SciMLBase.LeftRootFind -# -# # with only positive edge affect -# e = SymbolicContinuousCallback(eqs[], affect, affect_neg = nothing) -# @test e isa SymbolicContinuousCallback -# @test isequal(equations(e), eqs) -# @test isnothing(e.affect_neg) -# @test e.rootfind == SciMLBase.LeftRootFind -# -# # with explicit edge affects -# e = SymbolicContinuousCallback(eqs[], affect, affect_neg = affect_neg) -# @test e isa SymbolicContinuousCallback -# @test isequal(equations(e), eqs) -# @test e.rootfind == SciMLBase.LeftRootFind -# -# # with different root finding ops -# e = SymbolicContinuousCallback( -# eqs[], affect, affect_neg = affect_neg, rootfind = SciMLBase.LeftRootFind) -# @test e isa SymbolicContinuousCallback -# @test isequal(equations(e), eqs) -# @test e.rootfind == SciMLBase.LeftRootFind -# -# # test plural constructor -# e = SymbolicContinuousCallbacks(eqs[]) -# @test e isa Vector{SymbolicContinuousCallback} -# @test isequal(equations(e[]), eqs) -# @test e[].affect == nothing -# -# e = SymbolicContinuousCallbacks(eqs) -# @test e isa Vector{SymbolicContinuousCallback} -# @test isequal(equations(e[]), eqs) -# @test e[].affect == nothing -# -# e = SymbolicContinuousCallbacks(eqs[] => affect) -# @test e isa Vector{SymbolicContinuousCallback} -# @test isequal(equations(e[]), eqs) -# @test e[].affect isa AffectSystem -# -# e = SymbolicContinuousCallbacks(eqs => affect) -# @test e isa Vector{SymbolicContinuousCallback} -# @test isequal(equations(e[]), eqs) -# @test e[].affect isa AffectSystem -# -# e = SymbolicContinuousCallbacks([eqs[] => affect]) -# @test e isa Vector{SymbolicContinuousCallback} -# @test isequal(equations(e[]), eqs) -# @test e[].affect isa AffectSystem -# -# e = SymbolicContinuousCallbacks([eqs => affect]) -# @test e isa Vector{SymbolicContinuousCallback} -# @test isequal(equations(e[]), eqs) -# @test e[].affect isa AffectSystem -#end -# -#@testset "ImperativeAffect constructors" begin -# fmfa(o, x, i, c) = nothing -# m = ModelingToolkit.ImperativeAffect(fmfa) -# @test m isa ModelingToolkit.ImperativeAffect -# @test m.f == fmfa -# @test m.obs == [] -# @test m.obs_syms == [] -# @test m.modified == [] -# @test m.mod_syms == [] -# @test m.ctx === nothing -# -# m = ModelingToolkit.ImperativeAffect(fmfa, (;)) -# @test m isa ModelingToolkit.ImperativeAffect -# @test m.f == fmfa -# @test m.obs == [] -# @test m.obs_syms == [] -# @test m.modified == [] -# @test m.mod_syms == [] -# @test m.ctx === nothing -# -# m = ModelingToolkit.ImperativeAffect(fmfa, (; x)) -# @test m isa ModelingToolkit.ImperativeAffect -# @test m.f == fmfa -# @test isequal(m.obs, []) -# @test m.obs_syms == [] -# @test isequal(m.modified, [x]) -# @test m.mod_syms == [:x] -# @test m.ctx === nothing -# -# m = ModelingToolkit.ImperativeAffect(fmfa, (; y = x)) -# @test m isa ModelingToolkit.ImperativeAffect -# @test m.f == fmfa -# @test isequal(m.obs, []) -# @test m.obs_syms == [] -# @test isequal(m.modified, [x]) -# @test m.mod_syms == [:y] -# @test m.ctx === nothing -# -# m = ModelingToolkit.ImperativeAffect(fmfa; observed = (; y = x)) -# @test m isa ModelingToolkit.ImperativeAffect -# @test m.f == fmfa -# @test isequal(m.obs, [x]) -# @test m.obs_syms == [:y] -# @test m.modified == [] -# @test m.mod_syms == [] -# @test m.ctx === nothing -# -# m = ModelingToolkit.ImperativeAffect(fmfa; modified = (; x)) -# @test m isa ModelingToolkit.ImperativeAffect -# @test m.f == fmfa -# @test isequal(m.obs, []) -# @test m.obs_syms == [] -# @test isequal(m.modified, [x]) -# @test m.mod_syms == [:x] -# @test m.ctx === nothing -# -# m = ModelingToolkit.ImperativeAffect(fmfa; modified = (; y = x)) -# @test m isa ModelingToolkit.ImperativeAffect -# @test m.f == fmfa -# @test isequal(m.obs, []) -# @test m.obs_syms == [] -# @test isequal(m.modified, [x]) -# @test m.mod_syms == [:y] -# @test m.ctx === nothing -# -# m = ModelingToolkit.ImperativeAffect(fmfa, (; x), (; x)) -# @test m isa ModelingToolkit.ImperativeAffect -# @test m.f == fmfa -# @test isequal(m.obs, [x]) -# @test m.obs_syms == [:x] -# @test isequal(m.modified, [x]) -# @test m.mod_syms == [:x] -# @test m.ctx === nothing -# -# m = ModelingToolkit.ImperativeAffect(fmfa, (; y = x), (; y = x)) -# @test m isa ModelingToolkit.ImperativeAffect -# @test m.f == fmfa -# @test isequal(m.obs, [x]) -# @test m.obs_syms == [:y] -# @test isequal(m.modified, [x]) -# @test m.mod_syms == [:y] -# @test m.ctx === nothing -# -# m = ModelingToolkit.ImperativeAffect( -# fmfa; modified = (; y = x), observed = (; y = x)) -# @test m isa ModelingToolkit.ImperativeAffect -# @test m.f == fmfa -# @test isequal(m.obs, [x]) -# @test m.obs_syms == [:y] -# @test isequal(m.modified, [x]) -# @test m.mod_syms == [:y] -# @test m.ctx === nothing -# -# m = ModelingToolkit.ImperativeAffect( -# fmfa; modified = (; y = x), observed = (; y = x), ctx = 3) -# @test m isa ModelingToolkit.ImperativeAffect -# @test m.f == fmfa -# @test isequal(m.obs, [x]) -# @test m.obs_syms == [:y] -# @test isequal(m.modified, [x]) -# @test m.mod_syms == [:y] -# @test m.ctx === 3 -# -# m = ModelingToolkit.ImperativeAffect(fmfa, (; x), (; x), 3) -# @test m isa ModelingToolkit.ImperativeAffect -# @test m.f == fmfa -# @test isequal(m.obs, [x]) -# @test m.obs_syms == [:x] -# @test isequal(m.modified, [x]) -# @test m.mod_syms == [:x] -# @test m.ctx === 3 -#end -# -#@testset "Condition Compilation" begin -# @named sys = ODESystem(eqs, t, continuous_events = [x ~ 1]) -# @test getfield(sys, :continuous_events)[] == -# SymbolicContinuousCallback(Equation[x ~ 1], nothing) -# @test isequal(equations(getfield(sys, :continuous_events))[], x ~ 1) -# fsys = flatten(sys) -# @test isequal(equations(getfield(fsys, :continuous_events))[], x ~ 1) -# -# @named sys2 = ODESystem([D(x) ~ 1], t, continuous_events = [x ~ 2], systems = [sys]) -# @test getfield(sys2, :continuous_events)[] == -# SymbolicContinuousCallback(Equation[x ~ 2], nothing) -# @test all(ModelingToolkit.continuous_events(sys2) .== [ -# SymbolicContinuousCallback(Equation[x ~ 2], nothing), -# SymbolicContinuousCallback(Equation[sys.x ~ 1], nothing) -# ]) -# -# @test isequal(equations(getfield(sys2, :continuous_events))[1], x ~ 2) -# @test length(ModelingToolkit.continuous_events(sys2)) == 2 -# @test isequal(equations(ModelingToolkit.continuous_events(sys2)[1])[], x ~ 2) -# @test isequal(equations(ModelingToolkit.continuous_events(sys2)[2])[], sys.x ~ 1) -# -# sys = complete(sys) -# sys_nosplit = complete(sys; split = false) -# sys2 = complete(sys2) -# -# # Test proper rootfinding -# prob = ODEProblem(sys, Pair[], (0.0, 2.0)) -# p0 = 0 -# t0 = 0 -# @test get_callback(prob) isa ModelingToolkit.DiffEqCallbacks.ContinuousCallback -# cb = ModelingToolkit.generate_continuous_callbacks(sys) -# cond = cb.condition -# out = [0.0] -# cond.f_iip(out, [0], p0, t0) -# @test out[] ≈ -1 # signature is u,p,t -# cond.f_iip(out, [1], p0, t0) -# @test out[] ≈ 0 # signature is u,p,t -# cond.f_iip(out, [2], p0, t0) -# @test out[] ≈ 1 # signature is u,p,t -# -# prob = ODEProblem(sys, Pair[], (0.0, 2.0)) -# prob_nosplit = ODEProblem(sys_nosplit, Pair[], (0.0, 2.0)) -# sol = solve(prob, Tsit5()) -# sol_nosplit = solve(prob_nosplit, Tsit5()) -# @test minimum(t -> abs(t - 1), sol.t) < 1e-10 # test that the solver stepped at the root -# @test minimum(t -> abs(t - 1), sol_nosplit.t) < 1e-10 # test that the solver stepped at the root -# -# # Test user-provided callback is respected -# test_callback = DiscreteCallback(x -> x, x -> x) -# prob = ODEProblem(sys, Pair[], (0.0, 2.0), callback = test_callback) -# prob_nosplit = ODEProblem(sys_nosplit, Pair[], (0.0, 2.0), callback = test_callback) -# cbs = get_callback(prob) -# cbs_nosplit = get_callback(prob_nosplit) -# @test cbs isa CallbackSet -# @test cbs.discrete_callbacks[1] == test_callback -# @test cbs_nosplit isa CallbackSet -# @test cbs_nosplit.discrete_callbacks[1] == test_callback -# -# prob = ODEProblem(sys2, Pair[], (0.0, 3.0)) -# cb = get_callback(prob) -# @test cb isa ModelingToolkit.DiffEqCallbacks.VectorContinuousCallback -# -# cond = cb.condition -# out = [0.0, 0.0] -# # the root to find is 2 -# cond.f_iip(out, [0, 0], p0, t0) -# @test out[1] ≈ -2 # signature is u,p,t -# cond.f_iip(out, [1, 0], p0, t0) -# @test out[1] ≈ -1 # signature is u,p,t -# cond.f_iip(out, [2, 0], p0, t0) # this should return 0 -# @test out[1] ≈ 0 # signature is u,p,t -# -# # the root to find is 1 -# out = [0.0, 0.0] -# cond.f_iip(out, [0, 0], p0, t0) -# @test out[2] ≈ -1 # signature is u,p,t -# cond.f_iip(out, [0, 1], p0, t0) # this should return 0 -# @test out[2] ≈ 0 # signature is u,p,t -# cond.f_iip(out, [0, 2], p0, t0) -# @test out[2] ≈ 1 # signature is u,p,t -# -# sol = solve(prob, Tsit5()) -# @test minimum(t -> abs(t - 1), sol.t) < 1e-10 # test that the solver stepped at the first root -# @test minimum(t -> abs(t - 2), sol.t) < 1e-10 # test that the solver stepped at the second root -# -# @named sys = ODESystem(eqs, t, continuous_events = [x ~ 1, x ~ 2]) # two root eqs using the same unknown -# sys = complete(sys) -# prob = ODEProblem(sys, Pair[], (0.0, 3.0)) -# @test get_callback(prob) isa ModelingToolkit.DiffEqCallbacks.VectorContinuousCallback -# sol = solve(prob, Tsit5()) -# @test minimum(t -> abs(t - 1), sol.t) < 1e-10 # test that the solver stepped at the first root -# @test minimum(t -> abs(t - 2), sol.t) < 1e-10 # test that the solver stepped at the second root -#end -# -#@testset "Bouncing Ball" begin -# ###### 1D Bounce -# @variables x(t)=1 v(t)=0 -# -# root_eqs = [x ~ 0] -# affect = [v ~ -Pre(v)] -# -# @named ball = ODESystem( -# [D(x) ~ v -# D(v) ~ -9.8], t, continuous_events = root_eqs => affect) -# -# @test only(continuous_events(ball)) == -# SymbolicContinuousCallback(Equation[x ~ 0], Equation[v ~ -Pre(v)]) -# ball = structural_simplify(ball) -# -# @test length(ModelingToolkit.continuous_events(ball)) == 1 -# -# tspan = (0.0, 5.0) -# prob = ODEProblem(ball, Pair[], tspan) -# sol = solve(prob, Tsit5()) -# @test 0 <= minimum(sol[x]) <= 1e-10 # the ball never went through the floor but got very close -# -# ###### 2D bouncing ball -# @variables x(t)=1 y(t)=0 vx(t)=0 vy(t)=1 -# -# events = [[x ~ 0] => [vx ~ -Pre(vx)] -# [y ~ -1.5, y ~ 1.5] => [vy ~ -Pre(vy)]] -# -# @named ball = ODESystem( -# [D(x) ~ vx -# D(y) ~ vy -# D(vx) ~ -9.8 -# D(vy) ~ -0.01vy], t; continuous_events = events) -# -# _ball = ball -# ball = structural_simplify(_ball) -# ball_nosplit = structural_simplify(_ball; split = false) -# -# tspan = (0.0, 5.0) -# prob = ODEProblem(ball, Pair[], tspan) -# prob_nosplit = ODEProblem(ball_nosplit, Pair[], tspan) -# -# cb = get_callback(prob) -# @test cb isa ModelingToolkit.DiffEqCallbacks.VectorContinuousCallback -# @test getfield(ball, :continuous_events)[1] == -# SymbolicContinuousCallback(Equation[x ~ 0], Equation[vx ~ -Pre(vx)]) -# @test getfield(ball, :continuous_events)[2] == -# SymbolicContinuousCallback(Equation[y ~ -1.5, y ~ 1.5], Equation[vy ~ -Pre(vy)]) -# cond = cb.condition -# out = [0.0, 0.0, 0.0] -# p0 = 0. -# t0 = 0. -# cond.f_iip(out, [0, 0, 0, 0], p0, t0) -# @test out ≈ [0, 1.5, -1.5] -# -# sol = solve(prob, Tsit5()) -# sol_nosplit = solve(prob_nosplit, Tsit5()) -# @test 0 <= minimum(sol[x]) <= 1e-10 # the ball never went through the floor but got very close -# @test minimum(sol[y]) ≈ -1.5 # check wall conditions -# @test maximum(sol[y]) ≈ 1.5 # check wall conditions -# @test 0 <= minimum(sol_nosplit[x]) <= 1e-10 # the ball never went through the floor but got very close -# @test minimum(sol_nosplit[y]) ≈ -1.5 # check wall conditions -# @test maximum(sol_nosplit[y]) ≈ 1.5 # check wall conditions -# -# ## Test multi-variable affect -# # in this test, there are two variables affected by a single event. -# events = [[x ~ 0] => [vx ~ -Pre(vx), vy ~ -Pre(vy)]] -# -# @named ball = ODESystem([D(x) ~ vx -# D(y) ~ vy -# D(vx) ~ -1 -# D(vy) ~ 0], t; continuous_events = events) -# -# ball_nosplit = structural_simplify(ball) -# ball = structural_simplify(ball) -# -# tspan = (0.0, 5.0) -# prob = ODEProblem(ball, Pair[], tspan) -# prob_nosplit = ODEProblem(ball_nosplit, Pair[], tspan) -# sol = solve(prob, Tsit5()) -# sol_nosplit = solve(prob_nosplit, Tsit5()) -# @test 0 <= minimum(sol[x]) <= 1e-10 # the ball never went through the floor but got very close -# @test -minimum(sol[y]) ≈ maximum(sol[y]) ≈ sqrt(2) # the ball will never go further than √2 in either direction (gravity was changed to 1 to get this particular number) -# @test 0 <= minimum(sol_nosplit[x]) <= 1e-10 # the ball never went through the floor but got very close -# @test -minimum(sol_nosplit[y]) ≈ maximum(sol_nosplit[y]) ≈ sqrt(2) # the ball will never go further than √2 in either direction (gravity was changed to 1 to get this particular number) -#end -# -## issue https://github.com/SciML/ModelingToolkit.jl/issues/1386 -## tests that it works for ODAESystem -#@testset "ODAESystem" begin -# @variables vs(t) v(t) vmeasured(t) -# eq = [vs ~ sin(2pi * t) -# D(v) ~ vs - v -# D(vmeasured) ~ 0.0] -# ev = [sin(20pi * t) ~ 0.0] => [vmeasured ~ Pre(v)] -# @named sys = ODESystem(eq, t, continuous_events = ev) -# sys = structural_simplify(sys) -# prob = ODEProblem(sys, zeros(2), (0.0, 5.1)) -# sol = solve(prob, Tsit5()) -# @test all(minimum((0:0.1:5) .- sol.t', dims = 2) .< 0.0001) # test that the solver stepped every 0.1s as dictated by event -# @test sol([0.25])[vmeasured][] == sol([0.23])[vmeasured][] # test the hold property -#end -# -### https://github.com/SciML/ModelingToolkit.jl/issues/1528 -#@testset "Handle Empty Events" begin -# Dₜ = D -# -# @parameters u(t) [input = true] # Indicate that this is a controlled input -# @parameters y(t) [output = true] # Indicate that this is a measured output -# -# function Mass(; name, m = 1.0, p = 0, v = 0) -# ps = @parameters m = m -# sts = @variables pos(t)=p vel(t)=v -# eqs = Dₜ(pos) ~ vel -# ODESystem(eqs, t, [pos, vel], ps; name) -# end -# function Spring(; name, k = 1e4) -# ps = @parameters k = k -# @variables x(t) = 0 # Spring deflection -# ODESystem(Equation[], t, [x], ps; name) -# end -# function Damper(; name, c = 10) -# ps = @parameters c = c -# @variables vel(t) = 0 -# ODESystem(Equation[], t, [vel], ps; name) -# end -# function SpringDamper(; name, k = false, c = false) -# spring = Spring(; name = :spring, k) -# damper = Damper(; name = :damper, c) -# compose(ODESystem(Equation[], t; name), -# spring, damper) -# end -# connect_sd(sd, m1, m2) = [ -# sd.spring.x ~ m1.pos - m2.pos, sd.damper.vel ~ m1.vel - m2.vel] -# sd_force(sd) = -sd.spring.k * sd.spring.x - sd.damper.c * sd.damper.vel -# @named mass1 = Mass(; m = 1) -# @named mass2 = Mass(; m = 1) -# @named sd = SpringDamper(; k = 1000, c = 10) -# function Model(u, d = 0) -# eqs = [connect_sd(sd, mass1, mass2) -# Dₜ(mass1.vel) ~ (sd_force(sd) + u) / mass1.m -# Dₜ(mass2.vel) ~ (-sd_force(sd) + d) / mass2.m] -# @named _model = ODESystem(eqs, t; observed = [y ~ mass2.pos]) -# @named model = compose(_model, mass1, mass2, sd) -# end -# model = Model(sin(30t)) -# sys = structural_simplify(model) -# @test isempty(ModelingToolkit.continuous_events(sys)) -#end +@testset "SymbolicContinuousCallback constructors" begin + e = SymbolicContinuousCallback(eqs[]) + @test e isa SymbolicContinuousCallback + @test isequal(equations(e), eqs) + @test e.affect == nothing + @test e.affect_neg == nothing + @test e.rootfind == SciMLBase.LeftRootFind + + e = SymbolicContinuousCallback(eqs) + @test e isa SymbolicContinuousCallback + @test isequal(equations(e), eqs) + @test e.affect == nothing + @test e.affect_neg == nothing + @test e.rootfind == SciMLBase.LeftRootFind + + e = SymbolicContinuousCallback(eqs, nothing) + @test e isa SymbolicContinuousCallback + @test isequal(equations(e), eqs) + @test e.affect == nothing + @test e.affect_neg == nothing + @test e.rootfind == SciMLBase.LeftRootFind + + e = SymbolicContinuousCallback(eqs[], nothing) + @test e isa SymbolicContinuousCallback + @test isequal(equations(e), eqs) + @test e.affect == nothing + @test e.affect_neg == nothing + @test e.rootfind == SciMLBase.LeftRootFind + + e = SymbolicContinuousCallback(eqs => nothing) + @test e isa SymbolicContinuousCallback + @test isequal(equations(e), eqs) + @test e.affect == nothing + @test e.affect_neg == nothing + @test e.rootfind == SciMLBase.LeftRootFind + + e = SymbolicContinuousCallback(eqs[] => nothing) + @test e isa SymbolicContinuousCallback + @test isequal(equations(e), eqs) + @test e.affect == nothing + @test e.affect_neg == nothing + @test e.rootfind == SciMLBase.LeftRootFind + + ## With affect + e = SymbolicContinuousCallback(eqs[], affect) + @test e isa SymbolicContinuousCallback + @test isequal(equations(e), eqs) + @test e.rootfind == SciMLBase.LeftRootFind + + # with only positive edge affect + e = SymbolicContinuousCallback(eqs[], affect, affect_neg = nothing) + @test e isa SymbolicContinuousCallback + @test isequal(equations(e), eqs) + @test isnothing(e.affect_neg) + @test e.rootfind == SciMLBase.LeftRootFind + + # with explicit edge affects + e = SymbolicContinuousCallback(eqs[], affect, affect_neg = affect_neg) + @test e isa SymbolicContinuousCallback + @test isequal(equations(e), eqs) + @test e.rootfind == SciMLBase.LeftRootFind + + # with different root finding ops + e = SymbolicContinuousCallback( + eqs[], affect, affect_neg = affect_neg, rootfind = SciMLBase.LeftRootFind) + @test e isa SymbolicContinuousCallback + @test isequal(equations(e), eqs) + @test e.rootfind == SciMLBase.LeftRootFind + + # test plural constructor + e = SymbolicContinuousCallbacks(eqs[]) + @test e isa Vector{SymbolicContinuousCallback} + @test isequal(equations(e[]), eqs) + @test e[].affect == nothing + + e = SymbolicContinuousCallbacks(eqs) + @test e isa Vector{SymbolicContinuousCallback} + @test isequal(equations(e[]), eqs) + @test e[].affect == nothing + + e = SymbolicContinuousCallbacks(eqs[] => affect) + @test e isa Vector{SymbolicContinuousCallback} + @test isequal(equations(e[]), eqs) + @test e[].affect isa AffectSystem + + e = SymbolicContinuousCallbacks(eqs => affect) + @test e isa Vector{SymbolicContinuousCallback} + @test isequal(equations(e[]), eqs) + @test e[].affect isa AffectSystem + + e = SymbolicContinuousCallbacks([eqs[] => affect]) + @test e isa Vector{SymbolicContinuousCallback} + @test isequal(equations(e[]), eqs) + @test e[].affect isa AffectSystem + + e = SymbolicContinuousCallbacks([eqs => affect]) + @test e isa Vector{SymbolicContinuousCallback} + @test isequal(equations(e[]), eqs) + @test e[].affect isa AffectSystem +end + +@testset "ImperativeAffect constructors" begin + fmfa(o, x, i, c) = nothing + m = ModelingToolkit.ImperativeAffect(fmfa) + @test m isa ModelingToolkit.ImperativeAffect + @test m.f == fmfa + @test m.obs == [] + @test m.obs_syms == [] + @test m.modified == [] + @test m.mod_syms == [] + @test m.ctx === nothing + + m = ModelingToolkit.ImperativeAffect(fmfa, (;)) + @test m isa ModelingToolkit.ImperativeAffect + @test m.f == fmfa + @test m.obs == [] + @test m.obs_syms == [] + @test m.modified == [] + @test m.mod_syms == [] + @test m.ctx === nothing + + m = ModelingToolkit.ImperativeAffect(fmfa, (; x)) + @test m isa ModelingToolkit.ImperativeAffect + @test m.f == fmfa + @test isequal(m.obs, []) + @test m.obs_syms == [] + @test isequal(m.modified, [x]) + @test m.mod_syms == [:x] + @test m.ctx === nothing + + m = ModelingToolkit.ImperativeAffect(fmfa, (; y = x)) + @test m isa ModelingToolkit.ImperativeAffect + @test m.f == fmfa + @test isequal(m.obs, []) + @test m.obs_syms == [] + @test isequal(m.modified, [x]) + @test m.mod_syms == [:y] + @test m.ctx === nothing + + m = ModelingToolkit.ImperativeAffect(fmfa; observed = (; y = x)) + @test m isa ModelingToolkit.ImperativeAffect + @test m.f == fmfa + @test isequal(m.obs, [x]) + @test m.obs_syms == [:y] + @test m.modified == [] + @test m.mod_syms == [] + @test m.ctx === nothing + + m = ModelingToolkit.ImperativeAffect(fmfa; modified = (; x)) + @test m isa ModelingToolkit.ImperativeAffect + @test m.f == fmfa + @test isequal(m.obs, []) + @test m.obs_syms == [] + @test isequal(m.modified, [x]) + @test m.mod_syms == [:x] + @test m.ctx === nothing + + m = ModelingToolkit.ImperativeAffect(fmfa; modified = (; y = x)) + @test m isa ModelingToolkit.ImperativeAffect + @test m.f == fmfa + @test isequal(m.obs, []) + @test m.obs_syms == [] + @test isequal(m.modified, [x]) + @test m.mod_syms == [:y] + @test m.ctx === nothing + + m = ModelingToolkit.ImperativeAffect(fmfa, (; x), (; x)) + @test m isa ModelingToolkit.ImperativeAffect + @test m.f == fmfa + @test isequal(m.obs, [x]) + @test m.obs_syms == [:x] + @test isequal(m.modified, [x]) + @test m.mod_syms == [:x] + @test m.ctx === nothing + + m = ModelingToolkit.ImperativeAffect(fmfa, (; y = x), (; y = x)) + @test m isa ModelingToolkit.ImperativeAffect + @test m.f == fmfa + @test isequal(m.obs, [x]) + @test m.obs_syms == [:y] + @test isequal(m.modified, [x]) + @test m.mod_syms == [:y] + @test m.ctx === nothing + + m = ModelingToolkit.ImperativeAffect( + fmfa; modified = (; y = x), observed = (; y = x)) + @test m isa ModelingToolkit.ImperativeAffect + @test m.f == fmfa + @test isequal(m.obs, [x]) + @test m.obs_syms == [:y] + @test isequal(m.modified, [x]) + @test m.mod_syms == [:y] + @test m.ctx === nothing + + m = ModelingToolkit.ImperativeAffect( + fmfa; modified = (; y = x), observed = (; y = x), ctx = 3) + @test m isa ModelingToolkit.ImperativeAffect + @test m.f == fmfa + @test isequal(m.obs, [x]) + @test m.obs_syms == [:y] + @test isequal(m.modified, [x]) + @test m.mod_syms == [:y] + @test m.ctx === 3 + + m = ModelingToolkit.ImperativeAffect(fmfa, (; x), (; x), 3) + @test m isa ModelingToolkit.ImperativeAffect + @test m.f == fmfa + @test isequal(m.obs, [x]) + @test m.obs_syms == [:x] + @test isequal(m.modified, [x]) + @test m.mod_syms == [:x] + @test m.ctx === 3 +end + +@testset "Condition Compilation" begin + @named sys = ODESystem(eqs, t, continuous_events = [x ~ 1]) + @test getfield(sys, :continuous_events)[] == + SymbolicContinuousCallback(Equation[x ~ 1], nothing) + @test isequal(equations(getfield(sys, :continuous_events))[], x ~ 1) + fsys = flatten(sys) + @test isequal(equations(getfield(fsys, :continuous_events))[], x ~ 1) + + @named sys2 = ODESystem([D(x) ~ 1], t, continuous_events = [x ~ 2], systems = [sys]) + @test getfield(sys2, :continuous_events)[] == + SymbolicContinuousCallback(Equation[x ~ 2], nothing) + @test all(ModelingToolkit.continuous_events(sys2) .== [ + SymbolicContinuousCallback(Equation[x ~ 2], nothing), + SymbolicContinuousCallback(Equation[sys.x ~ 1], nothing) + ]) + + @test isequal(equations(getfield(sys2, :continuous_events))[1], x ~ 2) + @test length(ModelingToolkit.continuous_events(sys2)) == 2 + @test isequal(equations(ModelingToolkit.continuous_events(sys2)[1])[], x ~ 2) + @test isequal(equations(ModelingToolkit.continuous_events(sys2)[2])[], sys.x ~ 1) + + sys = complete(sys) + sys_nosplit = complete(sys; split = false) + sys2 = complete(sys2) + + # Test proper rootfinding + prob = ODEProblem(sys, Pair[], (0.0, 2.0)) + p0 = 0 + t0 = 0 + @test get_callback(prob) isa ModelingToolkit.DiffEqCallbacks.ContinuousCallback + cb = ModelingToolkit.generate_continuous_callbacks(sys) + cond = cb.condition + out = [0.0] + cond.f_iip(out, [0], p0, t0) + @test out[] ≈ -1 # signature is u,p,t + cond.f_iip(out, [1], p0, t0) + @test out[] ≈ 0 # signature is u,p,t + cond.f_iip(out, [2], p0, t0) + @test out[] ≈ 1 # signature is u,p,t + + prob = ODEProblem(sys, Pair[], (0.0, 2.0)) + prob_nosplit = ODEProblem(sys_nosplit, Pair[], (0.0, 2.0)) + sol = solve(prob, Tsit5()) + sol_nosplit = solve(prob_nosplit, Tsit5()) + @test minimum(t -> abs(t - 1), sol.t) < 1e-10 # test that the solver stepped at the root + @test minimum(t -> abs(t - 1), sol_nosplit.t) < 1e-10 # test that the solver stepped at the root + + # Test user-provided callback is respected + test_callback = DiscreteCallback(x -> x, x -> x) + prob = ODEProblem(sys, Pair[], (0.0, 2.0), callback = test_callback) + prob_nosplit = ODEProblem(sys_nosplit, Pair[], (0.0, 2.0), callback = test_callback) + cbs = get_callback(prob) + cbs_nosplit = get_callback(prob_nosplit) + @test cbs isa CallbackSet + @test cbs.discrete_callbacks[1] == test_callback + @test cbs_nosplit isa CallbackSet + @test cbs_nosplit.discrete_callbacks[1] == test_callback + + prob = ODEProblem(sys2, Pair[], (0.0, 3.0)) + cb = get_callback(prob) + @test cb isa ModelingToolkit.DiffEqCallbacks.VectorContinuousCallback + + cond = cb.condition + out = [0.0, 0.0] + # the root to find is 2 + cond.f_iip(out, [0, 0], p0, t0) + @test out[1] ≈ -2 # signature is u,p,t + cond.f_iip(out, [1, 0], p0, t0) + @test out[1] ≈ -1 # signature is u,p,t + cond.f_iip(out, [2, 0], p0, t0) # this should return 0 + @test out[1] ≈ 0 # signature is u,p,t + + # the root to find is 1 + out = [0.0, 0.0] + cond.f_iip(out, [0, 0], p0, t0) + @test out[2] ≈ -1 # signature is u,p,t + cond.f_iip(out, [0, 1], p0, t0) # this should return 0 + @test out[2] ≈ 0 # signature is u,p,t + cond.f_iip(out, [0, 2], p0, t0) + @test out[2] ≈ 1 # signature is u,p,t + + sol = solve(prob, Tsit5()) + @test minimum(t -> abs(t - 1), sol.t) < 1e-10 # test that the solver stepped at the first root + @test minimum(t -> abs(t - 2), sol.t) < 1e-10 # test that the solver stepped at the second root + + @named sys = ODESystem(eqs, t, continuous_events = [x ~ 1, x ~ 2]) # two root eqs using the same unknown + sys = complete(sys) + prob = ODEProblem(sys, Pair[], (0.0, 3.0)) + @test get_callback(prob) isa ModelingToolkit.DiffEqCallbacks.VectorContinuousCallback + sol = solve(prob, Tsit5()) + @test minimum(t -> abs(t - 1), sol.t) < 1e-10 # test that the solver stepped at the first root + @test minimum(t -> abs(t - 2), sol.t) < 1e-10 # test that the solver stepped at the second root +end + +@testset "Bouncing Ball" begin + ###### 1D Bounce + @variables x(t)=1 v(t)=0 + + root_eqs = [x ~ 0] + affect = [v ~ -Pre(v)] + + @named ball = ODESystem( + [D(x) ~ v + D(v) ~ -9.8], t, continuous_events = root_eqs => affect) + + @test only(continuous_events(ball)) == + SymbolicContinuousCallback(Equation[x ~ 0], Equation[v ~ -Pre(v)]) + ball = structural_simplify(ball) + + @test length(ModelingToolkit.continuous_events(ball)) == 1 + + tspan = (0.0, 5.0) + prob = ODEProblem(ball, Pair[], tspan) + sol = solve(prob, Tsit5()) + @test 0 <= minimum(sol[x]) <= 1e-10 # the ball never went through the floor but got very close + + ###### 2D bouncing ball + @variables x(t)=1 y(t)=0 vx(t)=0 vy(t)=1 + + events = [[x ~ 0] => [vx ~ -Pre(vx)] + [y ~ -1.5, y ~ 1.5] => [vy ~ -Pre(vy)]] + + @named ball = ODESystem( + [D(x) ~ vx + D(y) ~ vy + D(vx) ~ -9.8 + D(vy) ~ -0.01vy], t; continuous_events = events) + + _ball = ball + ball = structural_simplify(_ball) + ball_nosplit = structural_simplify(_ball; split = false) + + tspan = (0.0, 5.0) + prob = ODEProblem(ball, Pair[], tspan) + prob_nosplit = ODEProblem(ball_nosplit, Pair[], tspan) + + cb = get_callback(prob) + @test cb isa ModelingToolkit.DiffEqCallbacks.VectorContinuousCallback + @test getfield(ball, :continuous_events)[1] == + SymbolicContinuousCallback(Equation[x ~ 0], Equation[vx ~ -Pre(vx)]) + @test getfield(ball, :continuous_events)[2] == + SymbolicContinuousCallback(Equation[y ~ -1.5, y ~ 1.5], Equation[vy ~ -Pre(vy)]) + cond = cb.condition + out = [0.0, 0.0, 0.0] + p0 = 0. + t0 = 0. + cond.f_iip(out, [0, 0, 0, 0], p0, t0) + @test out ≈ [0, 1.5, -1.5] + + sol = solve(prob, Tsit5()) + sol_nosplit = solve(prob_nosplit, Tsit5()) + @test 0 <= minimum(sol[x]) <= 1e-10 # the ball never went through the floor but got very close + @test minimum(sol[y]) ≈ -1.5 # check wall conditions + @test maximum(sol[y]) ≈ 1.5 # check wall conditions + @test 0 <= minimum(sol_nosplit[x]) <= 1e-10 # the ball never went through the floor but got very close + @test minimum(sol_nosplit[y]) ≈ -1.5 # check wall conditions + @test maximum(sol_nosplit[y]) ≈ 1.5 # check wall conditions + + ## Test multi-variable affect + # in this test, there are two variables affected by a single event. + events = [[x ~ 0] => [vx ~ -Pre(vx), vy ~ -Pre(vy)]] + + @named ball = ODESystem([D(x) ~ vx + D(y) ~ vy + D(vx) ~ -1 + D(vy) ~ 0], t; continuous_events = events) + + ball_nosplit = structural_simplify(ball) + ball = structural_simplify(ball) + + tspan = (0.0, 5.0) + prob = ODEProblem(ball, Pair[], tspan) + prob_nosplit = ODEProblem(ball_nosplit, Pair[], tspan) + sol = solve(prob, Tsit5()) + sol_nosplit = solve(prob_nosplit, Tsit5()) + @test 0 <= minimum(sol[x]) <= 1e-10 # the ball never went through the floor but got very close + @test -minimum(sol[y]) ≈ maximum(sol[y]) ≈ sqrt(2) # the ball will never go further than √2 in either direction (gravity was changed to 1 to get this particular number) + @test 0 <= minimum(sol_nosplit[x]) <= 1e-10 # the ball never went through the floor but got very close + @test -minimum(sol_nosplit[y]) ≈ maximum(sol_nosplit[y]) ≈ sqrt(2) # the ball will never go further than √2 in either direction (gravity was changed to 1 to get this particular number) +end + +# issue https://github.com/SciML/ModelingToolkit.jl/issues/1386 +# tests that it works for ODAESystem +@testset "ODAESystem" begin + @variables vs(t) v(t) vmeasured(t) + eq = [vs ~ sin(2pi * t) + D(v) ~ vs - v + D(vmeasured) ~ 0.0] + ev = [sin(20pi * t) ~ 0.0] => [vmeasured ~ Pre(v)] + @named sys = ODESystem(eq, t, continuous_events = ev) + sys = structural_simplify(sys) + prob = ODEProblem(sys, zeros(2), (0.0, 5.1)) + sol = solve(prob, Tsit5()) + @test all(minimum((0:0.1:5) .- sol.t', dims = 2) .< 0.0001) # test that the solver stepped every 0.1s as dictated by event + @test sol([0.25])[vmeasured][] == sol([0.23])[vmeasured][] # test the hold property +end + +## https://github.com/SciML/ModelingToolkit.jl/issues/1528 +@testset "Handle Empty Events" begin + Dₜ = D + + @parameters u(t) [input = true] # Indicate that this is a controlled input + @parameters y(t) [output = true] # Indicate that this is a measured output + + function Mass(; name, m = 1.0, p = 0, v = 0) + ps = @parameters m = m + sts = @variables pos(t)=p vel(t)=v + eqs = Dₜ(pos) ~ vel + ODESystem(eqs, t, [pos, vel], ps; name) + end + function Spring(; name, k = 1e4) + ps = @parameters k = k + @variables x(t) = 0 # Spring deflection + ODESystem(Equation[], t, [x], ps; name) + end + function Damper(; name, c = 10) + ps = @parameters c = c + @variables vel(t) = 0 + ODESystem(Equation[], t, [vel], ps; name) + end + function SpringDamper(; name, k = false, c = false) + spring = Spring(; name = :spring, k) + damper = Damper(; name = :damper, c) + compose(ODESystem(Equation[], t; name), + spring, damper) + end + connect_sd(sd, m1, m2) = [ + sd.spring.x ~ m1.pos - m2.pos, sd.damper.vel ~ m1.vel - m2.vel] + sd_force(sd) = -sd.spring.k * sd.spring.x - sd.damper.c * sd.damper.vel + @named mass1 = Mass(; m = 1) + @named mass2 = Mass(; m = 1) + @named sd = SpringDamper(; k = 1000, c = 10) + function Model(u, d = 0) + eqs = [connect_sd(sd, mass1, mass2) + Dₜ(mass1.vel) ~ (sd_force(sd) + u) / mass1.m + Dₜ(mass2.vel) ~ (-sd_force(sd) + d) / mass2.m] + @named _model = ODESystem(eqs, t; observed = [y ~ mass2.pos]) + @named model = compose(_model, mass1, mass2, sd) + end + model = Model(sin(30t)) + sys = structural_simplify(model) + @test isempty(ModelingToolkit.continuous_events(sys)) +end @testset "SDE/ODESystem Discrete Callbacks" begin function testsol(sys, probtype, solver, u0, p, tspan; tstops = Float64[], paramtotest = nothing, @@ -1319,7 +1319,7 @@ end eqs = [y ~ g^2 - x, D(x) ~ x] c_evt = SymbolicContinuousCallback([t ~ 5.0], [x ~ Pre(x) + 1, g ~ Pre(g) + 1], discrete_parameters = [g], iv = t) @mtkbuild sys = ODESystem(eqs, t, continuous_events = c_evt) - prob = ODEProblem(sys, [x => 1.0], (0., 10.), [g => 2], guesses = [y => 0.]) + prob = ODEProblem(sys, [x => 1.0], (0., 10.), [g => 2]) sol = solve(prob, FBDF()) @test sol.ps[g] ≈ [2., 3.] @test ≈(sol(5.00000001, idxs = x) - sol(4.9999999, idxs = x), 1; rtol = 1e-4) From 220017d0bde23371b0021248651c4ca61b385477 Mon Sep 17 00:00:00 2001 From: vyudu Date: Wed, 26 Mar 2025 15:45:28 -0400 Subject: [PATCH 29/52] fix: use is_diff_equation with flatten_equations --- src/systems/diffeqs/odesystem.jl | 2 +- src/systems/diffeqs/sdesystem.jl | 2 +- src/systems/discrete_system/implicit_discrete_system.jl | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/systems/diffeqs/odesystem.jl b/src/systems/diffeqs/odesystem.jl index 127ced51d1..5528c4a00b 100644 --- a/src/systems/diffeqs/odesystem.jl +++ b/src/systems/diffeqs/odesystem.jl @@ -311,7 +311,7 @@ function ODESystem(deqs::AbstractVector{<:Equation}, iv, dvs, ps; throw(ArgumentError("System names must be unique.")) end - algeeqs = filter(eq -> eq.lhs isa Union{Symbolic, Number} && !is_diff_equation(eq), deqs) + algeeqs = filter(eq -> eq.lhs isa Union{Symbolic, Number} && !is_diff_equation(eq), flatten_equations(deqs)) cont_callbacks = SymbolicContinuousCallbacks(continuous_events; algeeqs, iv) disc_callbacks = SymbolicDiscreteCallbacks(discrete_events; algeeqs, iv) diff --git a/src/systems/diffeqs/sdesystem.jl b/src/systems/diffeqs/sdesystem.jl index 22cc9b9131..9724996991 100644 --- a/src/systems/diffeqs/sdesystem.jl +++ b/src/systems/diffeqs/sdesystem.jl @@ -268,7 +268,7 @@ function SDESystem(deqs::AbstractVector{<:Equation}, neqs::AbstractArray, iv, dv Wfact = RefValue(EMPTY_JAC) Wfact_t = RefValue(EMPTY_JAC) - algeeqs = filter(eq -> eq.lhs isa Union{Symbolic, Number} && !is_diff_equation(eq), deqs) + algeeqs = filter(eq -> eq.lhs isa Union{Symbolic, Number} && !is_diff_equation(eq), flatten_equations(deqs)) cont_callbacks = SymbolicContinuousCallbacks(continuous_events; algeeqs, iv) disc_callbacks = SymbolicDiscreteCallbacks(discrete_events; algeeqs, iv) if is_dde === nothing diff --git a/src/systems/discrete_system/implicit_discrete_system.jl b/src/systems/discrete_system/implicit_discrete_system.jl index 9ba8212e74..0e83f2e050 100644 --- a/src/systems/discrete_system/implicit_discrete_system.jl +++ b/src/systems/discrete_system/implicit_discrete_system.jl @@ -298,11 +298,11 @@ function shift_u0map_forward(sys::ImplicitDiscreteSystem, u0map, defs) v = u0map[k] if !((op = operation(k)) isa Shift) isnothing(getunshifted(k)) && - @warn "Initial condition given in term of current state of the unknown. If `build_initializeprob = false, this may be overriden by the implicit discrete solver." + @warn "Initial condition given in term of current state of the unknown. If `build_initializeprob = false`, this may be overriden by the implicit discrete solver." updated[k] = v elseif op.steps > 0 - error("Initial conditions must be for the past state of the unknowns. Instead of providing the condition for $k, provide the condition for $(Shift(iv, -1)(only(arguments(k)))).") + error("Initial conditions must be for the current or past state of the unknowns. Instead of providing the condition for $k, provide the condition for $(Shift(iv, -1)(only(arguments(k)))).") else updated[k] = v end From dc69ba833d952a23ce98d415d3d1a58af7478843 Mon Sep 17 00:00:00 2001 From: vyudu Date: Wed, 26 Mar 2025 15:50:27 -0400 Subject: [PATCH 30/52] remove show --- src/systems/callbacks.jl | 2 +- src/systems/index_cache.jl | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/systems/callbacks.jl b/src/systems/callbacks.jl index 28a617678d..71f2960817 100644 --- a/src/systems/callbacks.jl +++ b/src/systems/callbacks.jl @@ -116,7 +116,7 @@ Base.show(io::IO, x::Pre) = print(io, "Pre") input_timedomain(::Pre, _ = nothing) = ContinuousClock() output_timedomain(::Pre, _ = nothing) = ContinuousClock() unPre(x::Num) = unPre(unwrap(x)) -unPre(x::BasicSymbolic) = operation(x) isa Pre ? only(arguments(x)) : x +unPre(x::BasicSymbolic) = (iscall(x) && operation(x) isa Pre) ? only(arguments(x)) : x function (p::Pre)(x) iw = Symbolics.iswrapped(x) diff --git a/src/systems/index_cache.jl b/src/systems/index_cache.jl index e3812c79a6..5141f71e76 100644 --- a/src/systems/index_cache.jl +++ b/src/systems/index_cache.jl @@ -127,7 +127,6 @@ function IndexCache(sys::AbstractSystem) end for sym in discs - @show sym is_parameter(sys, sym) || error("Expected discrete variable $sym in callback to be a parameter") From 4514a614eca6e24c36bced50fb7cdba93b113a0b Mon Sep 17 00:00:00 2001 From: vyudu Date: Wed, 26 Mar 2025 19:26:20 -0400 Subject: [PATCH 31/52] format --- src/systems/callbacks.jl | 167 +++++++++++------- src/systems/diffeqs/odesystem.jl | 3 +- src/systems/diffeqs/sdesystem.jl | 3 +- .../discrete_system/discrete_system.jl | 2 +- .../implicit_discrete_system.jl | 5 +- src/systems/index_cache.jl | 3 +- src/systems/model_parsing.jl | 3 +- src/systems/systemstructure.jl | 1 - test/accessor_functions.jl | 8 +- test/symbolic_events.jl | 72 ++++---- 10 files changed, 163 insertions(+), 104 deletions(-) diff --git a/src/systems/callbacks.jl b/src/systems/callbacks.jl index 71f2960817..97141b3405 100644 --- a/src/systems/callbacks.jl +++ b/src/systems/callbacks.jl @@ -72,7 +72,7 @@ aff_to_sys(a::AffectSystem) = a.aff_to_sys previous_vals(a::AffectSystem) = parameters(system(a)) all_equations(a::AffectSystem) = vcat(equations(system(a)), observed(system(a))) -function Base.show(iio::IO, aff::AffectSystem) +function Base.show(iio::IO, aff::AffectSystem) println(iio, "Affect system defined by equations:") eqs = all_equations(aff) show(iio, eqs) @@ -81,8 +81,8 @@ end function Base.:(==)(a1::AffectSystem, a2::AffectSystem) isequal(system(a1), system(a2)) && isequal(discretes(a1), discretes(a2)) && - isequal(unknowns(a1), unknowns(a2)) && - isequal(parameters(a1), parameters(a2)) && + isequal(unknowns(a1), unknowns(a2)) && + isequal(parameters(a1), parameters(a2)) && isequal(aff_to_sys(a1), aff_to_sys(a2)) end @@ -94,7 +94,7 @@ function Base.hash(a::AffectSystem, s::UInt) hash(aff_to_sys(a), s) end -function vars!(vars, aff::Union{FunctionalAffect, AffectSystem}; op = Differential) +function vars!(vars, aff::Union{FunctionalAffect, AffectSystem}; op = Differential) for var in Iterators.flatten((unknowns(aff), parameters(aff), discretes(aff))) vars!(vars, var) end @@ -202,7 +202,7 @@ Affects (i.e. `affect` and `affect_neg`) can be specified as either: DAEs will automatically be reinitialized. -Initial and final affects can also be specified with SCC, which are specified identically to positive and negative edge affects. Initialization affects +Initial and final affects can also be specified identically to positive and negative edge affects. Initialization affects will run as soon as the solver starts, while finalization affects will be executed after termination. """ struct SymbolicContinuousCallback <: AbstractCallback @@ -220,17 +220,20 @@ struct SymbolicContinuousCallback <: AbstractCallback affect_neg = affect, initialize = nothing, finalize = nothing, - rootfind = SciMLBase.LeftRootFind, + rootfind = SciMLBase.LeftRootFind, iv = nothing, algeeqs = Equation[]) - conditions = (conditions isa AbstractVector) ? conditions : [conditions] - new(conditions, make_affect(affect; iv, algeeqs, discrete_parameters), make_affect(affect_neg; iv, algeeqs, discrete_parameters), - make_affect(initialize; iv, algeeqs, discrete_parameters), make_affect(finalize; iv, algeeqs, discrete_parameters), rootfind) + new(conditions, make_affect(affect; iv, algeeqs, discrete_parameters), + make_affect(affect_neg; iv, algeeqs, discrete_parameters), + make_affect(initialize; iv, algeeqs, discrete_parameters), make_affect( + finalize; iv, algeeqs, discrete_parameters), rootfind) end # Default affect to nothing end -SymbolicContinuousCallback(p::Pair, args...; kwargs...) = SymbolicContinuousCallback(p[1], p[2], args...; kwargs...) +function SymbolicContinuousCallback(p::Pair, args...; kwargs...) + SymbolicContinuousCallback(p[1], p[2], args...; kwargs...) +end SymbolicContinuousCallback(cb::SymbolicContinuousCallback, args...; kwargs...) = cb make_affect(affect::Nothing; kwargs...) = nothing @@ -238,27 +241,31 @@ make_affect(affect::Tuple; kwargs...) = FunctionalAffect(affect...) make_affect(affect::NamedTuple; kwargs...) = FunctionalAffect(; affect...) make_affect(affect::Affect; kwargs...) = affect -function make_affect(affect::Vector{Equation}; discrete_parameters::AbstractVector = Any[], iv = nothing, algeeqs::Vector{Equation} = Equation[]) +function make_affect(affect::Vector{Equation}; discrete_parameters::AbstractVector = Any[], + iv = nothing, algeeqs::Vector{Equation} = Equation[]) isempty(affect) && return nothing - isempty(algeeqs) && @warn "No algebraic equations were found for the callback defined by $(join(affect, ", ")). If the system has no algebraic equations, this can be disregarded. Otherwise pass in `algeeqs` to the SymbolicContinuousCallback constructor." + isempty(algeeqs) && + @warn "No algebraic equations were found for the callback defined by $(join(affect, ", ")). If the system has no algebraic equations, this can be disregarded. Otherwise pass in `algeeqs` to the SymbolicContinuousCallback constructor." if isnothing(iv) iv = t_nounits @warn "No independent variable specified. Defaulting to t_nounits." end for p in discrete_parameters - occursin(unwrap(iv), unwrap(p)) || error("Non-time dependent parameter $p passed in as a discrete. Must be declared as @parameters $p(t).") + occursin(unwrap(iv), unwrap(p)) || + error("Non-time dependent parameter $p passed in as a discrete. Must be declared as @parameters $p(t).") end dvs = OrderedSet() params = OrderedSet() for eq in affect - if !haspre(eq) && !(symbolic_type(eq.rhs) === NotSymbolic() || symbolic_type(eq.lhs) === NotSymbolic()) + if !haspre(eq) && !(symbolic_type(eq.rhs) === NotSymbolic() || + symbolic_type(eq.lhs) === NotSymbolic()) @warn "Affect equation $eq has no `Pre` operator. As such it will be interpreted as an algebraic equation to be satisfied after the callback. If you intended to use the value of a variable x before the affect, use Pre(x)." end collect_vars!(dvs, params, eq, iv; op = Pre) end - for eq in algeeqs + for eq in algeeqs collect_vars!(dvs, params, eq, iv) end @@ -269,7 +276,10 @@ function make_affect(affect::Vector{Equation}; discrete_parameters::AbstractVect rev_map = Dict(zip(discrete_parameters, discretes)) affect = Symbolics.fast_substitute(affect, rev_map) algeeqs = Symbolics.fast_substitute(algeeqs, rev_map) - @mtkbuild affectsys = ImplicitDiscreteSystem(vcat(affect, algeeqs), iv, collect(union(dvs, discretes)), collect(union(pre_params, sys_params))) + @named affectsys = ImplicitDiscreteSystem( + vcat(affect, algeeqs), iv, collect(union(dvs, discretes)), + collect(union(pre_params, sys_params))) + affectsys = structural_simplify(affect_sys; fully_determined = false) # get accessed parameters p from Pre(p) in the callback parameters accessed_params = filter(isparameter, map(unPre, collect(pre_params))) union!(accessed_params, sys_params) @@ -278,7 +288,8 @@ function make_affect(affect::Vector{Equation}; discrete_parameters::AbstractVect aff_map[u] = u end - AffectSystem(affectsys, collect(dvs), collect(accessed_params), collect(discrete_parameters), aff_map) + AffectSystem(affectsys, collect(dvs), collect(accessed_params), + collect(discrete_parameters), aff_map) end function make_affect(affect; kwargs...) @@ -288,7 +299,8 @@ end """ Generate continuous callbacks. """ -function SymbolicContinuousCallbacks(events; discrete_parameters = Any[], algeeqs::Vector{Equation} = Equation[], iv = nothing) +function SymbolicContinuousCallbacks(events; discrete_parameters = Any[], + algeeqs::Vector{Equation} = Equation[], iv = nothing) callbacks = SymbolicContinuousCallback[] isnothing(events) && return callbacks @@ -297,7 +309,8 @@ function SymbolicContinuousCallbacks(events; discrete_parameters = Any[], algeeq for event in events cond, affs = event isa Pair ? (event[1], event[2]) : (event, nothing) - push!(callbacks, SymbolicContinuousCallback(cond, affs; iv, algeeqs, discrete_parameters)) + push!(callbacks, + SymbolicContinuousCallback(cond, affs; iv, algeeqs, discrete_parameters)) end callbacks end @@ -305,7 +318,8 @@ end function Base.show(io::IO, cb::AbstractCallback) indent = get(io, :indent, 0) iio = IOContext(io, :indent => indent + 1) - is_discrete(cb) ? print(io, "SymbolicDiscreteCallback(") : print(io, "SymbolicContinuousCallback(") + is_discrete(cb) ? print(io, "SymbolicDiscreteCallback(") : + print(io, "SymbolicContinuousCallback(") print(iio, "Conditions:") show(iio, equations(cb)) print(iio, "; ") @@ -334,7 +348,8 @@ end function Base.show(io::IO, mime::MIME"text/plain", cb::AbstractCallback) indent = get(io, :indent, 0) iio = IOContext(io, :indent => indent + 1) - is_discrete(cb) ? println(io, "SymbolicDiscreteCallback:") : println(io, "SymbolicContinuousCallback:") + is_discrete(cb) ? println(io, "SymbolicDiscreteCallback:") : + println(io, "SymbolicContinuousCallback:") println(iio, "Conditions:") show(iio, mime, equations(cb)) print(iio, "\n") @@ -405,21 +420,26 @@ struct SymbolicDiscreteCallback <: AbstractCallback function SymbolicDiscreteCallback( condition, affect = nothing; - initialize = nothing, finalize = nothing, iv = nothing, algeeqs = Equation[], discrete_parameters = Any[]) + initialize = nothing, finalize = nothing, iv = nothing, + algeeqs = Equation[], discrete_parameters = Any[]) c = is_timed_condition(condition) ? condition : value(scalarize(condition)) - new(c, make_affect(affect; iv, algeeqs, discrete_parameters), make_affect(initialize; iv, algeeqs, discrete_parameters), + new(c, make_affect(affect; iv, algeeqs, discrete_parameters), + make_affect(initialize; iv, algeeqs, discrete_parameters), make_affect(finalize; iv, algeeqs, discrete_parameters)) end # Default affect to nothing end -SymbolicDiscreteCallback(p::Pair, args...; kwargs...) = SymbolicDiscreteCallback(p[1], p[2], args...; kwargs...) +function SymbolicDiscreteCallback(p::Pair, args...; kwargs...) + SymbolicDiscreteCallback(p[1], p[2], args...; kwargs...) +end SymbolicDiscreteCallback(cb::SymbolicDiscreteCallback, args...; kwargs...) = cb """ Generate discrete callbacks. """ -function SymbolicDiscreteCallbacks(events; discrete_parameters::Vector = Any[], algeeqs::Vector{Equation} = Equation[], iv = nothing) +function SymbolicDiscreteCallbacks(events; discrete_parameters::Vector = Any[], + algeeqs::Vector{Equation} = Equation[], iv = nothing) callbacks = SymbolicDiscreteCallback[] isnothing(events) && return callbacks @@ -428,7 +448,8 @@ function SymbolicDiscreteCallbacks(events; discrete_parameters::Vector = Any[], for event in events cond, affs = event isa Pair ? (event[1], event[2]) : (event, nothing) - push!(callbacks, SymbolicDiscreteCallback(cond, affs; iv, algeeqs, discrete_parameters)) + push!(callbacks, + SymbolicDiscreteCallback(cond, affs; iv, algeeqs, discrete_parameters)) end callbacks end @@ -459,7 +480,7 @@ function namespace_affects(affect::FunctionalAffect, s) context(affect)) end -function namespace_affects(affect::AffectSystem, s) +function namespace_affects(affect::AffectSystem, s) AffectSystem(renamespace(s, system(affect)), renamespace.((s,), unknowns(affect)), renamespace.((s,), parameters(affect)), @@ -491,7 +512,8 @@ function namespace_callback(cb::SymbolicDiscreteCallback, s)::SymbolicDiscreteCa end function Base.hash(cb::AbstractCallback, s::UInt) - s = conditions(cb) isa AbstractVector ? foldr(hash, conditions(cb), init = s) : hash(conditions(cb), s) + s = conditions(cb) isa AbstractVector ? foldr(hash, conditions(cb), init = s) : + hash(conditions(cb), s) s = hash(affects(cb), s) !is_discrete(cb) && (s = hash(affect_negs(cb), s)) s = hash(initialize_affects(cb), s) @@ -533,8 +555,10 @@ end function Base.:(==)(e1::AbstractCallback, e2::AbstractCallback) (is_discrete(e1) === is_discrete(e2)) || return false (isequal(e1.conditions, e2.conditions) && isequal(e1.affect, e2.affect) && - isequal(e1.initialize, e2.initialize) && isequal(e1.finalize, e2.finalize)) || return false - is_discrete(e1) || (isequal(e1.affect_neg, e2.affect_neg) && isequal(e1.rootfind, e2.rootfind)) + isequal(e1.initialize, e2.initialize) && isequal(e1.finalize, e2.finalize)) || + return false + is_discrete(e1) || + (isequal(e1.affect_neg, e2.affect_neg) && isequal(e1.rootfind, e2.rootfind)) end Base.isempty(cb::AbstractCallback) = isempty(cb.conditions) @@ -553,7 +577,8 @@ Notes If set to `Val{false}` a `RuntimeGeneratedFunction` will be returned. - `kwargs` are passed through to `Symbolics.build_function`. """ -function compile_condition(cbs::Union{AbstractCallback, Vector{<:AbstractCallback}}, sys, dvs, ps; +function compile_condition( + cbs::Union{AbstractCallback, Vector{<:AbstractCallback}}, sys, dvs, ps; expression = Val{false}, eval_expression = false, eval_module = @__MODULE__, kwargs...) u = map(x -> time_varying_as_func(value(x), sys), dvs) p = map.(x -> time_varying_as_func(value(x), sys), reorder_parameters(sys, ps)) @@ -570,8 +595,8 @@ function compile_condition(cbs::Union{AbstractCallback, Vector{<:AbstractCallbac end fs = build_function_wrapper(sys, - condit, u, p..., t; expression, - kwargs...) + condit, u, p..., t; expression, + kwargs...) if expression == Val{true} fs = eval_or_rgf.(fs; eval_expression, eval_module) @@ -628,7 +653,8 @@ end is_discrete(cb::AbstractCallback) = cb isa SymbolicDiscreteCallback is_discrete(cb::Vector{<:AbstractCallback}) = eltype(cb) isa SymbolicDiscreteCallback -function generate_continuous_callbacks(sys::AbstractSystem, dvs = unknowns(sys), ps = parameters(sys; initial_parameters = true); kwargs...) +function generate_continuous_callbacks(sys::AbstractSystem, dvs = unknowns(sys), + ps = parameters(sys; initial_parameters = true); kwargs...) cbs = continuous_events(sys) isempty(cbs) && return nothing cb_classes = Dict{SciMLBase.RootfindOpt, Vector{SymbolicContinuousCallback}}() @@ -647,7 +673,8 @@ function generate_continuous_callbacks(sys::AbstractSystem, dvs = unknowns(sys), end end -function generate_discrete_callbacks(sys::AbstractSystem, dvs = unknowns(sys), ps = parameters(sys; initial_parameters = true); kwargs...) +function generate_discrete_callbacks(sys::AbstractSystem, dvs = unknowns(sys), + ps = parameters(sys; initial_parameters = true); kwargs...) dbs = discrete_events(sys) isempty(dbs) && return nothing [generate_callback(db, sys; kwargs...) for db in dbs] @@ -668,8 +695,9 @@ function generate_callback(cbs::Vector{SymbolicContinuousCallback}, sys; kwargs. cb_ind = findfirst(>(0), num_eqs) return generate_callback(cbs[cb_ind], sys; kwargs...) end - - trigger = compile_condition(cbs, sys, unknowns(sys), parameters(sys; initial_parameters = true); kwargs...) + + trigger = compile_condition( + cbs, sys, unknowns(sys), parameters(sys; initial_parameters = true); kwargs...) affects = [] affect_negs = [] inits = [] @@ -677,9 +705,13 @@ function generate_callback(cbs::Vector{SymbolicContinuousCallback}, sys; kwargs. for cb in cbs affect = compile_affect(cb.affect, cb, sys; default = EMPTY_AFFECT, kwargs...) push!(affects, affect) - affect_neg = (cb.affect_neg === cb.affect) ? affect : compile_affect(cb.affect_neg, cb, sys; default = EMPTY_AFFECT, kwargs...) + affect_neg = (cb.affect_neg === cb.affect) ? affect : + compile_affect( + cb.affect_neg, cb, sys; default = EMPTY_AFFECT, kwargs...) push!(affect_negs, affect_neg) - push!(inits, compile_affect(cb.initialize, cb, sys; default = nothing, is_init = true, kwargs...)) + push!(inits, + compile_affect( + cb.initialize, cb, sys; default = nothing, is_init = true, kwargs...)) push!(finals, compile_affect(cb.finalize, cb, sys; default = nothing, kwargs...)) end @@ -701,8 +733,8 @@ function generate_callback(cbs::Vector{SymbolicContinuousCallback}, sys; kwargs. finalize = wrap_vector_optional_affect(finals, SciMLBase.FINALIZE_DEFAULT) return VectorContinuousCallback( - trigger, affect, affect_neg, length(eqs); initialize, finalize, - rootfind = cbs[1].rootfind, initializealg = SciMLBase.NoInit()) + trigger, affect, affect_neg, length(eqs); initialize, finalize, + rootfind = cbs[1].rootfind, initializealg = SciMLBase.NoInit()) end function generate_callback(cb, sys; kwargs...) @@ -715,10 +747,13 @@ function generate_callback(cb, sys; kwargs...) affect_neg = if is_discrete(cb) nothing else - (cb.affect === cb.affect_neg) ? affect : compile_affect(cb.affect_neg, cb, sys; default = EMPTY_AFFECT, kwargs...) + (cb.affect === cb.affect_neg) ? affect : + compile_affect(cb.affect_neg, cb, sys; default = EMPTY_AFFECT, kwargs...) end - init = compile_affect(cb.initialize, cb, sys; default = SciMLBase.INITIALIZE_DEFAULT, is_init = true, kwargs...) - final = compile_affect(cb.finalize, cb, sys; default = SciMLBase.FINALIZE_DEFAULT, kwargs...) + init = compile_affect(cb.initialize, cb, sys; default = SciMLBase.INITIALIZE_DEFAULT, + is_init = true, kwargs...) + final = compile_affect( + cb.finalize, cb, sys; default = SciMLBase.FINALIZE_DEFAULT, kwargs...) initialize = isnothing(cb.initialize) ? init : ((c, u, t, i) -> init(i)) finalize = isnothing(cb.finalize) ? final : ((c, u, t, i) -> final(i)) @@ -726,16 +761,16 @@ function generate_callback(cb, sys; kwargs...) if is_discrete(cb) if is_timed && conditions(cb) isa AbstractVector return PresetTimeCallback(trigger, affect; initialize, - finalize, initializealg = SciMLBase.NoInit()) + finalize, initializealg = SciMLBase.NoInit()) elseif is_timed - return PeriodicCallback(affect, trigger; initialize, finalize) + return PeriodicCallback(affect, trigger; initialize, finalize, initializealg = SciMLBase.NoInit()) else return DiscreteCallback(trigger, affect; initialize, - finalize, initializealg = SciMLBase.NoInit()) + finalize, initializealg = SciMLBase.NoInit()) end else return ContinuousCallback(trigger, affect, affect_neg; initialize, finalize, - rootfind = cb.rootfind, initializealg = SciMLBase.NoInit()) + rootfind = cb.rootfind, initializealg = SciMLBase.NoInit()) end end @@ -756,7 +791,8 @@ Notes - `kwargs` are passed through to `Symbolics.build_function`. """ function compile_affect( - aff::Union{Nothing, Affect}, cb::AbstractCallback, sys::AbstractSystem; default = nothing, is_init = false, kwargs...) + aff::Union{Nothing, Affect}, cb::AbstractCallback, sys::AbstractSystem; + default = nothing, is_init = false, kwargs...) save_idxs = if !(has_index_cache(sys) && (ic = get_index_cache(sys)) !== nothing) Int[] else @@ -775,6 +811,7 @@ function compile_affect( end function wrap_save_discretes(f, save_idxs) + @show save_idxs let save_idxs = save_idxs if f === SciMLBase.INITIALIZE_DEFAULT (c, u, t, i) -> begin @@ -809,7 +846,7 @@ function wrap_vector_optional_affect(funs, default) end function add_integrator_header( - sys::AbstractSystem, integrator = gensym(:MTKIntegrator), out = :u) + sys::AbstractSystem, integrator = gensym(:MTKIntegrator), out = :u) expr -> Func([DestructuredArgs(expr.args, integrator, inds = [:u, :p, :t])], [], expr.body), expr -> Func( @@ -820,7 +857,8 @@ end """ Compile an affect defined by a set of equations. Systems with algebraic equations will solve implicit discrete problems to obtain their next state. Systems without will generate functions that perform explicit updates. """ -function compile_equational_affect(aff::Union{AffectSystem, Vector{Equation}}, sys; reset_jumps = false, kwargs...) +function compile_equational_affect( + aff::Union{AffectSystem, Vector{Equation}}, sys; reset_jumps = false, kwargs...) if aff isa AbstractVector aff = make_affect(aff; iv = get_iv(sys)) end @@ -831,7 +869,8 @@ function compile_equational_affect(aff::Union{AffectSystem, Vector{Equation}}, s sys_map = Dict([v => k for (k, v) in aff_map]) if isempty(equations(affsys)) - update_eqs = Symbolics.fast_substitute(observed(affsys), Dict([p => unPre(p) for p in parameters(affsys)])) + update_eqs = Symbolics.fast_substitute( + observed(affsys), Dict([p => unPre(p) for p in parameters(affsys)])) rhss = map(x -> x.rhs, update_eqs) lhss = map(x -> aff_map[x.lhs], update_eqs) is_p = [lhs ∈ Set(ps_to_update) for lhs in lhss] @@ -851,9 +890,13 @@ function compile_equational_affect(aff::Union{AffectSystem, Vector{Equation}}, s end _ps = reorder_parameters(sys, ps) integ = gensym(:MTKIntegrator) - - u_up, u_up! = build_function_wrapper(sys, (@view rhss[is_u]), dvs, _ps..., t; wrap_code = add_integrator_header(sys, integ, :u), expression = Val{false}, outputidxs = u_idxs, wrap_mtkparameters) - p_up, p_up! = build_function_wrapper(sys, (@view rhss[is_p]), dvs, _ps..., t; wrap_code = add_integrator_header(sys, integ, :p), expression = Val{false}, outputidxs = p_idxs, wrap_mtkparameters) + + u_up, u_up! = build_function_wrapper(sys, (@view rhss[is_u]), dvs, _ps..., t; + wrap_code = add_integrator_header(sys, integ, :u), + expression = Val{false}, outputidxs = u_idxs, wrap_mtkparameters) + p_up, p_up! = build_function_wrapper(sys, (@view rhss[is_p]), dvs, _ps..., t; + wrap_code = add_integrator_header(sys, integ, :p), + expression = Val{false}, outputidxs = p_idxs, wrap_mtkparameters) return function explicit_affect!(integ) isempty(dvs_to_update) || u_up!(integ) @@ -861,7 +904,9 @@ function compile_equational_affect(aff::Union{AffectSystem, Vector{Equation}}, s reset_jumps && reset_aggregated_jumps!(integ) end else - return let dvs_to_update = dvs_to_update, aff_map = aff_map, sys_map = sys_map, affsys = affsys, ps_to_update = ps_to_update, aff = aff + return let dvs_to_update = dvs_to_update, aff_map = aff_map, sys_map = sys_map, + affsys = affsys, ps_to_update = ps_to_update, aff = aff + function implicit_affect!(integ) pmap = Pair[] for pre_p in parameters(affsys) @@ -874,9 +919,11 @@ function compile_equational_affect(aff::Union{AffectSystem, Vector{Equation}}, s uval = isparameter(aff_map[u]) ? integ.ps[aff_map[u]] : integ[u] push!(u0, u => uval) end - affprob = ImplicitDiscreteProblem(affsys, u0, (integ.t, integ.t), pmap; build_initializeprob = false, check_length = false) + affprob = ImplicitDiscreteProblem(affsys, u0, (integ.t, integ.t), pmap; + build_initializeprob = false, check_length = false) affsol = init(affprob, IDSolve()) - (check_error(affsol) === ReturnCode.InitialFailure) && throw(UnsolvableCallbackError(all_equations(aff))) + (check_error(affsol) === ReturnCode.InitialFailure) && + throw(UnsolvableCallbackError(all_equations(aff))) for u in dvs_to_update integ[u] = affsol[sys_map[u]] end @@ -893,7 +940,8 @@ struct UnsolvableCallbackError end function Base.showerror(io::IO, err::UnsolvableCallbackError) - println(io, "The callback defined by the following equations:\n\n$(join(err.eqs, "\n"))\n\nis not solvable. Please check that the algebraic equations and affect equations are correct, and that all parameters intended to be changed are passed in as `discrete_parameters`.") + println(io, + "The callback defined by the following equations:\n\n$(join(err.eqs, "\n"))\n\nis not solvable. Please check that the algebraic equations and affect equations are correct, and that all parameters intended to be changed are passed in as `discrete_parameters`.") end merge_cb(::Nothing, ::Nothing) = nothing @@ -952,7 +1000,6 @@ function discrete_events_toplevel(sys::AbstractSystem) return get_discrete_events(sys) end - """ continuous_events(sys::AbstractSystem)::Vector{SymbolicContinuousCallback} diff --git a/src/systems/diffeqs/odesystem.jl b/src/systems/diffeqs/odesystem.jl index 5528c4a00b..c36729d408 100644 --- a/src/systems/diffeqs/odesystem.jl +++ b/src/systems/diffeqs/odesystem.jl @@ -311,7 +311,8 @@ function ODESystem(deqs::AbstractVector{<:Equation}, iv, dvs, ps; throw(ArgumentError("System names must be unique.")) end - algeeqs = filter(eq -> eq.lhs isa Union{Symbolic, Number} && !is_diff_equation(eq), flatten_equations(deqs)) + algeeqs = filter(eq -> eq.lhs isa Union{Symbolic, Number} && !is_diff_equation(eq), + flatten_equations(deqs)) cont_callbacks = SymbolicContinuousCallbacks(continuous_events; algeeqs, iv) disc_callbacks = SymbolicDiscreteCallbacks(discrete_events; algeeqs, iv) diff --git a/src/systems/diffeqs/sdesystem.jl b/src/systems/diffeqs/sdesystem.jl index 9724996991..3d73674b21 100644 --- a/src/systems/diffeqs/sdesystem.jl +++ b/src/systems/diffeqs/sdesystem.jl @@ -268,7 +268,8 @@ function SDESystem(deqs::AbstractVector{<:Equation}, neqs::AbstractArray, iv, dv Wfact = RefValue(EMPTY_JAC) Wfact_t = RefValue(EMPTY_JAC) - algeeqs = filter(eq -> eq.lhs isa Union{Symbolic, Number} && !is_diff_equation(eq), flatten_equations(deqs)) + algeeqs = filter(eq -> eq.lhs isa Union{Symbolic, Number} && !is_diff_equation(eq), + flatten_equations(deqs)) cont_callbacks = SymbolicContinuousCallbacks(continuous_events; algeeqs, iv) disc_callbacks = SymbolicDiscreteCallbacks(discrete_events; algeeqs, iv) if is_dde === nothing diff --git a/src/systems/discrete_system/discrete_system.jl b/src/systems/discrete_system/discrete_system.jl index b20ad32008..e2fc03e857 100644 --- a/src/systems/discrete_system/discrete_system.jl +++ b/src/systems/discrete_system/discrete_system.jl @@ -431,7 +431,7 @@ end function Base.:(==)(sys1::DiscreteSystem, sys2::DiscreteSystem) sys1 === sys2 && return true isequal(nameof(sys1), nameof(sys2)) && - isequal(get_iv(sys1), get_iv(sys2)) && + isequal(get_iv(sys1), get_iv(sys2)) && _eq_unordered(get_eqs(sys1), get_eqs(sys2)) && _eq_unordered(get_unknowns(sys1), get_unknowns(sys2)) && _eq_unordered(get_ps(sys1), get_ps(sys2)) && diff --git a/src/systems/discrete_system/implicit_discrete_system.jl b/src/systems/discrete_system/implicit_discrete_system.jl index 0e83f2e050..f5098a1d3c 100644 --- a/src/systems/discrete_system/implicit_discrete_system.jl +++ b/src/systems/discrete_system/implicit_discrete_system.jl @@ -270,7 +270,8 @@ function flatten(sys::ImplicitDiscreteSystem, noeqs = false) end function generate_function( - sys::ImplicitDiscreteSystem, dvs = unknowns(sys), ps = parameters(sys); wrap_code = identity, cachesyms::Tuple = (), kwargs...) + sys::ImplicitDiscreteSystem, dvs = unknowns(sys), ps = parameters(sys); + wrap_code = identity, cachesyms::Tuple = (), kwargs...) iv = get_iv(sys) # Algebraic equations get shifted forward 1, to match with differential equations exprs = map(equations(sys)) do eq @@ -453,7 +454,7 @@ end function Base.:(==)(sys1::ImplicitDiscreteSystem, sys2::ImplicitDiscreteSystem) sys1 === sys2 && return true isequal(nameof(sys1), nameof(sys2)) && - isequal(get_iv(sys1), get_iv(sys2)) && + isequal(get_iv(sys1), get_iv(sys2)) && _eq_unordered(get_eqs(sys1), get_eqs(sys2)) && _eq_unordered(get_unknowns(sys1), get_unknowns(sys2)) && _eq_unordered(get_ps(sys1), get_ps(sys2)) && diff --git a/src/systems/index_cache.jl b/src/systems/index_cache.jl index 5141f71e76..d71bcc60a9 100644 --- a/src/systems/index_cache.jl +++ b/src/systems/index_cache.jl @@ -117,7 +117,8 @@ function IndexCache(sys::AbstractSystem) affs = [affs] end for affect in affs - if affect isa AffectSystem || affect isa FunctionalAffect || affect isa ImperativeAffect + if affect isa AffectSystem || affect isa FunctionalAffect || + affect isa ImperativeAffect union!(discs, unwrap.(discretes(affect))) elseif isnothing(affect) continue diff --git a/src/systems/model_parsing.jl b/src/systems/model_parsing.jl index 3243824dc0..c360e0a5ce 100644 --- a/src/systems/model_parsing.jl +++ b/src/systems/model_parsing.jl @@ -119,7 +119,8 @@ function _model_macro(mod, name, expr, isconnector) Ref(dict), [:constants, :defaults, :kwargs, :structural_parameters]) sys = :($ODESystem($(flatten_equations)(equations), $iv, variables, parameters; - name, description = $description, systems, gui_metadata = $gui_metadata, defaults, continuous_events = cont_events, discrete_events = disc_events)) + name, description = $description, systems, gui_metadata = $gui_metadata, + defaults, continuous_events = cont_events, discrete_events = disc_events)) if length(ext) == 0 push!(exprs.args, :(var"#___sys___" = $sys)) diff --git a/src/systems/systemstructure.jl b/src/systems/systemstructure.jl index b1de95d074..c0c4a5ff4d 100644 --- a/src/systems/systemstructure.jl +++ b/src/systems/systemstructure.jl @@ -688,7 +688,6 @@ function _structural_simplify!(state::TearingState, io; simplify = false, check_consistency = true, fully_determined = true, warn_initialize_determined = false, dummy_derivative = true, kwargs...) - if fully_determined isa Bool check_consistency &= fully_determined else diff --git a/test/accessor_functions.jl b/test/accessor_functions.jl index 24fb245fed..9272fb9146 100644 --- a/test/accessor_functions.jl +++ b/test/accessor_functions.jl @@ -151,14 +151,16 @@ let # Checks `continuous_events_toplevel` and `discrete_events_toplevel` (straightforward # as I stored the same single 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. - bot_cev = ModelingToolkit.SymbolicContinuousCallback(cevs[1], algeeqs = [O ~ (d + p_bot) * X_bot + Y]) - mid_dev = ModelingToolkit.SymbolicDiscreteCallback(devs[1], algeeqs = [O ~ (d + p_mid1) * X_mid1 + Y]) + bot_cev = ModelingToolkit.SymbolicContinuousCallback( + cevs[1], algeeqs = [O ~ (d + p_bot) * X_bot + Y]) + mid_dev = ModelingToolkit.SymbolicDiscreteCallback( + devs[1], algeeqs = [O ~ (d + p_mid1) * X_mid1 + Y]) @test all_sets_equal( continuous_events_toplevel.([sys_bot, sys_bot_comp, sys_bot_ss])..., [bot_cev]) @test all_sets_equal( discrete_events_toplevel.( - [sys_mid1, sys_mid1_comp, sys_mid1_ss])..., + [sys_mid1, sys_mid1_comp, sys_mid1_ss])..., [mid_dev]) @test all(sym_issubset( continuous_events_toplevel(sys), get_continuous_events(sys)) diff --git a/test/symbolic_events.jl b/test/symbolic_events.jl index e6c1df4363..e933843856 100644 --- a/test/symbolic_events.jl +++ b/test/symbolic_events.jl @@ -3,7 +3,7 @@ using SciMLStructures: canonicalize, Discrete using ModelingToolkit: SymbolicContinuousCallback, SymbolicContinuousCallbacks, SymbolicDiscreteCallback, - SymbolicDiscreteCallbacks, + SymbolicDiscreteCallbacks, get_callback, t_nounits as t, D_nounits as D, @@ -340,7 +340,7 @@ end D(v) ~ -9.8], t, continuous_events = root_eqs => affect) @test only(continuous_events(ball)) == - SymbolicContinuousCallback(Equation[x ~ 0], Equation[v ~ -Pre(v)]) + SymbolicContinuousCallback(Equation[x ~ 0], Equation[v ~ -Pre(v)]) ball = structural_simplify(ball) @test length(ModelingToolkit.continuous_events(ball)) == 1 @@ -373,13 +373,13 @@ end cb = get_callback(prob) @test cb isa ModelingToolkit.DiffEqCallbacks.VectorContinuousCallback @test getfield(ball, :continuous_events)[1] == - SymbolicContinuousCallback(Equation[x ~ 0], Equation[vx ~ -Pre(vx)]) + SymbolicContinuousCallback(Equation[x ~ 0], Equation[vx ~ -Pre(vx)]) @test getfield(ball, :continuous_events)[2] == - SymbolicContinuousCallback(Equation[y ~ -1.5, y ~ 1.5], Equation[vy ~ -Pre(vy)]) + SymbolicContinuousCallback(Equation[y ~ -1.5, y ~ 1.5], Equation[vy ~ -Pre(vy)]) cond = cb.condition out = [0.0, 0.0, 0.0] - p0 = 0. - t0 = 0. + p0 = 0.0 + t0 = 0.0 cond.f_iip(out, [0, 0, 0, 0], p0, t0) @test out ≈ [0, 1.5, -1.5] @@ -396,10 +396,11 @@ end # in this test, there are two variables affected by a single event. events = [[x ~ 0] => [vx ~ -Pre(vx), vy ~ -Pre(vy)]] - @named ball = ODESystem([D(x) ~ vx - D(y) ~ vy - D(vx) ~ -1 - D(vy) ~ 0], t; continuous_events = events) + @named ball = ODESystem( + [D(x) ~ vx + D(y) ~ vy + D(vx) ~ -1 + D(vy) ~ 0], t; continuous_events = events) ball_nosplit = structural_simplify(ball) ball = structural_simplify(ball) @@ -479,12 +480,13 @@ end end @testset "SDE/ODESystem Discrete Callbacks" begin - function testsol(sys, probtype, solver, u0, p, tspan; tstops = Float64[], paramtotest = nothing, + function testsol( + sys, probtype, solver, u0, p, tspan; tstops = Float64[], paramtotest = nothing, kwargs...) prob = probtype(complete(sys), u0, tspan, p; kwargs...) sol = solve(prob, solver(); tstops = tstops, abstol = 1e-10, reltol = 1e-10) @test isapprox(sol(1.0000000001)[1] - sol(0.999999999)[1], 1.0; rtol = 1e-6) - paramtotest === nothing || (@test sol.ps[paramtotest] == [0., 1.]) + paramtotest === nothing || (@test sol.ps[paramtotest] == [0.0, 1.0]) @test isapprox(sol(4.0)[1], 2 * exp(-2.0); rtol = 1e-6) sol end @@ -503,8 +505,7 @@ end ∂ₜ = D eqs = [∂ₜ(A) ~ -k * A] @named osys = ODESystem(eqs, t, [A], [k, t1, t2], discrete_events = [cb1, cb2]) - @named ssys = SDESystem(eqs, [0.0], t, [A], [k, t1, t2], - discrete_events = [cb1, cb2]) + @named ssys = SDESystem(eqs, [0.0], t, [A], [k, t1, t2], discrete_events = [cb1, cb2]) u0 = [A => 1.0] p = [k => 0.0, t1 => 1.0, t2 => 2.0] tspan = (0.0, 4.0) @@ -518,10 +519,12 @@ end @named ssys1 = SDESystem(eqs, [0.0], t, [A, B], [k, t1, t2], discrete_events = [cb1a, cb2]) u0′ = [A => 1.0, B => 0.0] - sol = testsol(osys1, ODEProblem, Tsit5, u0′, p, tspan; tstops = [1.0, 2.0], check_length = false, paramtotest = k) + sol = testsol(osys1, ODEProblem, Tsit5, u0′, p, tspan; + tstops = [1.0, 2.0], check_length = false, paramtotest = k) @test sol(1.0000001, idxs = B) == 2.0 - sol = testsol(ssys1, SDEProblem, RI5, u0′, p, tspan; tstops = [1.0, 2.0], check_length = false, paramtotest = k) + sol = testsol(ssys1, SDEProblem, RI5, u0′, p, tspan; tstops = [1.0, 2.0], + check_length = false, paramtotest = k) @test sol(1.0000001, idxs = B) == 2.0 # same as above - but with set-time event syntax @@ -589,7 +592,7 @@ end jprob = JumpProblem(jsys, dprob, Direct(); kwargs...) sol = solve(jprob, SSAStepper(); tstops = tstops) @test (sol(1.000000000001)[1] - sol(0.99999999999)[1]) == 1 - paramtotest === nothing || (@test sol.ps[paramtotest] == [0., 1.0]) + paramtotest === nothing || (@test sol.ps[paramtotest] == [0.0, 1.0]) @test sol(40.0)[1] == 0 sol end @@ -1282,53 +1285,56 @@ end eqs = [D(D(x)) ~ λ * x D(D(y)) ~ λ * y - g x^2 + y^2 ~ 1] - c_evt = [t ~ 5.] => [x ~ Pre(x) + 0.1] + c_evt = [t ~ 5.0] => [x ~ Pre(x) + 0.1] @mtkbuild pend = ODESystem(eqs, t, continuous_events = c_evt) - prob = ODEProblem(pend, [x => -1, y => 0], (0., 10.), [g => 1], guesses = [λ => 1]) + prob = ODEProblem(pend, [x => -1, y => 0], (0.0, 10.0), [g => 1], guesses = [λ => 1]) sol = solve(prob, FBDF()) @test ≈(sol(5.000001, idxs = x) - sol(4.999999, idxs = x), 0.1, rtol = 1e-4) @test ≈(sol(5.000001, idxs = x)^2 + sol(5.000001, idxs = y)^2, 1, rtol = 1e-4) # Implicit affect with Pre - c_evt = [t ~ 5.] => [x ~ Pre(x) + y^2] + c_evt = [t ~ 5.0] => [x ~ Pre(x) + y^2] @mtkbuild pend = ODESystem(eqs, t, continuous_events = c_evt) - prob = ODEProblem(pend, [x => 1, y => 0], (0., 10.), [g => 1], guesses = [λ => 1]) + prob = ODEProblem(pend, [x => 1, y => 0], (0.0, 10.0), [g => 1], guesses = [λ => 1]) sol = solve(prob, FBDF()) - @test ≈(sol(5.000001, idxs = y)^2 + sol(4.999999, idxs = x), sol(5.000001, idxs = x), rtol = 1e-4) + @test ≈(sol(5.000001, idxs = y)^2 + sol(4.999999, idxs = x), + sol(5.000001, idxs = x), rtol = 1e-4) @test ≈(sol(5.000001, idxs = x)^2 + sol(5.000001, idxs = y)^2, 1, rtol = 1e-4) # Impossible affect errors - c_evt = [t ~ 5.] => [x ~ Pre(x) + 2] + c_evt = [t ~ 5.0] => [x ~ Pre(x) + 2] @mtkbuild pend = ODESystem(eqs, t, continuous_events = c_evt) - prob = ODEProblem(pend, [x => 1, y => 0], (0., 10.), [g => 1], guesses = [λ => 1]) - @test_throws UnsolvableCallbackError sol = solve(prob, FBDF()) - + prob = ODEProblem(pend, [x => 1, y => 0], (0.0, 10.0), [g => 1], guesses = [λ => 1]) + @test_throws UnsolvableCallbackError sol=solve(prob, FBDF()) + # Changing both variables and parameters in the same affect. @parameters g(t) eqs = [D(D(x)) ~ λ * x D(D(y)) ~ λ * y - g x^2 + y^2 ~ 1] - c_evt = SymbolicContinuousCallback([t ~ 5.0], [x ~ Pre(x) + 0.1, g ~ Pre(g) + 1], discrete_parameters = [g], iv = t) + c_evt = SymbolicContinuousCallback( + [t ~ 5.0], [x ~ Pre(x) + 0.1, g ~ Pre(g) + 1], discrete_parameters = [g], iv = t) @mtkbuild pend = ODESystem(eqs, t, continuous_events = c_evt) - prob = ODEProblem(pend, [x => 1, y => 0], (0., 10.), [g => 1], guesses = [λ => 1]) + prob = ODEProblem(pend, [x => 1, y => 0], (0.0, 10.0), [g => 1], guesses = [λ => 1]) sol = solve(prob, FBDF()) @test sol.ps[g] ≈ [1, 2] - @test ≈(sol(5.0000001, idxs = x) - sol(4.999999, idxs = x), .1, rtol = 1e-4) + @test ≈(sol(5.0000001, idxs = x) - sol(4.999999, idxs = x), 0.1, rtol = 1e-4) # Proper re-initialization after parameter change eqs = [y ~ g^2 - x, D(x) ~ x] - c_evt = SymbolicContinuousCallback([t ~ 5.0], [x ~ Pre(x) + 1, g ~ Pre(g) + 1], discrete_parameters = [g], iv = t) + c_evt = SymbolicContinuousCallback( + [t ~ 5.0], [x ~ Pre(x) + 1, g ~ Pre(g) + 1], discrete_parameters = [g], iv = t) @mtkbuild sys = ODESystem(eqs, t, continuous_events = c_evt) - prob = ODEProblem(sys, [x => 1.0], (0., 10.), [g => 2]) + prob = ODEProblem(sys, [x => 1.0], (0.0, 10.0), [g => 2]) sol = solve(prob, FBDF()) - @test sol.ps[g] ≈ [2., 3.] + @test sol.ps[g] ≈ [2.0, 3.0] @test ≈(sol(5.00000001, idxs = x) - sol(4.9999999, idxs = x), 1; rtol = 1e-4) @test ≈(sol(5.00000001, idxs = y), 9 - sol(5.00000001, idxs = x), rtol = 1e-4) # Parameters that don't appear in affects should not be mutated. c_evt = [t ~ 5.0] => [x ~ Pre(x) + 1] @mtkbuild sys = ODESystem(eqs, t, continuous_events = c_evt) - prob = ODEProblem(sys, [x => 0.5], (0., 10.), [g => 2], guesses = [y => 0]) + prob = ODEProblem(sys, [x => 0.5], (0.0, 10.0), [g => 2], guesses = [y => 0]) sol = solve(prob, FBDF()) @test prob.ps[g] == sol.ps[g] end From da8df68ad95529d70796e76d972877bf9b6e78e6 Mon Sep 17 00:00:00 2001 From: vyudu Date: Thu, 27 Mar 2025 08:39:50 -0400 Subject: [PATCH 32/52] fix: fix typos and to_term differentials in affect equations --- src/systems/callbacks.jl | 20 +++++++++++++------- src/utils.jl | 12 ------------ 2 files changed, 13 insertions(+), 19 deletions(-) diff --git a/src/systems/callbacks.jl b/src/systems/callbacks.jl index 97141b3405..8bdc40a728 100644 --- a/src/systems/callbacks.jl +++ b/src/systems/callbacks.jl @@ -264,6 +264,8 @@ function make_affect(affect::Vector{Equation}; discrete_parameters::AbstractVect @warn "Affect equation $eq has no `Pre` operator. As such it will be interpreted as an algebraic equation to be satisfied after the callback. If you intended to use the value of a variable x before the affect, use Pre(x)." end collect_vars!(dvs, params, eq, iv; op = Pre) + diffvs = collect_applied_operators(eq, Differential) + union!(dvs, diffvs) end for eq in algeeqs collect_vars!(dvs, params, eq, iv) @@ -272,23 +274,28 @@ function make_affect(affect::Vector{Equation}; discrete_parameters::AbstractVect pre_params = filter(haspre ∘ value, params) sys_params = collect(setdiff(params, union(discrete_parameters, pre_params))) discretes = map(tovar, discrete_parameters) + dvs = collect(dvs) + _dvs = map(default_toterm, dvs) + aff_map = Dict(zip(discretes, discrete_parameters)) rev_map = Dict(zip(discrete_parameters, discretes)) - affect = Symbolics.fast_substitute(affect, rev_map) - algeeqs = Symbolics.fast_substitute(algeeqs, rev_map) + subs = merge(rev_map, Dict(zip(dvs, _dvs))) + affect = Symbolics.fast_substitute(affect, subs) + algeeqs = Symbolics.fast_substitute(algeeqs, subs) + @named affectsys = ImplicitDiscreteSystem( - vcat(affect, algeeqs), iv, collect(union(dvs, discretes)), + vcat(affect, algeeqs), iv, collect(union(_dvs, discretes)), collect(union(pre_params, sys_params))) - affectsys = structural_simplify(affect_sys; fully_determined = false) + affectsys = structural_simplify(affectsys; fully_determined = false) # get accessed parameters p from Pre(p) in the callback parameters accessed_params = filter(isparameter, map(unPre, collect(pre_params))) union!(accessed_params, sys_params) # add unknowns to the map - for u in dvs + for u in _dvs aff_map[u] = u end - AffectSystem(affectsys, collect(dvs), collect(accessed_params), + AffectSystem(affectsys, collect(_dvs), collect(accessed_params), collect(discrete_parameters), aff_map) end @@ -811,7 +818,6 @@ function compile_affect( end function wrap_save_discretes(f, save_idxs) - @show save_idxs let save_idxs = save_idxs if f === SciMLBase.INITIALIZE_DEFAULT (c, u, t, i) -> begin diff --git a/src/utils.jl b/src/utils.jl index 2a0009b644..63f3897d74 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -513,18 +513,6 @@ function collect_applied_operators(x, op) end end -function find_derivatives!(vars, expr::Equation, f = identity) - (find_derivatives!(vars, expr.lhs, f); find_derivatives!(vars, expr.rhs, f); vars) -end -function find_derivatives!(vars, expr, f) - !iscall(O) && return vars - operation(O) isa Differential && push!(vars, f(O)) - for arg in arguments(O) - vars!(vars, arg) - end - return vars -end - """ $(TYPEDSIGNATURES) From d6df56921cf723f945f835ea9164bb493e8d2b0a Mon Sep 17 00:00:00 2001 From: vyudu Date: Thu, 27 Mar 2025 13:15:43 -0400 Subject: [PATCH 33/52] fix: add events to SDESystem after structural simplification --- src/systems/diffeqs/odesystem.jl | 2 +- src/systems/diffeqs/sdesystem.jl | 2 +- src/systems/systems.jl | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/systems/diffeqs/odesystem.jl b/src/systems/diffeqs/odesystem.jl index c36729d408..5178bb04e4 100644 --- a/src/systems/diffeqs/odesystem.jl +++ b/src/systems/diffeqs/odesystem.jl @@ -312,7 +312,7 @@ function ODESystem(deqs::AbstractVector{<:Equation}, iv, dvs, ps; end algeeqs = filter(eq -> eq.lhs isa Union{Symbolic, Number} && !is_diff_equation(eq), - flatten_equations(deqs)) + deqs) cont_callbacks = SymbolicContinuousCallbacks(continuous_events; algeeqs, iv) disc_callbacks = SymbolicDiscreteCallbacks(discrete_events; algeeqs, iv) diff --git a/src/systems/diffeqs/sdesystem.jl b/src/systems/diffeqs/sdesystem.jl index 3d73674b21..a0ae5a8f11 100644 --- a/src/systems/diffeqs/sdesystem.jl +++ b/src/systems/diffeqs/sdesystem.jl @@ -269,7 +269,7 @@ function SDESystem(deqs::AbstractVector{<:Equation}, neqs::AbstractArray, iv, dv Wfact_t = RefValue(EMPTY_JAC) algeeqs = filter(eq -> eq.lhs isa Union{Symbolic, Number} && !is_diff_equation(eq), - flatten_equations(deqs)) + deqs) cont_callbacks = SymbolicContinuousCallbacks(continuous_events; algeeqs, iv) disc_callbacks = SymbolicDiscreteCallbacks(discrete_events; algeeqs, iv) if is_dde === nothing diff --git a/src/systems/systems.jl b/src/systems/systems.jl index 6490c31ac2..da08186355 100644 --- a/src/systems/systems.jl +++ b/src/systems/systems.jl @@ -155,7 +155,7 @@ function __structural_simplify(sys::AbstractSystem, io = nothing; simplify = fal get_iv(ode_sys), unknowns(ode_sys), parameters(ode_sys); name = nameof(ode_sys), is_scalar_noise, observed = observed(ode_sys), defaults = defaults(sys), parameter_dependencies = parameter_dependencies(sys), assertions = assertions(sys), - guesses = guesses(sys), initialization_eqs = initialization_equations(sys)) + guesses = guesses(sys), initialization_eqs = initialization_equations(sys), continuous_events = continuous_events(sys), discrete_events = discrete_events(sys)) end end From c1b3cbd55a84710b3a8d5ec97e5e944cb31c2a81 Mon Sep 17 00:00:00 2001 From: vyudu Date: Fri, 28 Mar 2025 12:26:28 -0400 Subject: [PATCH 34/52] fix: add reinitalizealg back --- ext/MTKFMIExt.jl | 6 ++--- src/systems/callbacks.jl | 48 +++++++++++++++++++++++++----------- src/systems/model_parsing.jl | 18 ++++++++++---- test/fmi/fmi.jl | 4 +-- test/symbolic_events.jl | 7 ++---- 5 files changed, 53 insertions(+), 30 deletions(-) diff --git a/ext/MTKFMIExt.jl b/ext/MTKFMIExt.jl index 912799c4f8..0baf37c34b 100644 --- a/ext/MTKFMIExt.jl +++ b/ext/MTKFMIExt.jl @@ -93,7 +93,7 @@ with the name `namespace__variable`. - `name`: The name of the system. """ function MTK.FMIComponent(::Val{Ver}; fmu = nothing, tolerance = 1e-6, - communication_step_size = nothing, type, name) where {Ver} + communication_step_size = nothing, type, name, reinitializealg = nothing) where {Ver} if Ver != 2 && Ver != 3 throw(ArgumentError("FMI Version must be `2` or `3`")) end @@ -238,7 +238,7 @@ function MTK.FMIComponent(::Val{Ver}; fmu = nothing, tolerance = 1e-6, finalize_affect = MTK.FunctionalAffect(fmiFinalize!, [], [wrapper], []) step_affect = MTK.FunctionalAffect(Returns(nothing), [], [], []) instance_management_callback = MTK.SymbolicDiscreteCallback( - (t != t - 1), step_affect; finalize = finalize_affect) + (t != t - 1), step_affect; finalize = finalize_affect, reinitializealg) push!(params, wrapper) append!(observed, der_observed) @@ -279,7 +279,7 @@ function MTK.FMIComponent(::Val{Ver}; fmu = nothing, tolerance = 1e-6, fmiCSStep!; observed = cb_observed, modified = cb_modified, ctx = _functor) instance_management_callback = MTK.SymbolicDiscreteCallback( communication_step_size, step_affect; initialize = initialize_affect, - finalize = finalize_affect) + finalize = finalize_affect, reinitializealg) # guarded in case there are no outputs/states and the variable is `[]`. symbolic_type(__mtk_internal_o) == NotSymbolic() || push!(params, __mtk_internal_o) diff --git a/src/systems/callbacks.jl b/src/systems/callbacks.jl index 8bdc40a728..6f7c7e38cc 100644 --- a/src/systems/callbacks.jl +++ b/src/systems/callbacks.jl @@ -200,7 +200,9 @@ Affects (i.e. `affect` and `affect_neg`) can be specified as either: + `ctx` is a user-defined context object passed to `f!` when invoked. This value is aliased for each problem. * A [`ImperativeAffect`](@ref); refer to its documentation for details. -DAEs will automatically be reinitialized. +`reinitializealg` is used to set how the system will be reinitialized after the callback. +- Symbolic affects have reinitialization built in. In this case the algorithm will default to SciMLBase.NoInit(), and should **not** be provided. +- Functional and imperative affects will default to SciMLBase.CheckInit(), which will error if the system is not properly reinitialized after the callback. If your system is a DAE, pass in an algorithm like SciMLBase.BrownBasicFullInit() to properly re-initialize. Initial and final affects can also be specified identically to positive and negative edge affects. Initialization affects will run as soon as the solver starts, while finalization affects will be executed after termination. @@ -212,6 +214,7 @@ struct SymbolicContinuousCallback <: AbstractCallback initialize::Union{Affect, Nothing} finalize::Union{Affect, Nothing} rootfind::Union{Nothing, SciMLBase.RootfindOpt} + reinitializealg::SciMLBase.DAEInitializationAlgorithm function SymbolicContinuousCallback( conditions::Union{Equation, Vector{Equation}}, @@ -221,13 +224,21 @@ struct SymbolicContinuousCallback <: AbstractCallback initialize = nothing, finalize = nothing, rootfind = SciMLBase.LeftRootFind, + reinitializealg = nothing, iv = nothing, algeeqs = Equation[]) conditions = (conditions isa AbstractVector) ? conditions : [conditions] + + if isnothing(reinitializealg) + any(a -> (a isa FunctionalAffect || a isa ImperativeAffect), [affect, affect_neg, initialize, finalize]) ? + reinitializealg = SciMLBase.CheckInit() : + reinitializealg = SciMLBase.NoInit() + end + new(conditions, make_affect(affect; iv, algeeqs, discrete_parameters), make_affect(affect_neg; iv, algeeqs, discrete_parameters), make_affect(initialize; iv, algeeqs, discrete_parameters), make_affect( - finalize; iv, algeeqs, discrete_parameters), rootfind) + finalize; iv, algeeqs, discrete_parameters), rootfind, reinitializealg) end # Default affect to nothing end @@ -424,16 +435,22 @@ struct SymbolicDiscreteCallback <: AbstractCallback affect::Union{Affect, Nothing} initialize::Union{Affect, Nothing} finalize::Union{Affect, Nothing} + reinitializealg::SciMLBase.DAEInitializationAlgorithm function SymbolicDiscreteCallback( condition, affect = nothing; initialize = nothing, finalize = nothing, iv = nothing, - algeeqs = Equation[], discrete_parameters = Any[]) + algeeqs = Equation[], discrete_parameters = Any[], reinitializealg = nothing) c = is_timed_condition(condition) ? condition : value(scalarize(condition)) + if isnothing(reinitializealg) + any(a -> (a isa FunctionalAffect || a isa ImperativeAffect), [affect, affect_neg, initialize, finalize]) ? + reinitializealg = SciMLBase.CheckInit() : + reinitializealg = SciMLBase.NoInit() + end new(c, make_affect(affect; iv, algeeqs, discrete_parameters), make_affect(initialize; iv, algeeqs, discrete_parameters), - make_affect(finalize; iv, algeeqs, discrete_parameters)) + make_affect(finalize; iv, algeeqs, discrete_parameters), reinitializealg) end # Default affect to nothing end @@ -525,7 +542,8 @@ function Base.hash(cb::AbstractCallback, s::UInt) !is_discrete(cb) && (s = hash(affect_negs(cb), s)) s = hash(initialize_affects(cb), s) s = hash(finalize_affects(cb), s) - !is_discrete(cb) ? hash(cb.rootfind, s) : s + !is_discrete(cb) && (s = hash(cb.rootfind, s)) + hash(cb.reinitializealg, s) end ########################### @@ -562,7 +580,7 @@ end function Base.:(==)(e1::AbstractCallback, e2::AbstractCallback) (is_discrete(e1) === is_discrete(e2)) || return false (isequal(e1.conditions, e2.conditions) && isequal(e1.affect, e2.affect) && - isequal(e1.initialize, e2.initialize) && isequal(e1.finalize, e2.finalize)) || + isequal(e1.initialize, e2.initialize) && isequal(e1.finalize, e2.finalize)) && isequal(e1.reinitializealg, e2.reinitializealg) || return false is_discrete(e1) || (isequal(e1.affect_neg, e2.affect_neg) && isequal(e1.rootfind, e2.rootfind)) @@ -664,15 +682,15 @@ function generate_continuous_callbacks(sys::AbstractSystem, dvs = unknowns(sys), ps = parameters(sys; initial_parameters = true); kwargs...) cbs = continuous_events(sys) isempty(cbs) && return nothing - cb_classes = Dict{SciMLBase.RootfindOpt, Vector{SymbolicContinuousCallback}}() + cb_classes = Dict{Tuple{SciMLBase.RootfindOpt, SciMLBase.DAEReinitializationAlg}, Vector{SymbolicContinuousCallback}}() # Sort the callbacks by their rootfinding method for cb in cbs - _cbs = get!(() -> SymbolicContinuousCallback[], cb_classes, cb.rootfind) + _cbs = get!(() -> SymbolicContinuousCallback[], cb_classes, (cb.rootfind, cb.reinitializealg)) push!(_cbs, cb) end - cb_classes = sort!(OrderedDict(cb_classes)) - compiled_callbacks = [generate_callback(cb, sys; kwargs...) for (rf, cb) in cb_classes] + sort!(OrderedDict(cb_classes), by = cb -> cb.rootfind) + compiled_callbacks = [generate_callback(cb, sys; kwargs...) for ((rf, reinit), cb) in cb_classes] if length(compiled_callbacks) == 1 return only(compiled_callbacks) else @@ -741,7 +759,7 @@ function generate_callback(cbs::Vector{SymbolicContinuousCallback}, sys; kwargs. return VectorContinuousCallback( trigger, affect, affect_neg, length(eqs); initialize, finalize, - rootfind = cbs[1].rootfind, initializealg = SciMLBase.NoInit()) + rootfind = cbs[1].rootfind, initializealg = cbs[1].reinitializealg) end function generate_callback(cb, sys; kwargs...) @@ -768,16 +786,16 @@ function generate_callback(cb, sys; kwargs...) if is_discrete(cb) if is_timed && conditions(cb) isa AbstractVector return PresetTimeCallback(trigger, affect; initialize, - finalize, initializealg = SciMLBase.NoInit()) + finalize, initializealg = cb.reinitializealg) elseif is_timed - return PeriodicCallback(affect, trigger; initialize, finalize, initializealg = SciMLBase.NoInit()) + return PeriodicCallback(affect, trigger; initialize, finalize, initializealg = cb.reinitializealg) else return DiscreteCallback(trigger, affect; initialize, - finalize, initializealg = SciMLBase.NoInit()) + finalize, initializealg = cb.reinitializealg) end else return ContinuousCallback(trigger, affect, affect_neg; initialize, finalize, - rootfind = cb.rootfind, initializealg = SciMLBase.NoInit()) + rootfind = cb.rootfind, initializealg = cb.reinitializealg) end end diff --git a/src/systems/model_parsing.jl b/src/systems/model_parsing.jl index c360e0a5ce..d180af552e 100644 --- a/src/systems/model_parsing.jl +++ b/src/systems/model_parsing.jl @@ -64,8 +64,6 @@ function _model_macro(mod, name, expr, isconnector) push!(exprs.args, :(systems = ODESystem[])) push!(exprs.args, :(equations = Union{Equation, Vector{Equation}}[])) push!(exprs.args, :(defaults = Dict{Num, Union{Number, Symbol, Function}}())) - push!(exprs.args, :(disc_events = [])) - push!(exprs.args, :(cont_events = [])) Base.remove_linenums!(expr) for arg in expr.args @@ -107,8 +105,6 @@ function _model_macro(mod, name, expr, isconnector) push!(exprs.args, :(push!(parameters, $(ps...)))) push!(exprs.args, :(push!(systems, $(comps...)))) push!(exprs.args, :(push!(variables, $(vs...)))) - push!(exprs.args, :(push!(disc_events, $(d_evts...)))) - push!(exprs.args, :(push!(cont_events, $(c_evts...)))) gui_metadata = isassigned(icon) > 0 ? GUIMetadata(GlobalRef(mod, name), icon[]) : GUIMetadata(GlobalRef(mod, name)) @@ -120,7 +116,7 @@ function _model_macro(mod, name, expr, isconnector) sys = :($ODESystem($(flatten_equations)(equations), $iv, variables, parameters; name, description = $description, systems, gui_metadata = $gui_metadata, - defaults, continuous_events = cont_events, discrete_events = disc_events)) + defaults)) if length(ext) == 0 push!(exprs.args, :(var"#___sys___" = $sys)) @@ -131,6 +127,18 @@ function _model_macro(mod, name, expr, isconnector) isconnector && push!(exprs.args, :($Setfield.@set!(var"#___sys___".connector_type=$connector_type(var"#___sys___")))) + !isempty(c_evts) && push!(exprs.args, + :($Setfield.@set!(var"#___sys___".continuous_events=$SymbolicContinuousCallback.([ + $(c_evts...) + ])))) + + @show d_evts + !isempty(d_evts) && push!(exprs.args, + :($Setfield.@set!(var"#___sys___".discrete_events=$SymbolicDiscreteCallback.([ + $(d_evts...) + ])))) + + f = if length(where_types) == 0 :($(Symbol(:__, name, :__))(; name, $(kwargs...)) = $exprs) else diff --git a/test/fmi/fmi.jl b/test/fmi/fmi.jl index 0d10f3204a..e4c155270e 100644 --- a/test/fmi/fmi.jl +++ b/test/fmi/fmi.jl @@ -157,7 +157,7 @@ end @testset "v2, CS" begin fmu = loadFMU(joinpath(FMU_DIR, "SimpleAdder.fmu"); type = :CS) @named adder = MTK.FMIComponent( - Val(2); fmu, type = :CS, communication_step_size = 1e-6) + Val(2); fmu, type = :CS, communication_step_size = 1e-6, reinitializealg = BrownFullBasicInit()) @test MTK.isinput(adder.a) @test MTK.isinput(adder.b) @test MTK.isoutput(adder.out) @@ -209,7 +209,7 @@ end @testset "v3, CS" begin fmu = loadFMU(joinpath(FMU_DIR, "StateSpace.fmu"); type = :CS) @named sspace = MTK.FMIComponent( - Val(3); fmu, communication_step_size = 1e-6, type = :CS) + Val(3); fmu, communication_step_size = 1e-6, type = :CS, reinitializealg = BrownFullBasicInit()) @test MTK.isinput(sspace.u) @test MTK.isoutput(sspace.y) @test !MTK.isinput(sspace.x) && !MTK.isoutput(sspace.x) diff --git a/test/symbolic_events.jl b/test/symbolic_events.jl index e933843856..547993ec00 100644 --- a/test/symbolic_events.jl +++ b/test/symbolic_events.jl @@ -1204,7 +1204,7 @@ end @mtkmodel DECAY begin @parameters begin unrelated[1:2] = zeros(2) - k = 0.0 + k(t) = 0.0 end @variables begin x(t) = 10.0 @@ -1213,7 +1213,7 @@ end D(x) ~ -k * x end @discrete_events begin - (t == 1.0) => [k ~ 1.0] + (t == 1.0) => [k ~ 1.0], discrete_parameters = [k] end end @mtkbuild decay = DECAY() @@ -1338,7 +1338,4 @@ end sol = solve(prob, FBDF()) @test prob.ps[g] == sol.ps[g] end -# TODO: test: -# - Functional affects reinitialize correctly # - explicit equation of t in a functional affect -# - reinitialization after affects From 4e3274975bdaed99529c1602a31d8ae26ded477f Mon Sep 17 00:00:00 2001 From: vyudu Date: Mon, 31 Mar 2025 14:47:35 -0400 Subject: [PATCH 35/52] fix: use discrete_parameters in tests --- src/systems/callbacks.jl | 6 +- src/systems/index_cache.jl | 7 +- src/systems/model_parsing.jl | 1 - test/parameter_dependencies.jl | 20 +- test/symbolic_events.jl | 726 ++++++++++++++++----------------- test/symbolic_parameters.jl | 6 +- 6 files changed, 385 insertions(+), 381 deletions(-) diff --git a/src/systems/callbacks.jl b/src/systems/callbacks.jl index 6f7c7e38cc..9be33b0800 100644 --- a/src/systems/callbacks.jl +++ b/src/systems/callbacks.jl @@ -444,7 +444,7 @@ struct SymbolicDiscreteCallback <: AbstractCallback c = is_timed_condition(condition) ? condition : value(scalarize(condition)) if isnothing(reinitializealg) - any(a -> (a isa FunctionalAffect || a isa ImperativeAffect), [affect, affect_neg, initialize, finalize]) ? + any(a -> (a isa FunctionalAffect || a isa ImperativeAffect), [affect, initialize, finalize]) ? reinitializealg = SciMLBase.CheckInit() : reinitializealg = SciMLBase.NoInit() end @@ -682,14 +682,14 @@ function generate_continuous_callbacks(sys::AbstractSystem, dvs = unknowns(sys), ps = parameters(sys; initial_parameters = true); kwargs...) cbs = continuous_events(sys) isempty(cbs) && return nothing - cb_classes = Dict{Tuple{SciMLBase.RootfindOpt, SciMLBase.DAEReinitializationAlg}, Vector{SymbolicContinuousCallback}}() + cb_classes = Dict{Tuple{SciMLBase.RootfindOpt, SciMLBase.DAEInitializationAlgorithm}, Vector{SymbolicContinuousCallback}}() # Sort the callbacks by their rootfinding method for cb in cbs _cbs = get!(() -> SymbolicContinuousCallback[], cb_classes, (cb.rootfind, cb.reinitializealg)) push!(_cbs, cb) end - sort!(OrderedDict(cb_classes), by = cb -> cb.rootfind) + sort!(OrderedDict(cb_classes), by = cb -> cb[1]) compiled_callbacks = [generate_callback(cb, sys; kwargs...) for ((rf, reinit), cb) in cb_classes] if length(compiled_callbacks) == 1 return only(compiled_callbacks) diff --git a/src/systems/index_cache.jl b/src/systems/index_cache.jl index d71bcc60a9..22e3b3a161 100644 --- a/src/systems/index_cache.jl +++ b/src/systems/index_cache.jl @@ -345,8 +345,13 @@ function IndexCache(sys::AbstractSystem) vs = vars(eq.rhs; op = Nothing) timeseries = TimeseriesSetType() if is_time_dependent(sys) + unknown_set = Set(unknowns(sys)) for v in vs - if (idx = get(disc_idxs, v, nothing)) !== nothing + if in(v, unknown_set) + empty!(timeseries) + push!(timeseries, ContinuousTimeseries()) + break + elseif (idx = get(disc_idxs, v, nothing)) !== nothing push!(timeseries, idx.clock_idx) elseif iscall(v) && operation(v) === getindex && (idx = get(disc_idxs, arguments(v)[1], nothing)) !== nothing diff --git a/src/systems/model_parsing.jl b/src/systems/model_parsing.jl index d180af552e..3a1ff1cf1f 100644 --- a/src/systems/model_parsing.jl +++ b/src/systems/model_parsing.jl @@ -132,7 +132,6 @@ function _model_macro(mod, name, expr, isconnector) $(c_evts...) ])))) - @show d_evts !isempty(d_evts) && push!(exprs.args, :($Setfield.@set!(var"#___sys___".discrete_events=$SymbolicDiscreteCallback.([ $(d_evts...) diff --git a/test/parameter_dependencies.jl b/test/parameter_dependencies.jl index cc2f137392..89f0dc1e27 100644 --- a/test/parameter_dependencies.jl +++ b/test/parameter_dependencies.jl @@ -1,6 +1,6 @@ using ModelingToolkit using Test -using ModelingToolkit: t_nounits as t, D_nounits as D +using ModelingToolkit: t_nounits as t, D_nounits as D, SymbolicDiscreteCallback, SymbolicContinuousCallback using OrdinaryDiffEq using StochasticDiffEq using JumpProcesses @@ -10,14 +10,14 @@ using SymbolicIndexingInterface using NonlinearSolve @testset "ODESystem with callbacks" begin - @parameters p1=1.0 p2 + @parameters p1(t)=1.0 p2 @variables x(t) - cb1 = [x ~ 2.0] => [p1 ~ 2.0] # triggers at t=-2+√6 + cb1 = SymbolicContinuousCallback([x ~ 2.0] => [p1 ~ 2.0], discrete_parameters = [p1]) # triggers at t=-2+√6 function affect1!(integ, u, p, ctx) integ.ps[p[1]] = integ.ps[p[2]] end cb2 = [x ~ 4.0] => (affect1!, [], [p1, p2], [p1]) # triggers at t=-2+√7 - cb3 = [1.0] => [p1 ~ 5.0] + cb3 = SymbolicDiscreteCallback([1.0] => [p1 ~ 5.0], discrete_parameters = [p1]) @mtkbuild sys = ODESystem( [D(x) ~ p1 * t + p2], @@ -203,7 +203,7 @@ end @testset "Clock system" begin dt = 0.1 @variables x(t) y(t) u(t) yd(t) ud(t) r(t) z(t) - @parameters kp kq + @parameters kp(t) kq d = Clock(dt) k = ShiftIndex(d) @@ -225,7 +225,7 @@ end @test_nowarn solve(prob, Tsit5()) @mtkbuild sys = ODESystem(eqs, t; parameter_dependencies = [kq => 2kp], - discrete_events = [[0.5] => [kp ~ 2.0]]) + discrete_events = [SymbolicDiscreteCallback([0.5] => [kp ~ 2.0], discrete_parameters = [kp])]) prob = ODEProblem(sys, [x => 0.0, y => 0.0], (0.0, Tf), [kp => 1.0; z(k - 1) => 3.0; yd(k - 1) => 0.0; z(k - 2) => 4.0; yd(k - 2) => 2.0]) @@ -245,7 +245,7 @@ end end @testset "SDESystem" begin - @parameters σ ρ β + @parameters σ(t) ρ β @variables x(t) y(t) z(t) eqs = [D(x) ~ σ * (y - x), @@ -269,7 +269,7 @@ end @named sys = ODESystem(eqs, t) @named sdesys = SDESystem(sys, noiseeqs; parameter_dependencies = [ρ => 2σ], - discrete_events = [[10.0] => [σ ~ 15.0]]) + discrete_events = [SymbolicDiscreteCallback([10.0] => [σ ~ 15.0], discrete_parameters = [σ])]) sdesys = complete(sdesys) prob = SDEProblem( sdesys, [x => 1.0, y => 0.0, z => 0.0], (0.0, 100.0), [σ => 10.0, β => 2.33]) @@ -283,7 +283,7 @@ end @testset "JumpSystem" begin rng = StableRNG(12345) - @parameters β γ + @parameters β γ(t) @constants h = 1 @variables S(t) I(t) R(t) rate₁ = β * S * I * h @@ -308,7 +308,7 @@ end @named js2 = JumpSystem( [j₁, j₃], t, [S, I, R], [γ]; parameter_dependencies = [β => 0.01γ], - discrete_events = [[10.0] => [γ ~ 0.02]]) + discrete_events = [SymbolicDiscreteCallback([10.0] => [γ ~ 0.02], discrete_parameters = [γ])]) js2 = complete(js2) dprob = DiscreteProblem(js2, u₀map, tspan, parammap) jprob = JumpProblem(js2, dprob, Direct(), save_positions = (false, false), rng = rng) diff --git a/test/symbolic_events.jl b/test/symbolic_events.jl index 547993ec00..49c02bacc1 100644 --- a/test/symbolic_events.jl +++ b/test/symbolic_events.jl @@ -316,16 +316,16 @@ end @test out[2] ≈ 1 # signature is u,p,t sol = solve(prob, Tsit5()) - @test minimum(t -> abs(t - 1), sol.t) < 1e-10 # test that the solver stepped at the first root - @test minimum(t -> abs(t - 2), sol.t) < 1e-10 # test that the solver stepped at the second root + @test minimum(t -> abs(t - 1), sol.t) < 1e-9 # test that the solver stepped at the first root + @test minimum(t -> abs(t - 2), sol.t) < 1e-9 # test that the solver stepped at the second root @named sys = ODESystem(eqs, t, continuous_events = [x ~ 1, x ~ 2]) # two root eqs using the same unknown sys = complete(sys) prob = ODEProblem(sys, Pair[], (0.0, 3.0)) @test get_callback(prob) isa ModelingToolkit.DiffEqCallbacks.VectorContinuousCallback sol = solve(prob, Tsit5()) - @test minimum(t -> abs(t - 1), sol.t) < 1e-10 # test that the solver stepped at the first root - @test minimum(t -> abs(t - 2), sol.t) < 1e-10 # test that the solver stepped at the second root + @test minimum(t -> abs(t - 1), sol.t) < 1e-9 # test that the solver stepped at the first root + @test minimum(t -> abs(t - 2), sol.t) < 1e-9 # test that the solver stepped at the second root end @testset "Bouncing Ball" begin @@ -429,7 +429,7 @@ end prob = ODEProblem(sys, zeros(2), (0.0, 5.1)) sol = solve(prob, Tsit5()) @test all(minimum((0:0.1:5) .- sol.t', dims = 2) .< 0.0001) # test that the solver stepped every 0.1s as dictated by event - @test sol([0.25])[vmeasured][] == sol([0.23])[vmeasured][] # test the hold property + @test sol([0.25 - eps()])[vmeasured][] == sol([0.23])[vmeasured][] # test the hold property end ## https://github.com/SciML/ModelingToolkit.jl/issues/1528 @@ -823,363 +823,363 @@ end @test sign.(cos.(3 * (required_crossings_c2 .+ 1e-6))) == sign.(last.(cr2)) end # -#@testset "Discrete event reinitialization (#3142)" begin -# @connector LiquidPort begin -# p(t)::Float64, [description = "Set pressure in bar", -# guess = 1.01325] -# Vdot(t)::Float64, -# [description = "Volume flow rate in L/min", -# guess = 0.0, -# connect = Flow] -# end -# -# @mtkmodel PressureSource begin -# @components begin -# port = LiquidPort() -# end -# @parameters begin -# p_set::Float64 = 1.01325, [description = "Set pressure in bar"] -# end -# @equations begin -# port.p ~ p_set -# end -# end -# -# @mtkmodel BinaryValve begin -# @constants begin -# p_ref::Float64 = 1.0, [description = "Reference pressure drop in bar"] -# ρ_ref::Float64 = 1000.0, [description = "Reference density in kg/m^3"] -# end -# @components begin -# port_in = LiquidPort() -# port_out = LiquidPort() -# end -# @parameters begin -# k_V::Float64 = 1.0, [description = "Valve coefficient in L/min/bar"] -# k_leakage::Float64 = 1e-08, [description = "Leakage coefficient in L/min/bar"] -# ρ::Float64 = 1000.0, [description = "Density in kg/m^3"] -# end -# @variables begin -# S(t)::Float64, [description = "Valve state", guess = 1.0, irreducible = true] -# Δp(t)::Float64, [description = "Pressure difference in bar", guess = 1.0] -# Vdot(t)::Float64, [description = "Volume flow rate in L/min", guess = 1.0] -# end -# @equations begin -# # Port handling -# port_in.Vdot ~ -Vdot -# port_out.Vdot ~ Vdot -# Δp ~ port_in.p - port_out.p -# # System behavior -# D(S) ~ 0.0 -# Vdot ~ S * k_V * sign(Δp) * sqrt(abs(Δp) / p_ref * ρ_ref / ρ) + k_leakage * Δp # softplus alpha function to avoid negative values under the sqrt -# end -# end -# -# # Test System -# @mtkmodel TestSystem begin -# @components begin -# pressure_source_1 = PressureSource(p_set = 2.0) -# binary_valve_1 = BinaryValve(S = 1.0, k_leakage = 0.0) -# binary_valve_2 = BinaryValve(S = 1.0, k_leakage = 0.0) -# pressure_source_2 = PressureSource(p_set = 1.0) -# end -# @equations begin -# connect(pressure_source_1.port, binary_valve_1.port_in) -# connect(binary_valve_1.port_out, binary_valve_2.port_in) -# connect(binary_valve_2.port_out, pressure_source_2.port) -# end -# @discrete_events begin -# [30] => [binary_valve_1.S ~ 0.0, binary_valve_2.Δp ~ 0.0] -# [60] => [ -# binary_valve_1.S ~ 1.0, binary_valve_2.S ~ 0.0, binary_valve_2.Δp ~ 1.0] -# [120] => [binary_valve_1.S ~ 0.0, binary_valve_2.Δp ~ 0.0] -# end -# end -# -# # Test Simulation -# @mtkbuild sys = TestSystem() -# -# # Test Simulation -# prob = ODEProblem(sys, [], (0.0, 150.0)) -# sol = solve(prob) -# @test sol[end] == [0.0, 0.0, 0.0] -#end -# -#@testset "Discrete variable timeseries" begin -# @variables x(t) -# @parameters a(t) b(t) c(t) -# cb1 = [x ~ 1.0] => [a ~ -Pre(a)] -# function save_affect!(integ, u, p, ctx) -# integ.ps[p.b] = 5.0 -# end -# cb2 = [x ~ 0.5] => (save_affect!, [], [b], [b], nothing) -# cb3 = 1.0 => [c ~ t] -# -# @mtkbuild sys = ODESystem(D(x) ~ cos(t), t, [x], [a, b, c]; -# continuous_events = [cb1, cb2], discrete_events = [cb3]) -# prob = ODEProblem(sys, [x => 1.0], (0.0, 2pi), [a => 1.0, b => 2.0, c => 0.0]) -# @test sort(canonicalize(Discrete(), prob.p)[1]) == [0.0, 1.0, 2.0] -# sol = solve(prob, Tsit5()) -# -# @test sol[a] == [1.0, -1.0] -# @test sol[b] == [2.0, 5.0, 5.0] -# @test sol[c] == [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0] -#end -# -#@testset "Heater" begin -# @variables temp(t) -# params = @parameters furnace_on_threshold=0.5 furnace_off_threshold=0.7 furnace_power=1.0 leakage=0.1 furnace_on::Bool=false -# eqs = [ -# D(temp) ~ furnace_on * furnace_power - temp^2 * leakage -# ] -# -# furnace_off = ModelingToolkit.SymbolicContinuousCallback( -# [temp ~ furnace_off_threshold], -# ModelingToolkit.ImperativeAffect(modified = (; furnace_on)) do x, o, i, c -# @set! x.furnace_on = false -# end) -# furnace_enable = ModelingToolkit.SymbolicContinuousCallback( -# [temp ~ furnace_on_threshold], -# ModelingToolkit.ImperativeAffect(modified = (; furnace_on)) do x, o, i, c -# @set! x.furnace_on = true -# end) -# @named sys = ODESystem( -# eqs, t, [temp], params; continuous_events = [furnace_off, furnace_enable]) -# ss = structural_simplify(sys) -# prob = ODEProblem(ss, [temp => 0.0, furnace_on => true], (0.0, 100.0)) -# sol = solve(prob, Tsit5(); dtmax = 0.01) -# @test all(sol[temp][sol.t .> 1.0] .<= 0.79) && all(sol[temp][sol.t .> 1.0] .>= 0.49) -# -# furnace_off = ModelingToolkit.SymbolicContinuousCallback( -# [temp ~ furnace_off_threshold], -# ModelingToolkit.ImperativeAffect(modified = (; furnace_on)) do x, o, c, i -# @set! x.furnace_on = false -# end; initialize = ModelingToolkit.ImperativeAffect(modified = (; -# temp)) do x, o, c, i -# @set! x.temp = 0.2 -# end) -# furnace_enable = ModelingToolkit.SymbolicContinuousCallback( -# [temp ~ furnace_on_threshold], -# ModelingToolkit.ImperativeAffect(modified = (; furnace_on)) do x, o, c, i -# @set! x.furnace_on = true -# end) -# @named sys = ODESystem( -# eqs, t, [temp], params; continuous_events = [furnace_off, furnace_enable]) -# ss = structural_simplify(sys) -# prob = ODEProblem(ss, [temp => 0.0, furnace_on => true], (0.0, 100.0)) -# sol = solve(prob, Tsit5(); dtmax = 0.01) -# @test all(sol[temp][sol.t .> 1.0] .<= 0.79) && all(sol[temp][sol.t .> 1.0] .>= 0.49) -# @test all(sol[temp][sol.t .!= 0.0] .<= 0.79) && all(sol[temp][sol.t .!= 0.0] .>= 0.2) -#end -# -#@testset "ImperativeAffect errors and warnings" begin -# @variables temp(t) -# params = @parameters furnace_on_threshold=0.5 furnace_off_threshold=0.7 furnace_power=1.0 leakage=0.1 furnace_on::Bool=false -# eqs = [ -# D(temp) ~ furnace_on * furnace_power - temp^2 * leakage -# ] -# -# furnace_off = ModelingToolkit.SymbolicContinuousCallback( -# [temp ~ furnace_off_threshold], -# ModelingToolkit.ImperativeAffect( -# modified = (; furnace_on), observed = (; furnace_on)) do x, o, c, i -# @set! x.furnace_on = false -# end) -# @named sys = ODESystem(eqs, t, [temp], params; continuous_events = [furnace_off]) -# ss = structural_simplify(sys) -# @test_logs (:warn, -# "The symbols Any[:furnace_on] are declared as both observed and modified; this is a code smell because it becomes easy to confuse them and assign/not assign a value.") prob=ODEProblem( -# ss, [temp => 0.0, furnace_on => true], (0.0, 100.0)) -# -# @variables tempsq(t) # trivially eliminated -# eqs = [tempsq ~ temp^2 -# D(temp) ~ furnace_on * furnace_power - temp^2 * leakage] -# -# furnace_off = ModelingToolkit.SymbolicContinuousCallback( -# [temp ~ furnace_off_threshold], -# ModelingToolkit.ImperativeAffect( -# modified = (; furnace_on, tempsq), observed = (; furnace_on)) do x, o, c, i -# @set! x.furnace_on = false -# end) -# @named sys = ODESystem( -# eqs, t, [temp, tempsq], params; continuous_events = [furnace_off]) -# ss = structural_simplify(sys) -# @test_throws "refers to missing variable(s)" prob=ODEProblem( -# ss, [temp => 0.0, furnace_on => true], (0.0, 100.0)) -# -# @parameters not_actually_here -# furnace_off = ModelingToolkit.SymbolicContinuousCallback( -# [temp ~ furnace_off_threshold], -# ModelingToolkit.ImperativeAffect(modified = (; furnace_on), -# observed = (; furnace_on, not_actually_here)) do x, o, c, i -# @set! x.furnace_on = false -# end) -# @named sys = ODESystem( -# eqs, t, [temp, tempsq], params; continuous_events = [furnace_off]) -# ss = structural_simplify(sys) -# @test_throws "refers to missing variable(s)" prob=ODEProblem( -# ss, [temp => 0.0, furnace_on => true], (0.0, 100.0)) -# -# furnace_off = ModelingToolkit.SymbolicContinuousCallback( -# [temp ~ furnace_off_threshold], -# ModelingToolkit.ImperativeAffect(modified = (; furnace_on), -# observed = (; furnace_on)) do x, o, c, i -# return (; fictional2 = false) -# end) -# @named sys = ODESystem( -# eqs, t, [temp, tempsq], params; continuous_events = [furnace_off]) -# ss = structural_simplify(sys) -# prob = ODEProblem( -# ss, [temp => 0.0, furnace_on => true], (0.0, 100.0)) -# @test_throws "Tried to write back to" solve(prob, Tsit5()) -#end -# -#@testset "Quadrature" begin -# @variables theta(t) omega(t) -# params = @parameters qA=0 qB=0 hA=0 hB=0 cnt::Int=0 -# eqs = [D(theta) ~ omega -# omega ~ 1.0] -# function decoder(oldA, oldB, newA, newB) -# state = (oldA, oldB, newA, newB) -# if state == (0, 0, 1, 0) || state == (1, 0, 1, 1) || state == (1, 1, 0, 1) || -# state == (0, 1, 0, 0) -# return 1 -# elseif state == (0, 0, 0, 1) || state == (0, 1, 1, 1) || state == (1, 1, 1, 0) || -# state == (1, 0, 0, 0) -# return -1 -# elseif state == (0, 0, 0, 0) || state == (0, 1, 0, 1) || state == (1, 0, 1, 0) || -# state == (1, 1, 1, 1) -# return 0 -# else -# return 0 # err is interpreted as no movement -# end -# end -# qAevt = ModelingToolkit.SymbolicContinuousCallback([cos(100 * theta) ~ 0], -# ModelingToolkit.ImperativeAffect((; qA, hA, hB, cnt), (; qB)) do x, o, c, i -# @set! x.hA = x.qA -# @set! x.hB = o.qB -# @set! x.qA = 1 -# @set! x.cnt += decoder(x.hA, x.hB, x.qA, o.qB) -# x -# end, -# affect_neg = ModelingToolkit.ImperativeAffect( -# (; qA, hA, hB, cnt), (; qB)) do x, o, c, i -# @set! x.hA = x.qA -# @set! x.hB = o.qB -# @set! x.qA = 0 -# @set! x.cnt += decoder(x.hA, x.hB, x.qA, o.qB) -# x -# end; rootfind = SciMLBase.RightRootFind) -# qBevt = ModelingToolkit.SymbolicContinuousCallback([cos(100 * theta - π / 2) ~ 0], -# ModelingToolkit.ImperativeAffect((; qB, hA, hB, cnt), (; qA)) do x, o, c, i -# @set! x.hA = o.qA -# @set! x.hB = x.qB -# @set! x.qB = 1 -# @set! x.cnt += decoder(x.hA, x.hB, o.qA, x.qB) -# x -# end, -# affect_neg = ModelingToolkit.ImperativeAffect( -# (; qB, hA, hB, cnt), (; qA)) do x, o, c, i -# @set! x.hA = o.qA -# @set! x.hB = x.qB -# @set! x.qB = 0 -# @set! x.cnt += decoder(x.hA, x.hB, o.qA, x.qB) -# x -# end; rootfind = SciMLBase.RightRootFind) -# @named sys = ODESystem( -# eqs, t, [theta, omega], params; continuous_events = [qAevt, qBevt]) -# ss = structural_simplify(sys) -# prob = ODEProblem(ss, [theta => 1e-5], (0.0, pi)) -# sol = solve(prob, Tsit5(); dtmax = 0.01) -# @test getp(sol, cnt)(sol) == 198 # we get 2 pulses per phase cycle (cos 0 crossing) and we go to 100 cycles; we miss a few due to the initial state -#end -# -#@testset "Initialization" begin -# @variables x(t) -# seen = false -# f = ModelingToolkit.FunctionalAffect( -# f = (i, u, p, c) -> seen = true, sts = [], pars = [], discretes = []) -# cb1 = ModelingToolkit.SymbolicContinuousCallback( -# [x ~ 0], nothing, initialize = [x ~ 1.5], finalize = f) -# @mtkbuild sys = ODESystem(D(x) ~ -1, t, [x], []; continuous_events = [cb1]) -# prob = ODEProblem(sys, [x => 1.0], (0.0, 2), []) -# sol = solve(prob, Tsit5(); dtmax = 0.01) -# @test sol[x][1] ≈ 1.0 -# @test sol[x][2] ≈ 1.5 # the initialize affect has been applied -# @test seen == true -# -# @variables x(t) -# seen = false -# f = ModelingToolkit.FunctionalAffect( -# f = (i, u, p, c) -> seen = true, sts = [], pars = [], discretes = []) -# cb1 = ModelingToolkit.SymbolicContinuousCallback( -# [x ~ 0], nothing, initialize = [x ~ 1.5], finalize = f) -# inited = false -# finaled = false -# a = ModelingToolkit.FunctionalAffect( -# f = (i, u, p, c) -> inited = true, sts = [], pars = [], discretes = []) -# b = ModelingToolkit.FunctionalAffect( -# f = (i, u, p, c) -> finaled = true, sts = [], pars = [], discretes = []) -# cb2 = ModelingToolkit.SymbolicContinuousCallback( -# [x ~ 0.1], nothing, initialize = a, finalize = b) -# @mtkbuild sys = ODESystem(D(x) ~ -1, t, [x], []; continuous_events = [cb1, cb2]) -# prob = ODEProblem(sys, [x => 1.0], (0.0, 2), []) -# sol = solve(prob, Tsit5()) -# @test sol[x][1] ≈ 1.0 -# @test sol[x][2] ≈ 1.5 # the initialize affect has been applied -# @test seen == true -# @test inited == true -# @test finaled == true -# -# #periodic -# inited = false -# finaled = false -# cb3 = ModelingToolkit.SymbolicDiscreteCallback( -# 1.0, [x ~ 2], initialize = a, finalize = b) -# @mtkbuild sys = ODESystem(D(x) ~ -1, t, [x], []; discrete_events = [cb3]) -# prob = ODEProblem(sys, [x => 1.0], (0.0, 2), []) -# sol = solve(prob, Tsit5()) -# @test inited == true -# @test finaled == true -# @test isapprox(sol[x][3], 0.0, atol = 1e-9) -# @test sol[x][4] ≈ 2.0 -# @test sol[x][5] ≈ 1.0 -# -# seen = false -# inited = false -# finaled = false -# cb3 = ModelingToolkit.SymbolicDiscreteCallback(1.0, f, initialize = a, finalize = b) -# @mtkbuild sys = ODESystem(D(x) ~ -1, t, [x], []; discrete_events = [cb3]) -# prob = ODEProblem(sys, [x => 1.0], (0.0, 2), []) -# sol = solve(prob, Tsit5()) -# @test seen == true -# @test inited == true -# -# #preset -# seen = false -# inited = false -# finaled = false -# cb3 = ModelingToolkit.SymbolicDiscreteCallback([1.0], f, initialize = a, finalize = b) -# @mtkbuild sys = ODESystem(D(x) ~ -1, t, [x], []; discrete_events = [cb3]) -# prob = ODEProblem(sys, [x => 1.0], (0.0, 2), []) -# sol = solve(prob, Tsit5()) -# @test seen == true -# @test inited == true -# @test finaled == true -# -# #equational -# seen = false -# inited = false -# finaled = false -# cb3 = ModelingToolkit.SymbolicDiscreteCallback( -# t == 1.0, f, initialize = a, finalize = b) -# @mtkbuild sys = ODESystem(D(x) ~ -1, t, [x], []; discrete_events = [cb3]) -# prob = ODEProblem(sys, [x => 1.0], (0.0, 2), []) -# sol = solve(prob, Tsit5(); tstops = 1.0) -# @test seen == true -# @test inited == true -# @test finaled == true -#end +@testset "Discrete event reinitialization (#3142)" begin + @connector LiquidPort begin + p(t)::Float64, [description = "Set pressure in bar", + guess = 1.01325] + Vdot(t)::Float64, + [description = "Volume flow rate in L/min", + guess = 0.0, + connect = Flow] + end + + @mtkmodel PressureSource begin + @components begin + port = LiquidPort() + end + @parameters begin + p_set::Float64 = 1.01325, [description = "Set pressure in bar"] + end + @equations begin + port.p ~ p_set + end + end + + @mtkmodel BinaryValve begin + @constants begin + p_ref::Float64 = 1.0, [description = "Reference pressure drop in bar"] + ρ_ref::Float64 = 1000.0, [description = "Reference density in kg/m^3"] + end + @components begin + port_in = LiquidPort() + port_out = LiquidPort() + end + @parameters begin + k_V::Float64 = 1.0, [description = "Valve coefficient in L/min/bar"] + k_leakage::Float64 = 1e-08, [description = "Leakage coefficient in L/min/bar"] + ρ::Float64 = 1000.0, [description = "Density in kg/m^3"] + end + @variables begin + S(t)::Float64, [description = "Valve state", guess = 1.0, irreducible = true] + Δp(t)::Float64, [description = "Pressure difference in bar", guess = 1.0] + Vdot(t)::Float64, [description = "Volume flow rate in L/min", guess = 1.0] + end + @equations begin + # Port handling + port_in.Vdot ~ -Vdot + port_out.Vdot ~ Vdot + Δp ~ port_in.p - port_out.p + # System behavior + D(S) ~ 0.0 + Vdot ~ S * k_V * sign(Δp) * sqrt(abs(Δp) / p_ref * ρ_ref / ρ) + k_leakage * Δp # softplus alpha function to avoid negative values under the sqrt + end + end + + # Test System + @mtkmodel TestSystem begin + @components begin + pressure_source_1 = PressureSource(p_set = 2.0) + binary_valve_1 = BinaryValve(S = 1.0, k_leakage = 0.0) + binary_valve_2 = BinaryValve(S = 1.0, k_leakage = 0.0) + pressure_source_2 = PressureSource(p_set = 1.0) + end + @equations begin + connect(pressure_source_1.port, binary_valve_1.port_in) + connect(binary_valve_1.port_out, binary_valve_2.port_in) + connect(binary_valve_2.port_out, pressure_source_2.port) + end + @discrete_events begin + [30] => [binary_valve_1.S ~ 0.0, binary_valve_2.Δp ~ 0.0] + [60] => [ + binary_valve_1.S ~ 1.0, binary_valve_2.S ~ 0.0, binary_valve_2.Δp ~ 1.0] + [120] => [binary_valve_1.S ~ 0.0, binary_valve_2.Δp ~ 0.0] + end + end + + # Test Simulation + @mtkbuild sys = TestSystem() + + # Test Simulation + prob = ODEProblem(sys, [], (0.0, 150.0)) + sol = solve(prob) + @test sol[end] == [0.0, 0.0, 0.0] +end + +@testset "Discrete variable timeseries" begin + @variables x(t) + @parameters a(t) b(t) c(t) + cb1 = SymbolicContinuousCallback([x ~ 1.0] => [a ~ -Pre(a)], discrete_parameters = [a]) + function save_affect!(integ, u, p, ctx) + integ.ps[p.b] = 5.0 + end + cb2 = [x ~ 0.5] => (save_affect!, [], [b], [b], nothing) + cb3 = SymbolicDiscreteCallback(1.0 => [c ~ t], discrete_parameters = [c]) + + @mtkbuild sys = ODESystem(D(x) ~ cos(t), t, [x], [a, b, c]; + continuous_events = [cb1, cb2], discrete_events = [cb3]) + prob = ODEProblem(sys, [x => 1.0], (0.0, 2pi), [a => 1.0, b => 2.0, c => 0.0]) + @test sort(canonicalize(Discrete(), prob.p)[1]) == [0.0, 1.0, 2.0] + sol = solve(prob, Tsit5()) + + @test sol[a] == [1.0, -1.0] + @test sol[b] == [2.0, 5.0, 5.0] + @test sol[c] == [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0] +end + +@testset "Heater" begin + @variables temp(t) + params = @parameters furnace_on_threshold=0.5 furnace_off_threshold=0.7 furnace_power=1.0 leakage=0.1 furnace_on::Bool=false + eqs = [ + D(temp) ~ furnace_on * furnace_power - temp^2 * leakage + ] + + furnace_off = ModelingToolkit.SymbolicContinuousCallback( + [temp ~ furnace_off_threshold], + ModelingToolkit.ImperativeAffect(modified = (; furnace_on)) do x, o, i, c + @set! x.furnace_on = false + end) + furnace_enable = ModelingToolkit.SymbolicContinuousCallback( + [temp ~ furnace_on_threshold], + ModelingToolkit.ImperativeAffect(modified = (; furnace_on)) do x, o, i, c + @set! x.furnace_on = true + end) + @named sys = ODESystem( + eqs, t, [temp], params; continuous_events = [furnace_off, furnace_enable]) + ss = structural_simplify(sys) + prob = ODEProblem(ss, [temp => 0.0, furnace_on => true], (0.0, 100.0)) + sol = solve(prob, Tsit5(); dtmax = 0.01) + @test all(sol[temp][sol.t .> 1.0] .<= 0.79) && all(sol[temp][sol.t .> 1.0] .>= 0.49) + + furnace_off = ModelingToolkit.SymbolicContinuousCallback( + [temp ~ furnace_off_threshold], + ModelingToolkit.ImperativeAffect(modified = (; furnace_on)) do x, o, c, i + @set! x.furnace_on = false + end; initialize = ModelingToolkit.ImperativeAffect(modified = (; + temp)) do x, o, c, i + @set! x.temp = 0.2 + end) + furnace_enable = ModelingToolkit.SymbolicContinuousCallback( + [temp ~ furnace_on_threshold], + ModelingToolkit.ImperativeAffect(modified = (; furnace_on)) do x, o, c, i + @set! x.furnace_on = true + end) + @named sys = ODESystem( + eqs, t, [temp], params; continuous_events = [furnace_off, furnace_enable]) + ss = structural_simplify(sys) + prob = ODEProblem(ss, [temp => 0.0, furnace_on => true], (0.0, 100.0)) + sol = solve(prob, Tsit5(); dtmax = 0.01) + @test all(sol[temp][sol.t .> 1.0] .<= 0.79) && all(sol[temp][sol.t .> 1.0] .>= 0.49) + @test all(sol[temp][sol.t .!= 0.0] .<= 0.79) && all(sol[temp][sol.t .!= 0.0] .>= 0.2) +end + +@testset "ImperativeAffect errors and warnings" begin + @variables temp(t) + params = @parameters furnace_on_threshold=0.5 furnace_off_threshold=0.7 furnace_power=1.0 leakage=0.1 furnace_on::Bool=false + eqs = [ + D(temp) ~ furnace_on * furnace_power - temp^2 * leakage + ] + + furnace_off = ModelingToolkit.SymbolicContinuousCallback( + [temp ~ furnace_off_threshold], + ModelingToolkit.ImperativeAffect( + modified = (; furnace_on), observed = (; furnace_on)) do x, o, c, i + @set! x.furnace_on = false + end) + @named sys = ODESystem(eqs, t, [temp], params; continuous_events = [furnace_off]) + ss = structural_simplify(sys) + @test_logs (:warn, + "The symbols Any[:furnace_on] are declared as both observed and modified; this is a code smell because it becomes easy to confuse them and assign/not assign a value.") prob=ODEProblem( + ss, [temp => 0.0, furnace_on => true], (0.0, 100.0)) + + @variables tempsq(t) # trivially eliminated + eqs = [tempsq ~ temp^2 + D(temp) ~ furnace_on * furnace_power - temp^2 * leakage] + + furnace_off = ModelingToolkit.SymbolicContinuousCallback( + [temp ~ furnace_off_threshold], + ModelingToolkit.ImperativeAffect( + modified = (; furnace_on, tempsq), observed = (; furnace_on)) do x, o, c, i + @set! x.furnace_on = false + end) + @named sys = ODESystem( + eqs, t, [temp, tempsq], params; continuous_events = [furnace_off]) + ss = structural_simplify(sys) + @test_throws "refers to missing variable(s)" prob=ODEProblem( + ss, [temp => 0.0, furnace_on => true], (0.0, 100.0)) + + @parameters not_actually_here + furnace_off = ModelingToolkit.SymbolicContinuousCallback( + [temp ~ furnace_off_threshold], + ModelingToolkit.ImperativeAffect(modified = (; furnace_on), + observed = (; furnace_on, not_actually_here)) do x, o, c, i + @set! x.furnace_on = false + end) + @named sys = ODESystem( + eqs, t, [temp, tempsq], params; continuous_events = [furnace_off]) + ss = structural_simplify(sys) + @test_throws "refers to missing variable(s)" prob=ODEProblem( + ss, [temp => 0.0, furnace_on => true], (0.0, 100.0)) + + furnace_off = ModelingToolkit.SymbolicContinuousCallback( + [temp ~ furnace_off_threshold], + ModelingToolkit.ImperativeAffect(modified = (; furnace_on), + observed = (; furnace_on)) do x, o, c, i + return (; fictional2 = false) + end) + @named sys = ODESystem( + eqs, t, [temp, tempsq], params; continuous_events = [furnace_off]) + ss = structural_simplify(sys) + prob = ODEProblem( + ss, [temp => 0.0, furnace_on => true], (0.0, 100.0)) + @test_throws "Tried to write back to" solve(prob, Tsit5()) +end + +@testset "Quadrature" begin + @variables theta(t) omega(t) + params = @parameters qA=0 qB=0 hA=0 hB=0 cnt::Int=0 + eqs = [D(theta) ~ omega + omega ~ 1.0] + function decoder(oldA, oldB, newA, newB) + state = (oldA, oldB, newA, newB) + if state == (0, 0, 1, 0) || state == (1, 0, 1, 1) || state == (1, 1, 0, 1) || + state == (0, 1, 0, 0) + return 1 + elseif state == (0, 0, 0, 1) || state == (0, 1, 1, 1) || state == (1, 1, 1, 0) || + state == (1, 0, 0, 0) + return -1 + elseif state == (0, 0, 0, 0) || state == (0, 1, 0, 1) || state == (1, 0, 1, 0) || + state == (1, 1, 1, 1) + return 0 + else + return 0 # err is interpreted as no movement + end + end + qAevt = ModelingToolkit.SymbolicContinuousCallback([cos(100 * theta) ~ 0], + ModelingToolkit.ImperativeAffect((; qA, hA, hB, cnt), (; qB)) do x, o, c, i + @set! x.hA = x.qA + @set! x.hB = o.qB + @set! x.qA = 1 + @set! x.cnt += decoder(x.hA, x.hB, x.qA, o.qB) + x + end, + affect_neg = ModelingToolkit.ImperativeAffect( + (; qA, hA, hB, cnt), (; qB)) do x, o, c, i + @set! x.hA = x.qA + @set! x.hB = o.qB + @set! x.qA = 0 + @set! x.cnt += decoder(x.hA, x.hB, x.qA, o.qB) + x + end; rootfind = SciMLBase.RightRootFind) + qBevt = ModelingToolkit.SymbolicContinuousCallback([cos(100 * theta - π / 2) ~ 0], + ModelingToolkit.ImperativeAffect((; qB, hA, hB, cnt), (; qA)) do x, o, c, i + @set! x.hA = o.qA + @set! x.hB = x.qB + @set! x.qB = 1 + @set! x.cnt += decoder(x.hA, x.hB, o.qA, x.qB) + x + end, + affect_neg = ModelingToolkit.ImperativeAffect( + (; qB, hA, hB, cnt), (; qA)) do x, o, c, i + @set! x.hA = o.qA + @set! x.hB = x.qB + @set! x.qB = 0 + @set! x.cnt += decoder(x.hA, x.hB, o.qA, x.qB) + x + end; rootfind = SciMLBase.RightRootFind) + @named sys = ODESystem( + eqs, t, [theta, omega], params; continuous_events = [qAevt, qBevt]) + ss = structural_simplify(sys) + prob = ODEProblem(ss, [theta => 1e-5], (0.0, pi)) + sol = solve(prob, Tsit5(); dtmax = 0.01) + @test getp(sol, cnt)(sol) == 198 # we get 2 pulses per phase cycle (cos 0 crossing) and we go to 100 cycles; we miss a few due to the initial state +end + +@testset "Initialization" begin + @variables x(t) + seen = false + f = ModelingToolkit.FunctionalAffect( + f = (i, u, p, c) -> seen = true, sts = [], pars = [], discretes = []) + cb1 = ModelingToolkit.SymbolicContinuousCallback( + [x ~ 0], nothing, initialize = [x ~ 1.5], finalize = f) + @mtkbuild sys = ODESystem(D(x) ~ -1, t, [x], []; continuous_events = [cb1]) + prob = ODEProblem(sys, [x => 1.0], (0.0, 2), []) + sol = solve(prob, Tsit5(); dtmax = 0.01) + @test sol[x][1] ≈ 1.0 + @test sol[x][2] ≈ 1.5 # the initialize affect has been applied + @test seen == true + + @variables x(t) + seen = false + f = ModelingToolkit.FunctionalAffect( + f = (i, u, p, c) -> seen = true, sts = [], pars = [], discretes = []) + cb1 = ModelingToolkit.SymbolicContinuousCallback( + [x ~ 0], nothing, initialize = [x ~ 1.5], finalize = f) + inited = false + finaled = false + a = ModelingToolkit.FunctionalAffect( + f = (i, u, p, c) -> inited = true, sts = [], pars = [], discretes = []) + b = ModelingToolkit.FunctionalAffect( + f = (i, u, p, c) -> finaled = true, sts = [], pars = [], discretes = []) + cb2 = ModelingToolkit.SymbolicContinuousCallback( + [x ~ 0.1], nothing, initialize = a, finalize = b) + @mtkbuild sys = ODESystem(D(x) ~ -1, t, [x], []; continuous_events = [cb1, cb2]) + prob = ODEProblem(sys, [x => 1.0], (0.0, 2), []) + sol = solve(prob, Tsit5()) + @test sol[x][1] ≈ 1.0 + @test sol[x][2] ≈ 1.5 # the initialize affect has been applied + @test seen == true + @test inited == true + @test finaled == true + + #periodic + inited = false + finaled = false + cb3 = ModelingToolkit.SymbolicDiscreteCallback( + 1.0, [x ~ 2], initialize = a, finalize = b) + @mtkbuild sys = ODESystem(D(x) ~ -1, t, [x], []; discrete_events = [cb3]) + prob = ODEProblem(sys, [x => 1.0], (0.0, 2), []) + sol = solve(prob, Tsit5()) + @test inited == true + @test finaled == true + @test isapprox(sol[x][3], 0.0, atol = 1e-9) + @test sol[x][4] ≈ 2.0 + @test sol[x][5] ≈ 1.0 + + seen = false + inited = false + finaled = false + cb3 = ModelingToolkit.SymbolicDiscreteCallback(1.0, f, initialize = a, finalize = b) + @mtkbuild sys = ODESystem(D(x) ~ -1, t, [x], []; discrete_events = [cb3]) + prob = ODEProblem(sys, [x => 1.0], (0.0, 2), []) + sol = solve(prob, Tsit5()) + @test seen == true + @test inited == true + + #preset + seen = false + inited = false + finaled = false + cb3 = ModelingToolkit.SymbolicDiscreteCallback([1.0], f, initialize = a, finalize = b) + @mtkbuild sys = ODESystem(D(x) ~ -1, t, [x], []; discrete_events = [cb3]) + prob = ODEProblem(sys, [x => 1.0], (0.0, 2), []) + sol = solve(prob, Tsit5()) + @test seen == true + @test inited == true + @test finaled == true + + #equational + seen = false + inited = false + finaled = false + cb3 = ModelingToolkit.SymbolicDiscreteCallback( + t == 1.0, f, initialize = a, finalize = b) + @mtkbuild sys = ODESystem(D(x) ~ -1, t, [x], []; discrete_events = [cb3]) + prob = ODEProblem(sys, [x => 1.0], (0.0, 2), []) + sol = solve(prob, Tsit5(); tstops = 1.0) + @test seen == true + @test inited == true + @test finaled == true +end @testset "Bump" begin @variables x(t) [irreducible = true] y(t) [irreducible = true] @@ -1213,7 +1213,7 @@ end D(x) ~ -k * x end @discrete_events begin - (t == 1.0) => [k ~ 1.0], discrete_parameters = [k] + (t == 1.0) => [k ~ 1.0]#, discrete_parameters = [k] end end @mtkbuild decay = DECAY() diff --git a/test/symbolic_parameters.jl b/test/symbolic_parameters.jl index a29090912c..f4fa21e614 100644 --- a/test/symbolic_parameters.jl +++ b/test/symbolic_parameters.jl @@ -28,7 +28,7 @@ resolved = ModelingToolkit.varmap_to_vars(Dict(), parameters(ns), prob = NonlinearProblem(complete(ns), [u => 1.0], Pair[]) @test prob.u0 == [1.0, 1.1, 0.9] -@show sol = solve(prob, NewtonRaphson()) +sol = solve(prob, NewtonRaphson()) @variables a @parameters b @@ -43,12 +43,12 @@ res = ModelingToolkit.varmap_to_vars(Dict(), parameters(top), top = complete(top) prob = NonlinearProblem(top, [unknowns(ns, u) => 1.0, a => 1.0], []) @test prob.u0 == [1.0, 0.5, 1.1, 0.9] -@show sol = solve(prob, NewtonRaphson()) +sol = solve(prob, NewtonRaphson()) # test NullParameters+defaults prob = NonlinearProblem(top, [unknowns(ns, u) => 1.0, a => 1.0]) @test prob.u0 == [1.0, 0.5, 1.1, 0.9] -@show sol = solve(prob, NewtonRaphson()) +sol = solve(prob, NewtonRaphson()) # test initial conditions and parameters at the problem level pars = @parameters(begin From b07f2081e20f6fd48f476c884b2472674610e140 Mon Sep 17 00:00:00 2001 From: vyudu Date: Mon, 31 Mar 2025 17:23:19 -0400 Subject: [PATCH 36/52] fix: fix collect_var --- src/systems/callbacks.jl | 7 +++-- src/utils.jl | 1 + test/odesystem.jl | 57 +++++++++++++++++++++------------------- 3 files changed, 36 insertions(+), 29 deletions(-) diff --git a/src/systems/callbacks.jl b/src/systems/callbacks.jl index 9be33b0800..9343ada58b 100644 --- a/src/systems/callbacks.jl +++ b/src/systems/callbacks.jl @@ -301,7 +301,9 @@ function make_affect(affect::Vector{Equation}; discrete_parameters::AbstractVect # get accessed parameters p from Pre(p) in the callback parameters accessed_params = filter(isparameter, map(unPre, collect(pre_params))) union!(accessed_params, sys_params) - # add unknowns to the map + + # add scalarized unknowns to the map. + _dvs = reduce(vcat, map(scalarize, _dvs), init = Any[]) for u in _dvs aff_map[u] = u end @@ -616,7 +618,8 @@ function compile_condition( end if !is_discrete(cbs) - condit = [cond.lhs - cond.rhs for cond in condit] + condit = reduce(vcat, flatten_equations(condit)) + condit = condit isa AbstractVector ? [c.lhs - c.rhs for c in condit] : [condit.lhs - condit.rhs] end fs = build_function_wrapper(sys, diff --git a/src/utils.jl b/src/utils.jl index 63f3897d74..f6c7771e0c 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -595,6 +595,7 @@ end function collect_var!(unknowns, parameters, var, iv; depth = 0) isequal(var, iv) && return nothing + var = unwrap(var) check_scope_depth(getmetadata(var, SymScope, LocalScope()), depth) || return nothing if iscalledparameter(var) callable = getcalledparameter(var) diff --git a/test/odesystem.jl b/test/odesystem.jl index 78218c5107..8323da53cf 100644 --- a/test/odesystem.jl +++ b/test/odesystem.jl @@ -1032,24 +1032,26 @@ prob = ODEProblem(sys, [x => 1.0], (0.0, 10.0)) @test_nowarn solve(prob, Tsit5()) # Issue#2383 -@variables x(t)[1:3] -@parameters p[1:3, 1:3] -eqs = [ - D(x) ~ p * x -] -@mtkbuild sys = ODESystem(eqs, t; continuous_events = [[norm(x) ~ 3.0] => [x ~ ones(3)]]) -# array affect equations used to not work -prob1 = @test_nowarn ODEProblem(sys, [x => ones(3)], (0.0, 10.0), [p => ones(3, 3)]) -sol1 = @test_nowarn solve(prob1, Tsit5()) - -# array condition equations also used to not work -@mtkbuild sys = ODESystem( - eqs, t; continuous_events = [[x ~ sqrt(3) * ones(3)] => [x ~ ones(3)]]) -# array affect equations used to not work -prob2 = @test_nowarn ODEProblem(sys, [x => ones(3)], (0.0, 10.0), [p => ones(3, 3)]) -sol2 = @test_nowarn solve(prob2, Tsit5()) - -@test sol1 ≈ sol2 +@testset "Arrays in affect/condition equations" begin + @variables x(t)[1:3] + @parameters p[1:3, 1:3] + eqs = [ + D(x) ~ p * x + ] + @mtkbuild sys = ODESystem(eqs, t; continuous_events = [[norm(x) ~ 3.0] => [x ~ ones(3)]]) + # array affect equations used to not work + prob1 = @test_nowarn ODEProblem(sys, [x => ones(3)], (0.0, 10.0), [p => ones(3, 3)]) + sol1 = @test_nowarn solve(prob1, Tsit5()) + + # array condition equations also used to not work + @mtkbuild sys = ODESystem( + eqs, t; continuous_events = [[x ~ sqrt(3) * ones(3)] => [x ~ ones(3)]]) + # array affect equations used to not work + prob2 = @test_nowarn ODEProblem(sys, [x => ones(3)], (0.0, 10.0), [p => ones(3, 3)]) + sol2 = @test_nowarn solve(prob2, Tsit5()) + + @test sol1.u ≈ sol2.u[2:end] +end # Requires fix in symbolics for `linear_expansion(p * x, D(y))` @test_skip begin @@ -1196,10 +1198,12 @@ end end # Namespacing of array variables -@variables x(t)[1:2] -@named sys = ODESystem(Equation[], t) -@test getname(unknowns(sys, x)) == :sys₊x -@test size(unknowns(sys, x)) == size(x) +@testset "Namespacing of array variables" begin + @variables x(t)[1:2] + @named sys = ODESystem(Equation[], t) + @test getname(unknowns(sys, x)) == :sys₊x + @test size(unknowns(sys, x)) == size(x) +end # Issue#2667 and Issue#2953 @testset "ForwardDiff through ODEProblem constructor" begin @@ -1537,8 +1541,7 @@ end @testset "Observed variables dependent on discrete parameters" begin @variables x(t) obs(t) @parameters c(t) - @mtkbuild sys = ODESystem( - [D(x) ~ c * cos(x), obs ~ c], t, [x], [c]; discrete_events = [1.0 => [c ~ c + 1]]) + @mtkbuild sys = ODESystem([D(x) ~ c * cos(x), obs ~ c], t, [x], [c]; discrete_events = [SymbolicDiscreteCallback(1.0 => [c ~ Pre(c) + 1], discrete_parameters = [c])]) prob = ODEProblem(sys, [x => 0.0], (0.0, 2pi), [c => 1.0]) sol = solve(prob, Tsit5()) @test sol[obs] ≈ 1:7 @@ -1598,15 +1601,15 @@ end # Test `isequal` @testset "`isequal`" begin @variables X(t) - @parameters p d + @parameters p d(t) eq = D(X) ~ p - d * X osys1 = complete(ODESystem([eq], t; name = :osys)) osys2 = complete(ODESystem([eq], t; name = :osys)) @test osys1 == osys2 # true - continuous_events = [[X ~ 1.0] => [X ~ X + 5.0]] - discrete_events = [5.0 => [d ~ d / 2.0]] + continuous_events = [[X ~ 1.0] => [X ~ Pre(X) + 5.0]] + discrete_events = [SymbolicDiscreteCallback(5.0 => [d ~ d / 2.0], discrete_parameters = [d])] osys1 = complete(ODESystem([eq], t; name = :osys, continuous_events)) osys2 = complete(ODESystem([eq], t; name = :osys)) From ff966023c3ff060c1e8382cdccd06d1e3522c2d0 Mon Sep 17 00:00:00 2001 From: vyudu Date: Mon, 31 Mar 2025 17:34:09 -0400 Subject: [PATCH 37/52] format --- src/systems/callbacks.jl | 37 +++++++++++++++++++++------------- src/systems/model_parsing.jl | 1 - src/systems/systems.jl | 4 +++- test/fmi/fmi.jl | 6 ++++-- test/odesystem.jl | 17 +++++++++++----- test/parameter_dependencies.jl | 12 +++++++---- test/symbolic_events.jl | 2 +- 7 files changed, 51 insertions(+), 28 deletions(-) diff --git a/src/systems/callbacks.jl b/src/systems/callbacks.jl index 9343ada58b..f025531c3c 100644 --- a/src/systems/callbacks.jl +++ b/src/systems/callbacks.jl @@ -230,15 +230,17 @@ struct SymbolicContinuousCallback <: AbstractCallback conditions = (conditions isa AbstractVector) ? conditions : [conditions] if isnothing(reinitializealg) - any(a -> (a isa FunctionalAffect || a isa ImperativeAffect), [affect, affect_neg, initialize, finalize]) ? - reinitializealg = SciMLBase.CheckInit() : - reinitializealg = SciMLBase.NoInit() + any(a -> (a isa FunctionalAffect || a isa ImperativeAffect), + [affect, affect_neg, initialize, finalize]) ? + reinitializealg = SciMLBase.CheckInit() : + reinitializealg = SciMLBase.NoInit() end new(conditions, make_affect(affect; iv, algeeqs, discrete_parameters), make_affect(affect_neg; iv, algeeqs, discrete_parameters), make_affect(initialize; iv, algeeqs, discrete_parameters), make_affect( - finalize; iv, algeeqs, discrete_parameters), rootfind, reinitializealg) + finalize; iv, algeeqs, discrete_parameters), + rootfind, reinitializealg) end # Default affect to nothing end @@ -286,7 +288,7 @@ function make_affect(affect::Vector{Equation}; discrete_parameters::AbstractVect sys_params = collect(setdiff(params, union(discrete_parameters, pre_params))) discretes = map(tovar, discrete_parameters) dvs = collect(dvs) - _dvs = map(default_toterm, dvs) + _dvs = map(default_toterm, dvs) aff_map = Dict(zip(discretes, discrete_parameters)) rev_map = Dict(zip(discrete_parameters, discretes)) @@ -446,9 +448,10 @@ struct SymbolicDiscreteCallback <: AbstractCallback c = is_timed_condition(condition) ? condition : value(scalarize(condition)) if isnothing(reinitializealg) - any(a -> (a isa FunctionalAffect || a isa ImperativeAffect), [affect, initialize, finalize]) ? - reinitializealg = SciMLBase.CheckInit() : - reinitializealg = SciMLBase.NoInit() + any(a -> (a isa FunctionalAffect || a isa ImperativeAffect), + [affect, initialize, finalize]) ? + reinitializealg = SciMLBase.CheckInit() : + reinitializealg = SciMLBase.NoInit() end new(c, make_affect(affect; iv, algeeqs, discrete_parameters), make_affect(initialize; iv, algeeqs, discrete_parameters), @@ -582,7 +585,8 @@ end function Base.:(==)(e1::AbstractCallback, e2::AbstractCallback) (is_discrete(e1) === is_discrete(e2)) || return false (isequal(e1.conditions, e2.conditions) && isequal(e1.affect, e2.affect) && - isequal(e1.initialize, e2.initialize) && isequal(e1.finalize, e2.finalize)) && isequal(e1.reinitializealg, e2.reinitializealg) || + isequal(e1.initialize, e2.initialize) && isequal(e1.finalize, e2.finalize)) && + isequal(e1.reinitializealg, e2.reinitializealg) || return false is_discrete(e1) || (isequal(e1.affect_neg, e2.affect_neg) && isequal(e1.rootfind, e2.rootfind)) @@ -619,7 +623,8 @@ function compile_condition( if !is_discrete(cbs) condit = reduce(vcat, flatten_equations(condit)) - condit = condit isa AbstractVector ? [c.lhs - c.rhs for c in condit] : [condit.lhs - condit.rhs] + condit = condit isa AbstractVector ? [c.lhs - c.rhs for c in condit] : + [condit.lhs - condit.rhs] end fs = build_function_wrapper(sys, @@ -685,15 +690,18 @@ function generate_continuous_callbacks(sys::AbstractSystem, dvs = unknowns(sys), ps = parameters(sys; initial_parameters = true); kwargs...) cbs = continuous_events(sys) isempty(cbs) && return nothing - cb_classes = Dict{Tuple{SciMLBase.RootfindOpt, SciMLBase.DAEInitializationAlgorithm}, Vector{SymbolicContinuousCallback}}() + cb_classes = Dict{Tuple{SciMLBase.RootfindOpt, SciMLBase.DAEInitializationAlgorithm}, + Vector{SymbolicContinuousCallback}}() # Sort the callbacks by their rootfinding method for cb in cbs - _cbs = get!(() -> SymbolicContinuousCallback[], cb_classes, (cb.rootfind, cb.reinitializealg)) + _cbs = get!(() -> SymbolicContinuousCallback[], + cb_classes, (cb.rootfind, cb.reinitializealg)) push!(_cbs, cb) end sort!(OrderedDict(cb_classes), by = cb -> cb[1]) - compiled_callbacks = [generate_callback(cb, sys; kwargs...) for ((rf, reinit), cb) in cb_classes] + compiled_callbacks = [generate_callback(cb, sys; kwargs...) + for ((rf, reinit), cb) in cb_classes] if length(compiled_callbacks) == 1 return only(compiled_callbacks) else @@ -791,7 +799,8 @@ function generate_callback(cb, sys; kwargs...) return PresetTimeCallback(trigger, affect; initialize, finalize, initializealg = cb.reinitializealg) elseif is_timed - return PeriodicCallback(affect, trigger; initialize, finalize, initializealg = cb.reinitializealg) + return PeriodicCallback( + affect, trigger; initialize, finalize, initializealg = cb.reinitializealg) else return DiscreteCallback(trigger, affect; initialize, finalize, initializealg = cb.reinitializealg) diff --git a/src/systems/model_parsing.jl b/src/systems/model_parsing.jl index 3a1ff1cf1f..58016ad4ef 100644 --- a/src/systems/model_parsing.jl +++ b/src/systems/model_parsing.jl @@ -137,7 +137,6 @@ function _model_macro(mod, name, expr, isconnector) $(d_evts...) ])))) - f = if length(where_types) == 0 :($(Symbol(:__, name, :__))(; name, $(kwargs...)) = $exprs) else diff --git a/src/systems/systems.jl b/src/systems/systems.jl index da08186355..ac36bb0143 100644 --- a/src/systems/systems.jl +++ b/src/systems/systems.jl @@ -155,7 +155,9 @@ function __structural_simplify(sys::AbstractSystem, io = nothing; simplify = fal get_iv(ode_sys), unknowns(ode_sys), parameters(ode_sys); name = nameof(ode_sys), is_scalar_noise, observed = observed(ode_sys), defaults = defaults(sys), parameter_dependencies = parameter_dependencies(sys), assertions = assertions(sys), - guesses = guesses(sys), initialization_eqs = initialization_equations(sys), continuous_events = continuous_events(sys), discrete_events = discrete_events(sys)) + guesses = guesses(sys), initialization_eqs = initialization_equations(sys), + continuous_events = continuous_events(sys), + discrete_events = discrete_events(sys)) end end diff --git a/test/fmi/fmi.jl b/test/fmi/fmi.jl index e4c155270e..98c93398ff 100644 --- a/test/fmi/fmi.jl +++ b/test/fmi/fmi.jl @@ -157,7 +157,8 @@ end @testset "v2, CS" begin fmu = loadFMU(joinpath(FMU_DIR, "SimpleAdder.fmu"); type = :CS) @named adder = MTK.FMIComponent( - Val(2); fmu, type = :CS, communication_step_size = 1e-6, reinitializealg = BrownFullBasicInit()) + Val(2); fmu, type = :CS, communication_step_size = 1e-6, + reinitializealg = BrownFullBasicInit()) @test MTK.isinput(adder.a) @test MTK.isinput(adder.b) @test MTK.isoutput(adder.out) @@ -209,7 +210,8 @@ end @testset "v3, CS" begin fmu = loadFMU(joinpath(FMU_DIR, "StateSpace.fmu"); type = :CS) @named sspace = MTK.FMIComponent( - Val(3); fmu, communication_step_size = 1e-6, type = :CS, reinitializealg = BrownFullBasicInit()) + Val(3); fmu, communication_step_size = 1e-6, type = :CS, + reinitializealg = BrownFullBasicInit()) @test MTK.isinput(sspace.u) @test MTK.isoutput(sspace.y) @test !MTK.isinput(sspace.x) && !MTK.isoutput(sspace.x) diff --git a/test/odesystem.jl b/test/odesystem.jl index 8323da53cf..aabcf558c2 100644 --- a/test/odesystem.jl +++ b/test/odesystem.jl @@ -1038,18 +1038,19 @@ prob = ODEProblem(sys, [x => 1.0], (0.0, 10.0)) eqs = [ D(x) ~ p * x ] - @mtkbuild sys = ODESystem(eqs, t; continuous_events = [[norm(x) ~ 3.0] => [x ~ ones(3)]]) + @mtkbuild sys = ODESystem( + eqs, t; continuous_events = [[norm(x) ~ 3.0] => [x ~ ones(3)]]) # array affect equations used to not work prob1 = @test_nowarn ODEProblem(sys, [x => ones(3)], (0.0, 10.0), [p => ones(3, 3)]) sol1 = @test_nowarn solve(prob1, Tsit5()) - + # array condition equations also used to not work @mtkbuild sys = ODESystem( eqs, t; continuous_events = [[x ~ sqrt(3) * ones(3)] => [x ~ ones(3)]]) # array affect equations used to not work prob2 = @test_nowarn ODEProblem(sys, [x => ones(3)], (0.0, 10.0), [p => ones(3, 3)]) sol2 = @test_nowarn solve(prob2, Tsit5()) - + @test sol1.u ≈ sol2.u[2:end] end @@ -1541,7 +1542,12 @@ end @testset "Observed variables dependent on discrete parameters" begin @variables x(t) obs(t) @parameters c(t) - @mtkbuild sys = ODESystem([D(x) ~ c * cos(x), obs ~ c], t, [x], [c]; discrete_events = [SymbolicDiscreteCallback(1.0 => [c ~ Pre(c) + 1], discrete_parameters = [c])]) + @mtkbuild sys = ODESystem([D(x) ~ c * cos(x), obs ~ c], + t, + [x], + [c]; + discrete_events = [SymbolicDiscreteCallback( + 1.0 => [c ~ Pre(c) + 1], discrete_parameters = [c])]) prob = ODEProblem(sys, [x => 0.0], (0.0, 2pi), [c => 1.0]) sol = solve(prob, Tsit5()) @test sol[obs] ≈ 1:7 @@ -1609,7 +1615,8 @@ end @test osys1 == osys2 # true continuous_events = [[X ~ 1.0] => [X ~ Pre(X) + 5.0]] - discrete_events = [SymbolicDiscreteCallback(5.0 => [d ~ d / 2.0], discrete_parameters = [d])] + discrete_events = [SymbolicDiscreteCallback( + 5.0 => [d ~ d / 2.0], discrete_parameters = [d])] osys1 = complete(ODESystem([eq], t; name = :osys, continuous_events)) osys2 = complete(ODESystem([eq], t; name = :osys)) diff --git a/test/parameter_dependencies.jl b/test/parameter_dependencies.jl index 89f0dc1e27..bdaa2a391b 100644 --- a/test/parameter_dependencies.jl +++ b/test/parameter_dependencies.jl @@ -1,6 +1,7 @@ using ModelingToolkit using Test -using ModelingToolkit: t_nounits as t, D_nounits as D, SymbolicDiscreteCallback, SymbolicContinuousCallback +using ModelingToolkit: t_nounits as t, D_nounits as D, SymbolicDiscreteCallback, + SymbolicContinuousCallback using OrdinaryDiffEq using StochasticDiffEq using JumpProcesses @@ -225,7 +226,8 @@ end @test_nowarn solve(prob, Tsit5()) @mtkbuild sys = ODESystem(eqs, t; parameter_dependencies = [kq => 2kp], - discrete_events = [SymbolicDiscreteCallback([0.5] => [kp ~ 2.0], discrete_parameters = [kp])]) + discrete_events = [SymbolicDiscreteCallback( + [0.5] => [kp ~ 2.0], discrete_parameters = [kp])]) prob = ODEProblem(sys, [x => 0.0, y => 0.0], (0.0, Tf), [kp => 1.0; z(k - 1) => 3.0; yd(k - 1) => 0.0; z(k - 2) => 4.0; yd(k - 2) => 2.0]) @@ -269,7 +271,8 @@ end @named sys = ODESystem(eqs, t) @named sdesys = SDESystem(sys, noiseeqs; parameter_dependencies = [ρ => 2σ], - discrete_events = [SymbolicDiscreteCallback([10.0] => [σ ~ 15.0], discrete_parameters = [σ])]) + discrete_events = [SymbolicDiscreteCallback( + [10.0] => [σ ~ 15.0], discrete_parameters = [σ])]) sdesys = complete(sdesys) prob = SDEProblem( sdesys, [x => 1.0, y => 0.0, z => 0.0], (0.0, 100.0), [σ => 10.0, β => 2.33]) @@ -308,7 +311,8 @@ end @named js2 = JumpSystem( [j₁, j₃], t, [S, I, R], [γ]; parameter_dependencies = [β => 0.01γ], - discrete_events = [SymbolicDiscreteCallback([10.0] => [γ ~ 0.02], discrete_parameters = [γ])]) + discrete_events = [SymbolicDiscreteCallback( + [10.0] => [γ ~ 0.02], discrete_parameters = [γ])]) js2 = complete(js2) dprob = DiscreteProblem(js2, u₀map, tspan, parammap) jprob = JumpProblem(js2, dprob, Direct(), save_positions = (false, false), rng = rng) diff --git a/test/symbolic_events.jl b/test/symbolic_events.jl index 49c02bacc1..c41ab497b8 100644 --- a/test/symbolic_events.jl +++ b/test/symbolic_events.jl @@ -1100,7 +1100,7 @@ end f = ModelingToolkit.FunctionalAffect( f = (i, u, p, c) -> seen = true, sts = [], pars = [], discretes = []) cb1 = ModelingToolkit.SymbolicContinuousCallback( - [x ~ 0], nothing, initialize = [x ~ 1.5], finalize = f) + [x ~ 0], nothing, initialize = [x ~ 1.5], finalize = f) @mtkbuild sys = ODESystem(D(x) ~ -1, t, [x], []; continuous_events = [cb1]) prob = ODEProblem(sys, [x => 1.0], (0.0, 2), []) sol = solve(prob, Tsit5(); dtmax = 0.01) From 86f05e6a3b3b8e1a2c2fe7808ef69d8d62a0a26b Mon Sep 17 00:00:00 2001 From: vyudu Date: Tue, 1 Apr 2025 12:08:26 -0400 Subject: [PATCH 38/52] docs: add documentation for the symbolic affect changes --- docs/src/basics/Events.md | 89 +++++++++++++++++++++++++++++++++------ test/odesystem.jl | 3 +- 2 files changed, 77 insertions(+), 15 deletions(-) diff --git a/docs/src/basics/Events.md b/docs/src/basics/Events.md index 23e1e6d7d1..4829e698da 100644 --- a/docs/src/basics/Events.md +++ b/docs/src/basics/Events.md @@ -25,6 +25,67 @@ the event occurs). These can both be specified symbolically, but a more [general functional affect](@ref func_affects) representation is also allowed, as described below. +## Symbolic Callback Semantics (changed in V10) + +In callbacks, there is a distinction between values of the unknowns and parameters +*before* the callback, and the desired values *after* the callback. In MTK, this +is provided by the `Pre` operator. For example, if we would like to add 1 to an +unknown `x` in a callback, the equation would look like the following: + +```julia +x ~ Pre(x) + 1 +``` + +Non `Pre`-d values will be interpreted as values *after* the callback. As such, +writing + +```julia +x ~ x + 1 +``` + +will be interpreted as an algebraic equation to be satisfied after the callback. +Since this equation obviously cannot be satisfied, an error will result. + +Callbacks must maintain the consistency of DAEs, meaning that they must satisfy +all the algebraic equations of the system after their update. However, the affect +equations often do not fully specify which unknowns/parameters should be modified +to maintain consistency. To make this clear, MTK uses the following rules: + + 1. All unknowns are treated as modifiable by the callback. In order to enforce that an unknown `x` remains the same, one can add `x ~ Pre(x)` to the affect equations. + 2. All parameters are treated as un-modifiable, *unless* they are declared as `discrete_parameters` to the callback. In order to be a discrete parameter, the parameter must be time-dependent (the terminology *discretes* here means [discrete variables](@ref save_discretes)). + +For example, consider the following system. + +```julia +@variables x(t) y(t) +@parameters p(t) +@mtkbuild sys = ODESystem([x * y ~ p, D(x) ~ 0], t) +event = [t == 1] => [x ~ Pre(x) + 1] +``` + +By default what will happen is that `x` will increase by 1, `p` will remain constant, +and `y` will change in order to compensate the increase in `x`. But what if we +wanted to keep `y` constant and change `p` instead? We could use the callback +constructor as follows: + +```julia +event = SymbolicDiscreteCallback( + [t == 1] => [x ~ Pre(x) + 1, y ~ Pre(y)], discrete_parameters = [p]) +``` + +This way, we enforce that `y` will remain the same, and `p` will change. + +!!! warning + + Symbolic affects come with the guarantee that the state after the callback + will be consistent. However, when using [general functional affects](@ref func_affects) + or [imperative affects](@ref imp_affects) one must be more careful. In + particular, one can pass in `reinitializealg` as a keyword arg to the + callback constructor to re-initialize the system. This will default to + `SciMLBase.NoInit()` in the case of symbolic affects and `SciMLBase.CheckInit()` + in the case of functional affects. This keyword should *not* be provided + if the affect is purely symbolic. + ## Continuous Events The basic purely symbolic continuous event interface to encode *one* continuous @@ -91,7 +152,7 @@ like this @variables x(t)=1 v(t)=0 root_eqs = [x ~ 0] # the event happens at the ground x(t) = 0 -affect = [v ~ -v] # the effect is that the velocity changes sign +affect = [v ~ -Pre(v)] # the effect is that the velocity changes sign @mtkbuild ball = ODESystem([D(x) ~ v D(v) ~ -9.8], t; continuous_events = root_eqs => affect) # equation => affect @@ -110,8 +171,8 @@ Multiple events? No problem! This example models a bouncing ball in 2D that is e ```@example events @variables x(t)=1 y(t)=0 vx(t)=0 vy(t)=2 -continuous_events = [[x ~ 0] => [vx ~ -vx] - [y ~ -1.5, y ~ 1.5] => [vy ~ -vy]] +continuous_events = [[x ~ 0] => [vx ~ -Pre(vx)] + [y ~ -1.5, y ~ 1.5] => [vy ~ -Pre(vy)]] @mtkbuild ball = ODESystem( [ @@ -204,7 +265,7 @@ bb_sol = solve(bb_prob, Tsit5()) plot(bb_sol) ``` -## Discrete events support +## Discrete Events In addition to continuous events, discrete events are also supported. The general interface to represent a collection of discrete events is @@ -233,7 +294,7 @@ Dₜ = Differential(t) eqs = [Dₜ(N) ~ α - N] # at time tinject we inject M cells -injection = (t == tinject) => [N ~ N + M] +injection = (t == tinject) => [N ~ Pre(N) + M] u0 = [N => 0.0] tspan = (0.0, 20.0) @@ -255,7 +316,7 @@ its steady-state value (which is 100). We can encode this by modifying the event to ```@example events -injection = ((t == tinject) & (N < 50)) => [N ~ N + M] +injection = ((t == tinject) & (N < 50)) => [N ~ Pre(N) + M] @mtkbuild osys = ODESystem(eqs, t, [N], [M, tinject, α]; discrete_events = injection) oprob = ODEProblem(osys, u0, tspan, p) @@ -275,7 +336,7 @@ cells, modeled by setting `α = 0.0` @parameters tkill # we reset the first event to just occur at tinject -injection = (t == tinject) => [N ~ N + M] +injection = (t == tinject) => [N ~ Pre(N) + M] # at time tkill we turn off production of cells killing = (t == tkill) => [α ~ 0.0] @@ -298,7 +359,7 @@ A preset-time event is triggered at specific set times, which can be passed in a vector like ```julia -discrete_events = [[1.0, 4.0] => [v ~ -v]] +discrete_events = [[1.0, 4.0] => [v ~ -Pre(v)]] ``` This will change the sign of `v` *only* at `t = 1.0` and `t = 4.0`. @@ -306,7 +367,7 @@ This will change the sign of `v` *only* at `t = 1.0` and `t = 4.0`. As such, our last example with treatment and killing could instead be modeled by ```@example events -injection = [10.0] => [N ~ N + M] +injection = [10.0] => [N ~ Pre(N) + M] killing = [20.0] => [α ~ 0.0] p = [α => 100.0, M => 50] @@ -325,7 +386,7 @@ specify a periodic interval, pass the interval as the condition for the event. For example, ```julia -discrete_events = [1.0 => [v ~ -v]] +discrete_events = [1.0 => [v ~ -Pre(v)]] ``` will change the sign of `v` at `t = 1.0`, `2.0`, ... @@ -334,10 +395,10 @@ Finally, we note that to specify an event at precisely one time, say 2.0 below, one must still use a vector ```julia -discrete_events = [[2.0] => [v ~ -v]] +discrete_events = [[2.0] => [v ~ -Pre(v)]] ``` -## Saving discrete values +## [Saving discrete values](@id save_discretes) Time-dependent parameters which are updated in callbacks are termed as discrete variables. ModelingToolkit enables automatically saving the timeseries of these discrete variables, @@ -349,7 +410,7 @@ example: @parameters c(t) @mtkbuild sys = ODESystem( - D(x) ~ c * cos(x), t, [x], [c]; discrete_events = [1.0 => [c ~ c + 1]]) + D(x) ~ c * cos(x), t, [x], [c]; discrete_events = [1.0 => [c ~ Pre(c) + 1]]) prob = ODEProblem(sys, [x => 0.0], (0.0, 2pi), [c => 1.0]) sol = solve(prob, Tsit5()) @@ -370,7 +431,7 @@ this change: @parameters c @mtkbuild sys = ODESystem( - D(x) ~ c * cos(x), t, [x], [c]; discrete_events = [1.0 => [c ~ c + 1]]) + D(x) ~ c * cos(x), t, [x], [c]; discrete_events = [1.0 => [c ~ Pre(c) + 1]]) prob = ODEProblem(sys, [x => 0.0], (0.0, 2pi), [c => 1.0]) sol = solve(prob, Tsit5()) diff --git a/test/odesystem.jl b/test/odesystem.jl index aabcf558c2..31c2bf29b5 100644 --- a/test/odesystem.jl +++ b/test/odesystem.jl @@ -1,5 +1,6 @@ using ModelingToolkit, StaticArrays, LinearAlgebra -using ModelingToolkit: get_metadata, MTKParameters +using ModelingToolkit: get_metadata, MTKParameters, SymbolicDiscreteCallback, + SymbolicContinuousCallback using SymbolicIndexingInterface using OrdinaryDiffEq, Sundials using DiffEqBase, SparseArrays From fcaeef7bda343e0342441945df3ef02dacf9f873 Mon Sep 17 00:00:00 2001 From: vyudu Date: Tue, 1 Apr 2025 12:40:54 -0400 Subject: [PATCH 39/52] revert index cache --- src/systems/callbacks.jl | 51 ++++++++++++++++---------------- src/systems/diffeqs/odesystem.jl | 6 ++-- src/systems/diffeqs/sdesystem.jl | 6 ++-- src/systems/index_cache.jl | 7 +---- test/accessor_functions.jl | 4 +-- test/symbolic_events.jl | 5 ++-- 6 files changed, 37 insertions(+), 42 deletions(-) diff --git a/src/systems/callbacks.jl b/src/systems/callbacks.jl index f025531c3c..a7929f9c34 100644 --- a/src/systems/callbacks.jl +++ b/src/systems/callbacks.jl @@ -160,7 +160,7 @@ const Affect = Union{AffectSystem, FunctionalAffect, ImperativeAffect} """ SymbolicContinuousCallback(eqs::Vector{Equation}, affect = nothing, iv = nothing; - affect_neg = affect, initialize = nothing, finalize = nothing, rootfind = SciMLBase.LeftRootFind, algeeqs = Equation[]) + affect_neg = affect, initialize = nothing, finalize = nothing, rootfind = SciMLBase.LeftRootFind, alg_eqs = Equation[]) A [`ContinuousCallback`](@ref SciMLBase.ContinuousCallback) specified symbolically. Takes a vector of equations `eq` as well as the positive-edge `affect` and negative-edge `affect_neg` that apply when *any* of `eq` are satisfied. @@ -226,7 +226,7 @@ struct SymbolicContinuousCallback <: AbstractCallback rootfind = SciMLBase.LeftRootFind, reinitializealg = nothing, iv = nothing, - algeeqs = Equation[]) + alg_eqs = Equation[]) conditions = (conditions isa AbstractVector) ? conditions : [conditions] if isnothing(reinitializealg) @@ -236,18 +236,19 @@ struct SymbolicContinuousCallback <: AbstractCallback reinitializealg = SciMLBase.NoInit() end - new(conditions, make_affect(affect; iv, algeeqs, discrete_parameters), - make_affect(affect_neg; iv, algeeqs, discrete_parameters), - make_affect(initialize; iv, algeeqs, discrete_parameters), make_affect( - finalize; iv, algeeqs, discrete_parameters), + new(conditions, make_affect(affect; iv, alg_eqs, discrete_parameters), + make_affect(affect_neg; iv, alg_eqs, discrete_parameters), + make_affect(initialize; iv, alg_eqs, discrete_parameters), make_affect( + finalize; iv, alg_eqs, discrete_parameters), rootfind, reinitializealg) end # Default affect to nothing end -function SymbolicContinuousCallback(p::Pair, args...; kwargs...) - SymbolicContinuousCallback(p[1], p[2], args...; kwargs...) +SymbolicContinuousCallback(p::Pair, args...; kwargs...) = SymbolicContinuousCallback(p[1], p[2], args...; kwargs...) + +function SymbolicContinuousCallback(cb::SymbolicContinuousCallback, args...; iv = nothing, alg_eqs = Equation[], kwargs...) + cb end -SymbolicContinuousCallback(cb::SymbolicContinuousCallback, args...; kwargs...) = cb make_affect(affect::Nothing; kwargs...) = nothing make_affect(affect::Tuple; kwargs...) = FunctionalAffect(affect...) @@ -255,10 +256,10 @@ make_affect(affect::NamedTuple; kwargs...) = FunctionalAffect(; affect...) make_affect(affect::Affect; kwargs...) = affect function make_affect(affect::Vector{Equation}; discrete_parameters::AbstractVector = Any[], - iv = nothing, algeeqs::Vector{Equation} = Equation[]) + iv = nothing, alg_eqs::Vector{Equation} = Equation[]) isempty(affect) && return nothing - isempty(algeeqs) && - @warn "No algebraic equations were found for the callback defined by $(join(affect, ", ")). If the system has no algebraic equations, this can be disregarded. Otherwise pass in `algeeqs` to the SymbolicContinuousCallback constructor." + isempty(alg_eqs) && + @warn "No algebraic equations were found for the callback defined by $(join(affect, ", ")). If the system has no algebraic equations, this can be disregarded. Otherwise pass in `alg_eqs` to the SymbolicContinuousCallback constructor." if isnothing(iv) iv = t_nounits @warn "No independent variable specified. Defaulting to t_nounits." @@ -280,7 +281,7 @@ function make_affect(affect::Vector{Equation}; discrete_parameters::AbstractVect diffvs = collect_applied_operators(eq, Differential) union!(dvs, diffvs) end - for eq in algeeqs + for eq in alg_eqs collect_vars!(dvs, params, eq, iv) end @@ -294,10 +295,10 @@ function make_affect(affect::Vector{Equation}; discrete_parameters::AbstractVect rev_map = Dict(zip(discrete_parameters, discretes)) subs = merge(rev_map, Dict(zip(dvs, _dvs))) affect = Symbolics.fast_substitute(affect, subs) - algeeqs = Symbolics.fast_substitute(algeeqs, subs) + alg_eqs = Symbolics.fast_substitute(alg_eqs, subs) @named affectsys = ImplicitDiscreteSystem( - vcat(affect, algeeqs), iv, collect(union(_dvs, discretes)), + vcat(affect, alg_eqs), iv, collect(union(_dvs, discretes)), collect(union(pre_params, sys_params))) affectsys = structural_simplify(affectsys; fully_determined = false) # get accessed parameters p from Pre(p) in the callback parameters @@ -322,7 +323,7 @@ end Generate continuous callbacks. """ function SymbolicContinuousCallbacks(events; discrete_parameters = Any[], - algeeqs::Vector{Equation} = Equation[], iv = nothing) + alg_eqs::Vector{Equation} = Equation[], iv = nothing) callbacks = SymbolicContinuousCallback[] isnothing(events) && return callbacks @@ -332,7 +333,7 @@ function SymbolicContinuousCallbacks(events; discrete_parameters = Any[], for event in events cond, affs = event isa Pair ? (event[1], event[2]) : (event, nothing) push!(callbacks, - SymbolicContinuousCallback(cond, affs; iv, algeeqs, discrete_parameters)) + SymbolicContinuousCallback(cond, affs; iv, alg_eqs, discrete_parameters)) end callbacks end @@ -421,7 +422,7 @@ end # TODO: Iterative callbacks """ SymbolicDiscreteCallback(conditions::Vector{Equation}, affect = nothing, iv = nothing; - initialize = nothing, finalize = nothing, algeeqs = Equation[]) + initialize = nothing, finalize = nothing, alg_eqs = Equation[]) A callback that triggers at the first timestep that the conditions are satisfied. @@ -432,7 +433,7 @@ The condition can be one of: Arguments: - iv: The independent variable of the system. This must be specified if the independent variable appaers in one of the equations explicitly, as in x ~ t + 1. -- algeeqs: Algebraic equations of the system that must be satisfied after the callback occurs. +- alg_eqs: Algebraic equations of the system that must be satisfied after the callback occurs. """ struct SymbolicDiscreteCallback <: AbstractCallback conditions::Any @@ -444,7 +445,7 @@ struct SymbolicDiscreteCallback <: AbstractCallback function SymbolicDiscreteCallback( condition, affect = nothing; initialize = nothing, finalize = nothing, iv = nothing, - algeeqs = Equation[], discrete_parameters = Any[], reinitializealg = nothing) + alg_eqs = Equation[], discrete_parameters = Any[], reinitializealg = nothing) c = is_timed_condition(condition) ? condition : value(scalarize(condition)) if isnothing(reinitializealg) @@ -453,9 +454,9 @@ struct SymbolicDiscreteCallback <: AbstractCallback reinitializealg = SciMLBase.CheckInit() : reinitializealg = SciMLBase.NoInit() end - new(c, make_affect(affect; iv, algeeqs, discrete_parameters), - make_affect(initialize; iv, algeeqs, discrete_parameters), - make_affect(finalize; iv, algeeqs, discrete_parameters), reinitializealg) + new(c, make_affect(affect; iv, alg_eqs, discrete_parameters), + make_affect(initialize; iv, alg_eqs, discrete_parameters), + make_affect(finalize; iv, alg_eqs, discrete_parameters), reinitializealg) end # Default affect to nothing end @@ -468,7 +469,7 @@ SymbolicDiscreteCallback(cb::SymbolicDiscreteCallback, args...; kwargs...) = cb Generate discrete callbacks. """ function SymbolicDiscreteCallbacks(events; discrete_parameters::Vector = Any[], - algeeqs::Vector{Equation} = Equation[], iv = nothing) + alg_eqs::Vector{Equation} = Equation[], iv = nothing) callbacks = SymbolicDiscreteCallback[] isnothing(events) && return callbacks @@ -478,7 +479,7 @@ function SymbolicDiscreteCallbacks(events; discrete_parameters::Vector = Any[], for event in events cond, affs = event isa Pair ? (event[1], event[2]) : (event, nothing) push!(callbacks, - SymbolicDiscreteCallback(cond, affs; iv, algeeqs, discrete_parameters)) + SymbolicDiscreteCallback(cond, affs; iv, alg_eqs, discrete_parameters)) end callbacks end diff --git a/src/systems/diffeqs/odesystem.jl b/src/systems/diffeqs/odesystem.jl index 5178bb04e4..10f7c1729c 100644 --- a/src/systems/diffeqs/odesystem.jl +++ b/src/systems/diffeqs/odesystem.jl @@ -311,10 +311,10 @@ function ODESystem(deqs::AbstractVector{<:Equation}, iv, dvs, ps; throw(ArgumentError("System names must be unique.")) end - algeeqs = filter(eq -> eq.lhs isa Union{Symbolic, Number} && !is_diff_equation(eq), + alg_eqs = filter(eq -> eq.lhs isa Union{Symbolic, Number} && !is_diff_equation(eq), deqs) - cont_callbacks = SymbolicContinuousCallbacks(continuous_events; algeeqs, iv) - disc_callbacks = SymbolicDiscreteCallbacks(discrete_events; algeeqs, iv) + cont_callbacks = SymbolicContinuousCallbacks(continuous_events; alg_eqs, iv) + disc_callbacks = SymbolicDiscreteCallbacks(discrete_events; alg_eqs, iv) if is_dde === nothing is_dde = _check_if_dde(deqs, iv′, systems) diff --git a/src/systems/diffeqs/sdesystem.jl b/src/systems/diffeqs/sdesystem.jl index a0ae5a8f11..22ebf4d5b5 100644 --- a/src/systems/diffeqs/sdesystem.jl +++ b/src/systems/diffeqs/sdesystem.jl @@ -268,10 +268,10 @@ function SDESystem(deqs::AbstractVector{<:Equation}, neqs::AbstractArray, iv, dv Wfact = RefValue(EMPTY_JAC) Wfact_t = RefValue(EMPTY_JAC) - algeeqs = filter(eq -> eq.lhs isa Union{Symbolic, Number} && !is_diff_equation(eq), + alg_eqs = filter(eq -> eq.lhs isa Union{Symbolic, Number} && !is_diff_equation(eq), deqs) - cont_callbacks = SymbolicContinuousCallbacks(continuous_events; algeeqs, iv) - disc_callbacks = SymbolicDiscreteCallbacks(discrete_events; algeeqs, iv) + cont_callbacks = SymbolicContinuousCallbacks(continuous_events; alg_eqs, iv) + disc_callbacks = SymbolicDiscreteCallbacks(discrete_events; alg_eqs, iv) if is_dde === nothing is_dde = _check_if_dde(deqs, iv′, systems) end diff --git a/src/systems/index_cache.jl b/src/systems/index_cache.jl index 22e3b3a161..d71bcc60a9 100644 --- a/src/systems/index_cache.jl +++ b/src/systems/index_cache.jl @@ -345,13 +345,8 @@ function IndexCache(sys::AbstractSystem) vs = vars(eq.rhs; op = Nothing) timeseries = TimeseriesSetType() if is_time_dependent(sys) - unknown_set = Set(unknowns(sys)) for v in vs - if in(v, unknown_set) - empty!(timeseries) - push!(timeseries, ContinuousTimeseries()) - break - elseif (idx = get(disc_idxs, v, nothing)) !== nothing + if (idx = get(disc_idxs, v, nothing)) !== nothing push!(timeseries, idx.clock_idx) elseif iscall(v) && operation(v) === getindex && (idx = get(disc_idxs, arguments(v)[1], nothing)) !== nothing diff --git a/test/accessor_functions.jl b/test/accessor_functions.jl index 9272fb9146..4136736a8b 100644 --- a/test/accessor_functions.jl +++ b/test/accessor_functions.jl @@ -152,9 +152,9 @@ let # as I stored the same single 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. bot_cev = ModelingToolkit.SymbolicContinuousCallback( - cevs[1], algeeqs = [O ~ (d + p_bot) * X_bot + Y]) + cevs[1], alg_eqs = [O ~ (d + p_bot) * X_bot + Y]) mid_dev = ModelingToolkit.SymbolicDiscreteCallback( - devs[1], algeeqs = [O ~ (d + p_mid1) * X_mid1 + Y]) + devs[1], alg_eqs = [O ~ (d + p_mid1) * X_mid1 + Y]) @test all_sets_equal( continuous_events_toplevel.([sys_bot, sys_bot_comp, sys_bot_ss])..., [bot_cev]) diff --git a/test/symbolic_events.jl b/test/symbolic_events.jl index c41ab497b8..4c2ce45fd8 100644 --- a/test/symbolic_events.jl +++ b/test/symbolic_events.jl @@ -1321,7 +1321,7 @@ end @test ≈(sol(5.0000001, idxs = x) - sol(4.999999, idxs = x), 0.1, rtol = 1e-4) # Proper re-initialization after parameter change - eqs = [y ~ g^2 - x, D(x) ~ x] + eqs = [y ~ g^2, D(x) ~ x] c_evt = SymbolicContinuousCallback( [t ~ 5.0], [x ~ Pre(x) + 1, g ~ Pre(g) + 1], discrete_parameters = [g], iv = t) @mtkbuild sys = ODESystem(eqs, t, continuous_events = c_evt) @@ -1329,7 +1329,7 @@ end sol = solve(prob, FBDF()) @test sol.ps[g] ≈ [2.0, 3.0] @test ≈(sol(5.00000001, idxs = x) - sol(4.9999999, idxs = x), 1; rtol = 1e-4) - @test ≈(sol(5.00000001, idxs = y), 9 - sol(5.00000001, idxs = x), rtol = 1e-4) + @test ≈(sol(5.00000001, idxs = y), 9, rtol = 1e-4) # Parameters that don't appear in affects should not be mutated. c_evt = [t ~ 5.0] => [x ~ Pre(x) + 1] @@ -1338,4 +1338,3 @@ end sol = solve(prob, FBDF()) @test prob.ps[g] == sol.ps[g] end -# - explicit equation of t in a functional affect From b4c55bf1e0bb8472f5d512b6643571938212c118 Mon Sep 17 00:00:00 2001 From: vyudu Date: Tue, 1 Apr 2025 13:40:07 -0400 Subject: [PATCH 40/52] fix: use discrete_parameters in SII test --- src/systems/callbacks.jl | 5 ++--- test/symbolic_indexing_interface.jl | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/systems/callbacks.jl b/src/systems/callbacks.jl index a7929f9c34..233bbb3e87 100644 --- a/src/systems/callbacks.jl +++ b/src/systems/callbacks.jl @@ -307,6 +307,7 @@ function make_affect(affect::Vector{Equation}; discrete_parameters::AbstractVect # add scalarized unknowns to the map. _dvs = reduce(vcat, map(scalarize, _dvs), init = Any[]) + @show _dvs for u in _dvs aff_map[u] = u end @@ -460,9 +461,7 @@ struct SymbolicDiscreteCallback <: AbstractCallback end # Default affect to nothing end -function SymbolicDiscreteCallback(p::Pair, args...; kwargs...) - SymbolicDiscreteCallback(p[1], p[2], args...; kwargs...) -end +SymbolicDiscreteCallback(p::Pair, args...; kwargs...) = SymbolicDiscreteCallback(p[1], p[2], args...; kwargs...) SymbolicDiscreteCallback(cb::SymbolicDiscreteCallback, args...; kwargs...) = cb """ diff --git a/test/symbolic_indexing_interface.jl b/test/symbolic_indexing_interface.jl index 5979cd71ac..ad9350a506 100644 --- a/test/symbolic_indexing_interface.jl +++ b/test/symbolic_indexing_interface.jl @@ -229,7 +229,7 @@ end @testset "`timeseries_parameter_index` on unwrapped scalarized timeseries parameter" begin @variables x(t)[1:2] @parameters p(t)[1:2, 1:2] - ev = [x[1] ~ 2.0] => [p ~ -ones(2, 2)] + ev = SymbolicContinuousCallback([x[1] ~ 2.0] => [p ~ -ones(2, 2)], discrete_parameters = [p]) @mtkbuild sys = ODESystem(D(x) ~ p * x, t; continuous_events = [ev]) p = ModelingToolkit.unwrap(p) @test timeseries_parameter_index(sys, p) === ParameterTimeseriesIndex(1, (1, 1)) From 577d20f25a71d5c7a20e8ef14f9a9a03fd2b8c79 Mon Sep 17 00:00:00 2001 From: vyudu Date: Tue, 1 Apr 2025 15:01:47 -0400 Subject: [PATCH 41/52] fix: fix model parsing for events --- src/systems/callbacks.jl | 2 +- src/systems/model_parsing.jl | 56 +++++++++++++++++++++++++++++------- test/symbolic_events.jl | 2 +- 3 files changed, 47 insertions(+), 13 deletions(-) diff --git a/src/systems/callbacks.jl b/src/systems/callbacks.jl index 233bbb3e87..50cd2d1ba1 100644 --- a/src/systems/callbacks.jl +++ b/src/systems/callbacks.jl @@ -255,7 +255,7 @@ make_affect(affect::Tuple; kwargs...) = FunctionalAffect(affect...) make_affect(affect::NamedTuple; kwargs...) = FunctionalAffect(; affect...) make_affect(affect::Affect; kwargs...) = affect -function make_affect(affect::Vector{Equation}; discrete_parameters::AbstractVector = Any[], +function make_affect(affect::Vector{Equation}; discrete_parameters = Any[], iv = nothing, alg_eqs::Vector{Equation} = Equation[]) isempty(affect) && return nothing isempty(alg_eqs) && diff --git a/src/systems/model_parsing.jl b/src/systems/model_parsing.jl index 58016ad4ef..b45588de6e 100644 --- a/src/systems/model_parsing.jl +++ b/src/systems/model_parsing.jl @@ -127,15 +127,23 @@ function _model_macro(mod, name, expr, isconnector) isconnector && push!(exprs.args, :($Setfield.@set!(var"#___sys___".connector_type=$connector_type(var"#___sys___")))) - !isempty(c_evts) && push!(exprs.args, - :($Setfield.@set!(var"#___sys___".continuous_events=$SymbolicContinuousCallback.([ - $(c_evts...) - ])))) + push!(exprs.args, :(alg_eqs = $(alg_equations)(var"#___sys___"))) + d_evt_exs = map(d_evts) do evt + length(evt.args) == 2 ? + :($SymbolicDiscreteCallback($(evt.args[1]); iv = $iv, alg_eqs, $(evt.args[2]...))) : + :($SymbolicDiscreteCallback($(evt.args[1]); iv = $iv, alg_eqs)) + end !isempty(d_evts) && push!(exprs.args, - :($Setfield.@set!(var"#___sys___".discrete_events=$SymbolicDiscreteCallback.([ - $(d_evts...) - ])))) + :($Setfield.@set!(var"#___sys___".discrete_events=[$(d_evt_exs...)]))) + + c_evt_exs = map(c_evts) do evt + length(evt.args) == 2 ? + :($SymbolicContinuousCallback($(evt.args[1]); iv = $iv, alg_eqs, $(evt.args[2]...))) : + :($SymbolicContinuousCallback($(evt.args[1]); iv = $iv, alg_eqs)) + end + !isempty(c_evts) && push!(exprs.args, + :($Setfield.@set!(var"#___sys___".continuous_events=[$(c_evt_exs...)]))) f = if length(where_types) == 0 :($(Symbol(:__, name, :__))(; name, $(kwargs...)) = $exprs) @@ -1124,8 +1132,16 @@ end function parse_continuous_events!(c_evts, dict, body) dict[:continuous_events] = [] Base.remove_linenums!(body) - for arg in body.args - push!(c_evts, arg) + for line in body.args + if length(line.args) == 3 && line.args[1] == :(=>) + push!(c_evts, :(($line,))) + elseif length(line.args) == 2 + event = line.args[1] + kwargs = parse_event_kwargs(line.args[2]) + push!(c_evts, :(($event, $kwargs))) + else + error("Malformed continuous event $line.") + end push!(dict[:continuous_events], readable_code.(c_evts)...) end end @@ -1133,12 +1149,30 @@ end function parse_discrete_events!(d_evts, dict, body) dict[:discrete_events] = [] Base.remove_linenums!(body) - for arg in body.args - push!(d_evts, arg) + for line in body.args + if length(line.args) == 3 && line.args[1] == :(=>) + push!(d_evts, :(($line,))) + elseif length(line.args) == 2 + event = line.args[1] + kwargs = parse_event_kwargs(line.args[2]) + push!(d_evts, :(($event, $kwargs))) + else + error("Malformed discrete event $line.") + end push!(dict[:discrete_events], readable_code.(d_evts)...) end end +function parse_event_kwargs(disc_expr) + kwargs = :([]) + for arg in disc_expr.args + (arg.head != :(=)) && error("Malformed event kwarg $arg.") + (arg.args[1] isa Symbol) || error("Invalid keyword argument name $(arg.args[1]).") + push!(kwargs.args, arg) + end + kwargs +end + function parse_icon!(body::String, dict, icon, mod) icon_dir = get(ENV, "MTK_ICONS_DIR", joinpath(DEPOT_PATH[1], "mtk_icons")) dict[:icon] = icon[] = if isfile(body) diff --git a/test/symbolic_events.jl b/test/symbolic_events.jl index 4c2ce45fd8..115e681f5a 100644 --- a/test/symbolic_events.jl +++ b/test/symbolic_events.jl @@ -1213,7 +1213,7 @@ end D(x) ~ -k * x end @discrete_events begin - (t == 1.0) => [k ~ 1.0]#, discrete_parameters = [k] + (t == 1.0) => [k ~ 1.0], [discrete_parameters = k] end end @mtkbuild decay = DECAY() From 93428f56a8bd75d54fac6cb04edcc41d1913a68b Mon Sep 17 00:00:00 2001 From: vyudu Date: Tue, 1 Apr 2025 15:10:38 -0400 Subject: [PATCH 42/52] docs: document the discrete_parameters --- docs/src/basics/MTKLanguage.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/docs/src/basics/MTKLanguage.md b/docs/src/basics/MTKLanguage.md index 2bcec99b6a..f355333f67 100644 --- a/docs/src/basics/MTKLanguage.md +++ b/docs/src/basics/MTKLanguage.md @@ -202,6 +202,7 @@ getdefault(model_c3.model_a.k_array[2]) - Defining continuous events as described [here](https://docs.sciml.ai/ModelingToolkit/stable/basics/Events/#Continuous-Events). - If this block is not defined in the model, no continuous events will be added. + - Discrete parameters and other keyword arguments should be specified in a vector, as seen below. ```@example mtkmodel-example using ModelingToolkit @@ -209,7 +210,7 @@ using ModelingToolkit: t @mtkmodel M begin @parameters begin - k + k(t) end @variables begin x(t) @@ -222,21 +223,24 @@ using ModelingToolkit: t @continuous_events begin [x ~ 1.5] => [x ~ 5, y ~ 5] [t ~ 4] => [x ~ 10] + [t ~ 5] => [k ~ 3], [discrete_parameters = k] end end ``` + #### `@discrete_events` begin block - Defining discrete events as described [here](https://docs.sciml.ai/ModelingToolkit/stable/basics/Events/#Discrete-events-support). - If this block is not defined in the model, no discrete events will be added. + - Discrete parameters and other keyword arguments should be specified in a vector, as seen below. ```@example mtkmodel-example using ModelingToolkit @mtkmodel M begin @parameters begin - k + k(t) end @variables begin x(t) @@ -247,7 +251,8 @@ using ModelingToolkit D(y) ~ -k end @discrete_events begin - (t == 1.5) => [x ~ x + 5, y ~ 5] + (t == 1.5) => [x ~ Pre(x) + 5, y ~ 5] + (t == 2.5) => [k ~ Pre(k) * 2], [discrete_parameters = k] end end ``` From 44c4306edd144404d3fa4cce0fcdb5a8b1d4438f Mon Sep 17 00:00:00 2001 From: vyudu Date: Tue, 1 Apr 2025 15:20:50 -0400 Subject: [PATCH 43/52] format --- docs/src/basics/MTKLanguage.md | 1 - src/systems/callbacks.jl | 11 ++++++++--- src/systems/model_parsing.jl | 15 ++++++++------- test/symbolic_indexing_interface.jl | 3 ++- 4 files changed, 18 insertions(+), 12 deletions(-) diff --git a/docs/src/basics/MTKLanguage.md b/docs/src/basics/MTKLanguage.md index f355333f67..d5228f3fee 100644 --- a/docs/src/basics/MTKLanguage.md +++ b/docs/src/basics/MTKLanguage.md @@ -228,7 +228,6 @@ using ModelingToolkit: t end ``` - #### `@discrete_events` begin block - Defining discrete events as described [here](https://docs.sciml.ai/ModelingToolkit/stable/basics/Events/#Discrete-events-support). diff --git a/src/systems/callbacks.jl b/src/systems/callbacks.jl index 50cd2d1ba1..33163efaa8 100644 --- a/src/systems/callbacks.jl +++ b/src/systems/callbacks.jl @@ -244,9 +244,12 @@ struct SymbolicContinuousCallback <: AbstractCallback end # Default affect to nothing end -SymbolicContinuousCallback(p::Pair, args...; kwargs...) = SymbolicContinuousCallback(p[1], p[2], args...; kwargs...) +function SymbolicContinuousCallback(p::Pair, args...; kwargs...) + SymbolicContinuousCallback(p[1], p[2], args...; kwargs...) +end -function SymbolicContinuousCallback(cb::SymbolicContinuousCallback, args...; iv = nothing, alg_eqs = Equation[], kwargs...) +function SymbolicContinuousCallback(cb::SymbolicContinuousCallback, args...; + iv = nothing, alg_eqs = Equation[], kwargs...) cb end @@ -461,7 +464,9 @@ struct SymbolicDiscreteCallback <: AbstractCallback end # Default affect to nothing end -SymbolicDiscreteCallback(p::Pair, args...; kwargs...) = SymbolicDiscreteCallback(p[1], p[2], args...; kwargs...) +function SymbolicDiscreteCallback(p::Pair, args...; kwargs...) + SymbolicDiscreteCallback(p[1], p[2], args...; kwargs...) +end SymbolicDiscreteCallback(cb::SymbolicDiscreteCallback, args...; kwargs...) = cb """ diff --git a/src/systems/model_parsing.jl b/src/systems/model_parsing.jl index b45588de6e..9c1030fd00 100644 --- a/src/systems/model_parsing.jl +++ b/src/systems/model_parsing.jl @@ -129,7 +129,7 @@ function _model_macro(mod, name, expr, isconnector) push!(exprs.args, :(alg_eqs = $(alg_equations)(var"#___sys___"))) d_evt_exs = map(d_evts) do evt - length(evt.args) == 2 ? + length(evt.args) == 2 ? :($SymbolicDiscreteCallback($(evt.args[1]); iv = $iv, alg_eqs, $(evt.args[2]...))) : :($SymbolicDiscreteCallback($(evt.args[1]); iv = $iv, alg_eqs)) end @@ -138,8 +138,9 @@ function _model_macro(mod, name, expr, isconnector) :($Setfield.@set!(var"#___sys___".discrete_events=[$(d_evt_exs...)]))) c_evt_exs = map(c_evts) do evt - length(evt.args) == 2 ? - :($SymbolicContinuousCallback($(evt.args[1]); iv = $iv, alg_eqs, $(evt.args[2]...))) : + length(evt.args) == 2 ? + :($SymbolicContinuousCallback( + $(evt.args[1]); iv = $iv, alg_eqs, $(evt.args[2]...))) : :($SymbolicContinuousCallback($(evt.args[1]); iv = $iv, alg_eqs)) end !isempty(c_evts) && push!(exprs.args, @@ -1133,9 +1134,9 @@ function parse_continuous_events!(c_evts, dict, body) dict[:continuous_events] = [] Base.remove_linenums!(body) for line in body.args - if length(line.args) == 3 && line.args[1] == :(=>) + if length(line.args) == 3 && line.args[1] == :(=>) push!(c_evts, :(($line,))) - elseif length(line.args) == 2 + elseif length(line.args) == 2 event = line.args[1] kwargs = parse_event_kwargs(line.args[2]) push!(c_evts, :(($event, $kwargs))) @@ -1150,9 +1151,9 @@ function parse_discrete_events!(d_evts, dict, body) dict[:discrete_events] = [] Base.remove_linenums!(body) for line in body.args - if length(line.args) == 3 && line.args[1] == :(=>) + if length(line.args) == 3 && line.args[1] == :(=>) push!(d_evts, :(($line,))) - elseif length(line.args) == 2 + elseif length(line.args) == 2 event = line.args[1] kwargs = parse_event_kwargs(line.args[2]) push!(d_evts, :(($event, $kwargs))) diff --git a/test/symbolic_indexing_interface.jl b/test/symbolic_indexing_interface.jl index ad9350a506..3a5a9bd390 100644 --- a/test/symbolic_indexing_interface.jl +++ b/test/symbolic_indexing_interface.jl @@ -229,7 +229,8 @@ end @testset "`timeseries_parameter_index` on unwrapped scalarized timeseries parameter" begin @variables x(t)[1:2] @parameters p(t)[1:2, 1:2] - ev = SymbolicContinuousCallback([x[1] ~ 2.0] => [p ~ -ones(2, 2)], discrete_parameters = [p]) + ev = SymbolicContinuousCallback( + [x[1] ~ 2.0] => [p ~ -ones(2, 2)], discrete_parameters = [p]) @mtkbuild sys = ODESystem(D(x) ~ p * x, t; continuous_events = [ev]) p = ModelingToolkit.unwrap(p) @test timeseries_parameter_index(sys, p) === ParameterTimeseriesIndex(1, (1, 1)) From 451becb31ceb9bab6bb7ab6b775adda4d2ec5f7c Mon Sep 17 00:00:00 2001 From: vyudu Date: Tue, 1 Apr 2025 16:37:51 -0400 Subject: [PATCH 44/52] fix: remove the plural constructors --- docs/src/systems/DiscreteSystem.md | 1 - docs/src/systems/ImplicitDiscreteSystem.md | 1 - src/systems/callbacks.jl | 61 ++++------------------ src/systems/diffeqs/odesystem.jl | 7 ++- src/systems/diffeqs/sdesystem.jl | 6 ++- src/systems/jumps/jumpsystem.jl | 22 +------- test/symbolic_events.jl | 33 ------------ test/symbolic_indexing_interface.jl | 3 +- 8 files changed, 22 insertions(+), 112 deletions(-) diff --git a/docs/src/systems/DiscreteSystem.md b/docs/src/systems/DiscreteSystem.md index f8a71043ab..55a02e5714 100644 --- a/docs/src/systems/DiscreteSystem.md +++ b/docs/src/systems/DiscreteSystem.md @@ -12,7 +12,6 @@ DiscreteSystem - `get_unknowns(sys)` or `unknowns(sys)`: The set of unknowns in the discrete system. - `get_ps(sys)` or `parameters(sys)`: The parameters of the discrete system. - `get_iv(sys)`: The independent variable of the discrete system - - `discrete_events(sys)`: The set of discrete events in the discrete system. ## Transformations diff --git a/docs/src/systems/ImplicitDiscreteSystem.md b/docs/src/systems/ImplicitDiscreteSystem.md index d69f88f106..d687502b49 100644 --- a/docs/src/systems/ImplicitDiscreteSystem.md +++ b/docs/src/systems/ImplicitDiscreteSystem.md @@ -12,7 +12,6 @@ ImplicitDiscreteSystem - `get_unknowns(sys)` or `unknowns(sys)`: The set of unknowns in the implicit discrete system. - `get_ps(sys)` or `parameters(sys)`: The parameters of the implicit discrete system. - `get_iv(sys)`: The independent variable of the implicit discrete system - - `discrete_events(sys)`: The set of discrete events in the implicit discrete system. ## Transformations diff --git a/src/systems/callbacks.jl b/src/systems/callbacks.jl index 33163efaa8..f469c1a1ec 100644 --- a/src/systems/callbacks.jl +++ b/src/systems/callbacks.jl @@ -69,7 +69,6 @@ discretes(a::AffectSystem) = a.discretes unknowns(a::AffectSystem) = a.unknowns parameters(a::AffectSystem) = a.parameters aff_to_sys(a::AffectSystem) = a.aff_to_sys -previous_vals(a::AffectSystem) = parameters(system(a)) all_equations(a::AffectSystem) = vcat(equations(system(a)), observed(system(a))) function Base.show(iio::IO, aff::AffectSystem) @@ -149,7 +148,6 @@ function (p::Pre)(x) end return result end - haspre(eq::Equation) = haspre(eq.lhs) || haspre(eq.rhs) haspre(O) = recursive_hasoperator(Pre, O) @@ -247,11 +245,8 @@ end function SymbolicContinuousCallback(p::Pair, args...; kwargs...) SymbolicContinuousCallback(p[1], p[2], args...; kwargs...) end - -function SymbolicContinuousCallback(cb::SymbolicContinuousCallback, args...; - iv = nothing, alg_eqs = Equation[], kwargs...) - cb -end +SymbolicContinuousCallback(cb::SymbolicContinuousCallback, args...; kwargs...) = cb +SymbolicContinuousCallback(cb::Nothing, args...; kwargs...) = nothing make_affect(affect::Nothing; kwargs...) = nothing make_affect(affect::Tuple; kwargs...) = FunctionalAffect(affect...) @@ -310,7 +305,6 @@ function make_affect(affect::Vector{Equation}; discrete_parameters = Any[], # add scalarized unknowns to the map. _dvs = reduce(vcat, map(scalarize, _dvs), init = Any[]) - @show _dvs for u in _dvs aff_map[u] = u end @@ -323,25 +317,6 @@ function make_affect(affect; kwargs...) error("Malformed affect $(affect). This should be a vector of equations or a tuple specifying a functional affect.") end -""" -Generate continuous callbacks. -""" -function SymbolicContinuousCallbacks(events; discrete_parameters = Any[], - alg_eqs::Vector{Equation} = Equation[], iv = nothing) - callbacks = SymbolicContinuousCallback[] - isnothing(events) && return callbacks - - events isa AbstractVector || (events = [events]) - isempty(events) && return callbacks - - for event in events - cond, affs = event isa Pair ? (event[1], event[2]) : (event, nothing) - push!(callbacks, - SymbolicContinuousCallback(cond, affs; iv, alg_eqs, discrete_parameters)) - end - callbacks -end - function Base.show(io::IO, cb::AbstractCallback) indent = get(io, :indent, 0) iio = IOContext(io, :indent => indent + 1) @@ -422,8 +397,6 @@ end ################################ ######## Discrete events ####### ################################ - -# TODO: Iterative callbacks """ SymbolicDiscreteCallback(conditions::Vector{Equation}, affect = nothing, iv = nothing; initialize = nothing, finalize = nothing, alg_eqs = Equation[]) @@ -468,25 +441,7 @@ function SymbolicDiscreteCallback(p::Pair, args...; kwargs...) SymbolicDiscreteCallback(p[1], p[2], args...; kwargs...) end SymbolicDiscreteCallback(cb::SymbolicDiscreteCallback, args...; kwargs...) = cb - -""" -Generate discrete callbacks. -""" -function SymbolicDiscreteCallbacks(events; discrete_parameters::Vector = Any[], - alg_eqs::Vector{Equation} = Equation[], iv = nothing) - callbacks = SymbolicDiscreteCallback[] - - isnothing(events) && return callbacks - events isa AbstractVector || (events = [events]) - isempty(events) && return callbacks - - for event in events - cond, affs = event isa Pair ? (event[1], event[2]) : (event, nothing) - push!(callbacks, - SymbolicDiscreteCallback(cond, affs; iv, alg_eqs, discrete_parameters)) - end - callbacks -end +SymbolicDiscreteCallback(cb::Nothing, args...; kwargs...) = nothing function is_timed_condition(condition::T) where {T} if T === Num @@ -500,10 +455,14 @@ function is_timed_condition(condition::T) where {T} end end +to_cb_vector(cbs::Vector{<:AbstractCallback}) = cbs +to_cb_vector(cbs::Vector) = Vector{AbstractCallback}(cbs) +to_cb_vector(cbs::Nothing) = AbstractCallback[] +to_cb_vector(cb::AbstractCallback) = [cb] + ############################################ ########## Namespacing Utilities ########### ############################################ - function namespace_affects(affect::FunctionalAffect, s) FunctionalAffect(func(affect), renamespace.((s,), unknowns(affect)), @@ -530,7 +489,7 @@ function namespace_callback(cb::SymbolicContinuousCallback, s)::SymbolicContinuo affect_neg = namespace_affects(affect_negs(cb), s), initialize = namespace_affects(initialize_affects(cb), s), finalize = namespace_affects(finalize_affects(cb), s), - rootfind = cb.rootfind) + rootfind = cb.rootfind, reinitializealg = cb.reinitializealg) end function namespace_conditions(condition, s) @@ -542,7 +501,7 @@ function namespace_callback(cb::SymbolicDiscreteCallback, s)::SymbolicDiscreteCa namespace_conditions(conditions(cb), s), namespace_affects(affects(cb), s), initialize = namespace_affects(initialize_affects(cb), s), - finalize = namespace_affects(finalize_affects(cb), s)) + finalize = namespace_affects(finalize_affects(cb), s), reinitializealg = cb.reinitializealg) end function Base.hash(cb::AbstractCallback, s::UInt) diff --git a/src/systems/diffeqs/odesystem.jl b/src/systems/diffeqs/odesystem.jl index 10f7c1729c..99a098b686 100644 --- a/src/systems/diffeqs/odesystem.jl +++ b/src/systems/diffeqs/odesystem.jl @@ -313,8 +313,11 @@ function ODESystem(deqs::AbstractVector{<:Equation}, iv, dvs, ps; alg_eqs = filter(eq -> eq.lhs isa Union{Symbolic, Number} && !is_diff_equation(eq), deqs) - cont_callbacks = SymbolicContinuousCallbacks(continuous_events; alg_eqs, iv) - disc_callbacks = SymbolicDiscreteCallbacks(discrete_events; alg_eqs, iv) + @show continuous_events + @show discrete_events + cont_callbacks = to_cb_vector(SymbolicContinuousCallback.( + continuous_events; alg_eqs, iv)) + disc_callbacks = to_cb_vector(SymbolicDiscreteCallback.(discrete_events; alg_eqs, iv)) if is_dde === nothing is_dde = _check_if_dde(deqs, iv′, systems) diff --git a/src/systems/diffeqs/sdesystem.jl b/src/systems/diffeqs/sdesystem.jl index 22ebf4d5b5..d976eef770 100644 --- a/src/systems/diffeqs/sdesystem.jl +++ b/src/systems/diffeqs/sdesystem.jl @@ -270,8 +270,10 @@ function SDESystem(deqs::AbstractVector{<:Equation}, neqs::AbstractArray, iv, dv alg_eqs = filter(eq -> eq.lhs isa Union{Symbolic, Number} && !is_diff_equation(eq), deqs) - cont_callbacks = SymbolicContinuousCallbacks(continuous_events; alg_eqs, iv) - disc_callbacks = SymbolicDiscreteCallbacks(discrete_events; alg_eqs, iv) + cont_callbacks = to_cb_vector(SymbolicContinuousCallback.( + continuous_events; alg_eqs, iv)) + disc_callbacks = to_cb_vector(SymbolicDiscreteCallback.(discrete_events; alg_eqs, iv)) + if is_dde === nothing is_dde = _check_if_dde(deqs, iv′, systems) end diff --git a/src/systems/jumps/jumpsystem.jl b/src/systems/jumps/jumpsystem.jl index 8723cc28a4..19ff29a951 100644 --- a/src/systems/jumps/jumpsystem.jl +++ b/src/systems/jumps/jumpsystem.jl @@ -1,19 +1,5 @@ const JumpType = Union{VariableRateJump, ConstantRateJump, MassActionJump} -# modifies the expression representing an affect function to -# call reset_aggregated_jumps!(integrator). -# assumes iip -function _reset_aggregator!(expr, integrator) - @assert Meta.isexpr(expr, :function) - body = expr.args[end] - body = quote - $body - $reset_aggregated_jumps!($integrator) - end - expr.args[end] = body - return nothing -end - """ $(TYPEDEF) @@ -90,11 +76,6 @@ struct JumpSystem{U <: ArrayPartition} <: AbstractTimeDependentSystem """ connector_type::Any """ - A `Vector{SymbolicContinuousCallback}` that model events. - The integrator will use root finding to guarantee that it steps at each zero crossing. - """ - continuous_events::Vector{SymbolicContinuousCallback} - """ A `Vector{SymbolicDiscreteCallback}` that models events. Symbolic analog to `SciMLBase.DiscreteCallback` that executes an affect when a given condition is true at the end of an integration step. Note, one must make sure to call @@ -230,8 +211,7 @@ function JumpSystem(eqs, iv, unknowns, ps; end end - cont_callbacks = SymbolicContinuousCallbacks(continuous_events; iv) - disc_callbacks = SymbolicDiscreteCallbacks(discrete_events; iv) + disc_callbacks = to_cb_vector(SymbolicDiscreteCallback.(discrete_events; iv)) JumpSystem{typeof(ap)}(Threads.atomic_add!(SYSTEM_COUNT, UInt(1)), ap, iv′, us′, ps′, var_to_name, observed, name, description, systems, diff --git a/test/symbolic_events.jl b/test/symbolic_events.jl index 115e681f5a..adcb24dc76 100644 --- a/test/symbolic_events.jl +++ b/test/symbolic_events.jl @@ -1,9 +1,7 @@ using ModelingToolkit, OrdinaryDiffEq, StochasticDiffEq, JumpProcesses, Test using SciMLStructures: canonicalize, Discrete using ModelingToolkit: SymbolicContinuousCallback, - SymbolicContinuousCallbacks, SymbolicDiscreteCallback, - SymbolicDiscreteCallbacks, get_callback, t_nounits as t, D_nounits as D, @@ -88,37 +86,6 @@ affect_neg = [x ~ 1] @test e isa SymbolicContinuousCallback @test isequal(equations(e), eqs) @test e.rootfind == SciMLBase.LeftRootFind - - # test plural constructor - e = SymbolicContinuousCallbacks(eqs[]) - @test e isa Vector{SymbolicContinuousCallback} - @test isequal(equations(e[]), eqs) - @test e[].affect == nothing - - e = SymbolicContinuousCallbacks(eqs) - @test e isa Vector{SymbolicContinuousCallback} - @test isequal(equations(e[]), eqs) - @test e[].affect == nothing - - e = SymbolicContinuousCallbacks(eqs[] => affect) - @test e isa Vector{SymbolicContinuousCallback} - @test isequal(equations(e[]), eqs) - @test e[].affect isa AffectSystem - - e = SymbolicContinuousCallbacks(eqs => affect) - @test e isa Vector{SymbolicContinuousCallback} - @test isequal(equations(e[]), eqs) - @test e[].affect isa AffectSystem - - e = SymbolicContinuousCallbacks([eqs[] => affect]) - @test e isa Vector{SymbolicContinuousCallback} - @test isequal(equations(e[]), eqs) - @test e[].affect isa AffectSystem - - e = SymbolicContinuousCallbacks([eqs => affect]) - @test e isa Vector{SymbolicContinuousCallback} - @test isequal(equations(e[]), eqs) - @test e[].affect isa AffectSystem end @testset "ImperativeAffect constructors" begin diff --git a/test/symbolic_indexing_interface.jl b/test/symbolic_indexing_interface.jl index 3a5a9bd390..f63fcbcd7a 100644 --- a/test/symbolic_indexing_interface.jl +++ b/test/symbolic_indexing_interface.jl @@ -1,5 +1,6 @@ using ModelingToolkit, SymbolicIndexingInterface, SciMLBase -using ModelingToolkit: t_nounits as t, D_nounits as D, ParameterIndex +using ModelingToolkit: t_nounits as t, D_nounits as D, ParameterIndex, + SymbolicContinuousCallback using SciMLStructures: Tunable @testset "ODESystem" begin From fa763b160e6401459665a954ae633732369060a7 Mon Sep 17 00:00:00 2001 From: vyudu Date: Tue, 1 Apr 2025 23:51:47 -0400 Subject: [PATCH 45/52] fix: fix model parsing error --- src/systems/callbacks.jl | 1 + src/systems/diffeqs/odesystem.jl | 2 -- src/systems/jumps/jumpsystem.jl | 2 +- src/systems/model_parsing.jl | 38 ++++++++++++++++---------------- 4 files changed, 21 insertions(+), 22 deletions(-) diff --git a/src/systems/callbacks.jl b/src/systems/callbacks.jl index f469c1a1ec..cfe31511c4 100644 --- a/src/systems/callbacks.jl +++ b/src/systems/callbacks.jl @@ -263,6 +263,7 @@ function make_affect(affect::Vector{Equation}; discrete_parameters = Any[], @warn "No independent variable specified. Defaulting to t_nounits." end + discrete_parameters isa AbstractVector || (discrete_parameters = [discrete_parameters]) for p in discrete_parameters occursin(unwrap(iv), unwrap(p)) || error("Non-time dependent parameter $p passed in as a discrete. Must be declared as @parameters $p(t).") diff --git a/src/systems/diffeqs/odesystem.jl b/src/systems/diffeqs/odesystem.jl index 99a098b686..7e8529c6b0 100644 --- a/src/systems/diffeqs/odesystem.jl +++ b/src/systems/diffeqs/odesystem.jl @@ -313,8 +313,6 @@ function ODESystem(deqs::AbstractVector{<:Equation}, iv, dvs, ps; alg_eqs = filter(eq -> eq.lhs isa Union{Symbolic, Number} && !is_diff_equation(eq), deqs) - @show continuous_events - @show discrete_events cont_callbacks = to_cb_vector(SymbolicContinuousCallback.( continuous_events; alg_eqs, iv)) disc_callbacks = to_cb_vector(SymbolicDiscreteCallback.(discrete_events; alg_eqs, iv)) diff --git a/src/systems/jumps/jumpsystem.jl b/src/systems/jumps/jumpsystem.jl index 19ff29a951..53ccdae8a3 100644 --- a/src/systems/jumps/jumpsystem.jl +++ b/src/systems/jumps/jumpsystem.jl @@ -216,7 +216,7 @@ function JumpSystem(eqs, iv, unknowns, ps; JumpSystem{typeof(ap)}(Threads.atomic_add!(SYSTEM_COUNT, UInt(1)), ap, iv′, us′, ps′, var_to_name, observed, name, description, systems, defaults, guesses, initializesystem, initialization_eqs, connector_type, - cont_callbacks, disc_callbacks, + disc_callbacks, parameter_dependencies, metadata, gui_metadata, checks = checks) end diff --git a/src/systems/model_parsing.jl b/src/systems/model_parsing.jl index 9c1030fd00..f8ffe164ab 100644 --- a/src/systems/model_parsing.jl +++ b/src/systems/model_parsing.jl @@ -127,24 +127,24 @@ function _model_macro(mod, name, expr, isconnector) isconnector && push!(exprs.args, :($Setfield.@set!(var"#___sys___".connector_type=$connector_type(var"#___sys___")))) - push!(exprs.args, :(alg_eqs = $(alg_equations)(var"#___sys___"))) - d_evt_exs = map(d_evts) do evt - length(evt.args) == 2 ? - :($SymbolicDiscreteCallback($(evt.args[1]); iv = $iv, alg_eqs, $(evt.args[2]...))) : - :($SymbolicDiscreteCallback($(evt.args[1]); iv = $iv, alg_eqs)) - end - - !isempty(d_evts) && push!(exprs.args, - :($Setfield.@set!(var"#___sys___".discrete_events=[$(d_evt_exs...)]))) + if !isempty(d_evts) || !isempty(c_evts) + push!(exprs.args, :(alg_eqs = $(alg_equations)(var"#___sys___"))) + !isempty(d_evts) && begin + d_exprs = [:($(SymbolicDiscreteCallback)( + $(evt.args[1]); iv = $iv, alg_eqs, $(evt.args[2])...)) + for evt in d_evts] + push!(exprs.args, + :($Setfield.@set!(var"#___sys___".discrete_events=[$(d_exprs...)]))) + end - c_evt_exs = map(c_evts) do evt - length(evt.args) == 2 ? - :($SymbolicContinuousCallback( - $(evt.args[1]); iv = $iv, alg_eqs, $(evt.args[2]...))) : - :($SymbolicContinuousCallback($(evt.args[1]); iv = $iv, alg_eqs)) + !isempty(c_evts) && begin + c_exprs = [:($(SymbolicContinuousCallback)( + $(evt.args[1]); iv = $iv, alg_eqs, $(evt.args[2])...)) + for evt in c_evts] + push!(exprs.args, + :($Setfield.@set!(var"#___sys___".continuous_events=[$(c_exprs...)]))) + end end - !isempty(c_evts) && push!(exprs.args, - :($Setfield.@set!(var"#___sys___".continuous_events=[$(c_evt_exs...)]))) f = if length(where_types) == 0 :($(Symbol(:__, name, :__))(; name, $(kwargs...)) = $exprs) @@ -1135,7 +1135,7 @@ function parse_continuous_events!(c_evts, dict, body) Base.remove_linenums!(body) for line in body.args if length(line.args) == 3 && line.args[1] == :(=>) - push!(c_evts, :(($line,))) + push!(c_evts, :(($line, ()))) elseif length(line.args) == 2 event = line.args[1] kwargs = parse_event_kwargs(line.args[2]) @@ -1152,7 +1152,7 @@ function parse_discrete_events!(d_evts, dict, body) Base.remove_linenums!(body) for line in body.args if length(line.args) == 3 && line.args[1] == :(=>) - push!(d_evts, :(($line,))) + push!(d_evts, :(($line, ()))) elseif length(line.args) == 2 event = line.args[1] kwargs = parse_event_kwargs(line.args[2]) @@ -1169,7 +1169,7 @@ function parse_event_kwargs(disc_expr) for arg in disc_expr.args (arg.head != :(=)) && error("Malformed event kwarg $arg.") (arg.args[1] isa Symbol) || error("Invalid keyword argument name $(arg.args[1]).") - push!(kwargs.args, arg) + push!(kwargs.args, :($(QuoteNode(arg.args[1])) => $(arg.args[2]))) end kwargs end From 794c4adb846029678870513ce4c3f7202cee8b24 Mon Sep 17 00:00:00 2001 From: vyudu Date: Wed, 2 Apr 2025 00:07:58 -0400 Subject: [PATCH 46/52] fix: add continuous_events back --- src/systems/jumps/jumpsystem.jl | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/systems/jumps/jumpsystem.jl b/src/systems/jumps/jumpsystem.jl index 53ccdae8a3..33011cee5d 100644 --- a/src/systems/jumps/jumpsystem.jl +++ b/src/systems/jumps/jumpsystem.jl @@ -75,6 +75,7 @@ struct JumpSystem{U <: ArrayPartition} <: AbstractTimeDependentSystem Type of the system. """ connector_type::Any + continuous_events::Vector{SymbolicContinuousCallback} """ A `Vector{SymbolicDiscreteCallback}` that models events. Symbolic analog to `SciMLBase.DiscreteCallback` that executes an affect when a given condition is @@ -212,11 +213,12 @@ function JumpSystem(eqs, iv, unknowns, ps; end disc_callbacks = to_cb_vector(SymbolicDiscreteCallback.(discrete_events; iv)) + cont_callbacks = to_cb_vector(SymbolicContinuousCallback.(discrete_events; iv)) JumpSystem{typeof(ap)}(Threads.atomic_add!(SYSTEM_COUNT, UInt(1)), ap, iv′, us′, ps′, var_to_name, observed, name, description, systems, defaults, guesses, initializesystem, initialization_eqs, connector_type, - disc_callbacks, + cont_callbacks, disc_callbacks, parameter_dependencies, metadata, gui_metadata, checks = checks) end From 9718d16ef5863a7705532195fc0832be8fd4a0d1 Mon Sep 17 00:00:00 2001 From: vyudu Date: Wed, 2 Apr 2025 00:24:06 -0400 Subject: [PATCH 47/52] fix: allow Arr in tovar --- src/parameters.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/parameters.jl b/src/parameters.jl index 91121b7cbb..d1690da968 100644 --- a/src/parameters.jl +++ b/src/parameters.jl @@ -62,7 +62,7 @@ toparam(s::Num) = wrap(toparam(value(s))) Maps the variable to an unknown. """ -tovar(s::Symbolic) = setmetadata(s, MTKVariableTypeCtx, VARIABLE) +tovar(s::Union{Symbolic, Arr}) = setmetadata(s, MTKVariableTypeCtx, VARIABLE) tovar(s::Num) = Num(tovar(value(s))) """ From a9f0c3087d22bd9e2d73940d869fb226c9df66e5 Mon Sep 17 00:00:00 2001 From: vyudu Date: Wed, 2 Apr 2025 00:27:46 -0400 Subject: [PATCH 48/52] fix: allow Arr in tovar --- src/parameters.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/parameters.jl b/src/parameters.jl index d1690da968..ca8bc76c2b 100644 --- a/src/parameters.jl +++ b/src/parameters.jl @@ -62,7 +62,7 @@ toparam(s::Num) = wrap(toparam(value(s))) Maps the variable to an unknown. """ -tovar(s::Union{Symbolic, Arr}) = setmetadata(s, MTKVariableTypeCtx, VARIABLE) +tovar(s::Union{Symbolic, Symbolics.Arr}) = setmetadata(s, MTKVariableTypeCtx, VARIABLE) tovar(s::Num) = Num(tovar(value(s))) """ From 12760a40383ae87ece023eb59e0faa777318411d Mon Sep 17 00:00:00 2001 From: vyudu Date: Wed, 2 Apr 2025 00:30:46 -0400 Subject: [PATCH 49/52] fix JumpSystem --- src/systems/jumps/jumpsystem.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/systems/jumps/jumpsystem.jl b/src/systems/jumps/jumpsystem.jl index 33011cee5d..2aa07bd898 100644 --- a/src/systems/jumps/jumpsystem.jl +++ b/src/systems/jumps/jumpsystem.jl @@ -213,7 +213,7 @@ function JumpSystem(eqs, iv, unknowns, ps; end disc_callbacks = to_cb_vector(SymbolicDiscreteCallback.(discrete_events; iv)) - cont_callbacks = to_cb_vector(SymbolicContinuousCallback.(discrete_events; iv)) + cont_callbacks = to_cb_vector(SymbolicContinuousCallback.(continuous_events; iv)) JumpSystem{typeof(ap)}(Threads.atomic_add!(SYSTEM_COUNT, UInt(1)), ap, iv′, us′, ps′, var_to_name, observed, name, description, systems, From 332a96c9e324fe6b2e735a8c6bf7093597db891f Mon Sep 17 00:00:00 2001 From: vyudu Date: Wed, 2 Apr 2025 00:53:29 -0400 Subject: [PATCH 50/52] fix: unwrap s in tovar --- src/parameters.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/parameters.jl b/src/parameters.jl index ca8bc76c2b..35e4206743 100644 --- a/src/parameters.jl +++ b/src/parameters.jl @@ -62,7 +62,7 @@ toparam(s::Num) = wrap(toparam(value(s))) Maps the variable to an unknown. """ -tovar(s::Union{Symbolic, Symbolics.Arr}) = setmetadata(s, MTKVariableTypeCtx, VARIABLE) +tovar(s::Union{Symbolic, Symbolics.Arr}) = setmetadata(unwrap(s), MTKVariableTypeCtx, VARIABLE) tovar(s::Num) = Num(tovar(value(s))) """ From 7f483fd76aac92cedd6920c5a5c7a3f651699538 Mon Sep 17 00:00:00 2001 From: vyudu Date: Wed, 2 Apr 2025 00:54:53 -0400 Subject: [PATCH 51/52] up --- src/parameters.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/parameters.jl b/src/parameters.jl index 35e4206743..8c0f9f1b00 100644 --- a/src/parameters.jl +++ b/src/parameters.jl @@ -62,8 +62,8 @@ toparam(s::Num) = wrap(toparam(value(s))) Maps the variable to an unknown. """ -tovar(s::Union{Symbolic, Symbolics.Arr}) = setmetadata(unwrap(s), MTKVariableTypeCtx, VARIABLE) -tovar(s::Num) = Num(tovar(value(s))) +tovar(s::Symbolic) = setmetadata(s, MTKVariableTypeCtx, VARIABLE) +tovar(s::Union{Num, Symbolics.Arr}) = Num(tovar(value(s))) """ $(SIGNATURES) From 6ca8e22e882790c295c64b7ad170b7a0f0fe8e10 Mon Sep 17 00:00:00 2001 From: vyudu Date: Wed, 2 Apr 2025 02:39:17 -0400 Subject: [PATCH 52/52] fix: fix several tests --- src/systems/callbacks.jl | 41 +++++++++++++++++++++----------- src/systems/diffeqs/odesystem.jl | 5 ++-- src/systems/diffeqs/sdesystem.jl | 5 ++-- src/systems/jumps/jumpsystem.jl | 6 +++-- src/systems/model_parsing.jl | 20 +--------------- test/extensions/ad.jl | 3 ++- test/jumpsystem.jl | 34 +++++++++++++------------- test/mtkparameters.jl | 3 ++- 8 files changed, 59 insertions(+), 58 deletions(-) diff --git a/src/systems/callbacks.jl b/src/systems/callbacks.jl index cfe31511c4..1e6264e96a 100644 --- a/src/systems/callbacks.jl +++ b/src/systems/callbacks.jl @@ -217,14 +217,12 @@ struct SymbolicContinuousCallback <: AbstractCallback function SymbolicContinuousCallback( conditions::Union{Equation, Vector{Equation}}, affect = nothing; - discrete_parameters = Any[], affect_neg = affect, initialize = nothing, finalize = nothing, rootfind = SciMLBase.LeftRootFind, reinitializealg = nothing, - iv = nothing, - alg_eqs = Equation[]) + kwargs...) conditions = (conditions isa AbstractVector) ? conditions : [conditions] if isnothing(reinitializealg) @@ -233,11 +231,12 @@ struct SymbolicContinuousCallback <: AbstractCallback reinitializealg = SciMLBase.CheckInit() : reinitializealg = SciMLBase.NoInit() end + @show kwargs - new(conditions, make_affect(affect; iv, alg_eqs, discrete_parameters), - make_affect(affect_neg; iv, alg_eqs, discrete_parameters), - make_affect(initialize; iv, alg_eqs, discrete_parameters), make_affect( - finalize; iv, alg_eqs, discrete_parameters), + new(conditions, make_affect(affect; kwargs...), + make_affect(affect_neg; kwargs...), + make_affect(initialize; kwargs...), make_affect( + finalize; kwargs...), rootfind, reinitializealg) end # Default affect to nothing end @@ -247,6 +246,13 @@ function SymbolicContinuousCallback(p::Pair, args...; kwargs...) end SymbolicContinuousCallback(cb::SymbolicContinuousCallback, args...; kwargs...) = cb SymbolicContinuousCallback(cb::Nothing, args...; kwargs...) = nothing +function SymbolicContinuousCallback(cb::Tuple, args...; kwargs...) + if length(cb) == 2 + SymbolicContinuousCallback(cb[1]; kwargs..., cb[2]...) + else + error("Malformed tuple specifying callback. Should be a condition => affect pair, followed by a vector of kwargs.") + end +end make_affect(affect::Nothing; kwargs...) = nothing make_affect(affect::Tuple; kwargs...) = FunctionalAffect(affect...) @@ -254,9 +260,9 @@ make_affect(affect::NamedTuple; kwargs...) = FunctionalAffect(; affect...) make_affect(affect::Affect; kwargs...) = affect function make_affect(affect::Vector{Equation}; discrete_parameters = Any[], - iv = nothing, alg_eqs::Vector{Equation} = Equation[]) + iv = nothing, alg_eqs::Vector{Equation} = Equation[], warn_no_algebraic = true, kwargs...) isempty(affect) && return nothing - isempty(alg_eqs) && + isempty(alg_eqs) && warn_no_algebraic && @warn "No algebraic equations were found for the callback defined by $(join(affect, ", ")). If the system has no algebraic equations, this can be disregarded. Otherwise pass in `alg_eqs` to the SymbolicContinuousCallback constructor." if isnothing(iv) iv = t_nounits @@ -423,7 +429,7 @@ struct SymbolicDiscreteCallback <: AbstractCallback function SymbolicDiscreteCallback( condition, affect = nothing; initialize = nothing, finalize = nothing, iv = nothing, - alg_eqs = Equation[], discrete_parameters = Any[], reinitializealg = nothing) + reinitializealg = nothing, kwargs...) c = is_timed_condition(condition) ? condition : value(scalarize(condition)) if isnothing(reinitializealg) @@ -432,9 +438,9 @@ struct SymbolicDiscreteCallback <: AbstractCallback reinitializealg = SciMLBase.CheckInit() : reinitializealg = SciMLBase.NoInit() end - new(c, make_affect(affect; iv, alg_eqs, discrete_parameters), - make_affect(initialize; iv, alg_eqs, discrete_parameters), - make_affect(finalize; iv, alg_eqs, discrete_parameters), reinitializealg) + new(c, make_affect(affect; kwargs...), + make_affect(initialize; kwargs...), + make_affect(finalize; kwargs...), reinitializealg) end # Default affect to nothing end @@ -443,6 +449,13 @@ function SymbolicDiscreteCallback(p::Pair, args...; kwargs...) end SymbolicDiscreteCallback(cb::SymbolicDiscreteCallback, args...; kwargs...) = cb SymbolicDiscreteCallback(cb::Nothing, args...; kwargs...) = nothing +function SymbolicDiscreteCallback(cb::Tuple, args...; kwargs...) + if length(cb) == 2 + SymbolicDiscreteCallback(cb[1]; cb[2]...) + else + error("Malformed tuple specifying callback. Should be a condition => affect pair, followed by a vector of kwargs.") + end +end function is_timed_condition(condition::T) where {T} if T === Num @@ -861,7 +874,7 @@ Compile an affect defined by a set of equations. Systems with algebraic equation function compile_equational_affect( aff::Union{AffectSystem, Vector{Equation}}, sys; reset_jumps = false, kwargs...) if aff isa AbstractVector - aff = make_affect(aff; iv = get_iv(sys)) + aff = make_affect(aff; iv = get_iv(sys), warn_no_algebraic = false) end affsys = system(aff) ps_to_update = discretes(aff) diff --git a/src/systems/diffeqs/odesystem.jl b/src/systems/diffeqs/odesystem.jl index 7e8529c6b0..5a28d88e77 100644 --- a/src/systems/diffeqs/odesystem.jl +++ b/src/systems/diffeqs/odesystem.jl @@ -314,8 +314,9 @@ function ODESystem(deqs::AbstractVector{<:Equation}, iv, dvs, ps; alg_eqs = filter(eq -> eq.lhs isa Union{Symbolic, Number} && !is_diff_equation(eq), deqs) cont_callbacks = to_cb_vector(SymbolicContinuousCallback.( - continuous_events; alg_eqs, iv)) - disc_callbacks = to_cb_vector(SymbolicDiscreteCallback.(discrete_events; alg_eqs, iv)) + continuous_events; alg_eqs = alg_eqs, iv = iv, warn_no_algebraic = false)) + disc_callbacks = to_cb_vector(SymbolicDiscreteCallback.( + discrete_events; alg_eqs = alg_eqs, iv = iv, warn_no_algebraic = false)) if is_dde === nothing is_dde = _check_if_dde(deqs, iv′, systems) diff --git a/src/systems/diffeqs/sdesystem.jl b/src/systems/diffeqs/sdesystem.jl index d976eef770..0d66bd1614 100644 --- a/src/systems/diffeqs/sdesystem.jl +++ b/src/systems/diffeqs/sdesystem.jl @@ -271,8 +271,9 @@ function SDESystem(deqs::AbstractVector{<:Equation}, neqs::AbstractArray, iv, dv alg_eqs = filter(eq -> eq.lhs isa Union{Symbolic, Number} && !is_diff_equation(eq), deqs) cont_callbacks = to_cb_vector(SymbolicContinuousCallback.( - continuous_events; alg_eqs, iv)) - disc_callbacks = to_cb_vector(SymbolicDiscreteCallback.(discrete_events; alg_eqs, iv)) + continuous_events; alg_eqs = alg_eqs, iv = iv, warn_no_algebraic = false)) + disc_callbacks = to_cb_vector(SymbolicDiscreteCallback.( + discrete_events; alg_eqs = alg_eqs, iv = iv, warn_no_algebraic = false)) if is_dde === nothing is_dde = _check_if_dde(deqs, iv′, systems) diff --git a/src/systems/jumps/jumpsystem.jl b/src/systems/jumps/jumpsystem.jl index 2aa07bd898..17d335f326 100644 --- a/src/systems/jumps/jumpsystem.jl +++ b/src/systems/jumps/jumpsystem.jl @@ -212,8 +212,10 @@ function JumpSystem(eqs, iv, unknowns, ps; end end - disc_callbacks = to_cb_vector(SymbolicDiscreteCallback.(discrete_events; iv)) - cont_callbacks = to_cb_vector(SymbolicContinuousCallback.(continuous_events; iv)) + disc_callbacks = to_cb_vector(SymbolicDiscreteCallback.( + discrete_events; iv = iv, warn_no_algebraic = false)) + cont_callbacks = to_cb_vector(SymbolicContinuousCallback.( + continuous_events; iv = iv, warn_no_algebraic = false)) JumpSystem{typeof(ap)}(Threads.atomic_add!(SYSTEM_COUNT, UInt(1)), ap, iv′, us′, ps′, var_to_name, observed, name, description, systems, diff --git a/src/systems/model_parsing.jl b/src/systems/model_parsing.jl index f8ffe164ab..384535de6d 100644 --- a/src/systems/model_parsing.jl +++ b/src/systems/model_parsing.jl @@ -116,6 +116,7 @@ function _model_macro(mod, name, expr, isconnector) sys = :($ODESystem($(flatten_equations)(equations), $iv, variables, parameters; name, description = $description, systems, gui_metadata = $gui_metadata, + continuous_events = [$(c_evts...)], discrete_events = [$(d_evts...)], defaults)) if length(ext) == 0 @@ -127,25 +128,6 @@ function _model_macro(mod, name, expr, isconnector) isconnector && push!(exprs.args, :($Setfield.@set!(var"#___sys___".connector_type=$connector_type(var"#___sys___")))) - if !isempty(d_evts) || !isempty(c_evts) - push!(exprs.args, :(alg_eqs = $(alg_equations)(var"#___sys___"))) - !isempty(d_evts) && begin - d_exprs = [:($(SymbolicDiscreteCallback)( - $(evt.args[1]); iv = $iv, alg_eqs, $(evt.args[2])...)) - for evt in d_evts] - push!(exprs.args, - :($Setfield.@set!(var"#___sys___".discrete_events=[$(d_exprs...)]))) - end - - !isempty(c_evts) && begin - c_exprs = [:($(SymbolicContinuousCallback)( - $(evt.args[1]); iv = $iv, alg_eqs, $(evt.args[2])...)) - for evt in c_evts] - push!(exprs.args, - :($Setfield.@set!(var"#___sys___".continuous_events=[$(c_exprs...)]))) - end - end - f = if length(where_types) == 0 :($(Symbol(:__, name, :__))(; name, $(kwargs...)) = $exprs) else diff --git a/test/extensions/ad.jl b/test/extensions/ad.jl index adaf6117c6..845d1ad818 100644 --- a/test/extensions/ad.jl +++ b/test/extensions/ad.jl @@ -59,7 +59,8 @@ end @parameters a b[1:3] c(t) d::Integer e[1:3] f[1:3, 1:3]::Int g::Vector{AbstractFloat} h::String @named sys = ODESystem( Equation[], t, [], [a, b, c, d, e, f, g, h], - continuous_events = [[a ~ 0] => [c ~ 0]]) + continuous_events = [ModelingToolkit.SymbolicContinuousCallback( + [a ~ 0] => [c ~ 0], discrete_parameters = c)]) sys = complete(sys) ivs = Dict(c => 3a, b => ones(3), a => 1.0, d => 4, e => [5.0, 6.0, 7.0], diff --git a/test/jumpsystem.jl b/test/jumpsystem.jl index 2fe989bd6a..58f4cff6e3 100644 --- a/test/jumpsystem.jl +++ b/test/jumpsystem.jl @@ -80,7 +80,7 @@ function getmean(jprob, Nsims; use_stepper = true) end m / Nsims end -@btime m = $getmean($jprob, $Nsims) +m = getmean(jprob, Nsims) # test auto-alg selection works jprobb = JumpProblem(js2, dprob; save_positions = (false, false), rng) @@ -248,7 +248,7 @@ end rate = k affect = [X ~ X - 1] -crj = ConstantRateJump(1.0, [X ~ X - 1]) +crj = ConstantRateJump(1.0, [X ~ Pre(X) - 1]) js1 = complete(JumpSystem([crj], t, [X], [k]; name = :js1)) js2 = complete(JumpSystem([crj], t, [X], []; name = :js2)) @@ -275,9 +275,9 @@ dp4 = DiscreteProblem(js4, u0, tspan) @parameters k @variables X(t) rate = k -affect = [X ~ X - 1] +affect = [X ~ Pre(X) - 1] -j1 = ConstantRateJump(k, [X ~ X - 1]) +j1 = ConstantRateJump(k, [X ~ Pre(X) - 1]) @test_nowarn @mtkbuild js1 = JumpSystem([j1], t, [X], [k]) # test correct autosolver is selected, which implies appropriate dep graphs are available @@ -285,8 +285,8 @@ let @parameters k @variables X(t) rate = k - affect = [X ~ X - 1] - j1 = ConstantRateJump(k, [X ~ X - 1]) + affect = [X ~ Pre(X) - 1] + j1 = ConstantRateJump(k, [X ~ Pre(X) - 1]) Nv = [1, JumpProcesses.USE_DIRECT_THRESHOLD + 1, JumpProcesses.USE_RSSA_THRESHOLD + 1] algtypes = [Direct, RSSA, RSSACR] @@ -305,7 +305,7 @@ let Random.seed!(rng, 1111) @variables A(t) B(t) C(t) @parameters k - vrj = VariableRateJump(k * (sin(t) + 1), [A ~ A + 1, C ~ C + 2]) + vrj = VariableRateJump(k * (sin(t) + 1), [A ~ Pre(A) + 1, C ~ Pre(C) + 2]) js = complete(JumpSystem([vrj], t, [A, C], [k]; name = :js, observed = [B ~ C * A])) oprob = ODEProblem(js, [A => 0, C => 0], (0.0, 10.0), [k => 1.0]) jprob = JumpProblem(js, oprob, Direct(); rng) @@ -346,9 +346,9 @@ end let @variables x1(t) x2(t) x3(t) x4(t) x5(t) @parameters p1 p2 p3 p4 p5 - j1 = ConstantRateJump(p1, [x1 ~ x1 + 1]) + j1 = ConstantRateJump(p1, [x1 ~ Pre(x1) + 1]) j2 = MassActionJump(p2, [x2 => 1], [x3 => -1]) - j3 = VariableRateJump(p3, [x3 ~ x3 + 1, x4 ~ x4 + 1]) + j3 = VariableRateJump(p3, [x3 ~ Pre(x3) + 1, x4 ~ Pre(x4) + 1]) j4 = MassActionJump(p4 * p5, [x1 => 1, x5 => 1], [x1 => -1, x5 => -1, x2 => 1]) us = Set() ps = Set() @@ -390,9 +390,9 @@ let p4 = DelayParentScope(p4, 2) p5 = GlobalScope(p5) - j1 = ConstantRateJump(p1, [x1 ~ x1 + 1]) + j1 = ConstantRateJump(p1, [x1 ~ Pre(x1) + 1]) j2 = MassActionJump(p2, [x2 => 1], [x3 => -1]) - j3 = VariableRateJump(p3, [x3 ~ x3 + 1, x4 ~ x4 + 1]) + j3 = VariableRateJump(p3, [x3 ~ Pre(x3) + 1, x4 ~ Pre(x4) + 1]) j4 = MassActionJump(p4 * p5, [x1 => 1, x5 => 1], [x1 => -1, x5 => -1, x2 => 1]) @named js = JumpSystem([j1, j2, j3, j4], t, [x1, x2, x3, x4, x5], [p1, p2, p3, p4, p5]) @@ -430,8 +430,8 @@ let Random.seed!(rng, seed) @variables X(t) Y(t) @parameters k1 k2 - vrj1 = VariableRateJump(k1 * X, [X ~ X - 1]; save_positions = (false, false)) - vrj2 = VariableRateJump(k1, [Y ~ Y + 1]; save_positions = (false, false)) + vrj1 = VariableRateJump(k1 * X, [X ~ Pre(X) - 1]; save_positions = (false, false)) + vrj2 = VariableRateJump(k1, [Y ~ Pre(Y) + 1]; save_positions = (false, false)) eqs = [D(X) ~ k2, D(Y) ~ -k2 / 10 * Y] @named jsys = JumpSystem([vrj1, vrj2, eqs[1], eqs[2]], t, [X, Y], [k1, k2]) jsys = complete(jsys) @@ -472,8 +472,8 @@ let Random.seed!(rng, seed) @variables X(t) Y(t) @parameters α β - vrj = VariableRateJump(β * X, [X ~ X - 1]; save_positions = (false, false)) - crj = ConstantRateJump(β * Y, [Y ~ Y - 1]) + vrj = VariableRateJump(β * X, [X ~ Pre(X) - 1]; save_positions = (false, false)) + crj = ConstantRateJump(β * Y, [Y ~ Pre(Y) - 1]) maj = MassActionJump(α, [0 => 1], [Y => 1]) eqs = [D(X) ~ α * (1 + Y)] @named jsys = JumpSystem([maj, crj, vrj, eqs[1]], t, [X, Y], [α, β]) @@ -540,8 +540,8 @@ end @variables X(t) rate1 = p rate2 = X * d - affect1 = [X ~ X + 1] - affect2 = [X ~ X - 1] + affect1 = [X ~ Pre(X) + 1] + affect2 = [X ~ Pre(X) - 1] j1 = ConstantRateJump(rate1, affect1) j2 = ConstantRateJump(rate2, affect2) diff --git a/test/mtkparameters.jl b/test/mtkparameters.jl index f9d71f00bc..cf42c223a3 100644 --- a/test/mtkparameters.jl +++ b/test/mtkparameters.jl @@ -10,7 +10,8 @@ using JET @parameters a b c(t) d::Integer e[1:3] f[1:3, 1:3]::Int g::Vector{AbstractFloat} h::String @named sys = ODESystem( Equation[], t, [], [a, c, d, e, f, g, h], parameter_dependencies = [b ~ 2a], - continuous_events = [[a ~ 0] => [c ~ 0]], defaults = Dict(a => 0.0)) + continuous_events = [ModelingToolkit.SymbolicContinuousCallback( + [a ~ 0] => [c ~ 0], discrete_parameters = c)], defaults = Dict(a => 0.0)) sys = complete(sys) ivs = Dict(c => 3a, d => 4, e => [5.0, 6.0, 7.0],