From 08408c523b97b8d4e6ec2e9895eee0835c1e1b13 Mon Sep 17 00:00:00 2001 From: vyudu Date: Sat, 1 Mar 2025 17:35:49 -0500 Subject: [PATCH 01/59] 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 07809bf611..576b7cb7d2 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 29db5d9929dae1a3318eb2f3fccce088c581805a Mon Sep 17 00:00:00 2001 From: vyudu Date: Mon, 3 Mar 2025 17:07:52 -0500 Subject: [PATCH 02/59] refactor: refactor affect codegen --- src/ModelingToolkit.jl | 10 +-- src/systems/callbacks.jl | 164 +++++++++++++++++++++++++++++++++------ 2 files changed, 145 insertions(+), 29 deletions(-) diff --git a/src/ModelingToolkit.jl b/src/ModelingToolkit.jl index 9f69458528..c7ee5b059e 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 576b7cb7d2..9063534819 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 @@ -899,13 +1019,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 +1161,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 +1169,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) From 641e8f35f3ce26e69b08799213239e67c109457a Mon Sep 17 00:00:00 2001 From: vyudu Date: Wed, 5 Mar 2025 15:22:28 -0500 Subject: [PATCH 03/59] feat: correct affect system generation --- src/systems/callbacks.jl | 1369 +++++++++--------------------- src/systems/diffeqs/odesystem.jl | 6 +- src/systems/imperative_affect.jl | 45 +- src/systems/systems.jl | 8 +- 4 files changed, 448 insertions(+), 980 deletions(-) diff --git a/src/systems/callbacks.jl b/src/systems/callbacks.jl index 9063534819..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,331 +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..., cse = false) - # 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..., cse = false) - 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..., cse = false) - - 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) @@ -1100,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 @@ -1117,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 @@ -1271,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 @@ -1284,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 cbce569a9b..8ad05830a1 100644 --- a/src/systems/diffeqs/odesystem.jl +++ b/src/systems/diffeqs/odesystem.jl @@ -317,8 +317,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 0f8633f31f..58a9fe1d4c 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 e753d7e0dfdc77bb957619f877707ad1e8a1e666 Mon Sep 17 00:00:00 2001 From: vyudu Date: Mon, 10 Mar 2025 16:23:40 -0400 Subject: [PATCH 04/59] 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 c7ee5b059e..e3a1c94060 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, generate_W 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 8ad05830a1..e674a88bd7 100644 --- a/src/systems/diffeqs/odesystem.jl +++ b/src/systems/diffeqs/odesystem.jl @@ -319,8 +319,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 0bafc52f7ca3c2891f27fb3ac251aa6bf0fd3f20 Mon Sep 17 00:00:00 2001 From: vyudu Date: Tue, 11 Mar 2025 18:19:58 -0400 Subject: [PATCH 05/59] refactor: correct condition generation in --- src/systems/callbacks.jl | 344 ++++++++++------- src/systems/diffeqs/odesystem.jl | 4 +- src/systems/imperative_affect.jl | 2 - src/systems/index_cache.jl | 2 + test/symbolic_events.jl | 634 +++++++++++++------------------ 5 files changed, 479 insertions(+), 507 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/diffeqs/odesystem.jl b/src/systems/diffeqs/odesystem.jl index e674a88bd7..b6f27e63da 100644 --- a/src/systems/diffeqs/odesystem.jl +++ b/src/systems/diffeqs/odesystem.jl @@ -319,8 +319,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 cca30fc50bb5c8790d85b24573c3554986ea1a91 Mon Sep 17 00:00:00 2001 From: vyudu Date: Wed, 12 Mar 2025 03:59:29 -0400 Subject: [PATCH 06/59] some tests working --- src/systems/callbacks.jl | 132 ++++++++++++------ .../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 ++++++------ 6 files changed, 147 insertions(+), 99 deletions(-) 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/discrete_system/discrete_system.jl b/src/systems/discrete_system/discrete_system.jl index 5f7c986659..0722380fd5 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 3956c089d4..b977ba992e 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 0750585905..8b96463e3c 100644 --- a/src/systems/problem_utils.jl +++ b/src/systems/problem_utils.jl @@ -380,9 +380,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 31204155b243fcb5a6875d48405aa0a274607daf Mon Sep 17 00:00:00 2001 From: vyudu Date: Wed, 12 Mar 2025 11:37:33 -0400 Subject: [PATCH 07/59] 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 b6f27e63da..06eeb4b1ff 100644 --- a/src/systems/diffeqs/odesystem.jl +++ b/src/systems/diffeqs/odesystem.jl @@ -318,10 +318,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 3fa1302630..9feb66e053 100644 --- a/src/systems/diffeqs/sdesystem.jl +++ b/src/systems/diffeqs/sdesystem.jl @@ -269,8 +269,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 57a3aee7df..fb4d243a21 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 5cca419bc6bd4996d7ee4041a2b4be449d3f60bc Mon Sep 17 00:00:00 2001 From: vyudu Date: Wed, 12 Mar 2025 12:13:23 -0400 Subject: [PATCH 08/59] 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 06eeb4b1ff..b6f27e63da 100644 --- a/src/systems/diffeqs/odesystem.jl +++ b/src/systems/diffeqs/odesystem.jl @@ -318,6 +318,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 cd10455681bf06510df5752096b28be62b588e04 Mon Sep 17 00:00:00 2001 From: vyudu Date: Wed, 12 Mar 2025 15:29:00 -0400 Subject: [PATCH 09/59] 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 4898a27a1004a2e4c9a835b7a95c00780c9ee397 Mon Sep 17 00:00:00 2001 From: vyudu Date: Wed, 12 Mar 2025 17:11:33 -0400 Subject: [PATCH 10/59] 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 b6f27e63da..3e20f6ab41 100644 --- a/src/systems/diffeqs/odesystem.jl +++ b/src/systems/diffeqs/odesystem.jl @@ -318,7 +318,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 9feb66e053..75d65b5862 100644 --- a/src/systems/diffeqs/sdesystem.jl +++ b/src/systems/diffeqs/sdesystem.jl @@ -270,7 +270,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 40234b48f2dc69303cedc409c52ff86f4de278d1 Mon Sep 17 00:00:00 2001 From: vyudu Date: Wed, 12 Mar 2025 17:18:19 -0400 Subject: [PATCH 11/59] 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 e7b3c11d0b92e50cf9132010f6ded7a7aef8e10d Mon Sep 17 00:00:00 2001 From: vyudu Date: Wed, 12 Mar 2025 17:22:00 -0400 Subject: [PATCH 12/59] 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 3e20f6ab41..7eec328a45 100644 --- a/src/systems/diffeqs/odesystem.jl +++ b/src/systems/diffeqs/odesystem.jl @@ -319,8 +319,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 75d65b5862..d4e7b4bcb6 100644 --- a/src/systems/diffeqs/sdesystem.jl +++ b/src/systems/diffeqs/sdesystem.jl @@ -271,8 +271,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 70c96a3f7bc504798e424344de69ea3f9b23ae20 Mon Sep 17 00:00:00 2001 From: vyudu Date: Thu, 13 Mar 2025 20:53:56 -0400 Subject: [PATCH 13/59] 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 45f5424f1dfc8ef9b3792363f183aee139ad8d31 Mon Sep 17 00:00:00 2001 From: vyudu Date: Mon, 17 Mar 2025 10:06:00 -0400 Subject: [PATCH 14/59] 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 693a81e148..eb2233482d 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 7eec328a45..ecc6b389a1 100644 --- a/src/systems/diffeqs/odesystem.jl +++ b/src/systems/diffeqs/odesystem.jl @@ -319,8 +319,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 d4e7b4bcb6..b93385158b 100644 --- a/src/systems/diffeqs/sdesystem.jl +++ b/src/systems/diffeqs/sdesystem.jl @@ -271,8 +271,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 ae8eaa9567de1b8d7314bee016aaff06eb6e8cc1 Mon Sep 17 00:00:00 2001 From: vyudu Date: Tue, 18 Mar 2025 12:36:20 -0400 Subject: [PATCH 15/59] 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 ecc6b389a1..56bb246170 100644 --- a/src/systems/diffeqs/odesystem.jl +++ b/src/systems/diffeqs/odesystem.jl @@ -318,9 +318,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 b93385158b..857a6ec04f 100644 --- a/src/systems/diffeqs/sdesystem.jl +++ b/src/systems/diffeqs/sdesystem.jl @@ -270,9 +270,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 fb4d243a21..11c0db7137 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 45f97a2dab7132cfd037ca04815fbc2854134003 Mon Sep 17 00:00:00 2001 From: vyudu Date: Tue, 18 Mar 2025 16:20:46 -0400 Subject: [PATCH 16/59] 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 eb2233482d..303ac635fa 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 e3a1c94060..690f2cbdb7 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 4a626cb85939f395341df7f66b6710174ff1e3b9 Mon Sep 17 00:00:00 2001 From: vyudu Date: Thu, 20 Mar 2025 14:55:28 -0400 Subject: [PATCH 17/59] 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 b977ba992e..768a3a2191 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 d27e5c93a1..bed46caa95 100644 --- a/src/systems/systemstructure.jl +++ b/src/systems/systemstructure.jl @@ -687,6 +687,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 f7015b34b3a0fefeaf3d8a4e199966c2a633c92d Mon Sep 17 00:00:00 2001 From: vyudu Date: Thu, 20 Mar 2025 14:58:09 -0400 Subject: [PATCH 18/59] 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 318767bbb83300a954d5e0292aa69e927f58b8bc Mon Sep 17 00:00:00 2001 From: vyudu Date: Thu, 20 Mar 2025 17:24:23 -0400 Subject: [PATCH 19/59] 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 39ed68e60c6f7d301582b844e891449f6cfe235a Mon Sep 17 00:00:00 2001 From: vyudu Date: Sat, 22 Mar 2025 02:38:40 -0400 Subject: [PATCH 20/59] 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 11c0db7137..4f0c791d60 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 @@ -574,8 +571,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 a98d0fe4fca75ff282955a1e4dae17599de30c1a Mon Sep 17 00:00:00 2001 From: vyudu Date: Sat, 22 Mar 2025 07:08:56 -0400 Subject: [PATCH 21/59] 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 4f0c791d60..5c47c91114 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 @@ -543,6 +541,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 1bb24b4ccbc67558a92fba007e6c0c09ca77d8f7 Mon Sep 17 00:00:00 2001 From: vyudu Date: Sat, 22 Mar 2025 07:34:02 -0400 Subject: [PATCH 22/59] 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 7e0186994688b3fcfff56495167078bd9c16623b Mon Sep 17 00:00:00 2001 From: vyudu Date: Mon, 24 Mar 2025 12:38:46 -0400 Subject: [PATCH 23/59] 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 768a3a2191..df33d104fb 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 5c47c91114..7d15008184 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 @@ -541,7 +540,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 db394ba0d335d2056460ff0592c7c34f8f57863a Mon Sep 17 00:00:00 2001 From: vyudu Date: Mon, 24 Mar 2025 15:46:15 -0400 Subject: [PATCH 24/59] fix: more test fixes --- src/systems/callbacks.jl | 11 +++++++---- src/systems/model_parsing.jl | 20 ++++++++------------ test/discrete_system.jl | 1 - test/fmi/fmi.jl | 6 ++---- test/jumpsystem.jl | 6 +++--- test/model_parsing.jl | 2 +- 6 files changed, 21 insertions(+), 25 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 195b02118e..adec11f148 100644 --- a/src/systems/model_parsing.jl +++ b/src/systems/model_parsing.jl @@ -75,6 +75,8 @@ function _model_macro(mod, fullname::Union{Expr, Symbol}, expr, isconnector) push!(exprs.args, :(systems = ModelingToolkit.AbstractSystem[])) 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 @@ -116,6 +118,8 @@ function _model_macro(mod, fullname::Union{Expr, Symbol}, 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)) @@ -125,8 +129,8 @@ function _model_macro(mod, fullname::Union{Expr, Symbol}, expr, isconnector) @inline pop_structure_dict!.( Ref(dict), [:constants, :defaults, :kwargs, :structural_parameters]) - sys = :($type($(flatten_equations)(equations), $iv, variables, parameters; - name, description = $description, systems, gui_metadata = $gui_metadata, defaults)) + 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)) if length(ext) == 0 push!(exprs.args, :(var"#___sys___" = $sys)) @@ -137,16 +141,6 @@ function _model_macro(mod, fullname::Union{Expr, Symbol}, 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 @@ -1132,6 +1126,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 @@ -1141,6 +1136,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 0d215052d8..7fa8349c8e 100644 --- a/test/discrete_system.jl +++ b/test/discrete_system.jl @@ -252,7 +252,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 9568990e73..7289260083 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 e8464707de..0cdf294a1f 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 0b2e3814d3d809d051012e4d870d6fe77a2484b4 Mon Sep 17 00:00:00 2001 From: vyudu Date: Mon, 24 Mar 2025 19:28:21 -0400 Subject: [PATCH 25/59] 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 56bb246170..8c77b2e030 100644 --- a/src/systems/diffeqs/odesystem.jl +++ b/src/systems/diffeqs/odesystem.jl @@ -318,7 +318,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 857a6ec04f..f49f1019e2 100644 --- a/src/systems/diffeqs/sdesystem.jl +++ b/src/systems/diffeqs/sdesystem.jl @@ -270,7 +270,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 adec11f148..c5fd3454de 100644 --- a/src/systems/model_parsing.jl +++ b/src/systems/model_parsing.jl @@ -1126,7 +1126,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 @@ -1136,7 +1135,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 4ade3481cb..aac0566ef5 100644 --- a/test/initializationsystem.jl +++ b/test/initializationsystem.jl @@ -1285,9 +1285,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 b8d185fed1ca584250be082353f37cbd12f03687 Mon Sep 17 00:00:00 2001 From: vyudu Date: Tue, 25 Mar 2025 17:26:29 -0400 Subject: [PATCH 26/59] 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 df33d104fb..b63235bbdc 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 7289260083..84dd71d365 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 3c2e39fd111193e350c474382d043d91545812f0 Mon Sep 17 00:00:00 2001 From: vyudu Date: Wed, 26 Mar 2025 14:03:22 -0400 Subject: [PATCH 27/59] 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 b63235bbdc..a43595b25b 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 8f473b7554e2a347b256a96e2b9fac63e00611de Mon Sep 17 00:00:00 2001 From: vyudu Date: Wed, 26 Mar 2025 14:07:31 -0400 Subject: [PATCH 28/59] 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 9ec0f541099adf7e038fda5b3aa90bf379b9c3fa Mon Sep 17 00:00:00 2001 From: vyudu Date: Wed, 26 Mar 2025 15:45:28 -0400 Subject: [PATCH 29/59] 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 8c77b2e030..06fbf07d00 100644 --- a/src/systems/diffeqs/odesystem.jl +++ b/src/systems/diffeqs/odesystem.jl @@ -318,7 +318,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 f49f1019e2..b3fa1bacb7 100644 --- a/src/systems/diffeqs/sdesystem.jl +++ b/src/systems/diffeqs/sdesystem.jl @@ -270,7 +270,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 a43595b25b..ef64f92336 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 3aa40d6454ad8744947004fce62557c61706fcf1 Mon Sep 17 00:00:00 2001 From: vyudu Date: Wed, 26 Mar 2025 15:50:27 -0400 Subject: [PATCH 30/59] 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 94a864238451df7ea8f79b20e763a736e796411b Mon Sep 17 00:00:00 2001 From: vyudu Date: Wed, 26 Mar 2025 19:26:20 -0400 Subject: [PATCH 31/59] 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 06fbf07d00..79163bcc12 100644 --- a/src/systems/diffeqs/odesystem.jl +++ b/src/systems/diffeqs/odesystem.jl @@ -318,7 +318,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 b3fa1bacb7..2c98f407be 100644 --- a/src/systems/diffeqs/sdesystem.jl +++ b/src/systems/diffeqs/sdesystem.jl @@ -270,7 +270,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 0722380fd5..1d0832bb36 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 ef64f92336..eb3a094ef5 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 c5fd3454de..4235c4bb42 100644 --- a/src/systems/model_parsing.jl +++ b/src/systems/model_parsing.jl @@ -130,7 +130,8 @@ function _model_macro(mod, fullname::Union{Expr, Symbol}, 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 bed46caa95..d27e5c93a1 100644 --- a/src/systems/systemstructure.jl +++ b/src/systems/systemstructure.jl @@ -687,7 +687,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 66f149ad2d820c40af2c1828964d5ee9fbb89810 Mon Sep 17 00:00:00 2001 From: vyudu Date: Thu, 27 Mar 2025 08:39:50 -0400 Subject: [PATCH 32/59] 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 1884a91c19..d82f3ddc3c 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 c61f87e06a61dbb5d8d30f413ebec67d6433eeb3 Mon Sep 17 00:00:00 2001 From: vyudu Date: Thu, 27 Mar 2025 13:15:43 -0400 Subject: [PATCH 33/59] fix: add events to SDESystem after structural simplification --- src/systems/diffeqs/odesystem.jl | 2 +- src/systems/diffeqs/sdesystem.jl | 2 +- src/systems/systems.jl | 4 +--- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/systems/diffeqs/odesystem.jl b/src/systems/diffeqs/odesystem.jl index 79163bcc12..30c322c0b3 100644 --- a/src/systems/diffeqs/odesystem.jl +++ b/src/systems/diffeqs/odesystem.jl @@ -319,7 +319,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 2c98f407be..77fc851d45 100644 --- a/src/systems/diffeqs/sdesystem.jl +++ b/src/systems/diffeqs/sdesystem.jl @@ -271,7 +271,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 58a9fe1d4c..a111430e1b 100644 --- a/src/systems/systems.jl +++ b/src/systems/systems.jl @@ -155,9 +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)) - @set! ssys.tearing_state = get_tearing_state(ode_sys) - return ssys + guesses = guesses(sys), initialization_eqs = initialization_equations(sys), continuous_events = continuous_events(sys), discrete_events = discrete_events(sys)) end end From 1e9843aa0a3dc79fbd5c5c41daf0c7ed2f798b84 Mon Sep 17 00:00:00 2001 From: vyudu Date: Fri, 28 Mar 2025 12:26:28 -0400 Subject: [PATCH 34/59] 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 4235c4bb42..5ae9a056a8 100644 --- a/src/systems/model_parsing.jl +++ b/src/systems/model_parsing.jl @@ -75,8 +75,6 @@ function _model_macro(mod, fullname::Union{Expr, Symbol}, expr, isconnector) push!(exprs.args, :(systems = ModelingToolkit.AbstractSystem[])) 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 @@ -118,8 +116,6 @@ function _model_macro(mod, fullname::Union{Expr, Symbol}, 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)) @@ -131,7 +127,7 @@ function _model_macro(mod, fullname::Union{Expr, Symbol}, 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)) @@ -142,6 +138,18 @@ function _model_macro(mod, fullname::Union{Expr, Symbol}, 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 386b22fa4ca39ad3b00cf56222bb7c7cdfe9b9c8 Mon Sep 17 00:00:00 2001 From: vyudu Date: Mon, 31 Mar 2025 14:47:35 -0400 Subject: [PATCH 35/59] 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 5ae9a056a8..0bc4e0fee5 100644 --- a/src/systems/model_parsing.jl +++ b/src/systems/model_parsing.jl @@ -143,7 +143,6 @@ function _model_macro(mod, fullname::Union{Expr, Symbol}, 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 f86949e77f62acbd1e731b4e65e2f59588c6fc51 Mon Sep 17 00:00:00 2001 From: vyudu Date: Mon, 31 Mar 2025 17:23:19 -0400 Subject: [PATCH 36/59] 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 d82f3ddc3c..6ca95341b1 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -591,6 +591,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 var = setmetadata(var, SymScope, LocalScope()) if iscalledparameter(var) diff --git a/test/odesystem.jl b/test/odesystem.jl index 4b76da6e9d..39884e7336 100644 --- a/test/odesystem.jl +++ b/test/odesystem.jl @@ -1028,24 +1028,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 @@ -1192,10 +1194,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 @@ -1533,8 +1537,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 @@ -1594,15 +1597,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 35de20456244ae8cb3beb99a04bb23e095a5b8b3 Mon Sep 17 00:00:00 2001 From: vyudu Date: Mon, 31 Mar 2025 17:34:09 -0400 Subject: [PATCH 37/59] 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 0bc4e0fee5..e04f27b253 100644 --- a/src/systems/model_parsing.jl +++ b/src/systems/model_parsing.jl @@ -148,7 +148,6 @@ function _model_macro(mod, fullname::Union{Expr, Symbol}, 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 a111430e1b..4d7216d081 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 39884e7336..e6bd6d2777 100644 --- a/test/odesystem.jl +++ b/test/odesystem.jl @@ -1034,18 +1034,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 @@ -1537,7 +1538,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 @@ -1605,7 +1611,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 b6ec048ab8877e43b2327a602d30b3c2dbd36c01 Mon Sep 17 00:00:00 2001 From: vyudu Date: Tue, 1 Apr 2025 12:08:26 -0400 Subject: [PATCH 38/59] 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 e6bd6d2777..20d70bbfda 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 2b3904052c53ccf6910365818818319c150b7863 Mon Sep 17 00:00:00 2001 From: vyudu Date: Tue, 1 Apr 2025 12:40:54 -0400 Subject: [PATCH 39/59] 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 30c322c0b3..915c825071 100644 --- a/src/systems/diffeqs/odesystem.jl +++ b/src/systems/diffeqs/odesystem.jl @@ -318,10 +318,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 77fc851d45..0e96dd94ff 100644 --- a/src/systems/diffeqs/sdesystem.jl +++ b/src/systems/diffeqs/sdesystem.jl @@ -270,10 +270,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 0b8c224046a1bc057dc5ed7cb1a3890f83876fe7 Mon Sep 17 00:00:00 2001 From: vyudu Date: Tue, 1 Apr 2025 13:40:07 -0400 Subject: [PATCH 40/59] 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 8b3da5fd72..e352533fbd 100644 --- a/test/symbolic_indexing_interface.jl +++ b/test/symbolic_indexing_interface.jl @@ -230,7 +230,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 2aac0b41e8a8521a6c7ed6a2d58eb216a3913fad Mon Sep 17 00:00:00 2001 From: vyudu Date: Tue, 1 Apr 2025 15:01:47 -0400 Subject: [PATCH 41/59] 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 e04f27b253..9a27258c95 100644 --- a/src/systems/model_parsing.jl +++ b/src/systems/model_parsing.jl @@ -138,15 +138,23 @@ function _model_macro(mod, fullname::Union{Expr, Symbol}, 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) @@ -1135,8 +1143,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 @@ -1144,12 +1160,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 b9a13754decef8639f6c9ceaa8efb9f4a6bdf5e2 Mon Sep 17 00:00:00 2001 From: vyudu Date: Tue, 1 Apr 2025 15:10:38 -0400 Subject: [PATCH 42/59] 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 e91f2bcb67..ea9c9da3df 100644 --- a/docs/src/basics/MTKLanguage.md +++ b/docs/src/basics/MTKLanguage.md @@ -203,6 +203,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 @@ -210,7 +211,7 @@ using ModelingToolkit: t @mtkmodel M begin @parameters begin - k + k(t) end @variables begin x(t) @@ -223,21 +224,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) @@ -248,7 +252,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 3f4dd04a1aa53459747127e70a962bd4763684d1 Mon Sep 17 00:00:00 2001 From: vyudu Date: Tue, 1 Apr 2025 15:20:50 -0400 Subject: [PATCH 43/59] 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 ea9c9da3df..ba6d2c34b5 100644 --- a/docs/src/basics/MTKLanguage.md +++ b/docs/src/basics/MTKLanguage.md @@ -229,7 +229,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 9a27258c95..9204d9b7a1 100644 --- a/src/systems/model_parsing.jl +++ b/src/systems/model_parsing.jl @@ -140,7 +140,7 @@ function _model_macro(mod, fullname::Union{Expr, Symbol}, 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 @@ -149,8 +149,9 @@ function _model_macro(mod, fullname::Union{Expr, Symbol}, 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, @@ -1144,9 +1145,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))) @@ -1161,9 +1162,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 e352533fbd..44821987b5 100644 --- a/test/symbolic_indexing_interface.jl +++ b/test/symbolic_indexing_interface.jl @@ -230,7 +230,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 b31687cbea140e473f7aa13baf42649ab6944643 Mon Sep 17 00:00:00 2001 From: vyudu Date: Tue, 1 Apr 2025 16:37:51 -0400 Subject: [PATCH 44/59] 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 915c825071..c3da246d80 100644 --- a/src/systems/diffeqs/odesystem.jl +++ b/src/systems/diffeqs/odesystem.jl @@ -320,8 +320,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 0e96dd94ff..33e9b99dc4 100644 --- a/src/systems/diffeqs/sdesystem.jl +++ b/src/systems/diffeqs/sdesystem.jl @@ -272,8 +272,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 7d15008184..b6edfbb14d 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 44821987b5..613bfc8213 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 52dc66622c3e8244a7dfc1d6bbb4ddebba73e0d0 Mon Sep 17 00:00:00 2001 From: vyudu Date: Tue, 1 Apr 2025 23:51:47 -0400 Subject: [PATCH 45/59] 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 c3da246d80..12475f8dbb 100644 --- a/src/systems/diffeqs/odesystem.jl +++ b/src/systems/diffeqs/odesystem.jl @@ -320,8 +320,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 b6edfbb14d..cac17208b0 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 9204d9b7a1..6ea289e94d 100644 --- a/src/systems/model_parsing.jl +++ b/src/systems/model_parsing.jl @@ -138,24 +138,24 @@ function _model_macro(mod, fullname::Union{Expr, Symbol}, 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) @@ -1146,7 +1146,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]) @@ -1163,7 +1163,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]) @@ -1180,7 +1180,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 8132599a88d6ae12460055f549670caa6a5225e5 Mon Sep 17 00:00:00 2001 From: vyudu Date: Wed, 2 Apr 2025 00:07:58 -0400 Subject: [PATCH 46/59] 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 cac17208b0..7e9f2478b7 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 f5b7420361843b98d011d5bfbeb5dcd447c087a5 Mon Sep 17 00:00:00 2001 From: vyudu Date: Wed, 2 Apr 2025 00:24:06 -0400 Subject: [PATCH 47/59] 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 6abe69d803403b81ef16f14b1ea07b6dceb39e04 Mon Sep 17 00:00:00 2001 From: vyudu Date: Wed, 2 Apr 2025 00:27:46 -0400 Subject: [PATCH 48/59] 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 ffffe5a63896bdf1a35b3288007882d8bed4342f Mon Sep 17 00:00:00 2001 From: vyudu Date: Wed, 2 Apr 2025 00:30:46 -0400 Subject: [PATCH 49/59] 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 7e9f2478b7..d74d4c4a6e 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 c5cdad3781e2b5f192dad2db042a5f669509b543 Mon Sep 17 00:00:00 2001 From: vyudu Date: Wed, 2 Apr 2025 00:53:29 -0400 Subject: [PATCH 50/59] 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 8aeff5585556b4ac46f78fbb7b3270ff98fca8e7 Mon Sep 17 00:00:00 2001 From: vyudu Date: Wed, 2 Apr 2025 00:54:53 -0400 Subject: [PATCH 51/59] 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 911333d0caefcca76afb04f49fb57d8024f8305d Mon Sep 17 00:00:00 2001 From: vyudu Date: Wed, 2 Apr 2025 02:39:17 -0400 Subject: [PATCH 52/59] 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 12475f8dbb..75417b7012 100644 --- a/src/systems/diffeqs/odesystem.jl +++ b/src/systems/diffeqs/odesystem.jl @@ -321,8 +321,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 33e9b99dc4..4c9a096a7f 100644 --- a/src/systems/diffeqs/sdesystem.jl +++ b/src/systems/diffeqs/sdesystem.jl @@ -273,8 +273,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 d74d4c4a6e..62ce703bae 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 6ea289e94d..fac7d77585 100644 --- a/src/systems/model_parsing.jl +++ b/src/systems/model_parsing.jl @@ -127,6 +127,7 @@ function _model_macro(mod, fullname::Union{Expr, Symbol}, 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 @@ -138,25 +139,6 @@ function _model_macro(mod, fullname::Union{Expr, Symbol}, 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 84dd71d365..82299adb8c 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) 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 22201b1988..9fefe40af2 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], From a160bc32441c1c4e151ddfe0e35a99b50ba24267 Mon Sep 17 00:00:00 2001 From: vyudu Date: Wed, 2 Apr 2025 02:56:40 -0400 Subject: [PATCH 53/59] docs: fix doc discrete_events example --- docs/src/basics/Events.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/src/basics/Events.md b/docs/src/basics/Events.md index 4829e698da..8360648e40 100644 --- a/docs/src/basics/Events.md +++ b/docs/src/basics/Events.md @@ -409,8 +409,9 @@ example: @variables x(t) @parameters c(t) +ev = SymbolicDiscreteCallback(1.0 => [c ~ Pre(c) + 1], discrete_parameters = c) @mtkbuild sys = ODESystem( - D(x) ~ c * cos(x), t, [x], [c]; discrete_events = [1.0 => [c ~ Pre(c) + 1]]) + D(x) ~ c * cos(x), t, [x], [c]; discrete_events = [ev]) prob = ODEProblem(sys, [x => 0.0], (0.0, 2pi), [c => 1.0]) sol = solve(prob, Tsit5()) @@ -423,12 +424,12 @@ The solution object can also be interpolated with the discrete variables sol([1.0, 2.0], idxs = [c, c * cos(x)]) ``` -Note that only time-dependent parameters will be saved. If we repeat the above example with -this change: +Note that only time-dependent parameters that are explicitly passed as `discrete_parameters` +will be saved. If we repeat the above example with `c` not a `discrete_parameter`: ```@example events @variables x(t) -@parameters c +@parameters c(t) @mtkbuild sys = ODESystem( D(x) ~ c * cos(x), t, [x], [c]; discrete_events = [1.0 => [c ~ Pre(c) + 1]]) From b28b416973449f17845f94e6b2b11c0cd79e0097 Mon Sep 17 00:00:00 2001 From: vyudu Date: Wed, 2 Apr 2025 03:26:40 -0400 Subject: [PATCH 54/59] docs: fix doc example --- docs/src/basics/Events.md | 2 +- src/systems/callbacks.jl | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/docs/src/basics/Events.md b/docs/src/basics/Events.md index 8360648e40..5c7162a875 100644 --- a/docs/src/basics/Events.md +++ b/docs/src/basics/Events.md @@ -409,7 +409,7 @@ example: @variables x(t) @parameters c(t) -ev = SymbolicDiscreteCallback(1.0 => [c ~ Pre(c) + 1], discrete_parameters = c) +ev = ModelingToolkit.SymbolicDiscreteCallback(1.0 => [c ~ Pre(c) + 1], discrete_parameters = c, iv = t) @mtkbuild sys = ODESystem( D(x) ~ c * cos(x), t, [x], [c]; discrete_events = [ev]) diff --git a/src/systems/callbacks.jl b/src/systems/callbacks.jl index 1e6264e96a..bd2901188b 100644 --- a/src/systems/callbacks.jl +++ b/src/systems/callbacks.jl @@ -231,7 +231,6 @@ struct SymbolicContinuousCallback <: AbstractCallback reinitializealg = SciMLBase.CheckInit() : reinitializealg = SciMLBase.NoInit() end - @show kwargs new(conditions, make_affect(affect; kwargs...), make_affect(affect_neg; kwargs...), @@ -420,15 +419,15 @@ Arguments: - alg_eqs: Algebraic equations of the system that must be satisfied after the callback occurs. """ struct SymbolicDiscreteCallback <: AbstractCallback - conditions::Any + conditions::Union{Number, Vector{<:Number}} 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, + condition::Union{Number, Vector{<:Number}}, affect = nothing; + initialize = nothing, finalize = nothing, reinitializealg = nothing, kwargs...) c = is_timed_condition(condition) ? condition : value(scalarize(condition)) From 00a736cef01726373b122ae2a7283a73e2b23f89 Mon Sep 17 00:00:00 2001 From: vyudu Date: Wed, 2 Apr 2025 10:15:20 -0400 Subject: [PATCH 55/59] docs: fix more doc examples --- docs/src/basics/Events.md | 14 +++++++++----- docs/src/tutorials/fmi.md | 8 +++++--- ext/MTKFMIExt.jl | 2 +- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/docs/src/basics/Events.md b/docs/src/basics/Events.md index 5c7162a875..e9cca7f85f 100644 --- a/docs/src/basics/Events.md +++ b/docs/src/basics/Events.md @@ -330,7 +330,8 @@ event time, the event condition now returns false. Here we used logical and, cannot be used within symbolic expressions. Let's now also add a drug at time `tkill` that turns off production of new -cells, modeled by setting `α = 0.0` +cells, modeled by setting `α = 0.0`. Since this is a parameter we must explicitly +set it as `discrete_parameters`. ```@example events @parameters tkill @@ -339,7 +340,8 @@ cells, modeled by setting `α = 0.0` injection = (t == tinject) => [N ~ Pre(N) + M] # at time tkill we turn off production of cells -killing = (t == tkill) => [α ~ 0.0] +killing = ModelingToolkit.SymbolicDiscreteCallback( + (t == tkill) => [α ~ 0.0]; discrete_parameters = α, iv = t) tspan = (0.0, 30.0) p = [α => 100.0, tinject => 10.0, M => 50, tkill => 20.0] @@ -368,7 +370,8 @@ As such, our last example with treatment and killing could instead be modeled by ```@example events injection = [10.0] => [N ~ Pre(N) + M] -killing = [20.0] => [α ~ 0.0] +killing = ModelingToolkit.SymbolicDiscreteCallback( + [20.0] => [α ~ 0.0], discrete_parameters = α, iv = t) p = [α => 100.0, M => 50] @mtkbuild osys = ODESystem(eqs, t, [N], [α, M]; @@ -409,7 +412,8 @@ example: @variables x(t) @parameters c(t) -ev = ModelingToolkit.SymbolicDiscreteCallback(1.0 => [c ~ Pre(c) + 1], discrete_parameters = c, iv = t) +ev = ModelingToolkit.SymbolicDiscreteCallback( + 1.0 => [c ~ Pre(c) + 1], discrete_parameters = c, iv = t) @mtkbuild sys = ODESystem( D(x) ~ c * cos(x), t, [x], [c]; discrete_events = [ev]) @@ -424,7 +428,7 @@ The solution object can also be interpolated with the discrete variables sol([1.0, 2.0], idxs = [c, c * cos(x)]) ``` -Note that only time-dependent parameters that are explicitly passed as `discrete_parameters` +Note that only time-dependent parameters that are explicitly passed as `discrete_parameters` will be saved. If we repeat the above example with `c` not a `discrete_parameter`: ```@example events diff --git a/docs/src/tutorials/fmi.md b/docs/src/tutorials/fmi.md index ef00477c78..ed4fb3e2a8 100644 --- a/docs/src/tutorials/fmi.md +++ b/docs/src/tutorials/fmi.md @@ -94,7 +94,8 @@ we will create a model from a CoSimulation FMU. ```@example fmi fmu = loadFMU("SpringPendulum1D", "Dymola", "2023x", "3.0"; type = :CS) @named inner = ModelingToolkit.FMIComponent( - Val(3); fmu, communication_step_size = 0.001, type = :CS) + Val(3); fmu, communication_step_size = 0.001, type = :CS, + reinitializealg = BrownFullBasicInit()) ``` This FMU has fewer equations, partly due to missing aliasing variables and partly due to being a CS FMU. @@ -170,7 +171,8 @@ end `a` and `b` are inputs, `c` is a state, and `out` and `out2` are outputs of the component. ```@repl fmi -@named adder = ModelingToolkit.FMIComponent(Val(2); fmu, type = :ME); +@named adder = ModelingToolkit.FMIComponent( + Val(2); fmu, type = :ME, reinitializealg = SciMLBase.BrownFullBasicInit()); isinput(adder.a) isinput(adder.b) isoutput(adder.out) @@ -214,7 +216,7 @@ fmu = loadFMU( type = :CS) @named adder = ModelingToolkit.FMIComponent( Val(2); fmu, type = :CS, communication_step_size = 1e-3, - reinitializealg = BrownFullBasicInit()) + reinitializealg = SciMLBase.BrownFullBasicInit()) @mtkbuild sys = ODESystem( [adder.a ~ a, adder.b ~ b, D(a) ~ t, D(b) ~ adder.out + adder.c, c^2 ~ adder.out + adder.value], diff --git a/ext/MTKFMIExt.jl b/ext/MTKFMIExt.jl index 0baf37c34b..32023d0749 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, reinitializealg = nothing) where {Ver} + communication_step_size = nothing, reinitializealg = nothing, type, name) where {Ver} if Ver != 2 && Ver != 3 throw(ArgumentError("FMI Version must be `2` or `3`")) end From 7584e80adbf8fcd3b952a91943678df8e6a834a4 Mon Sep 17 00:00:00 2001 From: vyudu Date: Wed, 2 Apr 2025 15:27:53 -0400 Subject: [PATCH 56/59] allow symbolic in Discrete condition --- src/systems/callbacks.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/systems/callbacks.jl b/src/systems/callbacks.jl index bd2901188b..6a919615c9 100644 --- a/src/systems/callbacks.jl +++ b/src/systems/callbacks.jl @@ -412,14 +412,14 @@ A callback that triggers at the first timestep that the conditions are satisfied 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 +- eqs::Vector{Symbolic} - 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. - alg_eqs: Algebraic equations of the system that must be satisfied after the callback occurs. """ struct SymbolicDiscreteCallback <: AbstractCallback - conditions::Union{Number, Vector{<:Number}} + conditions::Union{Number, Vector{<:Number}, Symbolic} affect::Union{Affect, Nothing} initialize::Union{Affect, Nothing} finalize::Union{Affect, Nothing} From df78b9f45f8e61b358033836203fc002d1fd390c Mon Sep 17 00:00:00 2001 From: vyudu Date: Wed, 2 Apr 2025 15:31:59 -0400 Subject: [PATCH 57/59] require Bool --- 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 6a919615c9..166122b64c 100644 --- a/src/systems/callbacks.jl +++ b/src/systems/callbacks.jl @@ -419,7 +419,7 @@ Arguments: - alg_eqs: Algebraic equations of the system that must be satisfied after the callback occurs. """ struct SymbolicDiscreteCallback <: AbstractCallback - conditions::Union{Number, Vector{<:Number}, Symbolic} + conditions::Union{Number, Vector{<:Number}, Symbolic{Bool}} affect::Union{Affect, Nothing} initialize::Union{Affect, Nothing} finalize::Union{Affect, Nothing} From c6cb71c4b1a075a7846a9a22b59aed9ae6daa649 Mon Sep 17 00:00:00 2001 From: vyudu Date: Thu, 3 Apr 2025 10:19:23 -0400 Subject: [PATCH 58/59] more docs fixes --- docs/src/basics/Events.md | 2 +- docs/src/tutorials/fmi.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/src/basics/Events.md b/docs/src/basics/Events.md index e9cca7f85f..d473db7a89 100644 --- a/docs/src/basics/Events.md +++ b/docs/src/basics/Events.md @@ -288,7 +288,7 @@ Suppose we have a population of `N(t)` cells that can grow and die, and at time `t1` we want to inject `M` more cells into the population. We can model this by ```@example events -@parameters M tinject α +@parameters M tinject α(t) @variables N(t) Dₜ = Differential(t) eqs = [Dₜ(N) ~ α - N] diff --git a/docs/src/tutorials/fmi.md b/docs/src/tutorials/fmi.md index ed4fb3e2a8..0e01393652 100644 --- a/docs/src/tutorials/fmi.md +++ b/docs/src/tutorials/fmi.md @@ -172,7 +172,7 @@ end ```@repl fmi @named adder = ModelingToolkit.FMIComponent( - Val(2); fmu, type = :ME, reinitializealg = SciMLBase.BrownFullBasicInit()); + Val(2); fmu, type = :ME, reinitializealg = BrownFullBasicInit()); isinput(adder.a) isinput(adder.b) isoutput(adder.out) @@ -216,7 +216,7 @@ fmu = loadFMU( type = :CS) @named adder = ModelingToolkit.FMIComponent( Val(2); fmu, type = :CS, communication_step_size = 1e-3, - reinitializealg = SciMLBase.BrownFullBasicInit()) + reinitializealg = BrownFullBasicInit()) @mtkbuild sys = ODESystem( [adder.a ~ a, adder.b ~ b, D(a) ~ t, D(b) ~ adder.out + adder.c, c^2 ~ adder.out + adder.value], From 1ee0844b894fe2c58b69c98e2bb35a029aea907f Mon Sep 17 00:00:00 2001 From: vyudu Date: Mon, 21 Apr 2025 13:58:49 -0400 Subject: [PATCH 59/59] update NewsMD --- NEWS.md | 19 +++++++++++++++++++ docs/src/basics/Events.md | 2 +- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/NEWS.md b/NEWS.md index 038b1d79f6..65fc6778e4 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,3 +1,22 @@ +# ModelingToolkit v10 Release Notes + +### Callbacks + +Callback semantics have changed. + - There is a new `Pre` operator that is used to specify which values are before the callback. + For example, the affect `A ~ A + 1` should now be written as `A ~ Pre(A) + 1`. This is + **required** to be specified - `A ~ A + 1` will now be interpreted as an equation to be + satisfied after the callback (and will thus error since it is unsatisfiable). + + - All parameters that are changed by a callback must be declared as discrete parameters to + the callback constructor, using the `discrete_parameters` keyword argument. + +```julia +event = SymbolicDiscreteCallback( + [t == 1] => [p ~ Pre(p) + 1], discrete_parameters = [p]) +``` + + # ModelingToolkit v9 Release Notes ### Upgrade guide diff --git a/docs/src/basics/Events.md b/docs/src/basics/Events.md index d473db7a89..4901910bfa 100644 --- a/docs/src/basics/Events.md +++ b/docs/src/basics/Events.md @@ -25,7 +25,7 @@ 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) +## Symbolic Callback Semantics 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