From 32613f3862477bc73841cca02960f737cb4a44fe Mon Sep 17 00:00:00 2001 From: Herman Sletmoen Date: Thu, 25 Apr 2024 17:10:46 +0200 Subject: [PATCH 0001/1337] Fix structural simplification of non-1-indexed variable arrays --- Project.toml | 1 + src/structural_transformation/symbolics_tearing.jl | 6 +++++- test/odesystem.jl | 10 ++++++++++ 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 6a7a0bcecf..09f984444b 100644 --- a/Project.toml +++ b/Project.toml @@ -32,6 +32,7 @@ Libdl = "8f399da3-3557-5675-b5ff-fb832c97cbdb" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" MLStyle = "d8e11817-5142-5d16-987a-aa16d5891078" NaNMath = "77ba4419-2d1f-58cd-9bb1-8ffee604a2e3" +OffsetArrays = "6fe1bfb0-de20-5000-8ca7-80f57d26f881" OrdinaryDiffEq = "1dea7af3-3e70-54e6-95c3-0bf5283fa5ed" PrecompileTools = "aea7be01-6a6a-4083-8856-8a6e6704d82a" RecursiveArrayTools = "731186ca-8d62-57ce-b412-fbd966d074cd" diff --git a/src/structural_transformation/symbolics_tearing.jl b/src/structural_transformation/symbolics_tearing.jl index f2f27e8606..0764e16c3e 100644 --- a/src/structural_transformation/symbolics_tearing.jl +++ b/src/structural_transformation/symbolics_tearing.jl @@ -1,3 +1,5 @@ +using OffsetArrays: Origin + # N.B. assumes `slist` and `dlist` are unique function substitution_graph(graph, slist, dlist, var_eq_matching) ns = length(slist) @@ -573,7 +575,9 @@ function tearing_reassemble(state::TearingState, var_eq_matching, Symbolics.shape(lhs) !== Symbolics.Unknown() || continue arg1 = arguments(lhs)[1] haskey(obs_arr_subs, arg1) && continue - obs_arr_subs[arg1] = [arg1[i] for i in eachindex(arg1)] + obs_arr_subs[arg1] = [arg1[i] for i in eachindex(arg1)] # e.g. p => [p[1], p[2]] + index_first = eachindex(arg1)[1] + obs_arr_subs[arg1] = Origin(index_first)(obs_arr_subs[arg1]) # respect non-1-indexed arrays end for i in eachindex(neweqs) neweqs[i] = fast_substitute(neweqs[i], obs_arr_subs; operator = Symbolics.Operator) diff --git a/test/odesystem.jl b/test/odesystem.jl index f11ab62a06..bca7f7f99a 100644 --- a/test/odesystem.jl +++ b/test/odesystem.jl @@ -1159,3 +1159,13 @@ for sys in [sys1, sys2] @test variable_index(sys, x[i]) == variable_index(sys, x)[i] end end + +@testset "Non-1-indexed variable array (issue #2670)" begin + @variables x(t)[0:1] # 0-indexed variable array + sys = ODESystem([ + x[0] ~ 0.0 + D(x[1]) ~ x[0] + ], t, [x], []; name=:sys) + @test_nowarn sys = structural_simplify(sys) + @test equations(sys) == [D(x[1]) ~ 0.0] +end \ No newline at end of file From c06abfe86ad6815694732f81e5b757b4fa58ed7e Mon Sep 17 00:00:00 2001 From: Herman Sletmoen Date: Thu, 25 Apr 2024 17:21:08 +0200 Subject: [PATCH 0002/1337] Format code --- src/structural_transformation/symbolics_tearing.jl | 2 +- test/odesystem.jl | 7 ++----- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/structural_transformation/symbolics_tearing.jl b/src/structural_transformation/symbolics_tearing.jl index 0764e16c3e..d1d86cac1a 100644 --- a/src/structural_transformation/symbolics_tearing.jl +++ b/src/structural_transformation/symbolics_tearing.jl @@ -576,7 +576,7 @@ function tearing_reassemble(state::TearingState, var_eq_matching, arg1 = arguments(lhs)[1] haskey(obs_arr_subs, arg1) && continue obs_arr_subs[arg1] = [arg1[i] for i in eachindex(arg1)] # e.g. p => [p[1], p[2]] - index_first = eachindex(arg1)[1] + index_first = eachindex(arg1)[1] obs_arr_subs[arg1] = Origin(index_first)(obs_arr_subs[arg1]) # respect non-1-indexed arrays end for i in eachindex(neweqs) diff --git a/test/odesystem.jl b/test/odesystem.jl index bca7f7f99a..cbef18473f 100644 --- a/test/odesystem.jl +++ b/test/odesystem.jl @@ -1162,10 +1162,7 @@ end @testset "Non-1-indexed variable array (issue #2670)" begin @variables x(t)[0:1] # 0-indexed variable array - sys = ODESystem([ - x[0] ~ 0.0 - D(x[1]) ~ x[0] - ], t, [x], []; name=:sys) + @named sys = ODESystem([x[0] ~ 0.0, D(x[1]) ~ x[0]], t, [x], []) @test_nowarn sys = structural_simplify(sys) @test equations(sys) == [D(x[1]) ~ 0.0] -end \ No newline at end of file +end From c8c2fc55b212ec51d379902d62bad1876dbc1031 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 11 Sep 2024 17:52:32 +0530 Subject: [PATCH 0003/1337] feat: handle case of indexed array variable in SII impl for IndexCache --- src/systems/index_cache.jl | 48 +++++++++++++++++++++----------------- 1 file changed, 26 insertions(+), 22 deletions(-) diff --git a/src/systems/index_cache.jl b/src/systems/index_cache.jl index 3f2b4ddebe..64e9c134f6 100644 --- a/src/systems/index_cache.jl +++ b/src/systems/index_cache.jl @@ -288,11 +288,7 @@ function IndexCache(sys::AbstractSystem) end function SymbolicIndexingInterface.is_variable(ic::IndexCache, sym) - if sym isa Symbol - sym = get(ic.symbol_to_variable, sym, nothing) - sym === nothing && return false - end - return check_index_map(ic.unknown_idx, sym) !== nothing + variable_index(ic, sym) !== nothing end function SymbolicIndexingInterface.variable_index(ic::IndexCache, sym) @@ -300,18 +296,17 @@ function SymbolicIndexingInterface.variable_index(ic::IndexCache, sym) sym = get(ic.symbol_to_variable, sym, nothing) sym === nothing && return nothing end - return check_index_map(ic.unknown_idx, sym) + idx = check_index_map(ic.unknown_idx, sym) + idx === nothing || return idx + iscall(sym) && operation(sym) == getindex || return nothing + args = arguments(sym) + idx = variable_index(ic, args[1]) + idx === nothing && return nothing + return idx[args[2:end]...] end function SymbolicIndexingInterface.is_parameter(ic::IndexCache, sym) - if sym isa Symbol - sym = get(ic.symbol_to_variable, sym, nothing) - sym === nothing && return false - end - return check_index_map(ic.tunable_idx, sym) !== nothing || - check_index_map(ic.discrete_idx, sym) !== nothing || - check_index_map(ic.constant_idx, sym) !== nothing || - check_index_map(ic.nonnumeric_idx, sym) !== nothing + parameter_index(ic, sym) !== nothing end function SymbolicIndexingInterface.parameter_index(ic::IndexCache, sym) @@ -331,17 +326,21 @@ function SymbolicIndexingInterface.parameter_index(ic::IndexCache, sym) ParameterIndex(SciMLStructures.Constants(), idx, validate_size) elseif (idx = check_index_map(ic.nonnumeric_idx, sym)) !== nothing ParameterIndex(NONNUMERIC_PORTION, idx, validate_size) - else - nothing + elseif iscall(sym) && operation(sym) == getindex + args = arguments(sym) + pidx = parameter_index(ic, args[1]) + pidx === nothing && return nothing + if pidx.portion == SciMLStructures.Tunable() + ParameterIndex(pidx.portion, reshape(pidx.idx, size(args[1]))[args[2:end]...], + pidx.validate_size) + else + ParameterIndex(pidx.portion, (pidx.idx..., args[2:end]...), pidx.validate_size) + end end end function SymbolicIndexingInterface.is_timeseries_parameter(ic::IndexCache, sym) - if sym isa Symbol - sym = get(ic.symbol_to_variable, sym, nothing) - sym === nothing && return false - end - return check_index_map(ic.discrete_idx, sym) !== nothing + timeseries_parameter_index(ic, sym) !== nothing end function SymbolicIndexingInterface.timeseries_parameter_index(ic::IndexCache, sym) @@ -350,8 +349,13 @@ function SymbolicIndexingInterface.timeseries_parameter_index(ic::IndexCache, sy sym === nothing && return nothing end idx = check_index_map(ic.discrete_idx, sym) + idx === nothing || + return ParameterTimeseriesIndex(idx.clock_idx, (idx.buffer_idx, idx.idx_in_clock)) + iscall(sym) && operation(sym) == getindex || return nothing + args = arguments(sym) + idx = timeseries_parameter_index(ic, args[1]) idx === nothing && return nothing - return ParameterTimeseriesIndex(idx.clock_idx, (idx.buffer_idx, idx.idx_in_clock)) + ParameterIndex(idx.portion, (idx.idx..., args[2:end]...), idx.validate_size) end function check_index_map(idxmap, sym) From 58ae68b67ad162b1d0d690c055191059a59e315d Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 11 Sep 2024 17:53:38 +0530 Subject: [PATCH 0004/1337] feat: make parameter type validation error more descriptive --- src/systems/parameter_buffer.jl | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/systems/parameter_buffer.jl b/src/systems/parameter_buffer.jl index 95ca13adad..c2705bab3f 100644 --- a/src/systems/parameter_buffer.jl +++ b/src/systems/parameter_buffer.jl @@ -336,14 +336,15 @@ function Base.copy(p::MTKParameters) end function SymbolicIndexingInterface.parameter_values(p::MTKParameters, pind::ParameterIndex) + _ducktyped_parameter_values(p, pind) +end +function _ducktyped_parameter_values(p, pind::ParameterIndex) @unpack portion, idx = pind if portion isa SciMLStructures.Tunable return idx isa Int ? p.tunable[idx] : view(p.tunable, idx) end i, j, k... = idx - if portion isa SciMLStructures.Tunable - return isempty(k) ? p.tunable[i][j] : p.tunable[i][j][k...] - elseif portion isa SciMLStructures.Discrete + if portion isa SciMLStructures.Discrete return isempty(k) ? p.discrete[i][j] : p.discrete[i][j][k...] elseif portion isa SciMLStructures.Constants return isempty(k) ? p.constant[i][j] : p.constant[i][j][k...] @@ -444,11 +445,13 @@ function validate_parameter_type(ic::IndexCache, stype, sz, sym, index, val) # Nonnumeric parameters have to match the type if portion === NONNUMERIC_PORTION val isa stype && return nothing - throw(ParameterTypeException(:validate_parameter_type, sym, stype, val)) + throw(ParameterTypeException( + :validate_parameter_type, sym === nothing ? index : sym, stype, val)) end # Array parameters need array values... if stype <: AbstractArray && !isa(val, AbstractArray) - throw(ParameterTypeException(:validate_parameter_type, sym, stype, val)) + throw(ParameterTypeException( + :validate_parameter_type, sym === nothing ? index : sym, stype, val)) end # ... and must match sizes if stype <: AbstractArray && sz != Symbolics.Unknown() && size(val) != sz @@ -465,7 +468,7 @@ function validate_parameter_type(ic::IndexCache, stype, sz, sym, index, val) # This is for duals and other complicated number types etype = SciMLBase.parameterless_type(etype) eltype(val) <: etype || throw(ParameterTypeException( - :validate_parameter_type, sym, AbstractArray{etype}, val)) + :validate_parameter_type, sym === nothing ? index : sym, AbstractArray{etype}, val)) else # Real check if stype <: Real @@ -473,7 +476,8 @@ function validate_parameter_type(ic::IndexCache, stype, sz, sym, index, val) end stype = SciMLBase.parameterless_type(stype) val isa stype || - throw(ParameterTypeException(:validate_parameter_type, sym, stype, val)) + throw(ParameterTypeException( + :validate_parameter_type, sym === nothing ? index : sym, stype, val)) end end From a246d5595d410fc672db486e3540563c786d56ff Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 11 Sep 2024 17:54:04 +0530 Subject: [PATCH 0005/1337] feat: add adjoint for `remake_buffer` --- ext/MTKChainRulesCoreExt.jl | 90 ++++++++++++++++++++++++++++++++- src/systems/parameter_buffer.jl | 9 +++- test/extensions/Project.toml | 2 + test/extensions/ad.jl | 46 +++++++++++++++++ 4 files changed, 145 insertions(+), 2 deletions(-) diff --git a/ext/MTKChainRulesCoreExt.jl b/ext/MTKChainRulesCoreExt.jl index e7019e25df..a3a7f10dad 100644 --- a/ext/MTKChainRulesCoreExt.jl +++ b/ext/MTKChainRulesCoreExt.jl @@ -2,14 +2,102 @@ module MTKChainRulesCoreExt import ModelingToolkit as MTK import ChainRulesCore -import ChainRulesCore: NoTangent +import ChainRulesCore: Tangent, ZeroTangent, NoTangent, zero_tangent, unthunk function ChainRulesCore.rrule(::Type{MTK.MTKParameters}, tunables, args...) function mtp_pullback(dt) + dt = unthunk(dt) (NoTangent(), dt.tunable[1:length(tunables)], ntuple(_ -> NoTangent(), length(args))...) end MTK.MTKParameters(tunables, args...), mtp_pullback end +notangent_or_else(::NoTangent, _, x) = x +notangent_or_else(_, x, _) = x +notangent_fallback(x, y) = notangent_or_else(x, x, y) +reduce_to_notangent(x, y) = notangent_or_else(x, y, x) + +function subset_idxs(idxs, portion, template) + ntuple(Val(length(template))) do subi + [Base.tail(idx.idx) for idx in idxs if idx.portion == portion && idx.idx[1] == subi] + end +end + +selected_tangents(::NoTangent, _) = () +selected_tangents(::ZeroTangent, _) = ZeroTangent() +function selected_tangents( + tangents::AbstractArray{T}, idxs::Vector{Tuple{Int}}) where {T <: Number} + selected_tangents(tangents, map(only, idxs)) +end +function selected_tangents(tangents::AbstractArray{T}, idxs...) where {T <: Number} + newtangents = copy(tangents) + view(newtangents, idxs...) .= zero(T) + newtangents +end +function selected_tangents( + tangents::AbstractVector{T}, idxs) where {S <: Number, T <: AbstractArray{S}} + newtangents = copy(tangents) + for i in idxs + j, k... = i + if k == () + newtangents[j] = zero(newtangents[j]) + else + newtangents[j] = selected_tangents(newtangents[j], k...) + end + end + newtangents +end +function selected_tangents(tangents::AbstractVector{T}, idxs) where {T <: AbstractArray} + newtangents = similar(tangents, Union{T, NoTangent}) + copyto!(newtangents, tangents) + for i in idxs + j, k... = i + if k == () + newtangents[j] = NoTangent() + else + newtangents[j] = selected_tangents(newtangents[j], k...) + end + end + newtangents +end +function selected_tangents( + tangents::Union{Tangent{<:Tuple}, Tangent{T, <:Tuple}}, idxs) where {T} + ntuple(Val(length(tangents))) do i + selected_tangents(tangents[i], idxs[i]) + end +end + +function ChainRulesCore.rrule( + ::typeof(MTK.remake_buffer), indp, oldbuf::MTK.MTKParameters, idxs, vals) + if idxs isa AbstractSet + idxs = collect(idxs) + end + idxs = map(idxs) do i + i isa MTK.ParameterIndex ? i : MTK.parameter_index(indp, i) + end + newbuf = MTK.remake_buffer(indp, oldbuf, idxs, vals) + tunable_idxs = reduce( + vcat, (idx.idx for idx in idxs if idx.portion isa MTK.SciMLStructures.Tunable)) + disc_idxs = subset_idxs(idxs, MTK.SciMLStructures.Discrete(), oldbuf.discrete) + const_idxs = subset_idxs(idxs, MTK.SciMLStructures.Constants(), oldbuf.constant) + nn_idxs = subset_idxs(idxs, MTK.NONNUMERIC_PORTION, oldbuf.nonnumeric) + + function remake_buffer_pullback(buf′) + buf′ = unthunk(buf′) + f′ = NoTangent() + indp′ = NoTangent() + + tunable = selected_tangents(buf′.tunable, tunable_idxs) + discrete = selected_tangents(buf′.discrete, disc_idxs) + constant = selected_tangents(buf′.constant, const_idxs) + nonnumeric = selected_tangents(buf′.nonnumeric, nn_idxs) + oldbuf′ = Tangent{typeof(oldbuf)}(; tunable, discrete, constant, nonnumeric) + idxs′ = NoTangent() + vals′ = map(i -> MTK._ducktyped_parameter_values(buf′, i), idxs) + return f′, indp′, oldbuf′, idxs′, vals′ + end + newbuf, remake_buffer_pullback +end + end diff --git a/src/systems/parameter_buffer.jl b/src/systems/parameter_buffer.jl index c2705bab3f..ad001b32dc 100644 --- a/src/systems/parameter_buffer.jl +++ b/src/systems/parameter_buffer.jl @@ -436,8 +436,12 @@ function validate_parameter_type(ic::IndexCache, p, idx::ParameterIndex, val) end function validate_parameter_type(ic::IndexCache, idx::ParameterIndex, val) + stype = get_buffer_template(ic, idx).type + if idx.portion == SciMLStructures.Tunable() && !(idx.idx isa Int) + stype = AbstractArray{<:stype} + end validate_parameter_type( - ic, get_buffer_template(ic, idx).type, Symbolics.Unknown(), nothing, idx, val) + ic, stype, Symbolics.Unknown(), nothing, idx, val) end function validate_parameter_type(ic::IndexCache, stype, sz, sym, index, val) @@ -489,6 +493,9 @@ function indp_to_system(indp) end function SymbolicIndexingInterface.remake_buffer(indp, oldbuf::MTKParameters, idxs, vals) + _remake_buffer(indp, oldbuf, idxs, vals) +end +function _remake_buffer(indp, oldbuf::MTKParameters, idxs, vals; validate = true) newbuf = @set oldbuf.tunable = similar(oldbuf.tunable, Any) @set! newbuf.discrete = Tuple(similar(buf, Any) for buf in newbuf.discrete) @set! newbuf.constant = Tuple(similar(buf, Any) for buf in newbuf.constant) diff --git a/test/extensions/Project.toml b/test/extensions/Project.toml index d8d3d64605..81097d98d2 100644 --- a/test/extensions/Project.toml +++ b/test/extensions/Project.toml @@ -1,5 +1,7 @@ [deps] BifurcationKit = "0f109fa4-8a5d-4b75-95aa-f515264e7665" +ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4" +ChainRulesTestUtils = "cdddcdb0-9152-4a09-a978-84456f9df70a" ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210" LabelledArrays = "2ee39098-c373-598a-b85f-a56591580800" ModelingToolkit = "961ee093-0014-501f-94e3-6117800e7a78" diff --git a/test/extensions/ad.jl b/test/extensions/ad.jl index 1946db5277..d0db435ecb 100644 --- a/test/extensions/ad.jl +++ b/test/extensions/ad.jl @@ -6,6 +6,9 @@ using SciMLStructures using OrdinaryDiffEq using SciMLSensitivity using ForwardDiff +using ChainRulesCore +using ChainRulesCore: NoTangent +using ChainRulesTestUtils: test_rrule @variables x(t)[1:3] y(t) @parameters p[1:3, 1:3] q @@ -51,3 +54,46 @@ end @test ForwardDiff.gradient(x_at_0, [0.3, 0.7]) == zeros(2) 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]]) +sys = complete(sys) + +ivs = Dict(c => 3a, b => ones(3), a => 1.0, d => 4, e => [5.0, 6.0, 7.0], + f => ones(Int, 3, 3), g => [0.1, 0.2, 0.3], h => "foo") + +ps = MTKParameters(sys, ivs) + +varmap = Dict(a => 1.0f0, b => 3ones(Float32, 3), c => 2.0, + e => Float32[0.4, 0.5, 0.6], g => ones(Float32, 4)) +get_values = getp(sys, [a, b..., c, e...]) +get_g = getp(sys, g) +for (_idxs, vals) in [ + # all portions + (collect(keys(varmap)), collect(values(varmap))), + # non-arrays + (keys(varmap), values(varmap)), + # tunable only + ([a], [varmap[a]]), + ([a, b], (varmap[a], varmap[b])), + ([a, b[2]], (varmap[a], varmap[b][2])) +] + for idxs in [_idxs, map(i -> parameter_index(sys, i), collect(_idxs))] + loss = function (p) + newps = remake_buffer(sys, ps, idxs, p) + return sum(get_values(newps)) + sum(get_g(newps)) + end + + grad = Zygote.gradient(loss, vals)[1] + for (val, g) in zip(vals, grad) + @test eltype(val) == eltype(g) + if val isa Number + @test isone(g) + else + @test all(isone, g) + end + end + end +end From 55e3452e9fb82ee4af681ed99e6e9dea65fc804b Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 16 Sep 2024 12:25:31 +0530 Subject: [PATCH 0006/1337] feat: improve type-stability of adjoint --- ext/MTKChainRulesCoreExt.jl | 33 +++++++++++++++------------------ test/extensions/ad.jl | 8 +++++++- 2 files changed, 22 insertions(+), 19 deletions(-) diff --git a/ext/MTKChainRulesCoreExt.jl b/ext/MTKChainRulesCoreExt.jl index a3a7f10dad..f84690e23f 100644 --- a/ext/MTKChainRulesCoreExt.jl +++ b/ext/MTKChainRulesCoreExt.jl @@ -13,11 +13,6 @@ function ChainRulesCore.rrule(::Type{MTK.MTKParameters}, tunables, args...) MTK.MTKParameters(tunables, args...), mtp_pullback end -notangent_or_else(::NoTangent, _, x) = x -notangent_or_else(_, x, _) = x -notangent_fallback(x, y) = notangent_or_else(x, x, y) -reduce_to_notangent(x, y) = notangent_or_else(x, y, x) - function subset_idxs(idxs, portion, template) ntuple(Val(length(template))) do subi [Base.tail(idx.idx) for idx in idxs if idx.portion == portion && idx.idx[1] == subi] @@ -83,21 +78,23 @@ function ChainRulesCore.rrule( const_idxs = subset_idxs(idxs, MTK.SciMLStructures.Constants(), oldbuf.constant) nn_idxs = subset_idxs(idxs, MTK.NONNUMERIC_PORTION, oldbuf.nonnumeric) - function remake_buffer_pullback(buf′) - buf′ = unthunk(buf′) - f′ = NoTangent() - indp′ = NoTangent() + pullback = let idxs = idxs + function remake_buffer_pullback(buf′) + buf′ = unthunk(buf′) + f′ = NoTangent() + indp′ = NoTangent() - tunable = selected_tangents(buf′.tunable, tunable_idxs) - discrete = selected_tangents(buf′.discrete, disc_idxs) - constant = selected_tangents(buf′.constant, const_idxs) - nonnumeric = selected_tangents(buf′.nonnumeric, nn_idxs) - oldbuf′ = Tangent{typeof(oldbuf)}(; tunable, discrete, constant, nonnumeric) - idxs′ = NoTangent() - vals′ = map(i -> MTK._ducktyped_parameter_values(buf′, i), idxs) - return f′, indp′, oldbuf′, idxs′, vals′ + tunable = selected_tangents(buf′.tunable, tunable_idxs) + discrete = selected_tangents(buf′.discrete, disc_idxs) + constant = selected_tangents(buf′.constant, const_idxs) + nonnumeric = selected_tangents(buf′.nonnumeric, nn_idxs) + oldbuf′ = Tangent{typeof(oldbuf)}(; tunable, discrete, constant, nonnumeric) + idxs′ = NoTangent() + vals′ = map(i -> MTK._ducktyped_parameter_values(buf′, i), idxs) + return f′, indp′, oldbuf′, idxs′, vals′ + end end - newbuf, remake_buffer_pullback + newbuf, pullback end end diff --git a/test/extensions/ad.jl b/test/extensions/ad.jl index d0db435ecb..954b868a1e 100644 --- a/test/extensions/ad.jl +++ b/test/extensions/ad.jl @@ -8,7 +8,7 @@ using SciMLSensitivity using ForwardDiff using ChainRulesCore using ChainRulesCore: NoTangent -using ChainRulesTestUtils: test_rrule +using ChainRulesTestUtils: test_rrule, rand_tangent @variables x(t)[1:3] y(t) @parameters p[1:3, 1:3] q @@ -97,3 +97,9 @@ for (_idxs, vals) in [ end end end + +idxs = (parameter_index(sys, a), parameter_index(sys, b)) +vals = (1.0f0, 3ones(Float32, 3)) +tangent = rand_tangent(ps) +fwd, back = ChainRulesCore.rrule(remake_buffer, sys, ps, idxs, vals) +@inferred back(tangent) From ea01bae44debf1eefec3fde5b77e9de0023e5d21 Mon Sep 17 00:00:00 2001 From: Herman Sletmoen Date: Mon, 16 Sep 2024 15:14:33 +0200 Subject: [PATCH 0007/1337] Merge system guesses before dummy derivative substitution --- src/systems/nonlinear/initializesystem.jl | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/systems/nonlinear/initializesystem.jl b/src/systems/nonlinear/initializesystem.jl index aa1ecd9a8b..aa7ad8bd69 100644 --- a/src/systems/nonlinear/initializesystem.jl +++ b/src/systems/nonlinear/initializesystem.jl @@ -29,7 +29,7 @@ function generate_initializesystem(sys::ODESystem; full_states = unique([sts; getfield.((observed(sys)), :lhs)]) set_full_states = Set(full_states) - guesses = todict(guesses) + guesses = merge(get_guesses(sys), todict(guesses)) schedule = getfield(sys, :schedule) if schedule !== nothing @@ -69,7 +69,6 @@ function generate_initializesystem(sys::ODESystem; end defs = merge(defaults(sys), filtered_u0) - guesses = merge(get_guesses(sys), todict(guesses), dd_guess) for st in full_states if st ∈ keys(defs) From a3bee38dce1f89db07d9d74600d8d0d671aab21c Mon Sep 17 00:00:00 2001 From: Herman Sletmoen Date: Mon, 16 Sep 2024 15:14:52 +0200 Subject: [PATCH 0008/1337] Test derivative guesses --- test/initializationsystem.jl | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/test/initializationsystem.jl b/test/initializationsystem.jl index 08c66e85e6..eda57c11b0 100644 --- a/test/initializationsystem.jl +++ b/test/initializationsystem.jl @@ -510,7 +510,7 @@ end end # https://github.com/SciML/ModelingToolkit.jl/issues/3029 -@testset "Derivatives in Initialization Equations" begin +@testset "Derivatives in initialization equations" begin @variables x(t) sys = ODESystem( [D(D(x)) ~ 0], t; initialization_eqs = [x ~ 0, D(x) ~ 1], name = :sys) |> @@ -523,6 +523,20 @@ end @test_nowarn ODEProblem(sys, [D(x) => 1.0], (0.0, 1.0), []) end +# https://github.com/SciML/ModelingToolkit.jl/issues/3049 +@testset "Derivatives in initialization guesses" begin + for sign in [-1.0, +1.0] + @variables x(t) + sys = ODESystem( + [D(D(x)) ~ 0], t; + initialization_eqs = [D(x)^2 ~ 1, x ~ 0], guesses = [D(x) => sign], name = :sys + ) |> structural_simplify + prob = ODEProblem(sys, [], (0.0, 1.0), []) + sol = solve(prob, Tsit5()) + @test sol(1.0, idxs=sys.x) ≈ sign # system with D(x(0)) = ±1 should solve to x(1) = ±1 + end +end + # https://github.com/SciML/ModelingToolkit.jl/issues/2619 @parameters k1 k2 ω @variables X(t) Y(t) From 556d09e5652ba0fcd88a9360ad1c1d92923d6b6e Mon Sep 17 00:00:00 2001 From: Herman Sletmoen Date: Mon, 16 Sep 2024 15:47:04 +0200 Subject: [PATCH 0009/1337] Format --- test/initializationsystem.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/initializationsystem.jl b/test/initializationsystem.jl index eda57c11b0..82d129b901 100644 --- a/test/initializationsystem.jl +++ b/test/initializationsystem.jl @@ -533,7 +533,7 @@ end ) |> structural_simplify prob = ODEProblem(sys, [], (0.0, 1.0), []) sol = solve(prob, Tsit5()) - @test sol(1.0, idxs=sys.x) ≈ sign # system with D(x(0)) = ±1 should solve to x(1) = ±1 + @test sol(1.0, idxs = sys.x) ≈ sign # system with D(x(0)) = ±1 should solve to x(1) = ±1 end end From 9aadc7172647fe1d1bb6941a6cacad50ebf7d7d9 Mon Sep 17 00:00:00 2001 From: Christopher Rackauckas Date: Mon, 16 Sep 2024 23:19:34 +0200 Subject: [PATCH 0010/1337] Update Project.toml --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 39f94e545b..6bec6f5a22 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "ModelingToolkit" uuid = "961ee093-0014-501f-94e3-6117800e7a78" authors = ["Yingbo Ma ", "Chris Rackauckas and contributors"] -version = "9.39.1" +version = "9.40.0" [deps] AbstractTrees = "1520ce14-60c1-5f80-bbc7-55ef81b5835c" From 9671deb046ac66e0b6b64d98ffbfe521630925fc Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 28 Aug 2024 13:21:48 +0530 Subject: [PATCH 0011/1337] feat: support callable parameters --- Project.toml | 6 ++++- src/ModelingToolkit.jl | 4 ++- src/parameters.jl | 10 ++++++++ src/systems/index_cache.jl | 43 +++++++++++++++++++++---------- src/systems/parameter_buffer.jl | 3 +++ src/systems/systemstructure.jl | 3 ++- src/utils.jl | 12 +++++++-- test/split_parameters.jl | 45 +++++++++++++++++++++++++++++++++ 8 files changed, 107 insertions(+), 19 deletions(-) diff --git a/Project.toml b/Project.toml index 6bec6f5a22..b73fa95e6f 100644 --- a/Project.toml +++ b/Project.toml @@ -24,6 +24,7 @@ ExprTools = "e2ba6199-217a-4e67-a87a-7c52f15ade04" Expronicon = "6b7a57c9-7cc1-4fdf-b7f5-e857abae3636" FindFirstFunctions = "64ca27bc-2ba2-4a57-88aa-44e436879224" ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210" +FunctionWrappers = "069b7b12-0de2-55c6-9aab-29f3d0a68a2e" FunctionWrappersWrappers = "77dc65aa-8811-40c2-897b-53d922fa7daf" Graphs = "86223c79-3864-5bf0-83f7-82e725a168b6" InteractiveUtils = "b77e0a4c-d291-57a0-90e8-8db25a27a240" @@ -76,6 +77,7 @@ ChainRulesCore = "1" Combinatorics = "1" Compat = "3.42, 4" ConstructionBase = "1" +DataInterpolations = "6.4" DataStructures = "0.17, 0.18" DeepDiffs = "1" DiffEqBase = "6.103.0" @@ -91,6 +93,7 @@ ExprTools = "0.1.10" Expronicon = "0.8" FindFirstFunctions = "1" ForwardDiff = "0.10.3" +FunctionWrappers = "1.1" FunctionWrappersWrappers = "0.1" Graphs = "1.5.2" InteractiveUtils = "1" @@ -129,6 +132,7 @@ julia = "1.9" AmplNLWriter = "7c4d4715-977e-5154-bfe0-e096adeac482" BenchmarkTools = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf" ControlSystemsBase = "aaaaaaaa-a6ca-5380-bf3e-84a91bcd477e" +DataInterpolations = "82cc6244-b520-54b8-b5a6-8a565e85f1d0" DeepDiffs = "ab62b9b5-e342-54a8-a765-a90f495de1a6" DelayDiffEq = "bcd4f6db-9728-5f36-b5f7-82caef46ccdb" ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210" @@ -154,4 +158,4 @@ Sundials = "c3572dad-4567-51f8-b174-8c6c989267f4" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [targets] -test = ["AmplNLWriter", "BenchmarkTools", "ControlSystemsBase", "DelayDiffEq", "NonlinearSolve", "ForwardDiff", "Ipopt", "Ipopt_jll", "ModelingToolkitStandardLibrary", "Optimization", "OptimizationOptimJL", "OptimizationMOI", "OrdinaryDiffEq", "Random", "ReferenceTests", "SafeTestsets", "StableRNGs", "Statistics", "SteadyStateDiffEq", "Test", "StochasticDiffEq", "Sundials", "StochasticDelayDiffEq", "Pkg", "JET"] +test = ["AmplNLWriter", "BenchmarkTools", "ControlSystemsBase", "DataInterpolations", "DelayDiffEq", "NonlinearSolve", "ForwardDiff", "Ipopt", "Ipopt_jll", "ModelingToolkitStandardLibrary", "Optimization", "OptimizationOptimJL", "OptimizationMOI", "OrdinaryDiffEq", "Random", "ReferenceTests", "SafeTestsets", "StableRNGs", "Statistics", "SteadyStateDiffEq", "Test", "StochasticDiffEq", "Sundials", "StochasticDelayDiffEq", "Pkg", "JET"] diff --git a/src/ModelingToolkit.jl b/src/ModelingToolkit.jl index 13b29831e5..9a4b025d07 100644 --- a/src/ModelingToolkit.jl +++ b/src/ModelingToolkit.jl @@ -38,6 +38,7 @@ using Base: RefValue using Combinatorics import Distributions import FunctionWrappersWrappers +import FunctionWrappers: FunctionWrapper using URIs: URI using SciMLStructures using Compat @@ -63,7 +64,8 @@ using Symbolics: _parse_vars, value, @derivatives, get_variables, VariableSource, getname, variable, Connection, connect, NAMESPACE_SEPARATOR, set_scalar_metadata, setdefaultval, initial_state, transition, activeState, entry, hasnode, - ticksInState, timeInState, fixpoint_sub, fast_substitute + ticksInState, timeInState, fixpoint_sub, fast_substitute, + CallWithMetadata, CallWithParent const NAMESPACE_SEPARATOR_SYMBOL = Symbol(NAMESPACE_SEPARATOR) import Symbolics: rename, get_variables!, _solve, hessian_sparsity, jacobian_sparsity, isaffine, islinear, _iszero, _isone, diff --git a/src/parameters.jl b/src/parameters.jl index fb6b0b4d6e..ced7767718 100644 --- a/src/parameters.jl +++ b/src/parameters.jl @@ -26,6 +26,16 @@ function isparameter(x) end end +function iscalledparameter(x) + x = unwrap(x) + return isparameter(getmetadata(x, CallWithParent, nothing)) +end + +function getcalledparameter(x) + x = unwrap(x) + return getmetadata(x, CallWithParent) +end + """ toparam(s) diff --git a/src/systems/index_cache.jl b/src/systems/index_cache.jl index 64e9c134f6..82a17d01e9 100644 --- a/src/systems/index_cache.jl +++ b/src/systems/index_cache.jl @@ -32,6 +32,8 @@ struct DiscreteIndex end const ParamIndexMap = Dict{BasicSymbolic, Tuple{Int, Int}} +const NonnumericMap = Dict{ + Union{BasicSymbolic, Symbolics.CallWithMetadata}, Tuple{Int, Int}} const UnknownIndexMap = Dict{ BasicSymbolic, Union{Int, UnitRange{Int}, AbstractArray{Int}}} const TunableIndexMap = Dict{BasicSymbolic, @@ -45,20 +47,20 @@ struct IndexCache callback_to_clocks::Dict{Any, Vector{Int}} tunable_idx::TunableIndexMap constant_idx::ParamIndexMap - nonnumeric_idx::ParamIndexMap + nonnumeric_idx::NonnumericMap observed_syms::Set{BasicSymbolic} dependent_pars::Set{BasicSymbolic} discrete_buffer_sizes::Vector{Vector{BufferTemplate}} tunable_buffer_size::BufferTemplate constant_buffer_sizes::Vector{BufferTemplate} nonnumeric_buffer_sizes::Vector{BufferTemplate} - symbol_to_variable::Dict{Symbol, BasicSymbolic} + symbol_to_variable::Dict{Symbol, Union{BasicSymbolic, CallWithMetadata}} end function IndexCache(sys::AbstractSystem) unks = solved_unknowns(sys) unk_idxs = UnknownIndexMap() - symbol_to_variable = Dict{Symbol, BasicSymbolic}() + symbol_to_variable = Dict{Symbol, Union{BasicSymbolic, CallWithMetadata}}() let idx = 1 for sym in unks @@ -105,12 +107,11 @@ function IndexCache(sys::AbstractSystem) tunable_buffers = Dict{Any, Set{BasicSymbolic}}() constant_buffers = Dict{Any, Set{BasicSymbolic}}() - nonnumeric_buffers = Dict{Any, Set{BasicSymbolic}}() + nonnumeric_buffers = Dict{Any, Set{Union{BasicSymbolic, CallWithMetadata}}}() - function insert_by_type!(buffers::Dict{Any, Set{BasicSymbolic}}, sym) + function insert_by_type!(buffers::Dict{Any, S}, sym, ctype) where {S} sym = unwrap(sym) - ctype = symtype(sym) - buf = get!(buffers, ctype, Set{BasicSymbolic}()) + buf = get!(buffers, ctype, S()) push!(buf, sym) end @@ -142,7 +143,7 @@ function IndexCache(sys::AbstractSystem) clocks = get!(() -> Set{Int}(), disc_param_callbacks, sym) push!(clocks, i) else - insert_by_type!(constant_buffers, sym) + insert_by_type!(constant_buffers, sym, symtype(sym)) end end end @@ -197,6 +198,9 @@ function IndexCache(sys::AbstractSystem) for p in parameters(sys) p = unwrap(p) ctype = symtype(p) + if ctype <: FnType + ctype = fntype_to_function_type(ctype) + end haskey(disc_idxs, p) && continue haskey(constant_buffers, ctype) && p in constant_buffers[ctype] && continue insert_by_type!( @@ -212,12 +216,13 @@ function IndexCache(sys::AbstractSystem) else nonnumeric_buffers end, - p + p, + ctype ) end - function get_buffer_sizes_and_idxs(buffers::Dict{Any, Set{BasicSymbolic}}) - idxs = ParamIndexMap() + function get_buffer_sizes_and_idxs(T, buffers::Dict) + idxs = T() buffer_sizes = BufferTemplate[] for (i, (T, buf)) in enumerate(buffers) for (j, p) in enumerate(buf) @@ -229,13 +234,18 @@ function IndexCache(sys::AbstractSystem) idxs[rp] = (i, j) idxs[rttp] = (i, j) end + if T <: Symbolics.FnType + T = Any + end push!(buffer_sizes, BufferTemplate(T, length(buf))) end return idxs, buffer_sizes end - const_idxs, const_buffer_sizes = get_buffer_sizes_and_idxs(constant_buffers) - nonnumeric_idxs, nonnumeric_buffer_sizes = get_buffer_sizes_and_idxs(nonnumeric_buffers) + const_idxs, const_buffer_sizes = get_buffer_sizes_and_idxs( + ParamIndexMap, constant_buffers) + nonnumeric_idxs, nonnumeric_buffer_sizes = get_buffer_sizes_and_idxs( + NonnumericMap, nonnumeric_buffers) tunable_idxs = TunableIndexMap() tunable_buffer_size = 0 @@ -401,7 +411,8 @@ function reorder_parameters(ic::IndexCache, ps; drop_missing = false) for temp in ic.discrete_buffer_sizes) const_buf = Tuple(BasicSymbolic[unwrap(variable(:DEF)) for _ in 1:(temp.length)] for temp in ic.constant_buffer_sizes) - nonnumeric_buf = Tuple(BasicSymbolic[unwrap(variable(:DEF)) for _ in 1:(temp.length)] + nonnumeric_buf = Tuple(Union{BasicSymbolic, CallWithMetadata}[unwrap(variable(:DEF)) + for _ in 1:(temp.length)] for temp in ic.nonnumeric_buffer_sizes) for p in ps p = unwrap(p) @@ -481,3 +492,7 @@ function get_buffer_template(ic::IndexCache, pidx::ParameterIndex) error("Unhandled portion $portion") end end + +fntype_to_function_type(::Type{FnType{A, R, T}}) where {A, R, T} = T +fntype_to_function_type(::Type{FnType{A, R, Nothing}}) where {A, R} = FunctionWrapper{R, A} +fntype_to_function_type(::Type{FnType{A, R}}) where {A, R} = FunctionWrapper{R, A} diff --git a/src/systems/parameter_buffer.jl b/src/systems/parameter_buffer.jl index ad001b32dc..20ba3d731a 100644 --- a/src/systems/parameter_buffer.jl +++ b/src/systems/parameter_buffer.jl @@ -152,6 +152,9 @@ function MTKParameters( if symbolic_type(val) !== NotSymbolic() error("Could not evaluate value of parameter $sym. Missing values for variables in expression $val.") end + if ctype <: FnType + ctype = fntype_to_function_type(ctype) + end val = symconvert(ctype, val) done = set_value(sym, val) if !done && Symbolics.isarraysymbolic(sym) diff --git a/src/systems/systemstructure.jl b/src/systems/systemstructure.jl index 1b0b58d09a..71bdf6fe30 100644 --- a/src/systems/systemstructure.jl +++ b/src/systems/systemstructure.jl @@ -661,7 +661,8 @@ function structural_simplify!(state::TearingState, io = nothing; simplify = fals @set! sys.defaults = merge(ModelingToolkit.defaults(sys), Dict(v => 0.0 for v in Iterators.flatten(inputs))) end - ps = [setmetadata(sym, VariableTimeDomain, get(time_domains, sym, Continuous)) + ps = [sym isa CallWithMetadata ? sym : + setmetadata(sym, VariableTimeDomain, get(time_domains, sym, Continuous)) for sym in get_ps(sys)] @set! sys.ps = ps else diff --git a/src/utils.jl b/src/utils.jl index 095da9e247..4426c8a799 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -371,7 +371,11 @@ end vars(exprs::Num; op = Differential) = vars(unwrap(exprs); op) vars(exprs::Symbolics.Arr; op = Differential) = vars(unwrap(exprs); op) function vars(exprs; op = Differential) - foldl((x, y) -> vars!(x, unwrap(y); op = op), exprs; init = Set()) + if hasmethod(iterate, Tuple{typeof(exprs)}) + foldl((x, y) -> vars!(x, unwrap(y); op = op), exprs; init = Set()) + else + vars!(Set(), unwrap(exprs); op) + end end vars(eq::Equation; op = Differential) = vars!(Set(), eq; op = op) function vars!(vars, eq::Equation; op = Differential) @@ -479,7 +483,11 @@ end function collect_var!(unknowns, parameters, var, iv) isequal(var, iv) && return nothing - if isparameter(var) || (iscall(var) && isparameter(operation(var))) + if iscalledparameter(var) + callable = getcalledparameter(var) + push!(parameters, callable) + collect_vars!(unknowns, parameters, arguments(var), iv) + elseif isparameter(var) || (iscall(var) && isparameter(operation(var))) push!(parameters, var) elseif !isconstant(var) push!(unknowns, var) diff --git a/test/split_parameters.jl b/test/split_parameters.jl index acd98972db..9f124ef45d 100644 --- a/test/split_parameters.jl +++ b/test/split_parameters.jl @@ -1,11 +1,13 @@ using ModelingToolkit, Test using ModelingToolkitStandardLibrary.Blocks using OrdinaryDiffEq +using DataInterpolations using BlockArrays: BlockedArray using ModelingToolkit: t_nounits as t, D_nounits as D using ModelingToolkit: MTKParameters, ParameterIndex, NONNUMERIC_PORTION using SciMLStructures: Tunable, Discrete, Constants using StaticArrays: SizedVector +using SymbolicIndexingInterface: is_parameter, getp x = [1, 2.0, false, [1, 2, 3], Parameter(1.0)] @@ -219,3 +221,46 @@ S = get_sensitivity(closed_loop, :u) @test ps[ParameterIndex(Tunable(), 1:8)] == collect(1.0:8.0) .+ 0.5 @test ps[ParameterIndex(Discrete(), (2, 1, 2, 2))] == 5 end + +@testset "Callable parameters" begin + @testset "As FunctionWrapper" begin + _f1(x) = 2x + struct Foo end + (::Foo)(x) = 3x + @variables x(t) + @parameters fn(::Real) = _f1 + @mtkbuild sys = ODESystem(D(x) ~ fn(t), t) + @test is_parameter(sys, fn) + @test ModelingToolkit.defaults(sys)[fn] == _f1 + + getter = getp(sys, fn) + prob = ODEProblem(sys, [x => 1.0], (0.0, 1.0)) + @inferred getter(prob) + # cannot be inferred better since `FunctionWrapper` is only known to return `Real` + @inferred Vector{<:Real} prob.f(prob.u0, prob.p, prob.tspan[1]) + sol = solve(prob, Tsit5(); abstol = 1e-10, reltol = 1e-10) + @test sol.u[end][] ≈ 2.0 + + prob = ODEProblem(sys, [x => 1.0], (0.0, 1.0), [fn => Foo()]) + @inferred getter(prob) + @inferred Vector{<:Real} prob.f(prob.u0, prob.p, prob.tspan[1]) + sol = solve(prob; abstol = 1e-10, reltol = 1e-10) + @test sol.u[end][] ≈ 2.5 + end + + @testset "Concrete function type" begin + ts = 0.0:0.1:1.0 + interp = LinearInterpolation(ts .^ 2, ts; extrapolate = true) + @variables x(t) + @parameters (fn::typeof(interp))(..) + @mtkbuild sys = ODESystem(D(x) ~ fn(x), t) + @test is_parameter(sys, fn) + getter = getp(sys, fn) + prob = ODEProblem(sys, [x => 1.0], (0.0, 1.0), [fn => interp]) + @inferred getter(prob) + @inferred prob.f(prob.u0, prob.p, prob.tspan[1]) + @test_nowarn sol = solve(prob, Tsit5()) + @test_nowarn prob.ps[fn] = LinearInterpolation(ts .^ 3, ts; extrapolate = true) + @test_nowarn sol = solve(prob) + end +end From 2bed81c5bd4cece557241b099e94f32ed9f9acf8 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 19 Sep 2024 10:37:05 +0530 Subject: [PATCH 0012/1337] build: bump Symbolics, SymbolicUtils compat --- Project.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Project.toml b/Project.toml index b73fa95e6f..248ed16e6d 100644 --- a/Project.toml +++ b/Project.toml @@ -121,8 +121,8 @@ SparseArrays = "1" SpecialFunctions = "0.7, 0.8, 0.9, 0.10, 1.0, 2" StaticArrays = "0.10, 0.11, 0.12, 1.0" SymbolicIndexingInterface = "0.3.29" -SymbolicUtils = "3.2" -Symbolics = "6.3" +SymbolicUtils = "3.7" +Symbolics = "6.12" URIs = "1" UnPack = "0.1, 1.0" Unitful = "1.1" From 7579312a9b505df442ecce146c2a169a725d7931 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 19 Sep 2024 16:07:48 +0530 Subject: [PATCH 0013/1337] test: fix odesystem callable parameter tests --- test/odesystem.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/odesystem.jl b/test/odesystem.jl index e39649c1c2..9191ffc785 100644 --- a/test/odesystem.jl +++ b/test/odesystem.jl @@ -137,7 +137,7 @@ eqs = [D(x) ~ σ(t - 1) * (y - x), D(y) ~ x * (ρ - z) - y, D(z) ~ x * y - β * z * κ] @named de = ODESystem(eqs, t) -test_diffeq_inference("single internal iv-varying", de, t, (x, y, z), (σ(t - 1), ρ, β)) +test_diffeq_inference("single internal iv-varying", de, t, (x, y, z), (σ, ρ, β)) f = eval(generate_function(de, [x, y, z], [σ, ρ, β])[2]) du = [0.0, 0.0, 0.0] f(du, [1.0, 2.0, 3.0], [x -> x + 7, 2, 3], 5.0) @@ -145,7 +145,7 @@ f(du, [1.0, 2.0, 3.0], [x -> x + 7, 2, 3], 5.0) eqs = [D(x) ~ x + 10σ(t - 1) + 100σ(t - 2) + 1000σ(t^2)] @named de = ODESystem(eqs, t) -test_diffeq_inference("many internal iv-varying", de, t, (x,), (σ(t - 2), σ(t^2), σ(t - 1))) +test_diffeq_inference("many internal iv-varying", de, t, (x,), (σ,)) f = eval(generate_function(de, [x], [σ])[2]) du = [0.0] f(du, [1.0], [t -> t + 2], 5.0) From e85503548a8fda03a69d75591f518efd6bba02da Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 19 Sep 2024 15:57:26 +0530 Subject: [PATCH 0014/1337] fix: handle `nothing` passed as `u0` to `ODEProblem` fix: handle non-vector non-symbolic array u0 for ODEProblem --- src/systems/diffeqs/abstractodesystem.jl | 5 +++-- test/odesystem.jl | 7 +++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index d8085d7e33..f7f94504e4 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -817,11 +817,12 @@ function process_DEProblem(constructor, sys::AbstractODESystem, u0map, parammap; ModelingToolkit.get_tearing_state(sys) !== nothing) || !isempty(initialization_equations(sys))) && t !== nothing if eltype(u0map) <: Number - u0map = unknowns(sys) .=> u0map + u0map = unknowns(sys) .=> vec(u0map) end - if isempty(u0map) + if u0map === nothing || isempty(u0map) u0map = Dict() end + initializeprob = ModelingToolkit.InitializationProblem( sys, t, u0map, parammap; guesses, warn_initialize_determined, initialization_eqs, eval_expression, eval_module, fully_determined, check_units) diff --git a/test/odesystem.jl b/test/odesystem.jl index e39649c1c2..c229eebe28 100644 --- a/test/odesystem.jl +++ b/test/odesystem.jl @@ -1387,3 +1387,10 @@ end @test obsfn(ones(2), 2ones(2), 3ones(4), 4.0) == 6ones(2) end + +@testset "Passing `nothing` to `u0`" begin + @variables x(t) = 1 + @mtkbuild sys = ODEProblem(D(x) ~ t, t) + prob = @test_nowarn ODEProblem(sys, nothing, (0.0, 1.0)) + @test_nowarn solve(prob) +end From bbc8bf788d2d069faadb752a0e3e1ebda680fff0 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 19 Sep 2024 17:41:59 +0530 Subject: [PATCH 0015/1337] fix: handle non-symbolic and `nothing` u0 for `DiscreteProblem` --- src/systems/discrete_system/discrete_system.jl | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/systems/discrete_system/discrete_system.jl b/src/systems/discrete_system/discrete_system.jl index 4a2e9bca97..7103cfca80 100644 --- a/src/systems/discrete_system/discrete_system.jl +++ b/src/systems/discrete_system/discrete_system.jl @@ -247,6 +247,13 @@ function process_DiscreteProblem(constructor, sys::DiscreteSystem, u0map, paramm dvs = unknowns(sys) ps = parameters(sys) + if eltype(u0map) <: Number + u0map = unknowns(sys) .=> vec(u0map) + end + if u0map === nothing || isempty(u0map) + u0map = Dict() + end + trueu0map = Dict() for (k, v) in u0map k = unwrap(k) From f84e5715a366f05c7a096c99da3eb9009292ad17 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 19 Sep 2024 17:42:12 +0530 Subject: [PATCH 0016/1337] test: test passing `nothing` to u0 for all system types --- test/discrete_system.jl | 8 ++++++++ test/nonlinearsystem.jl | 7 +++++++ test/odesystem.jl | 2 +- test/optimizationsystem.jl | 7 +++++++ test/sdesystem.jl | 8 ++++++++ 5 files changed, 31 insertions(+), 1 deletion(-) diff --git a/test/discrete_system.jl b/test/discrete_system.jl index f8ed0a911f..7d8c0d9678 100644 --- a/test/discrete_system.jl +++ b/test/discrete_system.jl @@ -271,3 +271,11 @@ end k = ShiftIndex(t) @named sys = DiscreteSystem([x ~ x^2 + y^2, y ~ x(k - 1) + y(k - 1)], t) @test_throws ["algebraic equations", "not yet supported"] structural_simplify(sys) + +@testset "Passing `nothing` to `u0`" begin + @variables x(t) = 1 + k = ShiftIndex() + @mtkbuild sys = DiscreteSystem([x(k) ~ x(k - 1) + 1], t) + prob = @test_nowarn DiscreteProblem(sys, nothing, (0.0, 1.0)) + @test_nowarn solve(prob, FunctionMap()) +end diff --git a/test/nonlinearsystem.jl b/test/nonlinearsystem.jl index 841a026d0f..a71d34a880 100644 --- a/test/nonlinearsystem.jl +++ b/test/nonlinearsystem.jl @@ -318,3 +318,10 @@ sys = structural_simplify(ns; conservative = true) sol = solve(prob, NewtonRaphson()) @test sol[x] ≈ sol[y] ≈ sol[z] ≈ -3 end + +@testset "Passing `nothing` to `u0`" begin + @variables x = 1 + @mtkbuild sys = NonlinearSystem([0 ~ x^2 - x^3 + 3]) + prob = @test_nowarn NonlinearProblem(sys, nothing) + @test_nowarn solve(prob) +end diff --git a/test/odesystem.jl b/test/odesystem.jl index c229eebe28..0837573baa 100644 --- a/test/odesystem.jl +++ b/test/odesystem.jl @@ -1390,7 +1390,7 @@ end @testset "Passing `nothing` to `u0`" begin @variables x(t) = 1 - @mtkbuild sys = ODEProblem(D(x) ~ t, t) + @mtkbuild sys = ODESystem(D(x) ~ t, t) prob = @test_nowarn ODEProblem(sys, nothing, (0.0, 1.0)) @test_nowarn solve(prob) end diff --git a/test/optimizationsystem.jl b/test/optimizationsystem.jl index f182e3ef87..426d6d5de0 100644 --- a/test/optimizationsystem.jl +++ b/test/optimizationsystem.jl @@ -340,3 +340,10 @@ end prob.f.cons_h(H3, [1.0, 1.0], [1.0, 100.0]) @test prob.f.cons_h([1.0, 1.0], [1.0, 100.0]) == H3 end + +@testset "Passing `nothing` to `u0`" begin + @variables x = 1.0 + @mtkbuild sys = OptimizationSystem((x - 3)^2, [x], []) + prob = @test_nowarn OptimizationProblem(sys, nothing) + @test_nowarn solve(prob, NelderMead()) +end diff --git a/test/sdesystem.jl b/test/sdesystem.jl index 5628772041..cae9ec9376 100644 --- a/test/sdesystem.jl +++ b/test/sdesystem.jl @@ -776,3 +776,11 @@ end prob = SDEProblem(de, u0map, (0.0, 100.0), parammap) @test solve(prob, SOSRI()).retcode == ReturnCode.Success end + +@testset "Passing `nothing` to `u0`" begin + @variables x(t) = 1 + @brownian b + @mtkbuild sys = System([D(x) ~ x + b], t) + prob = @test_nowarn SDEProblem(sys, nothing, (0.0, 1.0)) + @test_nowarn solve(prob, ImplicitEM()) +end From a25b43a25dbd0507df3d72c9580998825d6fdb34 Mon Sep 17 00:00:00 2001 From: CompatHelper Julia Date: Sat, 21 Sep 2024 00:22:18 +0000 Subject: [PATCH 0017/1337] CompatHelper: bump compat for OptimizationOptimJL to 0.4 for package docs, (keep existing compat) --- docs/Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Project.toml b/docs/Project.toml index 078df7d696..d2523f08ba 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -31,7 +31,7 @@ ModelingToolkit = "8.33, 9" NonlinearSolve = "3" Optim = "1.7" Optimization = "3.9" -OptimizationOptimJL = "0.1" +OptimizationOptimJL = "0.1, 0.4" OrdinaryDiffEq = "6.31" Plots = "1.36" SciMLStructures = "1.1" From d19de0f65839c36fe084bf40dd18a52b27f186af Mon Sep 17 00:00:00 2001 From: CompatHelper Julia Date: Sat, 21 Sep 2024 00:22:22 +0000 Subject: [PATCH 0018/1337] CompatHelper: bump compat for Optimization to 4 for package docs, (keep existing compat) --- docs/Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Project.toml b/docs/Project.toml index 078df7d696..4029f6f293 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -30,7 +30,7 @@ DynamicQuantities = "^0.11.2, 0.12, 1" ModelingToolkit = "8.33, 9" NonlinearSolve = "3" Optim = "1.7" -Optimization = "3.9" +Optimization = "3.9, 4" OptimizationOptimJL = "0.1" OrdinaryDiffEq = "6.31" Plots = "1.36" From 52a1ebfd71e3ab8bded0c69f647502e4ee7b0446 Mon Sep 17 00:00:00 2001 From: Christopher Rackauckas Date: Sat, 21 Sep 2024 14:40:53 +0200 Subject: [PATCH 0019/1337] Update Project.toml --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 248ed16e6d..85b970f181 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "ModelingToolkit" uuid = "961ee093-0014-501f-94e3-6117800e7a78" authors = ["Yingbo Ma ", "Chris Rackauckas and contributors"] -version = "9.40.0" +version = "9.40.1" [deps] AbstractTrees = "1520ce14-60c1-5f80-bbc7-55ef81b5835c" From 00c56f218d4d7f06fc0c4e23be154fc48eed188d Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 23 Sep 2024 13:06:48 +0530 Subject: [PATCH 0020/1337] feat: reorder parameters in `complete(sys)` according to portions --- src/systems/abstractsystem.jl | 38 +++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index 9aa4bfc92d..e8ec8cc06f 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -882,6 +882,44 @@ namespace its subsystems or variables, i.e. `isequal(complete(sys).v.i, v.i)`. function complete(sys::AbstractSystem; split = true) if split && has_index_cache(sys) @set! sys.index_cache = IndexCache(sys) + all_ps = parameters(sys) + if !isempty(all_ps) + # reorder parameters by portions + ps_split = reorder_parameters(sys, all_ps) + # if there are no tunables, vcat them + if isempty(get_index_cache(sys).tunable_idx) + ordered_ps = reduce(vcat, ps_split) + else + # if there are tunables, they will all be in `ps_split[1]` + # and the arrays will have been scalarized + ordered_ps = eltype(all_ps)[] + i = 1 + # go through all the tunables + while i <= length(ps_split[1]) + sym = ps_split[1][i] + # if the sym is not a scalarized array symbolic OR it was already scalarized, + # just push it as-is + if !iscall(sym) || operation(sym) != getindex || + any(isequal(sym), all_ps) + push!(ordered_ps, sym) + i += 1 + continue + end + # the next `length(sym)` symbols should be scalarized versions of the same + # array symbolic + if !allequal(first(arguments(x)) + for x in view(ps_split[1], i:(i + length(sym) - 1))) + error("This should not be possible. Please open an issue in ModelingToolkit.jl with an MWE and stacktrace.") + end + arrsym = first(arguments(sym)) + push!(ordered_ps, arrsym) + i += length(arrsym) + end + ordered_ps = vcat( + ordered_ps, reduce(vcat, ps_split[2:end]; init = eltype(ordered_ps)[])) + end + @set! sys.ps = ordered_ps + end end if isdefined(sys, :initializesystem) && get_initializesystem(sys) !== nothing @set! sys.initializesystem = complete(get_initializesystem(sys); split) From 8f6828c3fcf22a493050b8b6358bb35d3e88003f Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 23 Sep 2024 13:07:56 +0530 Subject: [PATCH 0021/1337] test: ensure `tunable_parameters` are now correctly ordered --- src/variables.jl | 7 ++++++- test/index_cache.jl | 22 +++++++++++++++++++++- test/runtests.jl | 2 +- 3 files changed, 28 insertions(+), 3 deletions(-) diff --git a/src/variables.jl b/src/variables.jl index 767dec686b..4e5f860513 100644 --- a/src/variables.jl +++ b/src/variables.jl @@ -381,7 +381,12 @@ Create a tunable parameter by @parameters u [tunable=true] ``` -See also [`getbounds`](@ref), [`istunable`](@ref) +For systems created with `split = true` (the default) and `default = true` passed to this function, the order +of parameters returned is the order in which they are stored in the tunables portion of `MTKParameters`. Note +that array variables will not be scalarized. To obtain the flattened representation of the tunables portion, +call `Symbolics.scalarize(tunable_parameters(sys))` and concatenate the resulting arrays. + +See also [`getbounds`](@ref), [`istunable`](@ref), [`MTKParameters`](@ref), [`complete`](@ref) """ function tunable_parameters(sys, p = parameters(sys); default = true) filter(x -> istunable(x, default), p) diff --git a/test/index_cache.jl b/test/index_cache.jl index 3bee1db381..d1301c260b 100644 --- a/test/index_cache.jl +++ b/test/index_cache.jl @@ -1,4 +1,4 @@ -using ModelingToolkit, SymbolicIndexingInterface +using ModelingToolkit, SymbolicIndexingInterface, SciMLStructures using ModelingToolkit: t_nounits as t # Ensure indexes of array symbolics are cached appropriately @@ -43,3 +43,23 @@ ic = ModelingToolkit.get_index_cache(sys) @test isequal(ic.symbol_to_variable[:x], x) @test isequal(ic.symbol_to_variable[:y], y) @test isequal(ic.symbol_to_variable[:z], z) + +@testset "tunable_parameters is ordered" begin + @parameters p q[1:3] r[1:2, 1:2] s [tunable = false] + @named sys = ODESystem(Equation[], t, [], [p, q, r, s]) + sys = complete(sys) + @test all(splat(isequal), zip(tunable_parameters(sys), parameters(sys)[1:3])) + + offset = 1 + for par in tunable_parameters(sys) + idx = parameter_index(sys, par) + @test idx.portion isa SciMLStructures.Tunable + if Symbolics.isarraysymbolic(par) + @test vec(idx.idx) == offset:(offset + length(par) - 1) + else + @test idx.idx == offset + end + offset += length(par) + end +end + diff --git a/test/runtests.jl b/test/runtests.jl index e45e2dfe42..0a9cf3c4db 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -24,7 +24,6 @@ end @safetestset "Parsing Test" include("variable_parsing.jl") @safetestset "Simplify Test" include("simplify.jl") @safetestset "Direct Usage Test" include("direct.jl") - @safetestset "IndexCache Test" include("index_cache.jl") @safetestset "System Linearity Test" include("linearity.jl") @safetestset "Input Output Test" include("input_output_handling.jl") @safetestset "Clock Test" include("clock.jl") @@ -65,6 +64,7 @@ end if GROUP == "All" || GROUP == "InterfaceII" @testset "InterfaceII" begin + @safetestset "IndexCache Test" include("index_cache.jl") @safetestset "Variable Utils Test" include("variable_utils.jl") @safetestset "Variable Metadata Test" include("test_variable_metadata.jl") @safetestset "OptimizationSystem Test" include("optimizationsystem.jl") From f1b0036ca4d25031e6756e301264318dd13c8d05 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 23 Sep 2024 13:08:11 +0530 Subject: [PATCH 0022/1337] feat: add `reorder_dimension_by_tunables!` --- src/ModelingToolkit.jl | 2 +- src/systems/index_cache.jl | 52 ++++++++++++++++++++++++++++++++++++++ test/index_cache.jl | 29 +++++++++++++++++++++ 3 files changed, 82 insertions(+), 1 deletion(-) diff --git a/src/ModelingToolkit.jl b/src/ModelingToolkit.jl index 9a4b025d07..2237ba8952 100644 --- a/src/ModelingToolkit.jl +++ b/src/ModelingToolkit.jl @@ -278,6 +278,6 @@ export debug_system export Sample, Hold, Shift, ShiftIndex, sampletime, SampleTime export Clock, SolverStepClock, TimeDomain -export MTKParameters +export MTKParameters, reorder_dimension_by_tunables!, reorder_dimension_by_tunables end # module diff --git a/src/systems/index_cache.jl b/src/systems/index_cache.jl index 82a17d01e9..ce74fd6a88 100644 --- a/src/systems/index_cache.jl +++ b/src/systems/index_cache.jl @@ -496,3 +496,55 @@ end fntype_to_function_type(::Type{FnType{A, R, T}}) where {A, R, T} = T fntype_to_function_type(::Type{FnType{A, R, Nothing}}) where {A, R} = FunctionWrapper{R, A} fntype_to_function_type(::Type{FnType{A, R}}) where {A, R} = FunctionWrapper{R, A} + +""" + reorder_dimension_by_tunables!(dest::AbstractArray, sys::AbstractSystem, arr::AbstractArray, syms; dim = 1) + +Assuming the order of values in dimension `dim` of `arr` correspond to the order of tunable +parameters in the system, reorder them according to the order described in `syms`. `syms` must +be a permutation of `tunable_parameters(sys)`. The result is written to `dest`. The `size` of `dest` and +`arr` must be equal. Return `dest`. + +See also: [`MTKParameters`](@ref), [`tunable_parameters`](@ref), [`reorder_dimension_by_tunables`](@ref). +""" +function reorder_dimension_by_tunables!( + dest::AbstractArray, sys::AbstractSystem, arr::AbstractArray, syms; dim = 1) + if !iscomplete(sys) + throw(ArgumentError("A completed system is required. Call `complete` or `structural_simplify` on the system.")) + end + if !has_index_cache(sys) || (ic = get_index_cache(sys)) === nothing + throw(ArgumentError("The system does not have an index cache. Call `complete` or `structural_simplify` on the system with `split = true`.")) + end + if size(dest) != size(arr) + throw(ArgumentError("Source and destination arrays must have the same size. Got source array with size $(size(arr)) and destination with size $(size(dest)).")) + end + + dsti = 1 + for sym in syms + idx = parameter_index(ic, sym) + if !(idx.portion isa SciMLStructures.Tunable) + throw(ArgumentError("`syms` must be a permutation of `tunable_parameters(sys)`. Found $sym which is not a tunable parameter.")) + end + + dstidx = ntuple( + i -> i == dim ? (dsti:(dsti + length(sym) - 1)) : (:), Val(ndims(arr))) + destv = @view dest[dstidx...] + dsti += length(sym) + arridx = ntuple(i -> i == dim ? (idx.idx) : (:), Val(ndims(arr))) + srcv = @view arr[arridx...] + copyto!(destv, srcv) + end + return dest +end + +""" + reorder_dimension_by_tunables(sys::AbstractSystem, arr::AbstractArray, syms; dim = 1) + +Out-of-place version of [`reorder_dimension_by_tunables!`](@ref). +""" +function reorder_dimension_by_tunables( + sys::AbstractSystem, arr::AbstractArray, syms; dim = 1) + buffer = similar(arr) + reorder_dimension_by_tunables!(buffer, sys, arr, syms; dim) + return buffer +end diff --git a/test/index_cache.jl b/test/index_cache.jl index d1301c260b..c479075797 100644 --- a/test/index_cache.jl +++ b/test/index_cache.jl @@ -63,3 +63,32 @@ ic = ModelingToolkit.get_index_cache(sys) end end +@testset "reorder_dimension_by_tunables" begin + @parameters p q[1:3] r[1:2, 1:2] s [tunable = false] + @named sys = ODESystem(Equation[], t, [], [p, q, r, s]) + src = ones(8) + dst = zeros(8) + # system must be complete... + @test_throws ArgumentError reorder_dimension_by_tunables!(dst, sys, src, [p, q, r]) + @test_throws ArgumentError reorder_dimension_by_tunables(sys, src, [p, q, r]) + sys = complete(sys; split = false) + # with split = true... + @test_throws ArgumentError reorder_dimension_by_tunables!(dst, sys, src, [p, q, r]) + @test_throws ArgumentError reorder_dimension_by_tunables(sys, src, [p, q, r]) + sys = complete(sys) + # and the arrays must have matching size + @test_throws ArgumentError reorder_dimension_by_tunables!( + zeros(2, 4), sys, src, [p, q, r]) + + ps = MTKParameters(sys, [p => 1.0, q => 3ones(3), r => 4ones(2, 2), s => 0.0]) + src = ps.tunable + reorder_dimension_by_tunables!(dst, sys, src, [q, r, p]) + @test dst ≈ vcat(3ones(3), 4ones(4), 1.0) + @test reorder_dimension_by_tunables(sys, src, [r, p, q]) ≈ vcat(4ones(4), 1.0, 3ones(3)) + reorder_dimension_by_tunables!(dst, sys, src, [q[1], r[:, 1], q[2], r[:, 2], q[3], p]) + @test dst ≈ vcat(3.0, 4ones(2), 3.0, 4ones(2), 3.0, 1.0) + src = stack([copy(ps.tunable) for i in 1:5]; dims = 1) + dst = zeros(size(src)) + reorder_dimension_by_tunables!(dst, sys, src, [r, q, p]; dim = 2) + @test dst ≈ stack([vcat(4ones(4), 3ones(3), 1.0) for i in 1:5]; dims = 1) +end From af1f7c6d24909bacd76df51276914f06e01df3d8 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 23 Sep 2024 13:31:04 +0530 Subject: [PATCH 0023/1337] docs: add documentation for tunable parameters (re-)ordering --- docs/src/basics/FAQ.md | 59 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/docs/src/basics/FAQ.md b/docs/src/basics/FAQ.md index 44f97c2b25..bb3d01fc30 100644 --- a/docs/src/basics/FAQ.md +++ b/docs/src/basics/FAQ.md @@ -244,3 +244,62 @@ D = Differential(x) @variables y(x) @named sys = ODESystem([D(y) ~ x], x) ``` + +## Ordering of tunable parameters + +Tunable parameters are floating point parameters, not used in callbacks and not marked with `tunable = false` in their metadata. These are expected to be used with AD +and optimization libraries. As such, they are stored together in one `Vector{T}`. To obtain the ordering of tunable parameters in this buffer, use: + +```@docs +tunable_parameters +``` + +If you have an array in which a particular dimension is in the order of tunable parameters (e.g. the jacobian with respect to tunables) then that dimension of the +array can be reordered into the required permutation using the symbolic variables: + +```@docs +reorder_dimension_by_tunables! +reorder_dimension_by_tunables +``` + +For example: + +```@example reorder +using ModelingToolkit + +@parameters p q[1:3] r[1:2, 1:2] + +@named sys = ODESystem(Equation[], ModelingToolkit.t_nounits, [], [p, q, r]) +sys = complete(sys) +``` + +The canonicalized tunables portion of `MTKParameters` will be in the order of tunables: + +```@example reorder +using SciMLStructures: canonicalize, Tunable + +ps = MTKParameters(sys, [p => 1.0, q => [2.0, 3.0, 4.0], r => [5.0 6.0; 7.0 8.0]]) +arr = canonicalize(Tunable(), ps)[1] +``` + +We can reorder this to contain the value for `p`, then all values for `q`, then for `r` using: + +```@example reorder +reorder_dimension_by_tunables(sys, arr, [p, q, r]) +``` + +This also works with interleaved subarrays of symbolics: + +```@example reorder +reorder_dimension_by_tunables(sys, arr, [q[1], r[1, :], q[2], r[2, :], q[3], p]) +``` + +And arbitrary dimensions of higher dimensional arrays: + +```@example reorder +highdimarr = stack([i * arr for i in 1:5]; dims = 1) +``` + +```@example reorder +reorder_dimension_by_tunables(sys, highdimarr, [q[1:2], r[1, :], q[3], r[2, :], p]; dim = 2) +``` From efca396cacfb0791a5dacddb2c99395983c42876 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 23 Sep 2024 18:02:34 +0530 Subject: [PATCH 0024/1337] fix: allow accessing parameter dependencies via `Symbol` names --- src/systems/index_cache.jl | 11 ++++++++++- test/symbolic_indexing_interface.jl | 9 +++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/systems/index_cache.jl b/src/systems/index_cache.jl index 82a17d01e9..8e5235245c 100644 --- a/src/systems/index_cache.jl +++ b/src/systems/index_cache.jl @@ -277,7 +277,16 @@ function IndexCache(sys::AbstractSystem) dependent_pars = Set{BasicSymbolic}() for eq in parameter_dependencies(sys) - push!(dependent_pars, eq.lhs) + sym = eq.lhs + ttsym = default_toterm(sym) + rsym = renamespace(sys, sym) + rttsym = renamespace(sys, ttsym) + for s in [sym, ttsym, rsym, rttsym] + push!(dependent_pars, s) + if hasname(s) && (!iscall(s) || operation(s) != getindex) + symbol_to_variable[getname(s)] = sym + end + end end return IndexCache( diff --git a/test/symbolic_indexing_interface.jl b/test/symbolic_indexing_interface.jl index 2531f4eef4..0f42ed7d34 100644 --- a/test/symbolic_indexing_interface.jl +++ b/test/symbolic_indexing_interface.jl @@ -185,3 +185,12 @@ get_dep = @test_nowarn getu(prob, 2p1) @test getu(prob, p1)(prob) == getu(prob, :p1)(prob) @test getu(prob, p2)(prob) == getu(prob, :p2)(prob) end + +@testset "Parameter dependencies as symbols" begin + @variables x(t) = 1.0 + @parameters a=1 b + @named model = ODESystem(D(x) ~ x + a - b, t, parameter_dependencies = [b ~ a + 1]) + sys = complete(model) + prob = ODEProblem(sys, [], (0.0, 1.0)) + @test prob.ps[b] == prob.ps[:b] +end From da612c0c02c3304408101ce8ac443483505bd719 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 23 Sep 2024 15:15:51 +0530 Subject: [PATCH 0025/1337] test: make tests independent of parameter order --- test/discrete_system.jl | 2 +- test/labelledarrays.jl | 3 ++- test/model_parsing.jl | 6 +++--- test/precompile_test.jl | 2 +- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/test/discrete_system.jl b/test/discrete_system.jl index 7d8c0d9678..b63f66d4e4 100644 --- a/test/discrete_system.jl +++ b/test/discrete_system.jl @@ -42,7 +42,7 @@ for df in [ # iip du = zeros(3) u = collect(1:3) - p = MTKParameters(syss, parameters(syss) .=> collect(1:5)) + p = MTKParameters(syss, [c, nsteps, δt, β, γ] .=> collect(1:5)) df.f(du, u, p, 0) @test du ≈ [0.01831563888873422, 0.9816849729159067, 4.999999388195359] diff --git a/test/labelledarrays.jl b/test/labelledarrays.jl index 0ec9e64f8d..c9ee7ee50b 100644 --- a/test/labelledarrays.jl +++ b/test/labelledarrays.jl @@ -87,5 +87,6 @@ u0 = @LArray [9998.0, 1.0, 1.0, 1.0] (:S, :I, :R, :C) problem = ODEProblem(SIR!, u0, tspan, p) sys = complete(modelingtoolkitize(problem)) -@test all(isequal.(parameters(sys), getproperty.(@variables(β, η, ω, φ, σ, μ), :val))) +@test all(any(isequal(x), parameters(sys)) +for x in ModelingToolkit.unwrap.(@variables(β, η, ω, φ, σ, μ))) @test all(isequal.(Symbol.(unknowns(sys)), Symbol.(@variables(S(t), I(t), R(t), C(t))))) diff --git a/test/model_parsing.jl b/test/model_parsing.jl index 40fb0a13f2..6332dc12a5 100644 --- a/test/model_parsing.jl +++ b/test/model_parsing.jl @@ -534,9 +534,9 @@ end @named else_in_sys = InsideTheBlock(flag = 3) else_in_sys = complete(else_in_sys) - @test getname.(parameters(if_in_sys)) == [:if_parameter, :eq] - @test getname.(parameters(elseif_in_sys)) == [:elseif_parameter, :eq] - @test getname.(parameters(else_in_sys)) == [:else_parameter, :eq] + @test sort(getname.(parameters(if_in_sys))) == [:eq, :if_parameter] + @test sort(getname.(parameters(elseif_in_sys))) == [:elseif_parameter, :eq] + @test sort(getname.(parameters(else_in_sys))) == [:else_parameter, :eq] @test getdefault(if_in_sys.if_parameter) == 100 @test getdefault(elseif_in_sys.elseif_parameter) == 101 diff --git a/test/precompile_test.jl b/test/precompile_test.jl index 6e31317de2..38051d9d49 100644 --- a/test/precompile_test.jl +++ b/test/precompile_test.jl @@ -10,7 +10,7 @@ using ODEPrecompileTest u = collect(1:3) p = ModelingToolkit.MTKParameters(ODEPrecompileTest.f_noeval_good.sys, - parameters(ODEPrecompileTest.f_noeval_good.sys) .=> collect(4:6)) + [:σ, :ρ, :β] .=> collect(4:6)) # These cases do not work, because they get defined in the ModelingToolkit's RGF cache. @test parentmodule(typeof(ODEPrecompileTest.f_bad.f.f_iip).parameters[2]) == ModelingToolkit From 083efb2f571ab9859b4e722fee7a8ce2ea073f5c Mon Sep 17 00:00:00 2001 From: Christopher Rackauckas Date: Tue, 24 Sep 2024 06:29:29 -0400 Subject: [PATCH 0026/1337] Update Project.toml --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 85b970f181..32a18bfdc0 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "ModelingToolkit" uuid = "961ee093-0014-501f-94e3-6117800e7a78" authors = ["Yingbo Ma ", "Chris Rackauckas and contributors"] -version = "9.40.1" +version = "9.41.0" [deps] AbstractTrees = "1520ce14-60c1-5f80-bbc7-55ef81b5835c" From 2bf2778f7448e7a944131ff929f3841d29a03cbc Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 25 Sep 2024 14:27:49 +0530 Subject: [PATCH 0027/1337] test: do not mark inversemodel tests as broken --- test/downstream/inversemodel.jl | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/test/downstream/inversemodel.jl b/test/downstream/inversemodel.jl index 56307aec31..8c2c6bebb9 100644 --- a/test/downstream/inversemodel.jl +++ b/test/downstream/inversemodel.jl @@ -151,13 +151,11 @@ x, _ = ModelingToolkit.get_u0_p(simplified_sys, op) p = ModelingToolkit.MTKParameters(simplified_sys, op) # If this somehow passes, mention it on # https://github.com/SciML/ModelingToolkit.jl/issues/2786 -@test_broken begin - matrices1 = Sf(x, p, 0) - matrices2, _ = Blocks.get_sensitivity(model, :y; op) # Test that we get the same result when calling the higher-level API - @test matrices1.f_x ≈ matrices2.A[1:7, 1:7] - nsys = get_named_sensitivity(model, :y; op) # Test that we get the same result when calling an even higher-level API - @test matrices2.A ≈ nsys.A -end +matrices1 = Sf(x, p, 0) +matrices2, _ = Blocks.get_sensitivity(model, :y; op) # Test that we get the same result when calling the higher-level API +@test_broken matrices1.f_x ≈ matrices2.A[1:7, 1:7] +nsys = get_named_sensitivity(model, :y; op) # Test that we get the same result when calling an even higher-level API +@test matrices2.A ≈ nsys.A # Test the same thing for comp sensitivities @@ -166,10 +164,8 @@ x, _ = ModelingToolkit.get_u0_p(simplified_sys, op) p = ModelingToolkit.MTKParameters(simplified_sys, op) # If this somehow passes, mention it on # https://github.com/SciML/ModelingToolkit.jl/issues/2786 -@test_broken begin - matrices1 = Sf(x, p, 0) - matrices2, _ = Blocks.get_comp_sensitivity(model, :y; op) # Test that we get the same result when calling the higher-level API - @test matrices1.f_x ≈ matrices2.A[1:7, 1:7] - nsys = get_named_comp_sensitivity(model, :y; op) # Test that we get the same result when calling an even higher-level API - @test matrices2.A ≈ nsys.A -end +matrices1 = Sf(x, p, 0) +matrices2, _ = Blocks.get_comp_sensitivity(model, :y; op) # Test that we get the same result when calling the higher-level API +@test_broken matrices1.f_x ≈ matrices2.A[1:7, 1:7] +nsys = get_named_comp_sensitivity(model, :y; op) # Test that we get the same result when calling an even higher-level API +@test matrices2.A ≈ nsys.A From 95e3d296aca95cced76419327d3b07ee87e5ec69 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 25 Sep 2024 12:39:03 +0530 Subject: [PATCH 0028/1337] fix: check scope when discovering variables from equations --- src/utils.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/src/utils.jl b/src/utils.jl index 4426c8a799..25731f495a 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -483,6 +483,7 @@ end function collect_var!(unknowns, parameters, var, iv) isequal(var, iv) && return nothing + getmetadata(var, SymScope, LocalScope()) == LocalScope() || return nothing if iscalledparameter(var) callable = getcalledparameter(var) push!(parameters, callable) From 98889f8e75e1cf1b2f03a34fe4b0dcf4fa63ac4e Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 25 Sep 2024 12:44:19 +0530 Subject: [PATCH 0029/1337] test: test properly scoped discovery of parameters --- test/variable_scope.jl | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/test/variable_scope.jl b/test/variable_scope.jl index 8c6e358c23..853d5ce757 100644 --- a/test/variable_scope.jl +++ b/test/variable_scope.jl @@ -1,9 +1,8 @@ using ModelingToolkit -using ModelingToolkit: SymScope -using Symbolics: arguments, value +using ModelingToolkit: SymScope, t_nounits as t, D_nounits as D +using Symbolics: arguments, value, getname using Test -@independent_variables t @variables a b(t) c d e(t) b = ParentScope(b) @@ -52,7 +51,6 @@ end @test renamed([:foo :bar :baz], c) == Symbol("foo₊c") @test renamed([:foo :bar :baz], d) == :d -@independent_variables t @parameters a b c d e f p = [a ParentScope(b) @@ -84,3 +82,22 @@ arr1 = ODESystem(Equation[], t, [], []; name = :arr1) ∘ arr0 arr_ps = ModelingToolkit.getname.(parameters(arr1)) @test isequal(arr_ps[1], Symbol("xx")) @test isequal(arr_ps[2], Symbol("arr0₊xx")) + +function Foo(; name, p = 1) + @parameters p = p + @variables x(t) + return ODESystem(D(x) ~ p, t; name) +end +function Bar(; name, p = 2) + @parameters p = p + @variables x(t) + @named foo = Foo(; p) + return ODESystem(D(x) ~ p + t, t; systems = [foo], name) +end +@named bar = Bar() +bar = complete(bar) +@test length(parameters(bar)) == 2 +@test sort(getname.(parameters(bar))) == [:foo₊p, :p] +defs = ModelingToolkit.defaults(bar) +@test defs[bar.p] == 2 +@test isequal(defs[bar.foo.p], bar.p) From d0967f3b2442fc1c72f31d05425ef30794451652 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20St-Jean?= Date: Fri, 27 Sep 2024 10:22:30 +0900 Subject: [PATCH 0030/1337] Fix duplicate text --- docs/src/basics/InputOutput.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/src/basics/InputOutput.md b/docs/src/basics/InputOutput.md index 4f42d9dcd7..4dc5a3d50f 100644 --- a/docs/src/basics/InputOutput.md +++ b/docs/src/basics/InputOutput.md @@ -55,7 +55,6 @@ We can inspect the state realization chosen by MTK x_sym ``` -as expected, `x` is chosen as the state variable. as expected, `x` is chosen as the state variable. ```@example inputoutput From 1c939fe51a00eed5036fe06a12e12d43ca89d739 Mon Sep 17 00:00:00 2001 From: Venkateshprasad <32921645+ven-k@users.noreply.github.com> Date: Wed, 18 Sep 2024 17:40:43 +0000 Subject: [PATCH 0031/1337] feat: allow users to set array length via args in `@mtkmodel` --- src/systems/model_parsing.jl | 79 ++++++++++++++++++++---------------- test/model_parsing.jl | 23 +++++++++++ 2 files changed, 66 insertions(+), 36 deletions(-) diff --git a/src/systems/model_parsing.jl b/src/systems/model_parsing.jl index fce2add6b7..df75965121 100644 --- a/src/systems/model_parsing.jl +++ b/src/systems/model_parsing.jl @@ -269,9 +269,12 @@ function parse_variable_def!(dict, mod, arg, varclass, kwargs, where_types; return var, def, Dict() end Expr(:ref, a, b...) => begin - indices = map(i -> UnitRange(i.args[2], i.args[end]), b) - parse_variable_def!(dict, mod, a, varclass, kwargs, where_types; - def, indices, type, meta) + if varclass == :parameters + var = :($a = $first(@parameters $a[$(b...)])) + else + var = :($a = $first(@variables $a[$(b...)])) + end + (:($a...), var), nothing, Dict() end _ => error("$arg cannot be parsed") end @@ -674,47 +677,51 @@ end function parse_variable_arg(dict, mod, arg, varclass, kwargs, where_types) vv, def, metadata_with_exprs = parse_variable_def!( dict, mod, arg, varclass, kwargs, where_types) - name = getname(vv) + if vv isa Tuple + return vv + else + name = getname(vv[1]) - varexpr = if haskey(metadata_with_exprs, VariableUnit) - unit = metadata_with_exprs[VariableUnit] - quote - $name = if $name === nothing - $setdefault($vv, $def) - else - try - $setdefault($vv, $convert_units($unit, $name)) - catch e - if isa(e, $(DynamicQuantities.DimensionError)) || - isa(e, $(Unitful.DimensionError)) - error("Unable to convert units for \'" * string(:($$vv)) * "\'") - elseif isa(e, MethodError) - error("No or invalid units provided for \'" * string(:($$vv)) * - "\'") - else - rethrow(e) + varexpr = if haskey(metadata_with_exprs, VariableUnit) + unit = metadata_with_exprs[VariableUnit] + quote + $name = if $name === nothing + $setdefault($vv, $def) + else + try + $setdefault($vv, $convert_units($unit, $name)) + catch e + if isa(e, $(DynamicQuantities.DimensionError)) || + isa(e, $(Unitful.DimensionError)) + error("Unable to convert units for \'" * string(:($$vv)) * "\'") + elseif isa(e, MethodError) + error("No or invalid units provided for \'" * string(:($$vv)) * + "\'") + else + rethrow(e) + end end end end - end - else - quote - $name = if $name === nothing - $setdefault($vv, $def) - else - $setdefault($vv, $name) + else + quote + $name = if $name === nothing + $setdefault($vv, $def) + else + $setdefault($vv, $name) + end end end - end - metadata_expr = Expr(:block) - for (k, v) in metadata_with_exprs - push!(metadata_expr.args, - :($name = $wrap($set_scalar_metadata($unwrap($name), $k, $v)))) - end + metadata_expr = Expr(:block) + for (k, v) in metadata_with_exprs + push!(metadata_expr.args, + :($name = $wrap($set_scalar_metadata($unwrap($name), $k, $v)))) + end - push!(varexpr.args, metadata_expr) - return vv isa Num ? name : :($name...), varexpr + push!(varexpr.args, metadata_expr) + return vv isa Num ? name : :($name...), varexpr + end end function handle_conditional_vars!( diff --git a/test/model_parsing.jl b/test/model_parsing.jl index 6332dc12a5..21cb4837ef 100644 --- a/test/model_parsing.jl +++ b/test/model_parsing.jl @@ -876,3 +876,26 @@ end end), false) end + +@testset "Array Length as an Input" begin + @mtkmodel VaryingLengthArray begin + @structural_parameters begin + N + M + end + @parameters begin + p1[1:N] + p2[1:N, 1:M] + end + @variables begin + v1(t)[1:N] + v2(t)[1:N, 1:M] + end + end + + @named model = VaryingLengthArray(N = 2, M = 3) + @test length(model.p1) == 2 + @test size(model.p2) == (2, 3) + @test length(model.v1) == 2 + @test size(model.v2) == (2, 3) +end From 51184bfc294ff7616af88ba8d9a8f9e6a84c41e3 Mon Sep 17 00:00:00 2001 From: Venkateshprasad <32921645+ven-k@users.noreply.github.com> Date: Mon, 23 Sep 2024 19:38:14 +0000 Subject: [PATCH 0032/1337] feat: support arbitrary length arrays with metadata and default --- src/systems/model_parsing.jl | 111 ++++++++++++++++++++++++++++++----- 1 file changed, 96 insertions(+), 15 deletions(-) diff --git a/src/systems/model_parsing.jl b/src/systems/model_parsing.jl index df75965121..ef89754dca 100644 --- a/src/systems/model_parsing.jl +++ b/src/systems/model_parsing.jl @@ -222,6 +222,96 @@ function parse_variable_def!(dict, mod, arg, varclass, kwargs, where_types; varclass, where_types, meta) return var, def, Dict() end + Expr(:tuple, Expr(:ref, a, b...), y) => begin + varname = Meta.isexpr(a, :call) ? a.args[1] : a + push!(kwargs, Expr(:kw, varname, nothing)) + if varclass == :parameters + var = :($varname = $first(@parameters $a[$(b...)] = ($varname, $y))) + else + var = :($varname = $first(@variables $a[$(b...)] = ($varname, $y))) + end + #TODO: update `dict` aka `Model.structure` with the metadata + (:($varname...), var), nothing, Dict() + end + Expr(:(=), Expr(:ref, a, b...), y) => begin + varname = Meta.isexpr(a, :call) ? a.args[1] : a + if Meta.isexpr(y, :tuple) + val, y = (y.args[1], y.args[2:end]) + push!(kwargs, Expr(:kw, varname, nothing)) + if varclass == :parameters + var = :($varname = $varname === nothing ? $val : $varname; + $varname = $first(@parameters $a[$(b...)] = ( + $varname, $(y...)))) + else + var = :($varname = $varname === nothing ? $val : $varname; + $varname = $first(@variables $a[$(b...)] = ( + $varname, $(y...)))) + end + else + push!(kwargs, Expr(:kw, varname, nothing)) + if varclass == :parameters + var = :($varname = $varname === nothing ? $y : $varname; $varname = $first(@parameters $a[$(b...)] = $varname)) + else + var = :($varname = $varname === nothing ? $y : $varname; $varname = $first(@variables $a[$(b...)] = $varname)) + end + end + #TODO: update `dict`` aka `Model.structure` with the metadata + (:($varname...), var), nothing, Dict() + end + Expr(:(=), Expr(:(::), Expr(:ref, a, b...), n), y) => begin + varname = Meta.isexpr(a, :call) ? a.args[1] : a + if Meta.isexpr(y, :tuple) + val, y = (y.args[1], y.args[2:end]) + push!(kwargs, Expr(:kw, varname, nothing)) + if varclass == :parameters + var = :( + $varname = $varname = $varname === nothing ? $val : $varname; + $varname = $first(@parameters $a[$(b...)]::$n = ($varname, $(y...))) + ) + else + var = :($varname = $varname === nothing ? $val : $varname; + $varname = $first(@variables $a[$(b...)]::$n = ( + $varname, $(y...)))) + end + else + push!(kwargs, Expr(:kw, varname, y)) + if varclass == :parameters + var = :($varname = $first(@parameters $a[$(b...)]::$n = $varname)) + else + var = :($varname = $first(@variables $a[$(b...)]::$n = $varname)) + end + end + #TODO: update `dict`` aka `Model.structure` with the metadata + (:($varname...), var), nothing, Dict() + end + Expr(:tuple, Expr(:(::), Expr(:ref, a, b...), n), y) => begin + varname = Meta.isexpr(a, :call) ? a.args[1] : a + push!(kwargs, Expr(:kw, varname, nothing)) + if varclass == :parameters + var = :($varname = $first(@parameters $a[$(b...)]::$n = ($varname, $y))) + else + var = :($varname = $first(@variables $a[$(b...)]::$n = ($varname, $y))) + end + #TODO: update `dict` aka `Model.structure` with the metadata + (:($varname...), var), nothing, Dict() + end + Expr(:ref, a, b...) => begin + varname = a isa Expr && a.head == :call ? a.args[1] : a + push!(kwargs, Expr(:kw, varname, nothing)) + if varclass == :parameters + var = :($varname = $first(@parameters $a[$(b...)] = $varname)) + elseif varclass == :variables + var = :($varname = $first(@variables $a[$(b...)] = $varname)) + else + throw("Symbolic array with arbitrary length is not handled for $varclass. + Please open an issue with an example.") + end + dict[varclass] = get!(dict, varclass) do + Dict{Symbol, Dict{Symbol, Any}}() + end + # dict[:kwargs][varname] = dict[varclass][varname] = Dict(:size => b) + (:($varname...), var), nothing, Dict() + end Expr(:(=), a, b) => begin Base.remove_linenums!(b) def, meta = parse_default(mod, b) @@ -268,14 +358,6 @@ function parse_variable_def!(dict, mod, arg, varclass, kwargs, where_types; end return var, def, Dict() end - Expr(:ref, a, b...) => begin - if varclass == :parameters - var = :($a = $first(@parameters $a[$(b...)])) - else - var = :($a = $first(@variables $a[$(b...)])) - end - (:($a...), var), nothing, Dict() - end _ => error("$arg cannot be parsed") end end @@ -677,11 +759,8 @@ end function parse_variable_arg(dict, mod, arg, varclass, kwargs, where_types) vv, def, metadata_with_exprs = parse_variable_def!( dict, mod, arg, varclass, kwargs, where_types) - if vv isa Tuple - return vv - else - name = getname(vv[1]) - + if !(vv isa Tuple) + name = getname(vv) varexpr = if haskey(metadata_with_exprs, VariableUnit) unit = metadata_with_exprs[VariableUnit] quote @@ -692,11 +771,11 @@ function parse_variable_arg(dict, mod, arg, varclass, kwargs, where_types) $setdefault($vv, $convert_units($unit, $name)) catch e if isa(e, $(DynamicQuantities.DimensionError)) || - isa(e, $(Unitful.DimensionError)) + isa(e, $(Unitful.DimensionError)) error("Unable to convert units for \'" * string(:($$vv)) * "\'") elseif isa(e, MethodError) error("No or invalid units provided for \'" * string(:($$vv)) * - "\'") + "\'") else rethrow(e) end @@ -721,6 +800,8 @@ function parse_variable_arg(dict, mod, arg, varclass, kwargs, where_types) push!(varexpr.args, metadata_expr) return vv isa Num ? name : :($name...), varexpr + else + return vv end end From 6cb3a28293a1848ea5b0d25b91475a2b2ad40be5 Mon Sep 17 00:00:00 2001 From: Venkateshprasad <32921645+ven-k@users.noreply.github.com> Date: Mon, 23 Sep 2024 19:39:06 +0000 Subject: [PATCH 0033/1337] test: mark broken tests These are related to: - Model.structure metadata related to variable/parameter array - Certain cases of symtypes (although neither case is unchanged by this PR) --- test/model_parsing.jl | 34 +++++++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/test/model_parsing.jl b/test/model_parsing.jl index 21cb4837ef..c9868c8ebf 100644 --- a/test/model_parsing.jl +++ b/test/model_parsing.jl @@ -259,7 +259,8 @@ end @test all(collect(hasmetadata.(model.l, ModelingToolkit.VariableDescription))) @test all(lastindex.([model.a2, model.b2, model.d2, model.e2, model.h2]) .== 2) - @test size(model.l) == MockModel.structure[:parameters][:l][:size] == (2, 3) + @test size(model.l) == (2, 3) + @test_broken MockModel.structure[:parameters][:l][:size] == (2, 3) model = complete(model) @test getdefault(model.cval) == 1 @@ -302,8 +303,8 @@ end @test symtype(type_model.par2) == Int @test symtype(type_model.par3) == BigFloat @test symtype(type_model.par4) == Float64 - @test symtype(type_model.par5[1]) == BigFloat - @test symtype(type_model.par6[1]) == BigFloat + @test_broken symtype(type_model.par5[1]) == BigFloat + @test_broken symtype(type_model.par6[1]) == BigFloat @test symtype(type_model.par7[1, 1]) == BigFloat @test_throws TypeError TypeModel(; name = :throws, flag = 1) @@ -313,11 +314,10 @@ end @test_throws TypeError TypeModel(; name = :throws, par3 = true) @test_throws TypeError TypeModel(; name = :throws, par4 = true) # par7 should be an AbstractArray of BigFloat. - @test_throws MethodError TypeModel(; name = :throws, par7 = rand(Int, 3, 3)) # Test that array types are correctly added. @named type_model2 = TypeModel(; par5 = rand(BigFloat, 3)) - @test symtype(type_model2.par5[1]) == BigFloat + @test_broken symtype(type_model2.par5[1]) == BigFloat @named type_model3 = TypeModel(; par7 = rand(BigFloat, 3, 3)) @test symtype(type_model3.par7[1, 1]) == BigFloat @@ -474,7 +474,8 @@ using ModelingToolkit: getdefault, scalarize @named model_with_component_array = ModelWithComponentArray() - @test eval(ModelWithComponentArray.structure[:parameters][:r][:unit]) == eval(u"Ω") + @test_broken eval(ModelWithComponentArray.structure[:parameters][:r][:unit]) == + eval(u"Ω") @test lastindex(parameters(model_with_component_array)) == 3 # Test the constant `k`. Manually k's value should be kept in sync here @@ -892,10 +893,29 @@ end v2(t)[1:N, 1:M] end end - + @named model = VaryingLengthArray(N = 2, M = 3) @test length(model.p1) == 2 @test size(model.p2) == (2, 3) @test length(model.v1) == 2 @test size(model.v2) == (2, 3) + + @mtkmodel WithMetadata begin + @structural_parameters begin + N + end + @parameters begin + p_only_default[1:N] = 101 + p_only_metadata[1:N], [description = "this only has metadata"] + p_both_default_and_metadata[1:N] = 102, + [description = "this has both default value and metadata"] + end + end + + @named with_metadata = WithMetadata(N = 10) + @test getdefault(with_metadata.p_only_default) == 101 + @test getdescription(with_metadata.p_only_metadata) == "this only has metadata" + @test getdefault(with_metadata.p_both_default_and_metadata) == 102 + @test getdescription(with_metadata.p_both_default_and_metadata) == + "this has both default value and metadata" end From 98471b6b96f762b8f6c0b02074e8d16dbbc4ca4c Mon Sep 17 00:00:00 2001 From: Venkateshprasad <32921645+ven-k@users.noreply.github.com> Date: Mon, 23 Sep 2024 20:32:56 +0000 Subject: [PATCH 0034/1337] docs: use symbolic array with arbitray length in ModelC example --- docs/src/basics/MTKLanguage.md | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/docs/src/basics/MTKLanguage.md b/docs/src/basics/MTKLanguage.md index a2fb7d0870..44fe8bbc07 100644 --- a/docs/src/basics/MTKLanguage.md +++ b/docs/src/basics/MTKLanguage.md @@ -63,13 +63,14 @@ end @structural_parameters begin f = sin N = 2 + M = 3 end begin v_var = 1.0 end @variables begin v(t) = v_var - v_array(t)[1:2, 1:3] + v_array(t)[1:N, 1:M] v_for_defaults(t) end @extend ModelB(; p1) @@ -310,10 +311,10 @@ end - `:defaults`: Dictionary of variables and default values specified in the `@defaults`. - `:extend`: The list of extended unknowns, name given to the base system, and name of the base system. - `:structural_parameters`: Dictionary of structural parameters mapped to their metadata. - - `:parameters`: Dictionary of symbolic parameters mapped to their metadata. For - parameter arrays, length is added to the metadata as `:size`. - - `:variables`: Dictionary of symbolic variables mapped to their metadata. For - variable arrays, length is added to the metadata as `:size`. + - `:parameters`: Dictionary of symbolic parameters mapped to their metadata. Metadata of + the parameter arrays is, for now, omitted. + - `:variables`: Dictionary of symbolic variables mapped to their metadata. Metadata of + the variable arrays is, for now, omitted. - `:kwargs`: Dictionary of keyword arguments mapped to their metadata. - `:independent_variable`: Independent variable, which is added while generating the Model. - `:equations`: List of equations (represented as strings). @@ -324,10 +325,10 @@ For example, the structure of `ModelC` is: julia> ModelC.structure Dict{Symbol, Any} with 10 entries: :components => Any[Union{Expr, Symbol}[:model_a, :ModelA], Union{Expr, Symbol}[:model_array_a, :ModelA, :(1:N)], Union{Expr, Symbol}[:model_array_b, :ModelA, :(1:N)]] - :variables => Dict{Symbol, Dict{Symbol, Any}}(:v=>Dict(:default=>:v_var, :type=>Real), :v_array=>Dict(:type=>Real, :size=>(2, 3)), :v_for_defaults=>Dict(:type=>Real)) + :variables => Dict{Symbol, Dict{Symbol, Any}}(:v=>Dict(:default=>:v_var, :type=>Real), :v_for_defaults=>Dict(:type=>Real)) :icon => URI("https://github.com/SciML/SciMLDocs/blob/main/docs/src/assets/logo.png") - :kwargs => Dict{Symbol, Dict}(:f=>Dict(:value=>:sin), :N=>Dict(:value=>2), :v=>Dict{Symbol, Any}(:value=>:v_var, :type=>Real), :v_array=>Dict{Symbol, Union{Nothing, UnionAll}}(:value=>nothing, :type=>AbstractArray{Real}), :v_for_defaults=>Dict{Symbol, Union{Nothing, DataType}}(:value=>nothing, :type=>Real), :p1=>Dict(:value=>nothing)) - :structural_parameters => Dict{Symbol, Dict}(:f=>Dict(:value=>:sin), :N=>Dict(:value=>2)) + :kwargs => Dict{Symbol, Dict}(:f => Dict(:value => :sin), :N => Dict(:value => 2), :M => Dict(:value => 3), :v => Dict{Symbol, Any}(:value => :v_var, :type => Real), :v_for_defaults => Dict{Symbol, Union{Nothing, DataType}}(:value => nothing, :type => Real), :p1 => Dict(:value => nothing)), + :structural_parameters => Dict{Symbol, Dict}(:f => Dict(:value => :sin), :N => Dict(:value => 2), :M => Dict(:value => 3)) :independent_variable => t :constants => Dict{Symbol, Dict}(:c=>Dict{Symbol, Any}(:value=>1, :type=>Int64, :description=>"Example constant.")) :extend => Any[[:p2, :p1], Symbol("#mtkmodel__anonymous__ModelB"), :ModelB] From 687dc7fe653ba6312c5225e53ab90bd0a925e747 Mon Sep 17 00:00:00 2001 From: Venkateshprasad <32921645+ven-k@users.noreply.github.com> Date: Tue, 24 Sep 2024 17:32:39 +0000 Subject: [PATCH 0035/1337] feat(mtkmodel): handle unit conversion for array inputs --- src/systems/model_parsing.jl | 54 ++++++++++++++++++++++++++---------- 1 file changed, 39 insertions(+), 15 deletions(-) diff --git a/src/systems/model_parsing.jl b/src/systems/model_parsing.jl index ef89754dca..e5e1c54328 100644 --- a/src/systems/model_parsing.jl +++ b/src/systems/model_parsing.jl @@ -180,6 +180,16 @@ function update_kwargs_and_metadata!(dict, kwargs, a, def, indices, type, var, end end +function unit_handled_variable_value(mod, y, varname) + meta = parse_metadata(mod, y) + varval = if meta isa Nothing || get(meta, VariableUnit, nothing) isa Nothing + varname + else + :($convert_units($(meta[VariableUnit]), $varname)) + end + return varval +end + function parse_variable_def!(dict, mod, arg, varclass, kwargs, where_types; def = nothing, indices::Union{Vector{UnitRange{Int}}, Nothing} = nothing, type::Type = Real, meta = Dict{DataType, Expr}()) @@ -225,10 +235,11 @@ function parse_variable_def!(dict, mod, arg, varclass, kwargs, where_types; Expr(:tuple, Expr(:ref, a, b...), y) => begin varname = Meta.isexpr(a, :call) ? a.args[1] : a push!(kwargs, Expr(:kw, varname, nothing)) + varval = unit_handled_variable_value(mod, y, varname) if varclass == :parameters - var = :($varname = $first(@parameters $a[$(b...)] = ($varname, $y))) + var = :($varname = $first(@parameters $a[$(b...)] = ($varval, $y))) else - var = :($varname = $first(@variables $a[$(b...)] = ($varname, $y))) + var = :($varname = $first(@variables $a[$(b...)] = ($varval, $y))) end #TODO: update `dict` aka `Model.structure` with the metadata (:($varname...), var), nothing, Dict() @@ -236,16 +247,17 @@ function parse_variable_def!(dict, mod, arg, varclass, kwargs, where_types; Expr(:(=), Expr(:ref, a, b...), y) => begin varname = Meta.isexpr(a, :call) ? a.args[1] : a if Meta.isexpr(y, :tuple) + varval = unit_handled_variable_value(mod, y, varname) val, y = (y.args[1], y.args[2:end]) push!(kwargs, Expr(:kw, varname, nothing)) if varclass == :parameters var = :($varname = $varname === nothing ? $val : $varname; $varname = $first(@parameters $a[$(b...)] = ( - $varname, $(y...)))) + $varval, $(y...)))) else var = :($varname = $varname === nothing ? $val : $varname; $varname = $first(@variables $a[$(b...)] = ( - $varname, $(y...)))) + $varval, $(y...)))) end else push!(kwargs, Expr(:kw, varname, nothing)) @@ -260,25 +272,24 @@ function parse_variable_def!(dict, mod, arg, varclass, kwargs, where_types; end Expr(:(=), Expr(:(::), Expr(:ref, a, b...), n), y) => begin varname = Meta.isexpr(a, :call) ? a.args[1] : a + varval = unit_handled_variable_value(mod, y, varname) if Meta.isexpr(y, :tuple) val, y = (y.args[1], y.args[2:end]) push!(kwargs, Expr(:kw, varname, nothing)) if varclass == :parameters - var = :( - $varname = $varname = $varname === nothing ? $val : $varname; - $varname = $first(@parameters $a[$(b...)]::$n = ($varname, $(y...))) - ) + var = :($varname = $varname = $varname === nothing ? $val : $varname; + $varname = $first(@parameters $a[$(b...)]::$n = ($varval, $(y...)))) else var = :($varname = $varname === nothing ? $val : $varname; $varname = $first(@variables $a[$(b...)]::$n = ( - $varname, $(y...)))) + $varval, $(y...)))) end else push!(kwargs, Expr(:kw, varname, y)) if varclass == :parameters - var = :($varname = $first(@parameters $a[$(b...)]::$n = $varname)) + var = :($varname = $first(@parameters $a[$(b...)]::$n = $varval)) else - var = :($varname = $first(@variables $a[$(b...)]::$n = $varname)) + var = :($varname = $first(@variables $a[$(b...)]::$n = $varval)) end end #TODO: update `dict`` aka `Model.structure` with the metadata @@ -286,11 +297,12 @@ function parse_variable_def!(dict, mod, arg, varclass, kwargs, where_types; end Expr(:tuple, Expr(:(::), Expr(:ref, a, b...), n), y) => begin varname = Meta.isexpr(a, :call) ? a.args[1] : a + varval = unit_handled_variable_value(mod, y, varname) push!(kwargs, Expr(:kw, varname, nothing)) if varclass == :parameters - var = :($varname = $first(@parameters $a[$(b...)]::$n = ($varname, $y))) + var = :($varname = $first(@parameters $a[$(b...)]::$n = ($varval, $y))) else - var = :($varname = $first(@variables $a[$(b...)]::$n = ($varname, $y))) + var = :($varname = $first(@variables $a[$(b...)]::$n = ($varval, $y))) end #TODO: update `dict` aka `Model.structure` with the metadata (:($varname...), var), nothing, Dict() @@ -465,14 +477,24 @@ function parse_default(mod, a) end end -function parse_metadata(mod, a) +function parse_metadata(mod, a::Expr) + @info a typeof(a) MLStyle.@match a begin - Expr(:vect, eles...) => Dict(parse_metadata(mod, e) for e in eles) + Expr(:vect, b...) => Dict(parse_metadata(mod, m) for m in b) + Expr(:tuple, a, b...) => parse_metadata(mod, b) Expr(:(=), a, b) => Symbolics.option_to_metadata_type(Val(a)) => get_var(mod, b) _ => error("Cannot parse metadata $a") end end +function parse_metadata(mod, metadata::AbstractArray) + ret = Dict() + for m in metadata + merge!(ret, parse_metadata(mod, m)) + end + ret +end + function _set_var_metadata!(metadata_with_exprs, a, m, v::Expr) push!(metadata_with_exprs, m => v) a @@ -730,6 +752,7 @@ function parse_variable_arg!(exprs, vs, dict, mod, arg, varclass, kwargs, where_ end function convert_units(varunits::DynamicQuantities.Quantity, value) + value isa Nothing && return nothing DynamicQuantities.ustrip(DynamicQuantities.uconvert( DynamicQuantities.SymbolicUnits.as_quantity(varunits), value)) end @@ -741,6 +764,7 @@ function convert_units( end function convert_units(varunits::Unitful.FreeUnits, value) + value isa Nothing && return nothing Unitful.ustrip(varunits, value) end From ed5e0fbdb808fb1acda6ba775838f3dc31aec7c0 Mon Sep 17 00:00:00 2001 From: Venkateshprasad <32921645+ven-k@users.noreply.github.com> Date: Tue, 24 Sep 2024 19:20:12 +0000 Subject: [PATCH 0036/1337] refactor: parse typed and untyped arrays within same block --- src/systems/model_parsing.jl | 58 ++++++++---------------------------- test/model_parsing.jl | 2 +- 2 files changed, 13 insertions(+), 47 deletions(-) diff --git a/src/systems/model_parsing.jl b/src/systems/model_parsing.jl index e5e1c54328..6560f8260e 100644 --- a/src/systems/model_parsing.jl +++ b/src/systems/model_parsing.jl @@ -232,19 +232,21 @@ function parse_variable_def!(dict, mod, arg, varclass, kwargs, where_types; varclass, where_types, meta) return var, def, Dict() end - Expr(:tuple, Expr(:ref, a, b...), y) => begin + Expr(:tuple, Expr(:ref, a, b...), y) || Expr(:tuple, Expr(:(::), Expr(:ref, a, b...), n), y) => begin + isdefined(mod, :n) || (n = Real) varname = Meta.isexpr(a, :call) ? a.args[1] : a push!(kwargs, Expr(:kw, varname, nothing)) varval = unit_handled_variable_value(mod, y, varname) if varclass == :parameters - var = :($varname = $first(@parameters $a[$(b...)] = ($varval, $y))) + var = :($varname = $first(@parameters $a[$(b...)]::$n = ($varval, $y))) else - var = :($varname = $first(@variables $a[$(b...)] = ($varval, $y))) + var = :($varname = $first(@variables $a[$(b...)]::$n = ($varval, $y))) end #TODO: update `dict` aka `Model.structure` with the metadata (:($varname...), var), nothing, Dict() end - Expr(:(=), Expr(:ref, a, b...), y) => begin + Expr(:(=), Expr(:ref, a, b...), y) || Expr(:(=), Expr(:(::), Expr(:ref, a, b...), n), y) => begin + isdefined(mod, :n) || (n = Real) varname = Meta.isexpr(a, :call) ? a.args[1] : a if Meta.isexpr(y, :tuple) varval = unit_handled_variable_value(mod, y, varname) @@ -252,61 +254,26 @@ function parse_variable_def!(dict, mod, arg, varclass, kwargs, where_types; push!(kwargs, Expr(:kw, varname, nothing)) if varclass == :parameters var = :($varname = $varname === nothing ? $val : $varname; - $varname = $first(@parameters $a[$(b...)] = ( + $varname = $first(@parameters $a[$(b...)]::$n = ( $varval, $(y...)))) - else - var = :($varname = $varname === nothing ? $val : $varname; - $varname = $first(@variables $a[$(b...)] = ( - $varval, $(y...)))) - end - else - push!(kwargs, Expr(:kw, varname, nothing)) - if varclass == :parameters - var = :($varname = $varname === nothing ? $y : $varname; $varname = $first(@parameters $a[$(b...)] = $varname)) - else - var = :($varname = $varname === nothing ? $y : $varname; $varname = $first(@variables $a[$(b...)] = $varname)) - end - end - #TODO: update `dict`` aka `Model.structure` with the metadata - (:($varname...), var), nothing, Dict() - end - Expr(:(=), Expr(:(::), Expr(:ref, a, b...), n), y) => begin - varname = Meta.isexpr(a, :call) ? a.args[1] : a - varval = unit_handled_variable_value(mod, y, varname) - if Meta.isexpr(y, :tuple) - val, y = (y.args[1], y.args[2:end]) - push!(kwargs, Expr(:kw, varname, nothing)) - if varclass == :parameters - var = :($varname = $varname = $varname === nothing ? $val : $varname; - $varname = $first(@parameters $a[$(b...)]::$n = ($varval, $(y...)))) else var = :($varname = $varname === nothing ? $val : $varname; $varname = $first(@variables $a[$(b...)]::$n = ( $varval, $(y...)))) end else - push!(kwargs, Expr(:kw, varname, y)) + push!(kwargs, Expr(:kw, varname, nothing)) if varclass == :parameters - var = :($varname = $first(@parameters $a[$(b...)]::$n = $varval)) + var = :($varname = $varname === nothing ? $y : $varname; + $varname = $first(@parameters $a[$(b...)]::$n = $varname)) else - var = :($varname = $first(@variables $a[$(b...)]::$n = $varval)) + var = :($varname = $varname === nothing ? $y : $varname; + $varname = $first(@variables $a[$(b...)]::$n = $varname)) end end #TODO: update `dict`` aka `Model.structure` with the metadata (:($varname...), var), nothing, Dict() end - Expr(:tuple, Expr(:(::), Expr(:ref, a, b...), n), y) => begin - varname = Meta.isexpr(a, :call) ? a.args[1] : a - varval = unit_handled_variable_value(mod, y, varname) - push!(kwargs, Expr(:kw, varname, nothing)) - if varclass == :parameters - var = :($varname = $first(@parameters $a[$(b...)]::$n = ($varval, $y))) - else - var = :($varname = $first(@variables $a[$(b...)]::$n = ($varval, $y))) - end - #TODO: update `dict` aka `Model.structure` with the metadata - (:($varname...), var), nothing, Dict() - end Expr(:ref, a, b...) => begin varname = a isa Expr && a.head == :call ? a.args[1] : a push!(kwargs, Expr(:kw, varname, nothing)) @@ -478,7 +445,6 @@ function parse_default(mod, a) end function parse_metadata(mod, a::Expr) - @info a typeof(a) MLStyle.@match a begin Expr(:vect, b...) => Dict(parse_metadata(mod, m) for m in b) Expr(:tuple, a, b...) => parse_metadata(mod, b) diff --git a/test/model_parsing.jl b/test/model_parsing.jl index c9868c8ebf..471e1f4651 100644 --- a/test/model_parsing.jl +++ b/test/model_parsing.jl @@ -305,7 +305,7 @@ end @test symtype(type_model.par4) == Float64 @test_broken symtype(type_model.par5[1]) == BigFloat @test_broken symtype(type_model.par6[1]) == BigFloat - @test symtype(type_model.par7[1, 1]) == BigFloat + @test_broken symtype(type_model.par7[1, 1]) == BigFloat @test_throws TypeError TypeModel(; name = :throws, flag = 1) @test_throws TypeError TypeModel(; name = :throws, par0 = 1) From 563dedcb8a6d46c72f696aae4bf46214efb85c63 Mon Sep 17 00:00:00 2001 From: Venkateshprasad <32921645+ven-k@users.noreply.github.com> Date: Tue, 24 Sep 2024 19:45:12 +0000 Subject: [PATCH 0037/1337] fix: handle more typed array cases This also fixes more tests --- src/systems/model_parsing.jl | 27 ++++++++++++++------------- test/model_parsing.jl | 8 ++++---- 2 files changed, 18 insertions(+), 17 deletions(-) diff --git a/src/systems/model_parsing.jl b/src/systems/model_parsing.jl index 6560f8260e..09ef05c172 100644 --- a/src/systems/model_parsing.jl +++ b/src/systems/model_parsing.jl @@ -232,21 +232,21 @@ function parse_variable_def!(dict, mod, arg, varclass, kwargs, where_types; varclass, where_types, meta) return var, def, Dict() end - Expr(:tuple, Expr(:ref, a, b...), y) || Expr(:tuple, Expr(:(::), Expr(:ref, a, b...), n), y) => begin - isdefined(mod, :n) || (n = Real) + Expr(:tuple, Expr(:(::), Expr(:ref, a, b...), type), y) || Expr(:tuple, Expr(:ref, a, b...), y) => begin + (@isdefined type) || (type = Real) varname = Meta.isexpr(a, :call) ? a.args[1] : a push!(kwargs, Expr(:kw, varname, nothing)) varval = unit_handled_variable_value(mod, y, varname) if varclass == :parameters - var = :($varname = $first(@parameters $a[$(b...)]::$n = ($varval, $y))) + var = :($varname = $first(@parameters $a[$(b...)]::$type = ($varval, $y))) else - var = :($varname = $first(@variables $a[$(b...)]::$n = ($varval, $y))) + var = :($varname = $first(@variables $a[$(b...)]::$type = ($varval, $y))) end #TODO: update `dict` aka `Model.structure` with the metadata (:($varname...), var), nothing, Dict() end - Expr(:(=), Expr(:ref, a, b...), y) || Expr(:(=), Expr(:(::), Expr(:ref, a, b...), n), y) => begin - isdefined(mod, :n) || (n = Real) + Expr(:(=), Expr(:(::), Expr(:ref, a, b...), type), y) || Expr(:(=), Expr(:ref, a, b...), y) => begin + (@isdefined type) || (type = Real) varname = Meta.isexpr(a, :call) ? a.args[1] : a if Meta.isexpr(y, :tuple) varval = unit_handled_variable_value(mod, y, varname) @@ -254,33 +254,34 @@ function parse_variable_def!(dict, mod, arg, varclass, kwargs, where_types; push!(kwargs, Expr(:kw, varname, nothing)) if varclass == :parameters var = :($varname = $varname === nothing ? $val : $varname; - $varname = $first(@parameters $a[$(b...)]::$n = ( + $varname = $first(@parameters $a[$(b...)]::$type = ( $varval, $(y...)))) else var = :($varname = $varname === nothing ? $val : $varname; - $varname = $first(@variables $a[$(b...)]::$n = ( + $varname = $first(@variables $a[$(b...)]::$type = ( $varval, $(y...)))) end else push!(kwargs, Expr(:kw, varname, nothing)) if varclass == :parameters var = :($varname = $varname === nothing ? $y : $varname; - $varname = $first(@parameters $a[$(b...)]::$n = $varname)) + $varname = $first(@parameters $a[$(b...)]::$type = $varname)) else var = :($varname = $varname === nothing ? $y : $varname; - $varname = $first(@variables $a[$(b...)]::$n = $varname)) + $varname = $first(@variables $a[$(b...)]::$type = $varname)) end end #TODO: update `dict`` aka `Model.structure` with the metadata (:($varname...), var), nothing, Dict() end - Expr(:ref, a, b...) => begin + Expr(:(::), Expr(:ref, a, b...), type) || Expr(:ref, a, b...) => begin + (@isdefined type) || (type = Real) varname = a isa Expr && a.head == :call ? a.args[1] : a push!(kwargs, Expr(:kw, varname, nothing)) if varclass == :parameters - var = :($varname = $first(@parameters $a[$(b...)] = $varname)) + var = :($varname = $first(@parameters $a[$(b...)]::$type = $varname)) elseif varclass == :variables - var = :($varname = $first(@variables $a[$(b...)] = $varname)) + var = :($varname = $first(@variables $a[$(b...)]::$type = $varname)) else throw("Symbolic array with arbitrary length is not handled for $varclass. Please open an issue with an example.") diff --git a/test/model_parsing.jl b/test/model_parsing.jl index 471e1f4651..9d903842c2 100644 --- a/test/model_parsing.jl +++ b/test/model_parsing.jl @@ -303,9 +303,9 @@ end @test symtype(type_model.par2) == Int @test symtype(type_model.par3) == BigFloat @test symtype(type_model.par4) == Float64 - @test_broken symtype(type_model.par5[1]) == BigFloat - @test_broken symtype(type_model.par6[1]) == BigFloat - @test_broken symtype(type_model.par7[1, 1]) == BigFloat + @test symtype(type_model.par5[1]) == BigFloat + @test symtype(type_model.par6[1]) == BigFloat + @test symtype(type_model.par7[1, 1]) == BigFloat @test_throws TypeError TypeModel(; name = :throws, flag = 1) @test_throws TypeError TypeModel(; name = :throws, par0 = 1) @@ -317,7 +317,7 @@ end # Test that array types are correctly added. @named type_model2 = TypeModel(; par5 = rand(BigFloat, 3)) - @test_broken symtype(type_model2.par5[1]) == BigFloat + @test symtype(type_model2.par5[1]) == BigFloat @named type_model3 = TypeModel(; par7 = rand(BigFloat, 3, 3)) @test symtype(type_model3.par7[1, 1]) == BigFloat From 153a293ba25b6896b3a6beaaefb107b6c5f1ba4a Mon Sep 17 00:00:00 2001 From: CompatHelper Julia Date: Sat, 28 Sep 2024 00:22:47 +0000 Subject: [PATCH 0038/1337] CompatHelper: bump compat for DiffEqCallbacks to 4, (keep existing compat) --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 32a18bfdc0..ff17093ae9 100644 --- a/Project.toml +++ b/Project.toml @@ -81,7 +81,7 @@ DataInterpolations = "6.4" DataStructures = "0.17, 0.18" DeepDiffs = "1" DiffEqBase = "6.103.0" -DiffEqCallbacks = "2.16, 3" +DiffEqCallbacks = "2.16, 3, 4" DiffEqNoiseProcess = "5" DiffRules = "0.1, 1.0" Distributed = "1" From 39a63f9e7afa4fc6bccdc567a58f6099336a8e91 Mon Sep 17 00:00:00 2001 From: ArnoStrouwen Date: Tue, 1 Oct 2024 03:30:30 +0200 Subject: [PATCH 0039/1337] Start rewriting programmatically generating systems docs --- .../tutorials/programmatically_generating.md | 44 +++++++++++-------- 1 file changed, 26 insertions(+), 18 deletions(-) diff --git a/docs/src/tutorials/programmatically_generating.md b/docs/src/tutorials/programmatically_generating.md index 8b8b03027a..bd1d2359f1 100644 --- a/docs/src/tutorials/programmatically_generating.md +++ b/docs/src/tutorials/programmatically_generating.md @@ -1,42 +1,44 @@ # [Programmatically Generating and Scripting ODESystems](@id programmatically) -In the following tutorial we will discuss how to programmatically generate `ODESystem`s. -This is for cases where one is writing functions that generating `ODESystem`s, for example -if implementing a reader which parses some file format to generate an `ODESystem` (for example, -SBML), or for writing functions that transform an `ODESystem` (for example, if you write a -function that log-transforms a variable in an `ODESystem`). +In the following tutorial, we will discuss how to programmatically generate `ODESystem`s. +This is useful for functions that generate `ODESystem`s, for example +when you implement a reader that parses some file format, such as SBML, to generate an `ODESystem`. +It is also useful for functions that transform an `ODESystem`, for example +when you write a function that log-transforms a variable in an `ODESystem`. ## The Representation of a ModelingToolkit System ModelingToolkit is built on [Symbolics.jl](https://symbolics.juliasymbolics.org/dev/), a symbolic Computer Algebra System (CAS) developed in Julia. As such, all CAS functionality -is available on ModelingToolkit systems, such as symbolic differentiation, Groebner basis +is also available to be used on ModelingToolkit systems, such as symbolic differentiation, Groebner basis calculations, and whatever else you can think of. Under the hood, all ModelingToolkit variables and expressions are Symbolics.jl variables and expressions. Thus when scripting a ModelingToolkit system, one simply needs to generate Symbolics.jl variables and equations as demonstrated in the Symbolics.jl documentation. This looks like: ```@example scripting -using Symbolics -using ModelingToolkit: t_nounits as t, D_nounits as D - -@variables x(t) y(t) # Define variables +using ModelingToolkit # reexports Symbolics +@variables t x(t) y(t) # Define variables +D = Differential(t) eqs = [D(x) ~ y D(y) ~ x] # Define an array of equations ``` +However, ModelingToolkit has many higher-level features which will make scripting ModelingToolkit systems more convenient. +For example, as shown in the next section, defining your own independent variables and differentials is rarely needed. + ## The Non-DSL (non-`@mtkmodel`) Way of Defining an ODESystem -Using `@mtkmodel` is the preferred way of defining ODEs with MTK. However, let us -look at how we can define the same system without `@mtkmodel`. This is useful for -defining PDESystem etc. +Using `@mtkmodel`, like in the [getting started tutorial](@ref getting_started), +is the preferred way of defining ODEs with MTK. +However generating the contents of a `@mtkmodel` programmatically can be tedious. +Let us look at how we can define the same system without `@mtkmodel`. ```@example scripting using ModelingToolkit using ModelingToolkit: t_nounits as t, D_nounits as D - -@variables x(t) # independent and dependent variables -@parameters τ # parameters +@variables x(t) = 0.0 # independent and dependent variables +@parameters τ = 3.0 # parameters @constants h = 1 # constants eqs = [D(x) ~ (h - x) / τ] # create an array of equations @@ -45,10 +47,16 @@ eqs = [D(x) ~ (h - x) / τ] # create an array of equations # Perform the standard transformations and mark the model complete # Note: Complete models cannot be subsystems of other models! -fol_model = structural_simplify(model) +fol = structural_simplify(model) +prob = ODEProblem(fol, [], (0.0, 10.0), []) +using DifferentialEquations: solve +sol = solve(prob) + +using Plots +plot(sol) ``` -As you can see, generating an ODESystem is as simple as creating the array of equations +As you can see, generating an ODESystem is as simple as creating an array of equations and passing it to the `ODESystem` constructor. ## Understanding the Difference Between the Julia Variable and the Symbolic Variable From 928ee8ddb057e65d173214caab30b95be5b75f90 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 20 Sep 2024 15:45:57 +0530 Subject: [PATCH 0040/1337] feat: add `is_dde` flag for `ODESystem` and `SDESystem` --- src/systems/abstractsystem.jl | 1 + src/systems/diffeqs/abstractodesystem.jl | 23 +++++++++++++++++++++++ src/systems/diffeqs/odesystem.jl | 16 ++++++++++++---- src/systems/diffeqs/sdesystem.jl | 15 ++++++++++++--- test/dde.jl | 5 +++++ test/odesystem.jl | 8 ++++++++ 6 files changed, 61 insertions(+), 7 deletions(-) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index e8ec8cc06f..cd7bc52346 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -971,6 +971,7 @@ for prop in [:eqs :solved_unknowns :split_idxs :parent + :is_dde :index_cache :is_scalar_noise :isscheduled] diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index f7f94504e4..982cef0121 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -3,6 +3,29 @@ struct Schedule dummy_sub::Any end +""" + is_dde(sys::AbstractSystem) + +Return a boolean indicating whether a system represents a set of delay +differential equations. +""" +is_dde(sys::AbstractSystem) = has_is_dde(sys) && get_is_dde(sys) + +function _check_if_dde(eqs, iv, subsystems) + is_dde = any(ModelingToolkit.is_dde, subsystems) + if !is_dde + vs = Set() + for eq in eqs + vars!(vs, eq) + is_dde = any(vs) do sym + isdelay(unwrap(sym), iv) + end + is_dde && break + end + end + return is_dde +end + function filter_kwargs(kwargs) kwargs = Dict(kwargs) for key in keys(kwargs) diff --git a/src/systems/diffeqs/odesystem.jl b/src/systems/diffeqs/odesystem.jl index a1fb333092..d746318b09 100644 --- a/src/systems/diffeqs/odesystem.jl +++ b/src/systems/diffeqs/odesystem.jl @@ -141,6 +141,10 @@ struct ODESystem <: AbstractODESystem """ gui_metadata::Union{Nothing, GUIMetadata} """ + A boolean indicating if the given `ODESystem` represents a system of DDEs. + """ + is_dde::Bool + """ Cache for intermediate tearing state. """ tearing_state::Any @@ -178,7 +182,7 @@ struct ODESystem <: AbstractODESystem torn_matching, initializesystem, initialization_eqs, schedule, connector_type, preface, cevents, devents, parameter_dependencies, - metadata = nothing, gui_metadata = nothing, + metadata = nothing, gui_metadata = nothing, is_dde = false, tearing_state = nothing, substitutions = nothing, complete = false, index_cache = nothing, discrete_subsystems = nothing, solved_unknowns = nothing, @@ -198,7 +202,7 @@ struct ODESystem <: AbstractODESystem ctrl_jac, Wfact, Wfact_t, name, systems, defaults, guesses, torn_matching, initializesystem, initialization_eqs, schedule, connector_type, preface, cevents, devents, parameter_dependencies, metadata, - gui_metadata, tearing_state, substitutions, complete, index_cache, + gui_metadata, is_dde, tearing_state, substitutions, complete, index_cache, discrete_subsystems, solved_unknowns, split_idxs, parent) end end @@ -223,7 +227,8 @@ function ODESystem(deqs::AbstractVector{<:Equation}, iv, dvs, ps; parameter_dependencies = Equation[], checks = true, metadata = nothing, - gui_metadata = nothing) + gui_metadata = nothing, + is_dde = nothing) name === nothing && throw(ArgumentError("The `name` keyword must be provided. Please consider using the `@named` macro")) @assert all(control -> any(isequal.(control, ps)), controls) "All controls must also be parameters." @@ -266,12 +271,15 @@ function ODESystem(deqs::AbstractVector{<:Equation}, iv, dvs, ps; disc_callbacks = SymbolicDiscreteCallbacks(discrete_events) parameter_dependencies, ps′ = process_parameter_dependencies( parameter_dependencies, ps′) + if is_dde === nothing + is_dde = _check_if_dde(deqs, iv′, systems) + end ODESystem(Threads.atomic_add!(SYSTEM_COUNT, UInt(1)), deqs, iv′, dvs′, ps′, tspan, var_to_name, ctrl′, observed, tgrad, jac, ctrl_jac, Wfact, Wfact_t, name, systems, defaults, guesses, nothing, initializesystem, initialization_eqs, schedule, connector_type, preface, cont_callbacks, disc_callbacks, parameter_dependencies, - metadata, gui_metadata, checks = checks) + metadata, gui_metadata, is_dde, checks = checks) end function ODESystem(eqs, iv; kwargs...) diff --git a/src/systems/diffeqs/sdesystem.jl b/src/systems/diffeqs/sdesystem.jl index ee16728133..1b368f7df3 100644 --- a/src/systems/diffeqs/sdesystem.jl +++ b/src/systems/diffeqs/sdesystem.jl @@ -133,6 +133,10 @@ struct SDESystem <: AbstractODESystem be `true` when `noiseeqs isa Vector`. """ is_scalar_noise::Bool + """ + A boolean indicating if the given `ODESystem` represents a system of DDEs. + """ + is_dde::Bool isscheduled::Bool function SDESystem(tag, deqs, neqs, iv, dvs, ps, tspan, var_to_name, ctrls, observed, @@ -141,6 +145,7 @@ struct SDESystem <: AbstractODESystem ctrl_jac, Wfact, Wfact_t, name, systems, defaults, connector_type, cevents, devents, parameter_dependencies, metadata = nothing, gui_metadata = nothing, complete = false, index_cache = nothing, parent = nothing, is_scalar_noise = false, + is_dde = false, isscheduled = false; checks::Union{Bool, Int} = true) if checks == true || (checks & CheckComponents) > 0 @@ -165,7 +170,7 @@ struct SDESystem <: AbstractODESystem ctrl_jac, Wfact, Wfact_t, name, systems, defaults, connector_type, cevents, devents, parameter_dependencies, metadata, gui_metadata, complete, index_cache, parent, is_scalar_noise, - isscheduled) + is_dde, isscheduled) end end @@ -188,7 +193,8 @@ function SDESystem(deqs::AbstractVector{<:Equation}, neqs::AbstractArray, iv, dv complete = false, index_cache = nothing, parent = nothing, - is_scalar_noise = false) + is_scalar_noise = false, + is_dde = nothing) name === nothing && throw(ArgumentError("The `name` keyword must be provided. Please consider using the `@named` macro")) iv′ = value(iv) @@ -223,11 +229,14 @@ function SDESystem(deqs::AbstractVector{<:Equation}, neqs::AbstractArray, iv, dv disc_callbacks = SymbolicDiscreteCallbacks(discrete_events) parameter_dependencies, ps′ = process_parameter_dependencies( parameter_dependencies, ps′) + if is_dde === nothing + is_dde = _check_if_dde(deqs, iv′, systems) + end SDESystem(Threads.atomic_add!(SYSTEM_COUNT, UInt(1)), deqs, neqs, iv′, dvs′, ps′, tspan, var_to_name, ctrl′, observed, tgrad, jac, ctrl_jac, Wfact, Wfact_t, name, systems, defaults, connector_type, cont_callbacks, disc_callbacks, parameter_dependencies, metadata, gui_metadata, - complete, index_cache, parent, is_scalar_noise; checks = checks) + complete, index_cache, parent, is_scalar_noise, is_dde; checks = checks) end function SDESystem(sys::ODESystem, neqs; kwargs...) diff --git a/test/dde.jl b/test/dde.jl index 439db46fe9..90f02cd963 100644 --- a/test/dde.jl +++ b/test/dde.jl @@ -38,6 +38,7 @@ eqs = [D(x₀) ~ (v0 / (1 + beta0 * (x₂(t - tau)^2))) * (p0 - q0) * x₀ - d0 (v1 / (1 + beta1 * (x₂(t - tau)^2))) * (p1 - q1) * x₁ - d1 * x₁ D(x₂(t)) ~ (v1 / (1 + beta1 * (x₂(t - tau)^2))) * (1 - p1 + q1) * x₁ - d2 * x₂(t)] @mtkbuild sys = System(eqs, t) +@test ModelingToolkit.is_dde(sys) prob = DDEProblem(sys, [x₀ => 1.0, x₁ => 1.0, x₂(t) => 1.0], tspan, @@ -77,6 +78,7 @@ sol = solve(prob, RKMil()) τ = 1.0 eqs = [D(x(t)) ~ a * x(t) + b * x(t - τ) + c + (α * x(t) + γ) * η] @mtkbuild sys = System(eqs, t) +@test ModelingToolkit.is_dde(sys) @test equations(sys) == [D(x(t)) ~ a * x(t) + b * x(t - τ) + c] @test isequal(ModelingToolkit.get_noiseeqs(sys), [α * x(t) + γ]) prob_mtk = SDDEProblem(sys, [x(t) => 1.0 + t], tspan; constant_lags = (τ,)); @@ -100,8 +102,11 @@ end eqs = [osc1.jcn ~ osc2.delx, osc2.jcn ~ osc1.delx] @named coupledOsc = System(eqs, t) +@test ModelingToolkit.is_dde(coupledOsc) @named coupledOsc = compose(coupledOsc, systems) +@test ModelingToolkit.is_dde(coupledOsc) @named coupledOsc2 = System(eqs, t; systems) +@test ModelingToolkit.is_dde(coupledOsc2) for coupledOsc in [coupledOsc, coupledOsc2] local sys = structural_simplify(coupledOsc) @test length(equations(sys)) == 4 diff --git a/test/odesystem.jl b/test/odesystem.jl index fa0aa0acb5..748a6a4df5 100644 --- a/test/odesystem.jl +++ b/test/odesystem.jl @@ -1394,3 +1394,11 @@ end prob = @test_nowarn ODEProblem(sys, nothing, (0.0, 1.0)) @test_nowarn solve(prob) end + +@testset "ODEs are not DDEs" begin + @variables x(t) + @named sys = ODESystem(D(x) ~ x, t) + @test !ModelingToolkit.is_dde(sys) + @named sys2 = ODESystem(Equation[], t; systems = [sys]) + @test !ModelingToolkit.is_dde(sys) +end From ea26d5825b651df96669ec9b294eaf9622a52c1d Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 17 Sep 2024 14:10:30 +0530 Subject: [PATCH 0041/1337] feat: support observed function generation for DDEs --- src/systems/abstractsystem.jl | 4 ++++ src/systems/diffeqs/odesystem.jl | 30 ++++++++++++++++++++++++++++-- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index cd7bc52346..9bf8d14b0e 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -805,6 +805,10 @@ function SymbolicIndexingInterface.observed( return let _fn = _fn fn1(u, p, t) = _fn(u, p, t) fn1(u, p::MTKParameters, t) = _fn(u, p..., t) + + # DDEs + fn1(u, histfn, p, t) = _fn(u, histfn, p, t) + fn1(u, histfn, p::MTKParameters, t) = _fn(u, histfn, p..., t) fn1 end else diff --git a/src/systems/diffeqs/odesystem.jl b/src/systems/diffeqs/odesystem.jl index d746318b09..a66e34b0c3 100644 --- a/src/systems/diffeqs/odesystem.jl +++ b/src/systems/diffeqs/odesystem.jl @@ -411,6 +411,9 @@ function build_explicit_observed_function(sys, ts; ts = [ts] end ts = unwrap.(ts) + if is_dde(sys) + ts = map(x -> delay_to_function(sys, x), ts) + end vars = Set() foreach(v -> vars!(vars, v; op), ts) @@ -483,8 +486,12 @@ function build_explicit_observed_function(sys, ts; end ts = map(t -> substitute(t, subs), ts) obsexprs = [] + for i in 1:maxidx eq = obs[i] + if is_dde(sys) + eq = delay_to_function(sys, eq) + end lhs = eq.lhs rhs = eq.rhs push!(obsexprs, lhs ← rhs) @@ -505,12 +512,17 @@ function build_explicit_observed_function(sys, ts; ps = (DestructuredArgs(unwrap.(ps), inbounds = !checkbounds),) end dvs = DestructuredArgs(unknowns(sys), inbounds = !checkbounds) + if is_dde(sys) + dvs = (dvs, DDE_HISTORY_FUN) + else + dvs = (dvs,) + end if inputs === nothing - args = param_only ? [ps..., ivs...] : [dvs, ps..., ivs...] + args = param_only ? [ps..., ivs...] : [dvs..., ps..., ivs...] else inputs = unwrap.(inputs) ipts = DestructuredArgs(inputs, inbounds = !checkbounds) - args = param_only ? [ipts, ps..., ivs...] : [dvs, ipts, ps..., ivs...] + args = param_only ? [ipts, ps..., ivs...] : [dvs..., ipts, ps..., ivs...] end pre = get_postprocess_fbody(sys) @@ -546,6 +558,20 @@ function build_explicit_observed_function(sys, ts; end end +function populate_delays(delays::Set, obsexprs, histfn, sys, sym) + _vars_util = vars(sym) + for v in _vars_util + v in delays && continue + iscall(v) && issym(operation(v)) && (args = arguments(v); length(args) == 1) && + iscall(only(args)) || continue + + idx = variable_index(sys, operation(v)(get_iv(sys))) + idx === nothing && error("Delay term $v is not an unknown in the system") + push!(delays, v) + push!(obsexprs, v ← histfn(only(args))[idx]) + end +end + function _eq_unordered(a, b) length(a) === length(b) || return false n = length(a) From 65777da8aa61d40d022554ea92dbfd3b445685d3 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 17 Sep 2024 15:07:09 +0530 Subject: [PATCH 0042/1337] test: test symbolic indexing of DDE solutions --- test/dde.jl | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/test/dde.jl b/test/dde.jl index 90f02cd963..7ab465e6f2 100644 --- a/test/dde.jl +++ b/test/dde.jl @@ -51,6 +51,8 @@ prob2 = DDEProblem(sys, constant_lags = [tau]) sol2_mtk = solve(prob2, alg, reltol = 1e-7, abstol = 1e-10) @test sol2_mtk.u[end] ≈ sol2.u[end] +@test_nowarn sol2_mtk[[x₀, x₁, x₂(t)]] +@test_nowarn sol2_mtk[[x₀, x₁, x₂(t - 0.1)]] using StochasticDelayDiffEq function hayes_modelf(du, u, h, p, t) @@ -102,7 +104,6 @@ end eqs = [osc1.jcn ~ osc2.delx, osc2.jcn ~ osc1.delx] @named coupledOsc = System(eqs, t) -@test ModelingToolkit.is_dde(coupledOsc) @named coupledOsc = compose(coupledOsc, systems) @test ModelingToolkit.is_dde(coupledOsc) @named coupledOsc2 = System(eqs, t; systems) @@ -112,3 +113,10 @@ for coupledOsc in [coupledOsc, coupledOsc2] @test length(equations(sys)) == 4 @test length(unknowns(sys)) == 4 end +sys = structural_simplify(coupledOsc) +prob = DDEProblem(sys, [], (0.0, 10.0); constant_lags = [sys.osc1.τ, sys.osc2.τ]) +sol = solve(prob, MethodOfSteps(Tsit5())) +obsfn = ModelingToolkit.build_explicit_observed_function( + sys, [sys.osc1.delx, sys.osc2.delx]) +@test_nowarn sol[[sys.osc1.delx, sys.osc2.delx]] +@test sol[sys.osc1.delx] ≈ sol(sol.t .- 0.01; idxs = sys.osc1.x) From 50148d4d8bef936c65fafaa286da8629e81c0f51 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 24 Sep 2024 11:25:15 +0530 Subject: [PATCH 0043/1337] fix: propagate `is_dde` through `flatten` and `compose` --- src/systems/abstractsystem.jl | 3 +++ src/systems/diffeqs/odesystem.jl | 1 + 2 files changed, 4 insertions(+) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index 9bf8d14b0e..35641ef77e 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -2960,6 +2960,9 @@ function compose(sys::AbstractSystem, systems::AbstractArray; name = nameof(sys) nsys == 0 && return sys @set! sys.name = name @set! sys.systems = [get_systems(sys); systems] + if has_is_dde(sys) + @set! sys.is_dde = _check_if_dde(equations(sys), get_iv(sys), get_systems(sys)) + end return sys end function compose(syss...; name = nameof(first(syss))) diff --git a/src/systems/diffeqs/odesystem.jl b/src/systems/diffeqs/odesystem.jl index a66e34b0c3..942c87afcc 100644 --- a/src/systems/diffeqs/odesystem.jl +++ b/src/systems/diffeqs/odesystem.jl @@ -382,6 +382,7 @@ function flatten(sys::ODESystem, noeqs = false) defaults = defaults(sys), name = nameof(sys), initialization_eqs = initialization_equations(sys), + is_dde = is_dde(sys), checks = false) end end From f8030f86a0ed7fe9faafbfb90ab231030f76d115 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 25 Sep 2024 14:06:19 +0530 Subject: [PATCH 0044/1337] refactor: allow changing name of parameter argument in `delay_to_function` --- src/systems/diffeqs/abstractodesystem.jl | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index 982cef0121..06feaac688 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -242,29 +242,32 @@ function isdelay(var, iv) return false end const DDE_HISTORY_FUN = Sym{Symbolics.FnType{Tuple{Any, <:Real}, Vector{Real}}}(:___history___) -function delay_to_function(sys::AbstractODESystem, eqs = full_equations(sys)) +const DEFAULT_PARAMS_ARG = Sym{Any}(:ˍ₋arg3) +function delay_to_function( + sys::AbstractODESystem, eqs = full_equations(sys); history_arg = DEFAULT_PARAMS_ARG) delay_to_function(eqs, get_iv(sys), Dict{Any, Int}(operation(s) => i for (i, s) in enumerate(unknowns(sys))), parameters(sys), - DDE_HISTORY_FUN) + DDE_HISTORY_FUN; history_arg) end -function delay_to_function(eqs::Vector, iv, sts, ps, h) - delay_to_function.(eqs, (iv,), (sts,), (ps,), (h,)) +function delay_to_function(eqs::Vector, iv, sts, ps, h; history_arg = DEFAULT_PARAMS_ARG) + delay_to_function.(eqs, (iv,), (sts,), (ps,), (h,); history_arg) end -function delay_to_function(eq::Equation, iv, sts, ps, h) - delay_to_function(eq.lhs, iv, sts, ps, h) ~ delay_to_function(eq.rhs, iv, sts, ps, h) +function delay_to_function(eq::Equation, iv, sts, ps, h; history_arg = DEFAULT_PARAMS_ARG) + delay_to_function(eq.lhs, iv, sts, ps, h; history_arg) ~ delay_to_function( + eq.rhs, iv, sts, ps, h; history_arg) end -function delay_to_function(expr, iv, sts, ps, h) +function delay_to_function(expr, iv, sts, ps, h; history_arg = DEFAULT_PARAMS_ARG) if isdelay(expr, iv) v = operation(expr) time = arguments(expr)[1] idx = sts[v] - return term(getindex, h(Sym{Any}(:ˍ₋arg3), time), idx, type = Real) # BIG BIG HACK + return term(getindex, h(history_arg, time), idx, type = Real) # BIG BIG HACK elseif iscall(expr) return maketerm(typeof(expr), operation(expr), - map(x -> delay_to_function(x, iv, sts, ps, h), arguments(expr)), + map(x -> delay_to_function(x, iv, sts, ps, h; history_arg), arguments(expr)), metadata(expr)) else return expr From 0e50dcf68c844f559a9637315ea7acd6e1759692 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 25 Sep 2024 14:07:20 +0530 Subject: [PATCH 0045/1337] fix: fix and document `wrap_mtkparameters` --- src/systems/abstractsystem.jl | 66 ++++++++++++++++++++++++----------- 1 file changed, 46 insertions(+), 20 deletions(-) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index 35641ef77e..9b5a3ab6dd 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -398,50 +398,76 @@ function wrap_array_vars( end end -function wrap_mtkparameters(sys::AbstractSystem, isscalar::Bool) +const MTKPARAMETERS_ARG = Sym{Vector{Vector}}(:___mtkparameters___) + +""" + wrap_mtkparameters(sys::AbstractSystem, isscalar::Bool, p_start = 2) + +Return function(s) to be passed to the `wrap_code` keyword of `build_function` which +allow the compiled function to be called as `f(u, p, t)` where `p isa MTKParameters` +instead of `f(u, p..., t)`. `isscalar` denotes whether the function expression being +wrapped is for a scalar value. `p_start` is the index of the argument containing +the first parameter vector in the out-of-place version of the function. For example, +if a history function (DDEs) was passed before `p`, then the function before wrapping +would have the signature `f(u, h, p..., t)` and hence `p_start` would need to be `3`. + +The returned function is `identity` if the system does not have an `IndexCache`. +""" +function wrap_mtkparameters(sys::AbstractSystem, isscalar::Bool, p_start = 2) if has_index_cache(sys) && get_index_cache(sys) !== nothing offset = Int(is_time_dependent(sys)) if isscalar function (expr) - p = gensym(:p) + param_args = expr.args[p_start:(end - offset)] + param_buffer_idxs = findall(x -> x isa DestructuredArgs, param_args) + param_buffer_args = param_args[param_buffer_idxs] + destructured_mtkparams = DestructuredArgs( + [x.name for x in param_buffer_args], + MTKPARAMETERS_ARG; inds = param_buffer_idxs) Func( [ - expr.args[1], - DestructuredArgs( - [arg.name for arg in expr.args[2:(end - offset)]], p), - (isone(offset) ? (expr.args[end],) : ())... + expr.args[begin:(p_start - 1)]..., + destructured_mtkparams, + expr.args[(end - offset + 1):end]... ], [], - Let(expr.args[2:(end - offset)], expr.body, false) + Let(param_buffer_args, expr.body, false) ) end else function (expr) - p = gensym(:p) + param_args = expr.args[p_start:(end - offset)] + param_buffer_idxs = findall(x -> x isa DestructuredArgs, param_args) + param_buffer_args = param_args[param_buffer_idxs] + destructured_mtkparams = DestructuredArgs( + [x.name for x in param_buffer_args], + MTKPARAMETERS_ARG; inds = param_buffer_idxs) Func( [ - expr.args[1], - DestructuredArgs( - [arg.name for arg in expr.args[2:(end - offset)]], p), - (isone(offset) ? (expr.args[end],) : ())... + expr.args[begin:(p_start - 1)]..., + destructured_mtkparams, + expr.args[(end - offset + 1):end]... ], [], - Let(expr.args[2:(end - offset)], expr.body, false) + Let(param_buffer_args, expr.body, false) ) end, function (expr) - p = gensym(:p) + param_args = expr.args[(p_start + 1):(end - offset)] + param_buffer_idxs = findall(x -> x isa DestructuredArgs, param_args) + param_buffer_args = param_args[param_buffer_idxs] + destructured_mtkparams = DestructuredArgs( + [x.name for x in param_buffer_args], + MTKPARAMETERS_ARG; inds = param_buffer_idxs) Func( [ - expr.args[1], - expr.args[2], - DestructuredArgs( - [arg.name for arg in expr.args[3:(end - offset)]], p), - (isone(offset) ? (expr.args[end],) : ())... + expr.args[begin:p_start]..., + destructured_mtkparams, + expr.args[(end - offset + 1):end]... ], [], - Let(expr.args[3:(end - offset)], expr.body, false) + Let(param_buffer_args, expr.body, false) ) end end From ab639ebea58cef4ee1deea864e5749d7d4a7d397 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 25 Sep 2024 14:08:01 +0530 Subject: [PATCH 0046/1337] feat: use `wrap_mtkparameters` in `build_explicit_observed_function` --- src/systems/diffeqs/odesystem.jl | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/src/systems/diffeqs/odesystem.jl b/src/systems/diffeqs/odesystem.jl index 942c87afcc..a063b006fd 100644 --- a/src/systems/diffeqs/odesystem.jl +++ b/src/systems/diffeqs/odesystem.jl @@ -412,8 +412,16 @@ function build_explicit_observed_function(sys, ts; ts = [ts] end ts = unwrap.(ts) + issplit = has_index_cache(sys) && get_index_cache(sys) !== nothing if is_dde(sys) - ts = map(x -> delay_to_function(sys, x), ts) + if issplit + ts = map( + x -> delay_to_function( + sys, x; history_arg = issplit ? MTKPARAMETERS_ARG : DEFAULT_PARAMS_ARG), + ts) + else + ts = map(x -> delay_to_function(sys, x), ts) + end end vars = Set() @@ -491,7 +499,8 @@ function build_explicit_observed_function(sys, ts; for i in 1:maxidx eq = obs[i] if is_dde(sys) - eq = delay_to_function(sys, eq) + eq = delay_to_function( + sys, eq; history_arg = issplit ? MTKPARAMETERS_ARG : DEFAULT_PARAMS_ARG) end lhs = eq.lhs rhs = eq.rhs @@ -518,12 +527,14 @@ function build_explicit_observed_function(sys, ts; else dvs = (dvs,) end + p_start = param_only ? 1 : (length(dvs) + 1) if inputs === nothing args = param_only ? [ps..., ivs...] : [dvs..., ps..., ivs...] else inputs = unwrap.(inputs) ipts = DestructuredArgs(inputs, inbounds = !checkbounds) args = param_only ? [ipts, ps..., ivs...] : [dvs..., ipts, ps..., ivs...] + p_start += 1 end pre = get_postprocess_fbody(sys) @@ -534,19 +545,27 @@ function build_explicit_observed_function(sys, ts; wrap_array_vars(sys, ts; ps = _ps, inputs) .∘ wrap_parameter_dependencies(sys, isscalar) end + mtkparams_wrapper = wrap_mtkparameters(sys, isscalar, p_start) + if mtkparams_wrapper isa Tuple + oop_mtkp_wrapper = mtkparams_wrapper[1] + else + oop_mtkp_wrapper = mtkparams_wrapper + end + # Need to keep old method of building the function since it uses `output_type`, # which can't be provided to `build_function` oop_fn = Func(args, [], pre(Let(obsexprs, isscalar ? ts[1] : MakeArray(ts, output_type), - false))) |> array_wrapper[1] |> toexpr + false))) |> array_wrapper[1] |> oop_mtkp_wrapper |> toexpr oop_fn = expression ? oop_fn : eval_or_rgf(oop_fn; eval_expression, eval_module) if !isscalar iip_fn = build_function(ts, args...; postprocess_fbody = pre, - wrap_code = array_wrapper .∘ wrap_assignments(isscalar, obsexprs), + wrap_code = array_wrapper .∘ wrap_assignments(isscalar, obsexprs) .∘ + mtkparams_wrapper, expression = Val{true})[2] if !expression iip_fn = eval_or_rgf(iip_fn; eval_expression, eval_module) From d5912be9bc21c2e21a2612692b5b49d0306ce3ec Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 25 Sep 2024 14:09:02 +0530 Subject: [PATCH 0047/1337] refactor: don't splat `MTKParameters` into observed functions anymore --- src/systems/abstractsystem.jl | 50 +++++++++-------------------------- test/clock.jl | 2 +- test/input_output_handling.jl | 4 +-- test/odesystem.jl | 2 +- test/reduction.jl | 2 +- test/serialization.jl | 2 +- 6 files changed, 19 insertions(+), 43 deletions(-) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index 9b5a3ab6dd..a30ad818f6 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -695,25 +695,17 @@ function SymbolicIndexingInterface.parameter_observed(sys::AbstractSystem, sym) if rawobs isa Tuple if is_time_dependent(sys) obsfn = let oop = rawobs[1], iip = rawobs[2] - f1a(p::MTKParameters, t) = oop(p..., t) - f1a(out, p::MTKParameters, t) = iip(out, p..., t) + f1a(p, t) = oop(p, t) + f1a(out, p, t) = iip(out, p, t) end else obsfn = let oop = rawobs[1], iip = rawobs[2] - f1b(p::MTKParameters) = oop(p...) - f1b(out, p::MTKParameters) = iip(out, p...) + f1b(p) = oop(p) + f1b(out, p) = iip(out, p) end end else - if is_time_dependent(sys) - obsfn = let rawobs = rawobs - f2a(p::MTKParameters, t) = rawobs(p..., t) - end - else - obsfn = let rawobs = rawobs - f2b(p::MTKParameters) = rawobs(p...) - end - end + obsfn = rawobs end else obsfn = build_explicit_observed_function(sys, sym; param_only = true) @@ -828,21 +820,11 @@ function SymbolicIndexingInterface.observed( _fn = build_explicit_observed_function(sys, sym; eval_expression, eval_module) if is_time_dependent(sys) - return let _fn = _fn - fn1(u, p, t) = _fn(u, p, t) - fn1(u, p::MTKParameters, t) = _fn(u, p..., t) - - # DDEs - fn1(u, histfn, p, t) = _fn(u, histfn, p, t) - fn1(u, histfn, p::MTKParameters, t) = _fn(u, histfn, p..., t) - fn1 - end + return _fn else return let _fn = _fn fn2(u, p) = _fn(u, p) - fn2(u, p::MTKParameters) = _fn(u, p...) fn2(::Nothing, p) = _fn([], p) - fn2(::Nothing, p::MTKParameters) = _fn([], p...) fn2 end end @@ -2380,8 +2362,8 @@ function linearization_function(sys::AbstractSystem, inputs, u_getter = u_getter function (u, p, t) - p_setter!(oldps, p_getter(u, p..., t)) - newu = u_getter(u, p..., t) + p_setter!(oldps, p_getter(u, p, t)) + newu = u_getter(u, p, t) return newu, oldps end end @@ -2392,20 +2374,15 @@ function linearization_function(sys::AbstractSystem, inputs, function (u, p, t) state = ProblemState(; u, p, t) - return u_getter(state), p_getter(state) + return u_getter( + state_values(state), parameter_values(state), current_time(state)), + p_getter(state) end end end initfn = NonlinearFunction(initsys; eval_expression, eval_module) initprobmap = build_explicit_observed_function( initsys, unknowns(sys); eval_expression, eval_module) - if has_index_cache(sys) && get_index_cache(sys) !== nothing - initprobmap = let inner = initprobmap - fn(u, p::MTKParameters) = inner(u, p...) - fn(u, p) = inner(u, p) - fn - end - end ps = parameters(sys) h = build_explicit_observed_function(sys, outputs; eval_expression, eval_module) lin_fun = let diff_idxs = diff_idxs, @@ -2452,7 +2429,7 @@ function linearization_function(sys::AbstractSystem, inputs, fg_xz = ForwardDiff.jacobian(uf, u) h_xz = ForwardDiff.jacobian( let p = p, t = t - xz -> p isa MTKParameters ? h(xz, p..., t) : h(xz, p, t) + xz -> h(xz, p, t) end, u) pf = SciMLBase.ParamJacobianWrapper(fun, t, u) fg_u = jacobian_wrt_vars(pf, p, input_idxs, chunk) @@ -2464,7 +2441,6 @@ function linearization_function(sys::AbstractSystem, inputs, end hp = let u = u, t = t _hp(p) = h(u, p, t) - _hp(p::MTKParameters) = h(u, p..., t) _hp end h_u = jacobian_wrt_vars(hp, p, input_idxs, chunk) @@ -2517,7 +2493,7 @@ function linearize_symbolic(sys::AbstractSystem, inputs, dx = fun(sts, p..., t) h = build_explicit_observed_function(sys, outputs; eval_expression, eval_module) - y = h(sts, p..., t) + y = h(sts, p, t) fg_xz = Symbolics.jacobian(dx, sts) fg_u = Symbolics.jacobian(dx, inputs) diff --git a/test/clock.jl b/test/clock.jl index 5bf5e917aa..91aaa8248e 100644 --- a/test/clock.jl +++ b/test/clock.jl @@ -514,7 +514,7 @@ eqs = [yd ~ Sample(dt)(y) @test sol.prob.kwargs[:disc_saved_values][1].t == sol.t[1:2:end] # Test that the discrete-time system executed at every step of the continuous solver. The solver saves each time step twice, one state value before discrete affect and one after. @test_nowarn ModelingToolkit.build_explicit_observed_function( - model, model.counter.ud)(sol.u[1], prob.p..., sol.t[1]) + model, model.counter.ud)(sol.u[1], prob.p, sol.t[1]) @variables x(t)=1.0 y(t)=1.0 eqs = [D(y) ~ Hold(x) diff --git a/test/input_output_handling.jl b/test/input_output_handling.jl index 19078bc98c..07a26d2d1a 100644 --- a/test/input_output_handling.jl +++ b/test/input_output_handling.jl @@ -144,9 +144,9 @@ if VERSION >= v"1.8" # :opaque_closure not supported before drop_expr = identity) x = randn(size(A, 1)) u = randn(size(B, 2)) - p = getindex.( + p = (getindex.( Ref(ModelingToolkit.defaults_and_guesses(ssys)), - parameters(ssys)) + parameters(ssys)),) y1 = obsf(x, u, p, 0) y2 = C * x + D * u @test y1[] ≈ y2[] diff --git a/test/odesystem.jl b/test/odesystem.jl index 748a6a4df5..154f8eb9f3 100644 --- a/test/odesystem.jl +++ b/test/odesystem.jl @@ -548,7 +548,7 @@ prob = ODEProblem( @test_nowarn solve(prob, Tsit5()) obsfn = ModelingToolkit.build_explicit_observed_function( outersys, bar(3outersys.sys.ms, 3outersys.sys.p)) -@test_nowarn obsfn(sol.u[1], prob.p..., sol.t[1]) +@test_nowarn obsfn(sol.u[1], prob.p, sol.t[1]) # x/x @variables x(t) diff --git a/test/reduction.jl b/test/reduction.jl index 064a2efd81..2d9e950e95 100644 --- a/test/reduction.jl +++ b/test/reduction.jl @@ -119,7 +119,7 @@ prob1 = ODEProblem(reduced_system, u0, (0.0, 100.0), pp) solve(prob1, Rodas5()) prob2 = SteadyStateProblem(reduced_system, u0, pp) -@test prob2.f.observed(lorenz2.u, prob2.u0, pp) === 1.0 +@test prob2.f.observed(lorenz2.u, prob2.u0, prob2.p) === 1.0 # issue #724 and #716 let diff --git a/test/serialization.jl b/test/serialization.jl index 5e09055a92..e10de51299 100644 --- a/test/serialization.jl +++ b/test/serialization.jl @@ -50,7 +50,7 @@ for var in all_obs f = ModelingToolkit.build_explicit_observed_function(ss, var; expression = true) sym = ModelingToolkit.getname(var) |> string ex = :(if name == Symbol($sym) - return $f(u0, p..., t) + return $f(u0, p, t) end) push!(obs_exps, ex) end From eb50d7404fb7233c4e1820a958b20b401cc2e007 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 25 Sep 2024 17:18:07 +0530 Subject: [PATCH 0048/1337] feat: implement and test SII interface for non-markovian systems --- src/systems/abstractsystem.jl | 2 ++ test/dde.jl | 5 +++++ test/odesystem.jl | 2 ++ 3 files changed, 9 insertions(+) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index a30ad818f6..35fc0324d1 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -840,6 +840,8 @@ end SymbolicIndexingInterface.is_time_dependent(::AbstractTimeDependentSystem) = true SymbolicIndexingInterface.is_time_dependent(::AbstractTimeIndependentSystem) = false +SymbolicIndexingInterface.is_markovian(sys::AbstractSystem) = !is_dde(sys) + SymbolicIndexingInterface.constant_structure(::AbstractSystem) = true function SymbolicIndexingInterface.all_variable_symbols(sys::AbstractSystem) diff --git a/test/dde.jl b/test/dde.jl index 7ab465e6f2..67f9e4f599 100644 --- a/test/dde.jl +++ b/test/dde.jl @@ -1,4 +1,5 @@ using ModelingToolkit, DelayDiffEq, Test +using SymbolicIndexingInterface: is_markovian using ModelingToolkit: t_nounits as t, D_nounits as D p0 = 0.2; @@ -39,6 +40,7 @@ eqs = [D(x₀) ~ (v0 / (1 + beta0 * (x₂(t - tau)^2))) * (p0 - q0) * x₀ - d0 D(x₂(t)) ~ (v1 / (1 + beta1 * (x₂(t - tau)^2))) * (1 - p1 + q1) * x₁ - d2 * x₂(t)] @mtkbuild sys = System(eqs, t) @test ModelingToolkit.is_dde(sys) +@test !is_markovian(sys) prob = DDEProblem(sys, [x₀ => 1.0, x₁ => 1.0, x₂(t) => 1.0], tspan, @@ -81,6 +83,7 @@ sol = solve(prob, RKMil()) eqs = [D(x(t)) ~ a * x(t) + b * x(t - τ) + c + (α * x(t) + γ) * η] @mtkbuild sys = System(eqs, t) @test ModelingToolkit.is_dde(sys) +@test !is_markovian(sys) @test equations(sys) == [D(x(t)) ~ a * x(t) + b * x(t - τ) + c] @test isequal(ModelingToolkit.get_noiseeqs(sys), [α * x(t) + γ]) prob_mtk = SDDEProblem(sys, [x(t) => 1.0 + t], tspan; constant_lags = (τ,)); @@ -106,8 +109,10 @@ eqs = [osc1.jcn ~ osc2.delx, @named coupledOsc = System(eqs, t) @named coupledOsc = compose(coupledOsc, systems) @test ModelingToolkit.is_dde(coupledOsc) +@test !is_markovian(coupledOsc) @named coupledOsc2 = System(eqs, t; systems) @test ModelingToolkit.is_dde(coupledOsc2) +@test !is_markovian(coupledOsc2) for coupledOsc in [coupledOsc, coupledOsc2] local sys = structural_simplify(coupledOsc) @test length(equations(sys)) == 4 diff --git a/test/odesystem.jl b/test/odesystem.jl index 154f8eb9f3..47c8ad9661 100644 --- a/test/odesystem.jl +++ b/test/odesystem.jl @@ -1399,6 +1399,8 @@ end @variables x(t) @named sys = ODESystem(D(x) ~ x, t) @test !ModelingToolkit.is_dde(sys) + @test is_markovian(sys) @named sys2 = ODESystem(Equation[], t; systems = [sys]) @test !ModelingToolkit.is_dde(sys) + @test is_markovian(sys) end From 65e59d0bb24f23953e69bf43d6f5ef762040484e Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 26 Sep 2024 14:55:10 +0530 Subject: [PATCH 0049/1337] feat: update `wrap_array_vars` to handle history function --- src/systems/abstractsystem.jl | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index 35fc0324d1..958000ec3e 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -230,7 +230,8 @@ function wrap_parameter_dependencies(sys::AbstractSystem, isscalar) end function wrap_array_vars( - sys::AbstractSystem, exprs; dvs = unknowns(sys), ps = parameters(sys), inputs = nothing) + sys::AbstractSystem, exprs; dvs = unknowns(sys), ps = parameters(sys), + inputs = nothing, history = false) isscalar = !(exprs isa AbstractArray) array_vars = Dict{Any, AbstractArray{Int}}() if dvs !== nothing @@ -328,6 +329,19 @@ function wrap_array_vars( array_parameters[p] = (idxs, buffer_idx, sz) end end + + inputind = if history + uind + 2 + else + uind + 1 + end + params_offset = if history && hasinputs + uind + 2 + elseif history || hasinputs + uind + 1 + else + uind + end if isscalar function (expr) Func( @@ -336,10 +350,10 @@ function wrap_array_vars( Let( vcat( [k ← :(view($(expr.args[uind].name), $v)) for (k, v) in array_vars], - [k ← :(view($(expr.args[uind + hasinputs].name), $v)) + [k ← :(view($(expr.args[inputind].name), $v)) for (k, v) in input_vars], [k ← :(reshape( - view($(expr.args[uind + hasinputs + buffer_idx].name), $idxs), + view($(expr.args[params_offset + buffer_idx].name), $idxs), $sz)) for (k, (idxs, buffer_idx, sz)) in array_parameters], [k ← Code.MakeArray(v, symtype(k)) @@ -358,10 +372,10 @@ function wrap_array_vars( Let( vcat( [k ← :(view($(expr.args[uind].name), $v)) for (k, v) in array_vars], - [k ← :(view($(expr.args[uind + hasinputs].name), $v)) + [k ← :(view($(expr.args[inputind].name), $v)) for (k, v) in input_vars], [k ← :(reshape( - view($(expr.args[uind + hasinputs + buffer_idx].name), $idxs), + view($(expr.args[params_offset + buffer_idx].name), $idxs), $sz)) for (k, (idxs, buffer_idx, sz)) in array_parameters], [k ← Code.MakeArray(v, symtype(k)) @@ -380,10 +394,10 @@ function wrap_array_vars( vcat( [k ← :(view($(expr.args[uind + 1].name), $v)) for (k, v) in array_vars], - [k ← :(view($(expr.args[uind + hasinputs + 1].name), $v)) + [k ← :(view($(expr.args[inputind + 1].name), $v)) for (k, v) in input_vars], [k ← :(reshape( - view($(expr.args[uind + hasinputs + buffer_idx + 1].name), + view($(expr.args[params_offset + buffer_idx + 1].name), $idxs), $sz)) for (k, (idxs, buffer_idx, sz)) in array_parameters], From b237706ba0a817ca69bc285c44505166e00365f2 Mon Sep 17 00:00:00 2001 From: Herman Sletmoen Date: Mon, 16 Sep 2024 17:06:27 +0200 Subject: [PATCH 0050/1337] Clean up generate_initializesystem() --- src/systems/nonlinear/initializesystem.jl | 133 ++++++++++------------ 1 file changed, 58 insertions(+), 75 deletions(-) diff --git a/src/systems/nonlinear/initializesystem.jl b/src/systems/nonlinear/initializesystem.jl index aa7ad8bd69..bf3bbb227d 100644 --- a/src/systems/nonlinear/initializesystem.jl +++ b/src/systems/nonlinear/initializesystem.jl @@ -5,109 +5,92 @@ Generate `NonlinearSystem` which initializes an ODE problem from specified initi """ function generate_initializesystem(sys::ODESystem; u0map = Dict(), - name = nameof(sys), - guesses = Dict(), check_defguess = false, - default_dd_value = 0.0, - algebraic_only = false, initialization_eqs = [], - check_units = true, - kwargs...) - sts, eqs = unknowns(sys), equations(sys) + guesses = Dict(), + default_dd_guess = 0.0, + algebraic_only = false, + check_units = true, check_defguess = false, + name = nameof(sys), kwargs...) + vars = unique([unknowns(sys); getfield.((observed(sys)), :lhs)]) + vars_set = Set(vars) # for efficient in-lookup + + eqs = equations(sys) idxs_diff = isdiffeq.(eqs) idxs_alge = .!idxs_diff - num_alge = sum(idxs_alge) - - # Start the equations list with algebraic equations - eqs_ics = eqs[idxs_alge] - u0 = Vector{Pair}(undef, 0) + # prepare map for dummy derivative substitution eqs_diff = eqs[idxs_diff] - diffmap = Dict(getfield.(eqs_diff, :lhs) .=> getfield.(eqs_diff, :rhs)) - observed_diffmap = Dict(Differential(get_iv(sys)).(getfield.((observed(sys)), :lhs)) .=> - Differential(get_iv(sys)).(getfield.((observed(sys)), :rhs))) - full_diffmap = merge(diffmap, observed_diffmap) + D = Differential(get_iv(sys)) + diffmap = merge( + Dict(eq.lhs => eq.rhs for eq in eqs_diff), + Dict(D(eq.lhs) => D(eq.rhs) for eq in observed(sys)) + ) - full_states = unique([sts; getfield.((observed(sys)), :lhs)]) - set_full_states = Set(full_states) + # 1) process dummy derivatives and u0map into initialization system + eqs_ics = eqs[idxs_alge] # start equation list with algebraic equations + defs = copy(defaults(sys)) # copy so we don't modify sys.defaults guesses = merge(get_guesses(sys), todict(guesses)) schedule = getfield(sys, :schedule) - - if schedule !== nothing - guessmap = [x[1] => get(guesses, x[1], default_dd_value) - for x in schedule.dummy_sub] - dd_guess = Dict(filter(x -> !isnothing(x[1]), guessmap)) - if u0map === nothing || isempty(u0map) - filtered_u0 = u0map - else - filtered_u0 = Pair[] - for x in u0map - y = get(schedule.dummy_sub, x[1], x[1]) - y = ModelingToolkit.fixpoint_sub(y, full_diffmap) - - if y ∈ set_full_states - # defer initialization until defaults are merged below - push!(filtered_u0, y => x[2]) + if !isnothing(schedule) + for x in filter(x -> !isnothing(x[1]), schedule.dummy_sub) + # set dummy derivatives to default_dd_guess unless specified + push!(defs, x[1] => get(guesses, x[1], default_dd_guess)) + end + if !isnothing(u0map) + for (y, x) in u0map + y = get(schedule.dummy_sub, y, y) + y = fixpoint_sub(y, diffmap) + if y ∈ vars_set + # variables specified in u0 overrides defaults + push!(defs, y => x) elseif y isa Symbolics.Arr - # scalarize array # TODO: don't scalarize arrays - _y = collect(y) - for i in eachindex(_y) - push!(filtered_u0, _y[i] => x[2][i]) - end + # TODO: don't scalarize arrays + push!(defs, collect(y) .=> x) elseif y isa Symbolics.BasicSymbolic - # y is a derivative expression expanded - # add to the initialization equations - push!(eqs_ics, y ~ x[2]) + # y is a derivative expression expanded; add it to the initialization equations + push!(eqs_ics, y ~ x) else error("Initialization expression $y is currently not supported. If its a higher order derivative expression, then only the dummy derivative expressions are supported.") end end - filtered_u0 = todict(filtered_u0) end - else - dd_guess = Dict() - filtered_u0 = todict(u0map) end - defs = merge(defaults(sys), filtered_u0) - - for st in full_states - if st ∈ keys(defs) - def = defs[st] - + # 2) process other variables + for var in vars + if var ∈ keys(defs) + def = defs[var] if def isa Equation - st ∉ keys(guesses) && check_defguess && - error("Invalid setup: unknown $(st) has an initial condition equation with no guess.") + # TODO: this behavior is not tested! + var ∉ keys(guesses) && check_defguess && + error("Invalid setup: variable $(var) has an initial condition equation with no guess.") push!(eqs_ics, def) - push!(u0, st => guesses[st]) + push!(defs, var => guesses[var]) else - push!(eqs_ics, st ~ def) - push!(u0, st => def) + push!(eqs_ics, var ~ def) end - elseif st ∈ keys(guesses) - push!(u0, st => guesses[st]) + elseif var ∈ keys(guesses) + push!(defs, var => guesses[var]) elseif check_defguess - error("Invalid setup: unknown $(st) has no default value or initial guess") + error("Invalid setup: variable $(var) has no default value or initial guess") end end + # 3) process explicitly provided initialization equations if !algebraic_only - for eq in [get_initialization_eqs(sys); initialization_eqs] - _eq = ModelingToolkit.fixpoint_sub(eq, full_diffmap) - push!(eqs_ics, _eq) + initialization_eqs = [get_initialization_eqs(sys); initialization_eqs] + for eq in initialization_eqs + eq = fixpoint_sub(eq, diffmap) # expand dummy derivatives + push!(eqs_ics, eq) end end - pars = [parameters(sys); get_iv(sys)] - nleqs = [eqs_ics; observed(sys)] - - sys_nl = NonlinearSystem(nleqs, - full_states, - pars; - defaults = merge(ModelingToolkit.defaults(sys), todict(u0), dd_guess), - parameter_dependencies = parameter_dependencies(sys), + pars = [parameters(sys); get_iv(sys)] # include independent variable as pseudo-parameter + eqs_ics = [eqs_ics; observed(sys)] + return NonlinearSystem( + eqs_ics, vars, pars; + defaults = defs, parameter_dependencies = parameter_dependencies(sys), checks = check_units, - name, - kwargs...) - - return sys_nl + name, kwargs... + ) end From cc12bb99fc4700accd1d9a586fba96afc7688efc Mon Sep 17 00:00:00 2001 From: Herman Sletmoen Date: Mon, 16 Sep 2024 18:14:13 +0200 Subject: [PATCH 0051/1337] Remove unsupported/untested/unworking behavior where defaults map variables to equations --- src/systems/nonlinear/initializesystem.jl | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src/systems/nonlinear/initializesystem.jl b/src/systems/nonlinear/initializesystem.jl index bf3bbb227d..6ce85ad1a2 100644 --- a/src/systems/nonlinear/initializesystem.jl +++ b/src/systems/nonlinear/initializesystem.jl @@ -59,16 +59,7 @@ function generate_initializesystem(sys::ODESystem; # 2) process other variables for var in vars if var ∈ keys(defs) - def = defs[var] - if def isa Equation - # TODO: this behavior is not tested! - var ∉ keys(guesses) && check_defguess && - error("Invalid setup: variable $(var) has an initial condition equation with no guess.") - push!(eqs_ics, def) - push!(defs, var => guesses[var]) - else - push!(eqs_ics, var ~ def) - end + push!(eqs_ics, var ~ defs[var]) elseif var ∈ keys(guesses) push!(defs, var => guesses[var]) elseif check_defguess From a61977a51525f97819239817f3d3456ebe7c6ba5 Mon Sep 17 00:00:00 2001 From: Herman Sletmoen Date: Mon, 16 Sep 2024 18:55:27 +0200 Subject: [PATCH 0052/1337] Splat scalarized array --- src/systems/nonlinear/initializesystem.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/systems/nonlinear/initializesystem.jl b/src/systems/nonlinear/initializesystem.jl index 6ce85ad1a2..e3d28719c8 100644 --- a/src/systems/nonlinear/initializesystem.jl +++ b/src/systems/nonlinear/initializesystem.jl @@ -45,7 +45,7 @@ function generate_initializesystem(sys::ODESystem; push!(defs, y => x) elseif y isa Symbolics.Arr # TODO: don't scalarize arrays - push!(defs, collect(y) .=> x) + push!(defs, (collect(y) .=> x)...) elseif y isa Symbolics.BasicSymbolic # y is a derivative expression expanded; add it to the initialization equations push!(eqs_ics, y ~ x) From 794a421bc86668605f1d7b4134d657fbc4aba37c Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 3 Oct 2024 12:55:39 +0530 Subject: [PATCH 0053/1337] fix: promote `resid_prototype` using tunables --- src/systems/nonlinear/nonlinearsystem.jl | 35 +++++++++++++++++++----- test/nonlinearsystem.jl | 19 +++++++++++++ 2 files changed, 47 insertions(+), 7 deletions(-) diff --git a/src/systems/nonlinear/nonlinearsystem.jl b/src/systems/nonlinear/nonlinearsystem.jl index 1f36d61601..498ad1e59b 100644 --- a/src/systems/nonlinear/nonlinearsystem.jl +++ b/src/systems/nonlinear/nonlinearsystem.jl @@ -295,7 +295,7 @@ function SciMLBase.NonlinearFunction(sys::NonlinearSystem, args...; kwargs...) end function SciMLBase.NonlinearFunction{iip}(sys::NonlinearSystem, dvs = unknowns(sys), - ps = parameters(sys), u0 = nothing; + ps = parameters(sys), u0 = nothing, p = nothing; version = nothing, jac = false, eval_expression = false, @@ -327,11 +327,22 @@ function SciMLBase.NonlinearFunction{iip}(sys::NonlinearSystem, dvs = unknowns(s observedfun = ObservedFunctionCache(sys; eval_expression, eval_module) + if length(dvs) == length(equations(sys)) + resid_prototype = nothing + else + u0ElType = u0 === nothing ? Float64 : eltype(u0) + if SciMLStructures.isscimlstructure(p) + u0ElType = promote_type( + eltype(SciMLStructures.canonicalize(SciMLStructures.Tunable(), p)[1]), + u0ElType) + end + resid_prototype = zeros(u0ElType, length(equations(sys))) + end + NonlinearFunction{iip}(f, sys = sys, jac = _jac === nothing ? nothing : _jac, - resid_prototype = length(dvs) == length(equations(sys)) ? nothing : - zeros(length(equations(sys))), + resid_prototype = resid_prototype, jac_prototype = sparse ? similar(calculate_jacobian(sys, sparse = sparse), Float64) : nothing, @@ -355,7 +366,7 @@ variable and parameter vectors, respectively. struct NonlinearFunctionExpr{iip} end function NonlinearFunctionExpr{iip}(sys::NonlinearSystem, dvs = unknowns(sys), - ps = parameters(sys), u0 = nothing; + ps = parameters(sys), u0 = nothing, p = nothing; version = nothing, tgrad = false, jac = false, linenumbers = false, @@ -376,8 +387,18 @@ function NonlinearFunctionExpr{iip}(sys::NonlinearSystem, dvs = unknowns(sys), end jp_expr = sparse ? :(similar($(get_jac(sys)[]), Float64)) : :nothing - resid_expr = length(dvs) == length(equations(sys)) ? :nothing : - :(zeros($(length(equations(sys))))) + if length(dvs) == length(equations(sys)) + resid_expr = :nothing + else + u0ElType = u0 === nothing ? Float64 : eltype(u0) + if SciMLStructures.isscimlstructure(p) + u0ElType = promote_type( + eltype(SciMLStructures.canonicalize(SciMLStructures.Tunable(), p)[1]), + u0ElType) + end + + resid_expr = :(zeros($u0ElType, $(length(equations(sys))))) + end ex = quote f = $f jac = $_jac @@ -412,7 +433,7 @@ function process_NonlinearProblem(constructor, sys::NonlinearSystem, u0map, para check_eqs_u0(eqs, dvs, u0; kwargs...) end - f = constructor(sys, dvs, ps, u0; jac = jac, checkbounds = checkbounds, + f = constructor(sys, dvs, ps, u0, p; jac = jac, checkbounds = checkbounds, linenumbers = linenumbers, parallel = parallel, simplify = simplify, sparse = sparse, eval_expression = eval_expression, eval_module = eval_module, kwargs...) diff --git a/test/nonlinearsystem.jl b/test/nonlinearsystem.jl index a71d34a880..400eb45e51 100644 --- a/test/nonlinearsystem.jl +++ b/test/nonlinearsystem.jl @@ -3,6 +3,7 @@ using ModelingToolkit: get_metadata using DiffEqBase, SparseArrays using Test using NonlinearSolve +using ForwardDiff using ModelingToolkit: value using ModelingToolkit: get_default_or_guess, MTKParameters @@ -325,3 +326,21 @@ end prob = @test_nowarn NonlinearProblem(sys, nothing) @test_nowarn solve(prob) end + +@testset "resid_prototype when system has no unknowns and an equation" begin + @variables x + @parameters p + @named sys = NonlinearSystem([x ~ 1, x^2 - p ~ 0]) + for sys in [ + structural_simplify(sys, fully_determined = false), + structural_simplify(sys, fully_determined = false, split = false) + ] + @test length(equations(sys)) == 1 + @test length(unknowns(sys)) == 0 + T = typeof(ForwardDiff.Dual(1.0)) + prob = NonlinearProblem(sys, [], [p => ForwardDiff.Dual(1.0)]; check_length = false) + @test prob.f(Float64[], prob.p) isa Vector{T} + @test prob.f.resid_prototype isa Vector{T} + @test_nowarn solve(prob) + end +end From 7de6abf9f9b2858fe47cd70f4c29dbce8d3daed4 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 26 Sep 2024 14:55:30 +0530 Subject: [PATCH 0054/1337] fix: fix DDE observed with array variables --- src/systems/diffeqs/odesystem.jl | 4 ++-- test/dde.jl | 37 ++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/src/systems/diffeqs/odesystem.jl b/src/systems/diffeqs/odesystem.jl index a063b006fd..784b071ef1 100644 --- a/src/systems/diffeqs/odesystem.jl +++ b/src/systems/diffeqs/odesystem.jl @@ -539,10 +539,10 @@ function build_explicit_observed_function(sys, ts; pre = get_postprocess_fbody(sys) array_wrapper = if param_only - wrap_array_vars(sys, ts; ps = _ps, dvs = nothing, inputs) .∘ + wrap_array_vars(sys, ts; ps = _ps, dvs = nothing, inputs, history = is_dde(sys)) .∘ wrap_parameter_dependencies(sys, isscalar) else - wrap_array_vars(sys, ts; ps = _ps, inputs) .∘ + wrap_array_vars(sys, ts; ps = _ps, inputs, history = is_dde(sys)) .∘ wrap_parameter_dependencies(sys, isscalar) end mtkparams_wrapper = wrap_mtkparameters(sys, isscalar, p_start) diff --git a/test/dde.jl b/test/dde.jl index 67f9e4f599..7a6f46f723 100644 --- a/test/dde.jl +++ b/test/dde.jl @@ -125,3 +125,40 @@ obsfn = ModelingToolkit.build_explicit_observed_function( sys, [sys.osc1.delx, sys.osc2.delx]) @test_nowarn sol[[sys.osc1.delx, sys.osc2.delx]] @test sol[sys.osc1.delx] ≈ sol(sol.t .- 0.01; idxs = sys.osc1.x) + +@testset "DDE observed with array variables" begin + @component function valve(; name) + @parameters begin + open(t)::Bool = false + Kp = 2 + Ksnap = 1.1 + τ = 0.1 + end + @variables begin + opening(..) + lag_opening(t) + snap_opening(t) + end + eqs = [D(opening(t)) ~ Kp * (open - opening(t)) + lag_opening ~ opening(t - τ) + snap_opening ~ clamp(Ksnap * lag_opening - 1 / Ksnap, 0, 1)] + return System(eqs, t; name = name) + end + + @component function veccy(; name) + @parameters dx[1:3] = ones(3) + @variables begin + x(t)[1:3] = zeros(3) + end + return System([D(x) ~ dx], t; name = name) + end + + @mtkbuild ssys = System( + Equation[], t; systems = [valve(name = :valve), veccy(name = :vvecs)]) + prob = DDEProblem(ssys, [ssys.valve.opening => 1.0], (0.0, 1.0)) + sol = solve(prob, MethodOfSteps(Tsit5())) + obsval = @test_nowarn sol[ssys.valve.lag_opening + sum(ssys.vvecs.x)] + @test obsval ≈ + sol(sol.t .- prob.ps[ssys.valve.τ]; idxs = ssys.valve.opening).u .+ + sum.(sol[ssys.vvecs.x]) +end From 14789d5c54ae3b971039639ccaca52db8867b139 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 2 Oct 2024 16:19:46 +0530 Subject: [PATCH 0055/1337] build: bump SciMLBase and SII compat --- Project.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Project.toml b/Project.toml index ff17093ae9..9a69a1467e 100644 --- a/Project.toml +++ b/Project.toml @@ -112,7 +112,7 @@ PrecompileTools = "1" RecursiveArrayTools = "3.26" Reexport = "0.2, 1" RuntimeGeneratedFunctions = "0.5.9" -SciMLBase = "2.52.1" +SciMLBase = "2.55" SciMLStructures = "1.0" Serialization = "1" Setfield = "0.7, 0.8, 1" @@ -120,7 +120,7 @@ SimpleNonlinearSolve = "0.1.0, 1" SparseArrays = "1" SpecialFunctions = "0.7, 0.8, 0.9, 0.10, 1.0, 2" StaticArrays = "0.10, 0.11, 0.12, 1.0" -SymbolicIndexingInterface = "0.3.29" +SymbolicIndexingInterface = "0.3.31" SymbolicUtils = "3.7" Symbolics = "6.12" URIs = "1" From b669cc9ce0e05401a85f6c0682c42180b8a0a70c Mon Sep 17 00:00:00 2001 From: Herman Sletmoen Date: Thu, 3 Oct 2024 15:07:01 +0200 Subject: [PATCH 0056/1337] Test vector in initial conditions --- test/initializationsystem.jl | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/test/initializationsystem.jl b/test/initializationsystem.jl index 82d129b901..466d3ea7e1 100644 --- a/test/initializationsystem.jl +++ b/test/initializationsystem.jl @@ -567,3 +567,12 @@ oprob_2nd_order_2 = ODEProblem(sys_2nd_order, u0_2nd_order_2, tspan, ps) sol = solve(oprob_2nd_order_2, Rosenbrock23()) # retcode: Success @test sol[Y][1] == 2.0 @test sol[D(Y)][1] == 0.5 + +@testset "Vector in initial conditions" begin + @variables x(t)[1:5] y(t)[1:5] + @named sys = ODESystem([D(x) ~ x, D(y) ~ y], t; initialization_eqs = [y ~ -x]) + sys = structural_simplify(sys) + prob = ODEProblem(sys, [sys.x => ones(5)], (0.0, 1.0), []) + sol = solve(prob, Tsit5(), reltol=1e-4) + @test all(sol(1.0, idxs=sys.x) .≈ +exp(1)) && all(sol(1.0, idxs=sys.y) .≈ -exp(1)) +end From 5cf19a907ab8c84fe018efc8100565e0e37d44e9 Mon Sep 17 00:00:00 2001 From: Herman Sletmoen Date: Thu, 3 Oct 2024 15:31:34 +0200 Subject: [PATCH 0057/1337] NonlinearSystem: put single equation input in vector, instead of scalarizing --- src/systems/nonlinear/nonlinearsystem.jl | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/systems/nonlinear/nonlinearsystem.jl b/src/systems/nonlinear/nonlinearsystem.jl index 1f36d61601..c5f79fdaf4 100644 --- a/src/systems/nonlinear/nonlinearsystem.jl +++ b/src/systems/nonlinear/nonlinearsystem.jl @@ -126,15 +126,13 @@ function NonlinearSystem(eqs, unknowns, ps; throw(ArgumentError("NonlinearSystem does not accept `continuous_events`, you provided $continuous_events")) discrete_events === nothing || isempty(discrete_events) || throw(ArgumentError("NonlinearSystem does not accept `discrete_events`, you provided $discrete_events")) - name === nothing && throw(ArgumentError("The `name` keyword must be provided. Please consider using the `@named` macro")) - # Move things over, but do not touch array expressions - # - # # we cannot scalarize in the loop because `eqs` itself might require - # scalarization - eqs = [x.lhs isa Union{Symbolic, Number} ? 0 ~ x.rhs - x.lhs : x - for x in scalarize(eqs)] + + # Accept a single (scalar/vector) equation, but make array for consistent internal handling + if !(eqs isa AbstractArray) + eqs = [eqs] + end if !(isempty(default_u0) && isempty(default_p)) Base.depwarn( From e9a06c56d85d96a76c4c0a0cbde90a86706a743b Mon Sep 17 00:00:00 2001 From: Herman Sletmoen Date: Thu, 3 Oct 2024 15:33:32 +0200 Subject: [PATCH 0058/1337] Scalarize initial conditions of vectors --- src/systems/nonlinear/initializesystem.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/systems/nonlinear/initializesystem.jl b/src/systems/nonlinear/initializesystem.jl index e3d28719c8..3cca51b1e9 100644 --- a/src/systems/nonlinear/initializesystem.jl +++ b/src/systems/nonlinear/initializesystem.jl @@ -45,7 +45,7 @@ function generate_initializesystem(sys::ODESystem; push!(defs, y => x) elseif y isa Symbolics.Arr # TODO: don't scalarize arrays - push!(defs, (collect(y) .=> x)...) + merge!(defs, Dict(scalarize(y .=> x))) elseif y isa Symbolics.BasicSymbolic # y is a derivative expression expanded; add it to the initialization equations push!(eqs_ics, y ~ x) From 7936f985551a1f6476f4de438ac6408dce0c4315 Mon Sep 17 00:00:00 2001 From: Herman Sletmoen Date: Thu, 3 Oct 2024 15:37:17 +0200 Subject: [PATCH 0059/1337] Do all checks at start of NonlinearSystem() --- src/systems/nonlinear/nonlinearsystem.jl | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/systems/nonlinear/nonlinearsystem.jl b/src/systems/nonlinear/nonlinearsystem.jl index c5f79fdaf4..3f082068e4 100644 --- a/src/systems/nonlinear/nonlinearsystem.jl +++ b/src/systems/nonlinear/nonlinearsystem.jl @@ -128,21 +128,18 @@ function NonlinearSystem(eqs, unknowns, ps; throw(ArgumentError("NonlinearSystem does not accept `discrete_events`, you provided $discrete_events")) name === nothing && throw(ArgumentError("The `name` keyword must be provided. Please consider using the `@named` macro")) + length(unique(nameof.(systems))) == length(systems) || + throw(ArgumentError("System names must be unique.")) + (isempty(default_u0) && isempty(default_p)) || + Base.depwarn( + "`default_u0` and `default_p` are deprecated. Use `defaults` instead.", + :NonlinearSystem, force = true) # Accept a single (scalar/vector) equation, but make array for consistent internal handling if !(eqs isa AbstractArray) eqs = [eqs] end - if !(isempty(default_u0) && isempty(default_p)) - Base.depwarn( - "`default_u0` and `default_p` are deprecated. Use `defaults` instead.", - :NonlinearSystem, force = true) - end - sysnames = nameof.(systems) - if length(unique(sysnames)) != length(sysnames) - throw(ArgumentError("System names must be unique.")) - end jac = RefValue{Any}(EMPTY_JAC) defaults = todict(defaults) defaults = Dict{Any, Any}(value(k) => value(v) From 4d1019bc79729d810395f0a836fb22eb7796f247 Mon Sep 17 00:00:00 2001 From: Herman Sletmoen Date: Thu, 3 Oct 2024 16:01:26 +0200 Subject: [PATCH 0060/1337] Remove unnecessary branch and just iterate over empty dictionary --- src/systems/nonlinear/initializesystem.jl | 30 +++++++++++------------ 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/src/systems/nonlinear/initializesystem.jl b/src/systems/nonlinear/initializesystem.jl index 3cca51b1e9..2097e2955e 100644 --- a/src/systems/nonlinear/initializesystem.jl +++ b/src/systems/nonlinear/initializesystem.jl @@ -36,22 +36,20 @@ function generate_initializesystem(sys::ODESystem; # set dummy derivatives to default_dd_guess unless specified push!(defs, x[1] => get(guesses, x[1], default_dd_guess)) end - if !isnothing(u0map) - for (y, x) in u0map - y = get(schedule.dummy_sub, y, y) - y = fixpoint_sub(y, diffmap) - if y ∈ vars_set - # variables specified in u0 overrides defaults - push!(defs, y => x) - elseif y isa Symbolics.Arr - # TODO: don't scalarize arrays - merge!(defs, Dict(scalarize(y .=> x))) - elseif y isa Symbolics.BasicSymbolic - # y is a derivative expression expanded; add it to the initialization equations - push!(eqs_ics, y ~ x) - else - error("Initialization expression $y is currently not supported. If its a higher order derivative expression, then only the dummy derivative expressions are supported.") - end + for (y, x) in u0map + y = get(schedule.dummy_sub, y, y) + y = fixpoint_sub(y, diffmap) + if y ∈ vars_set + # variables specified in u0 overrides defaults + push!(defs, y => x) + elseif y isa Symbolics.Arr + # TODO: don't scalarize arrays + merge!(defs, Dict(scalarize(y .=> x))) + elseif y isa Symbolics.BasicSymbolic + # y is a derivative expression expanded; add it to the initialization equations + push!(eqs_ics, y ~ x) + else + error("Initialization expression $y is currently not supported. If its a higher order derivative expression, then only the dummy derivative expressions are supported.") end end end From 28e9e5f47a5479f0b8fca6dfb5b60d9f7b762107 Mon Sep 17 00:00:00 2001 From: Herman Sletmoen Date: Thu, 3 Oct 2024 16:18:43 +0200 Subject: [PATCH 0061/1337] Format --- test/initializationsystem.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/initializationsystem.jl b/test/initializationsystem.jl index 466d3ea7e1..e0caf22ba4 100644 --- a/test/initializationsystem.jl +++ b/test/initializationsystem.jl @@ -573,6 +573,6 @@ sol = solve(oprob_2nd_order_2, Rosenbrock23()) # retcode: Success @named sys = ODESystem([D(x) ~ x, D(y) ~ y], t; initialization_eqs = [y ~ -x]) sys = structural_simplify(sys) prob = ODEProblem(sys, [sys.x => ones(5)], (0.0, 1.0), []) - sol = solve(prob, Tsit5(), reltol=1e-4) - @test all(sol(1.0, idxs=sys.x) .≈ +exp(1)) && all(sol(1.0, idxs=sys.y) .≈ -exp(1)) + sol = solve(prob, Tsit5(), reltol = 1e-4) + @test all(sol(1.0, idxs = sys.x) .≈ +exp(1)) && all(sol(1.0, idxs = sys.y) .≈ -exp(1)) end From 9182a7db83e66390e850866ea7933f1ca527189c Mon Sep 17 00:00:00 2001 From: Herman Sletmoen Date: Thu, 3 Oct 2024 16:48:16 +0200 Subject: [PATCH 0062/1337] Restore canonical equation form 0 ~ rhs - lhs --- src/systems/nonlinear/nonlinearsystem.jl | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/systems/nonlinear/nonlinearsystem.jl b/src/systems/nonlinear/nonlinearsystem.jl index 3f082068e4..050eea135c 100644 --- a/src/systems/nonlinear/nonlinearsystem.jl +++ b/src/systems/nonlinear/nonlinearsystem.jl @@ -140,6 +140,9 @@ function NonlinearSystem(eqs, unknowns, ps; eqs = [eqs] end + # Copy equations to canonical form, but do not touch array expressions + eqs = [wrap(eq.lhs) isa Symbolics.Arr ? eq : 0 ~ eq.rhs - eq.lhs for eq in eqs] + jac = RefValue{Any}(EMPTY_JAC) defaults = todict(defaults) defaults = Dict{Any, Any}(value(k) => value(v) From 21dc079af957b9a946a4b2c0bd2dd0fac2e654e7 Mon Sep 17 00:00:00 2001 From: Yingbo Ma Date: Thu, 3 Oct 2024 15:05:35 -0400 Subject: [PATCH 0063/1337] Revert "feat: allow users to set array length via args in `@mtkmodel`" --- docs/src/basics/MTKLanguage.md | 17 ++-- src/systems/model_parsing.jl | 161 +++++++++------------------------ test/model_parsing.jl | 49 +--------- 3 files changed, 52 insertions(+), 175 deletions(-) diff --git a/docs/src/basics/MTKLanguage.md b/docs/src/basics/MTKLanguage.md index 44fe8bbc07..a2fb7d0870 100644 --- a/docs/src/basics/MTKLanguage.md +++ b/docs/src/basics/MTKLanguage.md @@ -63,14 +63,13 @@ end @structural_parameters begin f = sin N = 2 - M = 3 end begin v_var = 1.0 end @variables begin v(t) = v_var - v_array(t)[1:N, 1:M] + v_array(t)[1:2, 1:3] v_for_defaults(t) end @extend ModelB(; p1) @@ -311,10 +310,10 @@ end - `:defaults`: Dictionary of variables and default values specified in the `@defaults`. - `:extend`: The list of extended unknowns, name given to the base system, and name of the base system. - `:structural_parameters`: Dictionary of structural parameters mapped to their metadata. - - `:parameters`: Dictionary of symbolic parameters mapped to their metadata. Metadata of - the parameter arrays is, for now, omitted. - - `:variables`: Dictionary of symbolic variables mapped to their metadata. Metadata of - the variable arrays is, for now, omitted. + - `:parameters`: Dictionary of symbolic parameters mapped to their metadata. For + parameter arrays, length is added to the metadata as `:size`. + - `:variables`: Dictionary of symbolic variables mapped to their metadata. For + variable arrays, length is added to the metadata as `:size`. - `:kwargs`: Dictionary of keyword arguments mapped to their metadata. - `:independent_variable`: Independent variable, which is added while generating the Model. - `:equations`: List of equations (represented as strings). @@ -325,10 +324,10 @@ For example, the structure of `ModelC` is: julia> ModelC.structure Dict{Symbol, Any} with 10 entries: :components => Any[Union{Expr, Symbol}[:model_a, :ModelA], Union{Expr, Symbol}[:model_array_a, :ModelA, :(1:N)], Union{Expr, Symbol}[:model_array_b, :ModelA, :(1:N)]] - :variables => Dict{Symbol, Dict{Symbol, Any}}(:v=>Dict(:default=>:v_var, :type=>Real), :v_for_defaults=>Dict(:type=>Real)) + :variables => Dict{Symbol, Dict{Symbol, Any}}(:v=>Dict(:default=>:v_var, :type=>Real), :v_array=>Dict(:type=>Real, :size=>(2, 3)), :v_for_defaults=>Dict(:type=>Real)) :icon => URI("https://github.com/SciML/SciMLDocs/blob/main/docs/src/assets/logo.png") - :kwargs => Dict{Symbol, Dict}(:f => Dict(:value => :sin), :N => Dict(:value => 2), :M => Dict(:value => 3), :v => Dict{Symbol, Any}(:value => :v_var, :type => Real), :v_for_defaults => Dict{Symbol, Union{Nothing, DataType}}(:value => nothing, :type => Real), :p1 => Dict(:value => nothing)), - :structural_parameters => Dict{Symbol, Dict}(:f => Dict(:value => :sin), :N => Dict(:value => 2), :M => Dict(:value => 3)) + :kwargs => Dict{Symbol, Dict}(:f=>Dict(:value=>:sin), :N=>Dict(:value=>2), :v=>Dict{Symbol, Any}(:value=>:v_var, :type=>Real), :v_array=>Dict{Symbol, Union{Nothing, UnionAll}}(:value=>nothing, :type=>AbstractArray{Real}), :v_for_defaults=>Dict{Symbol, Union{Nothing, DataType}}(:value=>nothing, :type=>Real), :p1=>Dict(:value=>nothing)) + :structural_parameters => Dict{Symbol, Dict}(:f=>Dict(:value=>:sin), :N=>Dict(:value=>2)) :independent_variable => t :constants => Dict{Symbol, Dict}(:c=>Dict{Symbol, Any}(:value=>1, :type=>Int64, :description=>"Example constant.")) :extend => Any[[:p2, :p1], Symbol("#mtkmodel__anonymous__ModelB"), :ModelB] diff --git a/src/systems/model_parsing.jl b/src/systems/model_parsing.jl index 09ef05c172..fce2add6b7 100644 --- a/src/systems/model_parsing.jl +++ b/src/systems/model_parsing.jl @@ -180,16 +180,6 @@ function update_kwargs_and_metadata!(dict, kwargs, a, def, indices, type, var, end end -function unit_handled_variable_value(mod, y, varname) - meta = parse_metadata(mod, y) - varval = if meta isa Nothing || get(meta, VariableUnit, nothing) isa Nothing - varname - else - :($convert_units($(meta[VariableUnit]), $varname)) - end - return varval -end - function parse_variable_def!(dict, mod, arg, varclass, kwargs, where_types; def = nothing, indices::Union{Vector{UnitRange{Int}}, Nothing} = nothing, type::Type = Real, meta = Dict{DataType, Expr}()) @@ -232,66 +222,6 @@ function parse_variable_def!(dict, mod, arg, varclass, kwargs, where_types; varclass, where_types, meta) return var, def, Dict() end - Expr(:tuple, Expr(:(::), Expr(:ref, a, b...), type), y) || Expr(:tuple, Expr(:ref, a, b...), y) => begin - (@isdefined type) || (type = Real) - varname = Meta.isexpr(a, :call) ? a.args[1] : a - push!(kwargs, Expr(:kw, varname, nothing)) - varval = unit_handled_variable_value(mod, y, varname) - if varclass == :parameters - var = :($varname = $first(@parameters $a[$(b...)]::$type = ($varval, $y))) - else - var = :($varname = $first(@variables $a[$(b...)]::$type = ($varval, $y))) - end - #TODO: update `dict` aka `Model.structure` with the metadata - (:($varname...), var), nothing, Dict() - end - Expr(:(=), Expr(:(::), Expr(:ref, a, b...), type), y) || Expr(:(=), Expr(:ref, a, b...), y) => begin - (@isdefined type) || (type = Real) - varname = Meta.isexpr(a, :call) ? a.args[1] : a - if Meta.isexpr(y, :tuple) - varval = unit_handled_variable_value(mod, y, varname) - val, y = (y.args[1], y.args[2:end]) - push!(kwargs, Expr(:kw, varname, nothing)) - if varclass == :parameters - var = :($varname = $varname === nothing ? $val : $varname; - $varname = $first(@parameters $a[$(b...)]::$type = ( - $varval, $(y...)))) - else - var = :($varname = $varname === nothing ? $val : $varname; - $varname = $first(@variables $a[$(b...)]::$type = ( - $varval, $(y...)))) - end - else - push!(kwargs, Expr(:kw, varname, nothing)) - if varclass == :parameters - var = :($varname = $varname === nothing ? $y : $varname; - $varname = $first(@parameters $a[$(b...)]::$type = $varname)) - else - var = :($varname = $varname === nothing ? $y : $varname; - $varname = $first(@variables $a[$(b...)]::$type = $varname)) - end - end - #TODO: update `dict`` aka `Model.structure` with the metadata - (:($varname...), var), nothing, Dict() - end - Expr(:(::), Expr(:ref, a, b...), type) || Expr(:ref, a, b...) => begin - (@isdefined type) || (type = Real) - varname = a isa Expr && a.head == :call ? a.args[1] : a - push!(kwargs, Expr(:kw, varname, nothing)) - if varclass == :parameters - var = :($varname = $first(@parameters $a[$(b...)]::$type = $varname)) - elseif varclass == :variables - var = :($varname = $first(@variables $a[$(b...)]::$type = $varname)) - else - throw("Symbolic array with arbitrary length is not handled for $varclass. - Please open an issue with an example.") - end - dict[varclass] = get!(dict, varclass) do - Dict{Symbol, Dict{Symbol, Any}}() - end - # dict[:kwargs][varname] = dict[varclass][varname] = Dict(:size => b) - (:($varname...), var), nothing, Dict() - end Expr(:(=), a, b) => begin Base.remove_linenums!(b) def, meta = parse_default(mod, b) @@ -338,6 +268,11 @@ function parse_variable_def!(dict, mod, arg, varclass, kwargs, where_types; end return var, def, Dict() end + Expr(:ref, a, b...) => begin + indices = map(i -> UnitRange(i.args[2], i.args[end]), b) + parse_variable_def!(dict, mod, a, varclass, kwargs, where_types; + def, indices, type, meta) + end _ => error("$arg cannot be parsed") end end @@ -445,23 +380,14 @@ function parse_default(mod, a) end end -function parse_metadata(mod, a::Expr) +function parse_metadata(mod, a) MLStyle.@match a begin - Expr(:vect, b...) => Dict(parse_metadata(mod, m) for m in b) - Expr(:tuple, a, b...) => parse_metadata(mod, b) + Expr(:vect, eles...) => Dict(parse_metadata(mod, e) for e in eles) Expr(:(=), a, b) => Symbolics.option_to_metadata_type(Val(a)) => get_var(mod, b) _ => error("Cannot parse metadata $a") end end -function parse_metadata(mod, metadata::AbstractArray) - ret = Dict() - for m in metadata - merge!(ret, parse_metadata(mod, m)) - end - ret -end - function _set_var_metadata!(metadata_with_exprs, a, m, v::Expr) push!(metadata_with_exprs, m => v) a @@ -719,7 +645,6 @@ function parse_variable_arg!(exprs, vs, dict, mod, arg, varclass, kwargs, where_ end function convert_units(varunits::DynamicQuantities.Quantity, value) - value isa Nothing && return nothing DynamicQuantities.ustrip(DynamicQuantities.uconvert( DynamicQuantities.SymbolicUnits.as_quantity(varunits), value)) end @@ -731,7 +656,6 @@ function convert_units( end function convert_units(varunits::Unitful.FreeUnits, value) - value isa Nothing && return nothing Unitful.ustrip(varunits, value) end @@ -750,50 +674,47 @@ end function parse_variable_arg(dict, mod, arg, varclass, kwargs, where_types) vv, def, metadata_with_exprs = parse_variable_def!( dict, mod, arg, varclass, kwargs, where_types) - if !(vv isa Tuple) - name = getname(vv) - varexpr = if haskey(metadata_with_exprs, VariableUnit) - unit = metadata_with_exprs[VariableUnit] - quote - $name = if $name === nothing - $setdefault($vv, $def) - else - try - $setdefault($vv, $convert_units($unit, $name)) - catch e - if isa(e, $(DynamicQuantities.DimensionError)) || - isa(e, $(Unitful.DimensionError)) - error("Unable to convert units for \'" * string(:($$vv)) * "\'") - elseif isa(e, MethodError) - error("No or invalid units provided for \'" * string(:($$vv)) * - "\'") - else - rethrow(e) - end + name = getname(vv) + + varexpr = if haskey(metadata_with_exprs, VariableUnit) + unit = metadata_with_exprs[VariableUnit] + quote + $name = if $name === nothing + $setdefault($vv, $def) + else + try + $setdefault($vv, $convert_units($unit, $name)) + catch e + if isa(e, $(DynamicQuantities.DimensionError)) || + isa(e, $(Unitful.DimensionError)) + error("Unable to convert units for \'" * string(:($$vv)) * "\'") + elseif isa(e, MethodError) + error("No or invalid units provided for \'" * string(:($$vv)) * + "\'") + else + rethrow(e) end end end - else - quote - $name = if $name === nothing - $setdefault($vv, $def) - else - $setdefault($vv, $name) - end - end end - - metadata_expr = Expr(:block) - for (k, v) in metadata_with_exprs - push!(metadata_expr.args, - :($name = $wrap($set_scalar_metadata($unwrap($name), $k, $v)))) + else + quote + $name = if $name === nothing + $setdefault($vv, $def) + else + $setdefault($vv, $name) + end end + end - push!(varexpr.args, metadata_expr) - return vv isa Num ? name : :($name...), varexpr - else - return vv + metadata_expr = Expr(:block) + for (k, v) in metadata_with_exprs + push!(metadata_expr.args, + :($name = $wrap($set_scalar_metadata($unwrap($name), $k, $v)))) end + + push!(varexpr.args, metadata_expr) + return vv isa Num ? name : :($name...), varexpr end function handle_conditional_vars!( diff --git a/test/model_parsing.jl b/test/model_parsing.jl index 9d903842c2..6332dc12a5 100644 --- a/test/model_parsing.jl +++ b/test/model_parsing.jl @@ -259,8 +259,7 @@ end @test all(collect(hasmetadata.(model.l, ModelingToolkit.VariableDescription))) @test all(lastindex.([model.a2, model.b2, model.d2, model.e2, model.h2]) .== 2) - @test size(model.l) == (2, 3) - @test_broken MockModel.structure[:parameters][:l][:size] == (2, 3) + @test size(model.l) == MockModel.structure[:parameters][:l][:size] == (2, 3) model = complete(model) @test getdefault(model.cval) == 1 @@ -314,6 +313,7 @@ end @test_throws TypeError TypeModel(; name = :throws, par3 = true) @test_throws TypeError TypeModel(; name = :throws, par4 = true) # par7 should be an AbstractArray of BigFloat. + @test_throws MethodError TypeModel(; name = :throws, par7 = rand(Int, 3, 3)) # Test that array types are correctly added. @named type_model2 = TypeModel(; par5 = rand(BigFloat, 3)) @@ -474,8 +474,7 @@ using ModelingToolkit: getdefault, scalarize @named model_with_component_array = ModelWithComponentArray() - @test_broken eval(ModelWithComponentArray.structure[:parameters][:r][:unit]) == - eval(u"Ω") + @test eval(ModelWithComponentArray.structure[:parameters][:r][:unit]) == eval(u"Ω") @test lastindex(parameters(model_with_component_array)) == 3 # Test the constant `k`. Manually k's value should be kept in sync here @@ -877,45 +876,3 @@ end end), false) end - -@testset "Array Length as an Input" begin - @mtkmodel VaryingLengthArray begin - @structural_parameters begin - N - M - end - @parameters begin - p1[1:N] - p2[1:N, 1:M] - end - @variables begin - v1(t)[1:N] - v2(t)[1:N, 1:M] - end - end - - @named model = VaryingLengthArray(N = 2, M = 3) - @test length(model.p1) == 2 - @test size(model.p2) == (2, 3) - @test length(model.v1) == 2 - @test size(model.v2) == (2, 3) - - @mtkmodel WithMetadata begin - @structural_parameters begin - N - end - @parameters begin - p_only_default[1:N] = 101 - p_only_metadata[1:N], [description = "this only has metadata"] - p_both_default_and_metadata[1:N] = 102, - [description = "this has both default value and metadata"] - end - end - - @named with_metadata = WithMetadata(N = 10) - @test getdefault(with_metadata.p_only_default) == 101 - @test getdescription(with_metadata.p_only_metadata) == "this only has metadata" - @test getdefault(with_metadata.p_both_default_and_metadata) == 102 - @test getdescription(with_metadata.p_both_default_and_metadata) == - "this has both default value and metadata" -end From 09fda2ff33c616b76ab369bd2eae171feb10e94e Mon Sep 17 00:00:00 2001 From: Herman Sletmoen Date: Thu, 3 Oct 2024 20:56:44 +0200 Subject: [PATCH 0064/1337] Test linear equation system with vector variable --- test/nonlinearsystem.jl | 14 ++++++++++++++ test/reduction.jl | 1 + 2 files changed, 15 insertions(+) diff --git a/test/nonlinearsystem.jl b/test/nonlinearsystem.jl index a71d34a880..c42eb6f87b 100644 --- a/test/nonlinearsystem.jl +++ b/test/nonlinearsystem.jl @@ -325,3 +325,17 @@ end prob = @test_nowarn NonlinearProblem(sys, nothing) @test_nowarn solve(prob) end + +@testset "System of linear equations with vector variable" begin + # 1st example in https://en.wikipedia.org/w/index.php?title=System_of_linear_equations&oldid=1247697953 + @variables x[1:3] + A = [3 2 -1 + 2 -2 4 + -1 1/2 -1] + b = [1, -2, 0] + @named sys = NonlinearSystem(A * x ~ b, [x], []) + sys = structural_simplify(sys) + prob = NonlinearProblem(sys, unknowns(sys) .=> 0.0) + sol = solve(prob) + @test all(sol[x] .≈ A \ b) +end diff --git a/test/reduction.jl b/test/reduction.jl index 064a2efd81..80969d5794 100644 --- a/test/reduction.jl +++ b/test/reduction.jl @@ -178,6 +178,7 @@ A = reshape(1:(N^2), N, N) eqs = xs ~ A * xs @named sys′ = NonlinearSystem(eqs, [xs], []) sys = structural_simplify(sys′) +@test length(equations(sys)) == 3 && length(observed(sys)) == 2 # issue 958 @parameters k₁ k₂ k₋₁ E₀ From 7d7316ee84b58de3fb1829d30138b5db3359f496 Mon Sep 17 00:00:00 2001 From: Christopher Rackauckas Date: Fri, 4 Oct 2024 12:39:13 -0400 Subject: [PATCH 0065/1337] Update Project.toml --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 9a69a1467e..a08da6f952 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "ModelingToolkit" uuid = "961ee093-0014-501f-94e3-6117800e7a78" authors = ["Yingbo Ma ", "Chris Rackauckas and contributors"] -version = "9.41.0" +version = "9.42.0" [deps] AbstractTrees = "1520ce14-60c1-5f80-bbc7-55ef81b5835c" From 8dea9b111f76efdc69b5c1e39997bd15e79f3e89 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 27 Sep 2024 18:33:09 +0530 Subject: [PATCH 0066/1337] feat: improve error message when system contains array equations --- src/systems/abstractsystem.jl | 9 +++++++ src/systems/diffeqs/abstractodesystem.jl | 1 + test/odesystem.jl | 31 ++++++++++++++++++++++++ 3 files changed, 41 insertions(+) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index 958000ec3e..d91659c0d7 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -2878,6 +2878,15 @@ function Base.eltype(::Type{<:TreeIterator{ModelingToolkit.AbstractSystem}}) ModelingToolkit.AbstractSystem end +function check_array_equations_unknowns(eqs, dvs) + if any(eq -> Symbolics.isarraysymbolic(eq.lhs), eqs) + throw(ArgumentError("The system has array equations. Call `structural_simplify` to handle such equations or scalarize them manually.")) + end + if any(x -> Symbolics.isarraysymbolic(x), dvs) + throw(ArgumentError("The system has array unknowns. Call `structural_simplify` to handle this or scalarize them manually.")) + end +end + function check_eqs_u0(eqs, dvs, u0; check_length = true, kwargs...) if u0 !== nothing if check_length diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index 06feaac688..a7ae411def 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -815,6 +815,7 @@ function process_DEProblem(constructor, sys::AbstractODESystem, u0map, parammap; ps = parameters(sys) iv = get_iv(sys) + check_array_equations_unknowns(eqs, dvs) # TODO: Pass already computed information to varmap_to_vars call # in process_u0? That would just be a small optimization varmap = u0map === nothing || isempty(u0map) || eltype(u0map) <: Number ? diff --git a/test/odesystem.jl b/test/odesystem.jl index 47c8ad9661..bdaf489a32 100644 --- a/test/odesystem.jl +++ b/test/odesystem.jl @@ -1404,3 +1404,34 @@ end @test !ModelingToolkit.is_dde(sys) @test is_markovian(sys) end + +@testset "Issue #2597" begin + @variables x(t)[1:2]=ones(2) y(t)=1.0 + + for eqs in [D(x) ~ x, collect(D(x) .~ x)] + for dvs in [[x], collect(x)] + @named sys = ODESystem(eqs, t, dvs, []) + sys = complete(sys) + if eqs isa Vector && length(eqs) == 2 && length(dvs) == 2 + @test_nowarn ODEProblem(sys, [], (0.0, 1.0)) + else + @test_throws [ + r"array (equations|unknowns)", "structural_simplify", "scalarize"] ODEProblem( + sys, [], (0.0, 1.0)) + end + end + end + for eqs in [[D(x) ~ x, D(y) ~ y], [collect(D(x) .~ x); D(y) ~ y]] + for dvs in [[x, y], [x..., y]] + @named sys = ODESystem(eqs, t, dvs, []) + sys = complete(sys) + if eqs isa Vector && length(eqs) == 3 && length(dvs) == 3 + @test_nowarn ODEProblem(sys, [], (0.0, 1.0)) + else + @test_throws [ + r"array (equations|unknowns)", "structural_simplify", "scalarize"] ODEProblem( + sys, [], (0.0, 1.0)) + end + end + end +end From e926d061706a9e098b412b2f993dfc8cccd7ca28 Mon Sep 17 00:00:00 2001 From: Christopher Rackauckas Date: Sat, 5 Oct 2024 10:14:15 -0400 Subject: [PATCH 0067/1337] Test master --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index d93a18e3f7..59ced159df 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ For information on using the package, [in-development documentation](https://docs.sciml.ai/ModelingToolkit/dev/) for the version of the documentation which contains the unreleased features. + ## Standard Library For a standard library of ModelingToolkit components and blocks, check out the From 90567a05bb7a52fb7f50bd0f08197dd75d497643 Mon Sep 17 00:00:00 2001 From: ArnoStrouwen Date: Sat, 5 Oct 2024 16:48:26 +0200 Subject: [PATCH 0068/1337] move symbolic details to Symbolics docs --- docs/pages.jl | 1 - docs/src/examples/parsing.md | 33 -------------- .../tutorials/programmatically_generating.md | 44 ++----------------- 3 files changed, 3 insertions(+), 75 deletions(-) delete mode 100644 docs/src/examples/parsing.md diff --git a/docs/pages.jl b/docs/pages.jl index f92f869def..d772c32471 100644 --- a/docs/pages.jl +++ b/docs/pages.jl @@ -17,7 +17,6 @@ pages = [ "Basic Examples" => Any["examples/higher_order.md", "examples/spring_mass.md", "examples/modelingtoolkitize_index_reduction.md", - "examples/parsing.md", "examples/remake.md"], "Advanced Examples" => Any["examples/tearing_parallelism.md", "examples/sparse_jacobians.md", diff --git a/docs/src/examples/parsing.md b/docs/src/examples/parsing.md deleted file mode 100644 index 66a4e4d82f..0000000000 --- a/docs/src/examples/parsing.md +++ /dev/null @@ -1,33 +0,0 @@ -# Parsing Expressions into Solvable Systems - -Many times when creating DSLs or creating ModelingToolkit extensions to read new file formats, -it can become imperative to parse expressions. In many cases, it can be easy to use `Base.parse` -to take things to standard Julia expressions, but how can you take a `Base.Expr` and generate -symbolic forms from that? For example, say we had the following system we wanted to solve: - -```@example parsing -ex = [:(y ~ x) - :(y ~ -2x + 3 / z) - :(z ~ 2)] -``` - -We can use the function `parse_expr_to_symbolic` from Symbolics.jl to generate the symbolic -form of the expression: - -```@example parsing -using Symbolics -eqs = parse_expr_to_symbolic.(ex, (Main,)) -``` - -From there, we can use ModelingToolkit to transform the symbolic equations into a numerical -nonlinear solve: - -```@example parsing -using ModelingToolkit, SymbolicIndexingInterface, NonlinearSolve -vars = union(ModelingToolkit.vars.(eqs)...) -@mtkbuild ns = NonlinearSystem(eqs, vars, []) - -varmap = Dict(SymbolicIndexingInterface.getname.(vars) .=> vars) -prob = NonlinearProblem(ns, [varmap[:x] => 1.0, varmap[:y] => 1.0, varmap[:z] => 1.0]) -sol = solve(prob, NewtonRaphson()) -``` diff --git a/docs/src/tutorials/programmatically_generating.md b/docs/src/tutorials/programmatically_generating.md index bd1d2359f1..76d12dba2a 100644 --- a/docs/src/tutorials/programmatically_generating.md +++ b/docs/src/tutorials/programmatically_generating.md @@ -59,51 +59,13 @@ plot(sol) As you can see, generating an ODESystem is as simple as creating an array of equations and passing it to the `ODESystem` constructor. -## Understanding the Difference Between the Julia Variable and the Symbolic Variable - -In the most basic usage of ModelingToolkit and Symbolics, the name of the Julia variable -and the symbolic variable are the same. For example, when we do: - -```@example scripting -@variables a -``` - -the name of the symbolic variable is `a` and same with the Julia variable. However, we can -de-couple these by setting `a` to a new symbolic variable, for example: - -```@example scripting -b = only(@variables(a)) -``` - -Now the Julia variable `b` refers to the variable named `a`. However, the downside of this current -approach is that it requires that the user writing the script knows the name `a` that they want to -place to the variable. But what if for example we needed to get the variable's name from a file? - -To do this, one can interpolate a symbol into the `@variables` macro using `$`. For example: - -```@example scripting -a = :c -b = only(@variables($a)) -``` - -In this example, `@variables($a)` created a variable named `c`, and set this variable to `b`. - -Variables are not the only thing with names. For example, when you build a system, it knows its name -that name is used in the namespacing. In the standard usage, again the Julia variable and the -symbolic name are made the same via: - -```@example scripting -@named fol_model = ODESystem(eqs, t) -``` - -However, one can decouple these two properties by noting that `@named` is simply shorthand for the -following: +`@named` automatically gives a name to the `ODESystem`, and is shorthand for ```@example scripting -fol_model = ODESystem(eqs, t; name = :fol_model) +fol_model = ODESystem(eqs, t; name = :fol_model) # @named fol_model = ODESystem(eqs, t) ``` -Thus if we had read a name from a file and wish to populate an `ODESystem` with said name, we could do: +Thus, if we had read a name from a file and wish to populate an `ODESystem` with said name, we could do: ```@example scripting namesym = :name_from_file From 54548346624df893e645c50577f436a06e692bd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Miclu=C8=9Ba-C=C3=A2mpeanu?= Date: Fri, 27 Sep 2024 00:59:35 +0300 Subject: [PATCH 0069/1337] refactor: relax type constraints to allow callable parameters in pdeps --- src/systems/abstractsystem.jl | 3 ++- src/systems/index_cache.jl | 16 ++++++++++++---- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index d91659c0d7..aa02a5fb73 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -3090,7 +3090,7 @@ function process_parameter_dependencies(pdeps, ps) end for p in pdeps] end - lhss = BasicSymbolic[] + lhss = [] for p in pdeps if !isparameter(p.lhs) error("LHS of parameter dependency must be a single parameter. Found $(p.lhs).") @@ -3101,6 +3101,7 @@ function process_parameter_dependencies(pdeps, ps) end push!(lhss, p.lhs) end + lhss = map(identity, lhss) pdeps = topsort_equations(pdeps, union(ps, lhss)) ps = filter(ps) do p !any(isequal(p), lhss) diff --git a/src/systems/index_cache.jl b/src/systems/index_cache.jl index 90ca3eb781..52565493db 100644 --- a/src/systems/index_cache.jl +++ b/src/systems/index_cache.jl @@ -39,7 +39,7 @@ const UnknownIndexMap = Dict{ const TunableIndexMap = Dict{BasicSymbolic, Union{Int, UnitRange{Int}, Base.ReshapedArray{Int, N, UnitRange{Int}} where {N}}} -struct IndexCache +struct IndexCache{D} unknown_idx::UnknownIndexMap # sym => (bufferidx, idx_in_buffer) discrete_idx::Dict{BasicSymbolic, DiscreteIndex} @@ -49,7 +49,7 @@ struct IndexCache constant_idx::ParamIndexMap nonnumeric_idx::NonnumericMap observed_syms::Set{BasicSymbolic} - dependent_pars::Set{BasicSymbolic} + dependent_pars::Set{D} discrete_buffer_sizes::Vector{Vector{BufferTemplate}} tunable_buffer_size::BufferTemplate constant_buffer_sizes::Vector{BufferTemplate} @@ -275,7 +275,15 @@ function IndexCache(sys::AbstractSystem) end end - dependent_pars = Set{BasicSymbolic}() + pdeps = parameter_dependencies(sys) + + D = if isempty(pdeps) + BasicSymbolic + else + mapreduce(typeof, promote_type, getproperty.(pdeps, :lhs)) + end + dependent_pars = Set{D}() + for eq in parameter_dependencies(sys) sym = eq.lhs ttsym = default_toterm(sym) @@ -289,7 +297,7 @@ function IndexCache(sys::AbstractSystem) end end - return IndexCache( + return IndexCache{D}( unk_idxs, disc_idxs, callback_to_clocks, From 61d986c413787e58b525bed66dec0885be52f448 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Miclu=C8=9Ba-C=C3=A2mpeanu?= Date: Fri, 27 Sep 2024 16:57:57 +0300 Subject: [PATCH 0070/1337] refactor: avoid type parameters in IndexCache --- src/systems/index_cache.jl | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/src/systems/index_cache.jl b/src/systems/index_cache.jl index 52565493db..00f7837407 100644 --- a/src/systems/index_cache.jl +++ b/src/systems/index_cache.jl @@ -39,7 +39,7 @@ const UnknownIndexMap = Dict{ const TunableIndexMap = Dict{BasicSymbolic, Union{Int, UnitRange{Int}, Base.ReshapedArray{Int, N, UnitRange{Int}} where {N}}} -struct IndexCache{D} +struct IndexCache unknown_idx::UnknownIndexMap # sym => (bufferidx, idx_in_buffer) discrete_idx::Dict{BasicSymbolic, DiscreteIndex} @@ -49,7 +49,7 @@ struct IndexCache{D} constant_idx::ParamIndexMap nonnumeric_idx::NonnumericMap observed_syms::Set{BasicSymbolic} - dependent_pars::Set{D} + dependent_pars::Set{Union{BasicSymbolic, CallWithMetadata}} discrete_buffer_sizes::Vector{Vector{BufferTemplate}} tunable_buffer_size::BufferTemplate constant_buffer_sizes::Vector{BufferTemplate} @@ -275,14 +275,7 @@ function IndexCache(sys::AbstractSystem) end end - pdeps = parameter_dependencies(sys) - - D = if isempty(pdeps) - BasicSymbolic - else - mapreduce(typeof, promote_type, getproperty.(pdeps, :lhs)) - end - dependent_pars = Set{D}() + dependent_pars = Set{Union{BasicSymbolic, CallWithMetadata}}() for eq in parameter_dependencies(sys) sym = eq.lhs @@ -297,7 +290,7 @@ function IndexCache(sys::AbstractSystem) end end - return IndexCache{D}( + return IndexCache( unk_idxs, disc_idxs, callback_to_clocks, From 535fd5216b8c0d51e9e756f3f6abf6136a8e40e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Miclu=C8=9Ba-C=C3=A2mpeanu?= Date: Fri, 27 Sep 2024 18:16:59 +0300 Subject: [PATCH 0071/1337] test: add test for callable pdeps --- test/parameter_dependencies.jl | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/test/parameter_dependencies.jl b/test/parameter_dependencies.jl index 034c27041e..61ddab20ba 100644 --- a/test/parameter_dependencies.jl +++ b/test/parameter_dependencies.jl @@ -177,6 +177,27 @@ end @test SciMLBase.successful_retcode(sol) end +struct CallableFoo + p +end + +(f::CallableFoo)(x) = f.p+x + +@testset "callable parameters" begin + @variables y(t) = 1 + @parameters p = 2 (i::CallableFoo)(..) + + eqs = [D(y) ~ i(t)+p] + @named model = ODESystem(eqs, t, [y], [p, i]; + parameter_dependencies = [i ~ CallableFoo(p)]) + sys = structural_simplify(model) + + prob = ODEProblem(sys, [], (0.0, 1.0)) + sol = solve(prob, Tsit5()) + + @test SciMLBase.successful_retcode(sol) +end + @testset "Clock system" begin dt = 0.1 @variables x(t) y(t) u(t) yd(t) ud(t) r(t) z(t) From 6f96622c6c41ae0630f25448d4d242185c02905d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Miclu=C8=9Ba-C=C3=A2mpeanu?= Date: Fri, 4 Oct 2024 18:25:05 +0300 Subject: [PATCH 0072/1337] test: fix pdeps test Co-authored-by: Aayush Sabharwal --- test/parameter_dependencies.jl | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/test/parameter_dependencies.jl b/test/parameter_dependencies.jl index 61ddab20ba..9cfd4ca5c5 100644 --- a/test/parameter_dependencies.jl +++ b/test/parameter_dependencies.jl @@ -178,16 +178,18 @@ end end struct CallableFoo - p + p::Any end -(f::CallableFoo)(x) = f.p+x +@register_symbolic CallableFoo(x) + +(f::CallableFoo)(x) = f.p + x @testset "callable parameters" begin @variables y(t) = 1 - @parameters p = 2 (i::CallableFoo)(..) + @parameters p=2 (i::CallableFoo)(..) - eqs = [D(y) ~ i(t)+p] + eqs = [D(y) ~ i(t) + p] @named model = ODESystem(eqs, t, [y], [p, i]; parameter_dependencies = [i ~ CallableFoo(p)]) sys = structural_simplify(model) From eb8b6175725bc9025ce593da7daf00a91e8d550b Mon Sep 17 00:00:00 2001 From: Christopher Rackauckas Date: Sat, 5 Oct 2024 12:58:47 -0400 Subject: [PATCH 0073/1337] Update tearing.jl --- test/structural_transformation/tearing.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/structural_transformation/tearing.jl b/test/structural_transformation/tearing.jl index dea6af0b11..605e6885ed 100644 --- a/test/structural_transformation/tearing.jl +++ b/test/structural_transformation/tearing.jl @@ -169,7 +169,7 @@ infprob = ODEProblem(structural_simplify(sys), [x => 1.0], (0, 1.0), [p => 0.2]) infprob.f(du, infprob.u0, pr, tt) @test any(isnan, du) -sol1 = solve(prob, RosShamp4(), reltol = 8e-7) +sol1 = solve(prob, RosShamp4(), reltol = 1e-7) sol2 = solve(ODEProblem{false}((u, p, t) -> [-asin(u[1] - pr * t)], [1.0], (0, 1.0), From b57f7f3555f80ed0c189ca027c06211192cb116c Mon Sep 17 00:00:00 2001 From: Christopher Rackauckas Date: Sat, 5 Oct 2024 15:21:47 -0400 Subject: [PATCH 0074/1337] Update tearing.jl --- test/structural_transformation/tearing.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/structural_transformation/tearing.jl b/test/structural_transformation/tearing.jl index 605e6885ed..0ac37aed05 100644 --- a/test/structural_transformation/tearing.jl +++ b/test/structural_transformation/tearing.jl @@ -169,7 +169,7 @@ infprob = ODEProblem(structural_simplify(sys), [x => 1.0], (0, 1.0), [p => 0.2]) infprob.f(du, infprob.u0, pr, tt) @test any(isnan, du) -sol1 = solve(prob, RosShamp4(), reltol = 1e-7) +sol1 = solve(prob, RosShamp4(), reltol = 1e-7, dtmax = 0.1) sol2 = solve(ODEProblem{false}((u, p, t) -> [-asin(u[1] - pr * t)], [1.0], (0, 1.0), From 8f8cf61b9ef30819f2e996365703c0cf0ec45c2c Mon Sep 17 00:00:00 2001 From: Karl Wessel Date: Mon, 7 Oct 2024 09:43:46 +0200 Subject: [PATCH 0075/1337] fix first ODE perturbation example --- docs/src/examples/perturbation.md | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/docs/src/examples/perturbation.md b/docs/src/examples/perturbation.md index f603178e37..fc3ec01aaf 100644 --- a/docs/src/examples/perturbation.md +++ b/docs/src/examples/perturbation.md @@ -2,13 +2,12 @@ ## Prelims -In the previous tutorial, [Mixed Symbolic-Numeric Perturbation Theory](https://symbolics.juliasymbolics.org/stable/examples/perturbation/), we discussed how to solve algebraic equations using **Symbolics.jl**. Here, our goal is to extend the method to differential equations. First, we import the following helper functions that were introduced in [Mixed Symbolic/Numerical Methods for Perturbation Theory - Algebraic Equations](@ref perturb_alg): +In the previous tutorial, [Mixed Symbolic-Numeric Perturbation Theory](https://symbolics.juliasymbolics.org/stable/examples/perturbation/), we discussed how to solve algebraic equations using **Symbolics.jl**. Here, our goal is to extend the method to differential equations. First, we import the following helper functions that were introduced in [Mixed Symbolic/Numerical Methods for Perturbation Theory - Algebraic Equations](https://symbolics.juliasymbolics.org/stable/examples/perturbation/): ```julia using Symbolics, SymbolicUtils -def_taylor(x, ps) = sum([a * x^i for (i, a) in enumerate(ps)]) -def_taylor(x, ps, p₀) = p₀ + def_taylor(x, ps) +def_taylor(x, ps) = sum([a * x^(i - 1) for (i, a) in enumerate(ps)]) function collect_powers(eq, x, ns; max_power = 100) eq = substitute(expand(eq), Dict(x^j => 0 for j in (last(ns) + 1):max_power)) @@ -16,7 +15,14 @@ function collect_powers(eq, x, ns; max_power = 100) eqs = [] for i in ns powers = Dict(x^j => (i == j ? 1 : 0) for j in 1:last(ns)) - push!(eqs, substitute(eq, powers)) + e = substitute(eq, powers) + + # manually remove zeroth order from higher orders + if 0 in ns && i != 0 + e = e - eqs[1] + end + + push!(eqs, e) end eqs end @@ -46,20 +52,21 @@ with the initial conditions $x(0) = 0$, and $\dot{x}(0) = 1$. Note that for $\ep ```julia using ModelingToolkit: t_nounits as t, D_nounits as D -n = 3 -@variables ϵ y[1:n](t) ∂∂y[1:n](t) +order = 2 +n = order + 1 +@variables ϵ (y(t))[1:n] (∂∂y(t))[1:n] ``` Next, we define $x$. ```julia -x = def_taylor(ϵ, y[3:end], y[2]) +x = def_taylor(ϵ, y) ``` We need the second derivative of `x`. It may seem that we can do this using `Differential(t)`; however, this operation needs to wait for a few steps because we need to manipulate the differentials as separate variables. Instead, we define dummy variables `∂∂y` as the placeholder for the second derivatives and define ```julia -∂∂x = def_taylor(ϵ, ∂∂y[3:end], ∂∂y[2]) +∂∂x = def_taylor(ϵ, ∂∂y) ``` as the second derivative of `x`. After rearrangement, our governing equation is $\ddot{x}(t)(1 + \epsilon x(t))^{-2} + 1 = 0$, or @@ -71,7 +78,7 @@ eq = ∂∂x * (1 + ϵ * x)^2 + 1 The next two steps are the same as the ones for algebraic equations (note that we pass `1:n` to `collect_powers` because the zeroth order term is needed here) ```julia -eqs = collect_powers(eq, ϵ, 1:n) +eqs = collect_powers(eq, ϵ, 0:order) ``` and, @@ -99,8 +106,8 @@ unknowns(sys) ```julia # the initial conditions # everything is zero except the initial velocity -u0 = zeros(2n + 2) -u0[3] = 1.0 # y₀ˍt +u0 = zeros(2order + 2) +u0[2] = 1.0 # yˍt₁ prob = ODEProblem(sys, u0, (0, 3.0)) sol = solve(prob; dtmax = 0.01) @@ -109,7 +116,7 @@ sol = solve(prob; dtmax = 0.01) Finally, we calculate the solution to the problem as a function of `ϵ` by substituting the solution to the ODE system back into the defining equation for `x`. Note that `𝜀` is a number, compared to `ϵ`, which is a symbolic variable. ```julia -X = 𝜀 -> sum([𝜀^(i - 1) * sol[y[i]] for i in eachindex(y)]) +X = 𝜀 -> sum([𝜀^(i - 1) * sol[yi] for (i, yi) in enumerate(y)]) ``` Using `X`, we can plot the trajectory for a range of $𝜀$s. From 163be441de8f7806b0245de80914dc2934d9a010 Mon Sep 17 00:00:00 2001 From: Herman Sletmoen Date: Mon, 7 Oct 2024 12:56:01 +0200 Subject: [PATCH 0076/1337] Ensure OffsetArrays dependency is removed when non-1-indexed array hack is removed --- src/structural_transformation/symbolics_tearing.jl | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/structural_transformation/symbolics_tearing.jl b/src/structural_transformation/symbolics_tearing.jl index f346e20b2a..84cee928cd 100644 --- a/src/structural_transformation/symbolics_tearing.jl +++ b/src/structural_transformation/symbolics_tearing.jl @@ -589,7 +589,10 @@ function tearing_reassemble(state::TearingState, var_eq_matching, haskey(obs_arr_subs, arg1) && continue obs_arr_subs[arg1] = [arg1[i] for i in eachindex(arg1)] # e.g. p => [p[1], p[2]] index_first = eachindex(arg1)[1] - obs_arr_subs[arg1] = Origin(index_first)(obs_arr_subs[arg1]) # respect non-1-indexed arrays + + # respect non-1-indexed arrays + # TODO: get rid of this hack together with the above hack, then remove OffsetArrays dependency + obs_arr_subs[arg1] = Origin(index_first)(obs_arr_subs[arg1]) end for i in eachindex(neweqs) neweqs[i] = fast_substitute(neweqs[i], obs_arr_subs; operator = Symbolics.Operator) From 91e5e70aa19791b958d41c1bc1f5bcdfbe974366 Mon Sep 17 00:00:00 2001 From: DhairyaLGandhi Date: Mon, 7 Oct 2024 19:16:48 +0530 Subject: [PATCH 0077/1337] chore: handle arrays in MTKParameters pullback --- ext/MTKChainRulesCoreExt.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ext/MTKChainRulesCoreExt.jl b/ext/MTKChainRulesCoreExt.jl index f84690e23f..635f9d12cf 100644 --- a/ext/MTKChainRulesCoreExt.jl +++ b/ext/MTKChainRulesCoreExt.jl @@ -7,7 +7,8 @@ import ChainRulesCore: Tangent, ZeroTangent, NoTangent, zero_tangent, unthunk function ChainRulesCore.rrule(::Type{MTK.MTKParameters}, tunables, args...) function mtp_pullback(dt) dt = unthunk(dt) - (NoTangent(), dt.tunable[1:length(tunables)], + dtunables = dt isa AbstractArray ? dt : dt.tunable + (NoTangent(), dtunables[1:length(tunables)], ntuple(_ -> NoTangent(), length(args))...) end MTK.MTKParameters(tunables, args...), mtp_pullback From 295edb61d5ca07ff3b78502fe2a5dad9699d8e02 Mon Sep 17 00:00:00 2001 From: Christopher Rackauckas Date: Mon, 7 Oct 2024 10:18:58 -0400 Subject: [PATCH 0078/1337] Update Project.toml --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 0843bb934d..3c21f3db31 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "ModelingToolkit" uuid = "961ee093-0014-501f-94e3-6117800e7a78" authors = ["Yingbo Ma ", "Chris Rackauckas and contributors"] -version = "9.42.0" +version = "9.43.0" [deps] AbstractTrees = "1520ce14-60c1-5f80-bbc7-55ef81b5835c" From a7adb5386da93044f424e3d078ff2cc51b9e3ebf Mon Sep 17 00:00:00 2001 From: Karl Royen Date: Mon, 7 Oct 2024 16:49:53 +0200 Subject: [PATCH 0079/1337] fix second part of example on perturbation theory for ODEs --- docs/src/examples/perturbation.md | 63 ++++++++++++++++--------------- 1 file changed, 32 insertions(+), 31 deletions(-) diff --git a/docs/src/examples/perturbation.md b/docs/src/examples/perturbation.md index fc3ec01aaf..6fb094f8af 100644 --- a/docs/src/examples/perturbation.md +++ b/docs/src/examples/perturbation.md @@ -4,8 +4,8 @@ In the previous tutorial, [Mixed Symbolic-Numeric Perturbation Theory](https://symbolics.juliasymbolics.org/stable/examples/perturbation/), we discussed how to solve algebraic equations using **Symbolics.jl**. Here, our goal is to extend the method to differential equations. First, we import the following helper functions that were introduced in [Mixed Symbolic/Numerical Methods for Perturbation Theory - Algebraic Equations](https://symbolics.juliasymbolics.org/stable/examples/perturbation/): -```julia -using Symbolics, SymbolicUtils +```@example perturbation +using Symbolics def_taylor(x, ps) = sum([a * x^(i - 1) for (i, a) in enumerate(ps)]) @@ -17,10 +17,10 @@ function collect_powers(eq, x, ns; max_power = 100) powers = Dict(x^j => (i == j ? 1 : 0) for j in 1:last(ns)) e = substitute(eq, powers) - # manually remove zeroth order from higher orders - if 0 in ns && i != 0 - e = e - eqs[1] - end + # manually remove zeroth order from higher orders + if 0 in ns && i != 0 + e = e - eqs[1] + end push!(eqs, e) end @@ -50,7 +50,7 @@ As the first ODE example, we have chosen a simple and well-behaved problem, whic with the initial conditions $x(0) = 0$, and $\dot{x}(0) = 1$. Note that for $\epsilon = 0$, this equation transforms back to the standard one. Let's start with defining the variables -```julia +```@example perturbation using ModelingToolkit: t_nounits as t, D_nounits as D order = 2 n = order + 1 @@ -59,69 +59,69 @@ n = order + 1 Next, we define $x$. -```julia +```@example perturbation x = def_taylor(ϵ, y) ``` We need the second derivative of `x`. It may seem that we can do this using `Differential(t)`; however, this operation needs to wait for a few steps because we need to manipulate the differentials as separate variables. Instead, we define dummy variables `∂∂y` as the placeholder for the second derivatives and define -```julia +```@example perturbation ∂∂x = def_taylor(ϵ, ∂∂y) ``` as the second derivative of `x`. After rearrangement, our governing equation is $\ddot{x}(t)(1 + \epsilon x(t))^{-2} + 1 = 0$, or -```julia +```@example perturbation eq = ∂∂x * (1 + ϵ * x)^2 + 1 ``` The next two steps are the same as the ones for algebraic equations (note that we pass `1:n` to `collect_powers` because the zeroth order term is needed here) -```julia +```@example perturbation eqs = collect_powers(eq, ϵ, 0:order) ``` and, -```julia +```@example perturbation vals = solve_coef(eqs, ∂∂y) ``` Our system of ODEs is forming. Now is the time to convert `∂∂`s to the correct **Symbolics.jl** form by substitution: -```julia +```@example perturbation subs = Dict(∂∂y[i] => D(D(y[i])) for i in eachindex(y)) eqs = [substitute(first(v), subs) ~ substitute(last(v), subs) for v in vals] ``` We are nearly there! From this point on, the rest is standard ODE solving procedures. Potentially, we can use a symbolic ODE solver to find a closed form solution to this problem. However, **Symbolics.jl** currently does not support this functionality. Instead, we solve the problem numerically. We form an `ODESystem`, lower the order (convert second derivatives to first), generate an `ODEProblem` (after passing the correct initial conditions), and, finally, solve it. -```julia +```@example perturbation using ModelingToolkit, DifferentialEquations @mtkbuild sys = ODESystem(eqs, t) unknowns(sys) ``` -```julia +```@example perturbation # the initial conditions # everything is zero except the initial velocity u0 = zeros(2order + 2) u0[2] = 1.0 # yˍt₁ prob = ODEProblem(sys, u0, (0, 3.0)) -sol = solve(prob; dtmax = 0.01) +sol = solve(prob; dtmax = 0.01); ``` Finally, we calculate the solution to the problem as a function of `ϵ` by substituting the solution to the ODE system back into the defining equation for `x`. Note that `𝜀` is a number, compared to `ϵ`, which is a symbolic variable. -```julia +```@example perturbation X = 𝜀 -> sum([𝜀^(i - 1) * sol[yi] for (i, yi) in enumerate(y)]) ``` Using `X`, we can plot the trajectory for a range of $𝜀$s. -```julia +```@example perturbation using Plots plot(sol.t, hcat([X(𝜀) for 𝜀 in 0.0:0.1:0.5]...)) @@ -135,17 +135,18 @@ For the next example, we have chosen a simple example from a very important clas The goal is to solve $\ddot{x} + 2\epsilon\dot{x} + x = 0$, where the dot signifies time-derivatives and the initial conditions are $x(0) = 0$ and $\dot{x}(0) = 1$. If $\epsilon = 0$, the problem reduces to the simple linear harmonic oscillator with the exact solution $x(t) = \sin(t)$. We follow the same steps as the previous example. -```julia -n = 3 -@variables ϵ t y[1:n](t) ∂y[1:n] ∂∂y[1:n] -x = def_taylor(ϵ, y[3:end], y[2]) -∂x = def_taylor(ϵ, ∂y[3:end], ∂y[2]) -∂∂x = def_taylor(ϵ, ∂∂y[3:end], ∂∂y[2]) +```@example perturbation +order = 2 +n = order + 1 +@variables ϵ (y(t))[1:n] (∂y)[1:n] (∂∂y)[1:n] +x = def_taylor(ϵ, y) +∂x = def_taylor(ϵ, ∂y) +∂∂x = def_taylor(ϵ, ∂∂y) ``` This time we also need the first derivative terms. Continuing, -```julia +```@example perturbation eq = ∂∂x + 2 * ϵ * ∂x + x eqs = collect_powers(eq, ϵ, 0:n) vals = solve_coef(eqs, ∂∂y) @@ -153,7 +154,7 @@ vals = solve_coef(eqs, ∂∂y) Next, we need to replace `∂`s and `∂∂`s with their **Symbolics.jl** counterparts: -```julia +```@example perturbation subs1 = Dict(∂y[i] => D(y[i]) for i in eachindex(y)) subs2 = Dict(∂∂y[i] => D(D(y[i])) for i in eachindex(y)) subs = subs1 ∪ subs2 @@ -162,19 +163,19 @@ eqs = [substitute(first(v), subs) ~ substitute(last(v), subs) for v in vals] We continue with converting 'eqs' to an `ODEProblem`, solving it, and finally plot the results against the exact solution to the original problem, which is $x(t, \epsilon) = (1 - \epsilon)^{-1/2} e^{-\epsilon t} \sin((1- \epsilon^2)^{1/2}t)$, -```julia +```@example perturbation @mtkbuild sys = ODESystem(eqs, t) ``` -```julia +```@example perturbation # the initial conditions -u0 = zeros(2n + 2) -u0[3] = 1.0 # y₀ˍt +u0 = zeros(2order + 2) +u0[1] = 1.0 # yˍt₁ prob = ODEProblem(sys, u0, (0, 50.0)) sol = solve(prob; dtmax = 0.01) -X = 𝜀 -> sum([𝜀^(i - 1) * sol[y[i]] for i in eachindex(y)]) +X = 𝜀 -> sum([𝜀^(i - 1) * sol[yi] for (i, yi) in enumerate(y)]) T = sol.t Y = 𝜀 -> exp.(-𝜀 * T) .* sin.(sqrt(1 - 𝜀^2) * T) / sqrt(1 - 𝜀^2) # exact solution From 87280efb223fe75495b80334f14f4347bd4b4fa6 Mon Sep 17 00:00:00 2001 From: romain veltz Date: Mon, 7 Oct 2024 20:51:08 +0200 Subject: [PATCH 0080/1337] towards BifurcationKit @0.4 --- Project.toml | 2 +- ext/MTKBifurcationKitExt.jl | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Project.toml b/Project.toml index 3c21f3db31..73c2fa4c8a 100644 --- a/Project.toml +++ b/Project.toml @@ -72,7 +72,7 @@ MTKLabelledArraysExt = "LabelledArrays" [compat] AbstractTrees = "0.3, 0.4" ArrayInterface = "6, 7" -BifurcationKit = "0.3" +BifurcationKit = "0.3, 0.4" BlockArrays = "1.1" ChainRulesCore = "1" Combinatorics = "1" diff --git a/ext/MTKBifurcationKitExt.jl b/ext/MTKBifurcationKitExt.jl index 6bc536958f..a0bd3bfc02 100644 --- a/ext/MTKBifurcationKitExt.jl +++ b/ext/MTKBifurcationKitExt.jl @@ -113,7 +113,7 @@ function BifurcationKit.BifurcationProblem(nsys::NonlinearSystem, # If the plot var is a normal state. if any(isequal(plot_var, var) for var in unknowns(nsys)) plot_idx = findfirst(isequal(plot_var), unknowns(nsys)) - record_from_solution = (x, p) -> x[plot_idx] + record_from_solution = (x, p; k...) -> x[plot_idx] # If the plot var is an observed state. elseif any(isequal(plot_var, eq.lhs) for eq in observed(nsys)) @@ -132,7 +132,7 @@ function BifurcationKit.BifurcationProblem(nsys::NonlinearSystem, return BifurcationKit.BifurcationProblem(F, u0_bif_vals, p_vals, - (@lens _[bif_idx]), + (BifurcationKit.@optic _[bif_idx]), args...; record_from_solution = record_from_solution, J = J, From 8eaa427c4c54639863b03d609a02c12ed64996ee Mon Sep 17 00:00:00 2001 From: Karl Wessel Date: Tue, 8 Oct 2024 06:59:47 +0200 Subject: [PATCH 0081/1337] use more robust definition of initial conditions for perturbation example --- docs/src/examples/perturbation.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/docs/src/examples/perturbation.md b/docs/src/examples/perturbation.md index 6fb094f8af..3ebdc3488a 100644 --- a/docs/src/examples/perturbation.md +++ b/docs/src/examples/perturbation.md @@ -106,8 +106,7 @@ unknowns(sys) ```@example perturbation # the initial conditions # everything is zero except the initial velocity -u0 = zeros(2order + 2) -u0[2] = 1.0 # yˍt₁ +u0 = Dict([unknowns(sys) .=> 0; D(y[1]) => 1]) prob = ODEProblem(sys, u0, (0, 3.0)) sol = solve(prob; dtmax = 0.01); @@ -169,8 +168,7 @@ We continue with converting 'eqs' to an `ODEProblem`, solving it, and finally pl ```@example perturbation # the initial conditions -u0 = zeros(2order + 2) -u0[1] = 1.0 # yˍt₁ +u0 = Dict([unknowns(sys) .=> 0; D(y[1]) => 1]) prob = ODEProblem(sys, u0, (0, 50.0)) sol = solve(prob; dtmax = 0.01) From ce64618e3c913a35613d999ae690b603b8383d90 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 8 Oct 2024 12:13:13 +0530 Subject: [PATCH 0082/1337] fix: fix test --- test/structural_transformation/tearing.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/structural_transformation/tearing.jl b/test/structural_transformation/tearing.jl index 0ac37aed05..32e839d802 100644 --- a/test/structural_transformation/tearing.jl +++ b/test/structural_transformation/tearing.jl @@ -169,7 +169,7 @@ infprob = ODEProblem(structural_simplify(sys), [x => 1.0], (0, 1.0), [p => 0.2]) infprob.f(du, infprob.u0, pr, tt) @test any(isnan, du) -sol1 = solve(prob, RosShamp4(), reltol = 1e-7, dtmax = 0.1) +sol1 = solve(prob, RosShamp4(), reltol = 9e-4) sol2 = solve(ODEProblem{false}((u, p, t) -> [-asin(u[1] - pr * t)], [1.0], (0, 1.0), From f63257691840b9ef2de353efb7095e2d22a3fe58 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 8 Oct 2024 12:13:19 +0530 Subject: [PATCH 0083/1337] refactor: format --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 59ced159df..d93a18e3f7 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,6 @@ For information on using the package, [in-development documentation](https://docs.sciml.ai/ModelingToolkit/dev/) for the version of the documentation which contains the unreleased features. - ## Standard Library For a standard library of ModelingToolkit components and blocks, check out the From b16031690acebc5d703c2eacfc640c41f6977d33 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 8 Oct 2024 13:03:22 +0530 Subject: [PATCH 0084/1337] test: fix depwarn in dde test --- test/dde.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/dde.jl b/test/dde.jl index 7a6f46f723..f5e72ee1bb 100644 --- a/test/dde.jl +++ b/test/dde.jl @@ -124,7 +124,7 @@ sol = solve(prob, MethodOfSteps(Tsit5())) obsfn = ModelingToolkit.build_explicit_observed_function( sys, [sys.osc1.delx, sys.osc2.delx]) @test_nowarn sol[[sys.osc1.delx, sys.osc2.delx]] -@test sol[sys.osc1.delx] ≈ sol(sol.t .- 0.01; idxs = sys.osc1.x) +@test sol[sys.osc1.delx] ≈ sol(sol.t .- 0.01; idxs = sys.osc1.x).u @testset "DDE observed with array variables" begin @component function valve(; name) From 7aac0fdd0cb413431c8ce21c4b744477a67b84fe Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 8 Oct 2024 14:35:06 +0530 Subject: [PATCH 0085/1337] test: decrease solve tolerance, increase test tolerance --- test/structural_transformation/tearing.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/structural_transformation/tearing.jl b/test/structural_transformation/tearing.jl index 32e839d802..60edcb76c5 100644 --- a/test/structural_transformation/tearing.jl +++ b/test/structural_transformation/tearing.jl @@ -169,7 +169,7 @@ infprob = ODEProblem(structural_simplify(sys), [x => 1.0], (0, 1.0), [p => 0.2]) infprob.f(du, infprob.u0, pr, tt) @test any(isnan, du) -sol1 = solve(prob, RosShamp4(), reltol = 9e-4) +sol1 = solve(prob, RosShamp4(), reltol = 2e-4) sol2 = solve(ODEProblem{false}((u, p, t) -> [-asin(u[1] - pr * t)], [1.0], (0, 1.0), @@ -179,11 +179,11 @@ sol2 = solve(ODEProblem{false}((u, p, t) -> [-asin(u[1] - pr * t)], @test sol1[x] == first.(sol1.u) @test sol1[y] == first.(sol1.u) -@test sin.(sol1[z]) .+ sol1[y]≈pr[1] * sol1.t atol=5e-5 +@test sin.(sol1[z]) .+ sol1[y]≈pr[1] * sol1.t atol=8e-4 @test sol1[sin(z) + y]≈sin.(sol1[z]) .+ sol1[y] rtol=1e-12 @test sol1[y, :] == sol1[x, :] -@test (@. sin(sol1[z, :]) + sol1[y, :])≈pr * sol1.t atol=5e-5 +@test (@. sin(sol1[z, :]) + sol1[y, :])≈pr * sol1.t atol=8e-4 # 1426 function Translational_Mass(; name, m = 1.0) From 1f81661c7cc0cb260e495a17e62156fdcac201ca Mon Sep 17 00:00:00 2001 From: Christopher Rackauckas Date: Tue, 8 Oct 2024 06:44:03 -0400 Subject: [PATCH 0086/1337] Update Project.toml --- Project.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/Project.toml b/Project.toml index 3c21f3db31..bb13481cf4 100644 --- a/Project.toml +++ b/Project.toml @@ -107,6 +107,7 @@ LinearAlgebra = "1" MLStyle = "0.4.17" NaNMath = "0.3, 1" NonlinearSolve = "3.14" +OffsetArrays = "1" OrderedCollections = "1" OrdinaryDiffEq = "6.82.0" PrecompileTools = "1" From 03c3861feb63338abf949e703718228f698687cd Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 9 Oct 2024 11:24:07 +0530 Subject: [PATCH 0087/1337] test: add REPL as a test dependency --- Project.toml | 4 +++- test/runtests.jl | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index a08da6f952..3e2b1e22dc 100644 --- a/Project.toml +++ b/Project.toml @@ -109,6 +109,7 @@ NonlinearSolve = "3.14" OrderedCollections = "1" OrdinaryDiffEq = "6.82.0" PrecompileTools = "1" +REPL = "1" RecursiveArrayTools = "3.26" Reexport = "0.2, 1" RuntimeGeneratedFunctions = "0.5.9" @@ -146,6 +147,7 @@ OptimizationMOI = "fd9f6733-72f4-499f-8506-86b2bdd0dea1" OptimizationOptimJL = "36348300-93cb-4f02-beb5-3c3902f8871e" OrdinaryDiffEq = "1dea7af3-3e70-54e6-95c3-0bf5283fa5ed" Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" +REPL = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" ReferenceTests = "324d217c-45ce-50fc-942e-d289b448e8cf" SafeTestsets = "1bc83da4-3b8d-516f-aca4-4fe02f6d838f" @@ -158,4 +160,4 @@ Sundials = "c3572dad-4567-51f8-b174-8c6c989267f4" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [targets] -test = ["AmplNLWriter", "BenchmarkTools", "ControlSystemsBase", "DataInterpolations", "DelayDiffEq", "NonlinearSolve", "ForwardDiff", "Ipopt", "Ipopt_jll", "ModelingToolkitStandardLibrary", "Optimization", "OptimizationOptimJL", "OptimizationMOI", "OrdinaryDiffEq", "Random", "ReferenceTests", "SafeTestsets", "StableRNGs", "Statistics", "SteadyStateDiffEq", "Test", "StochasticDiffEq", "Sundials", "StochasticDelayDiffEq", "Pkg", "JET"] +test = ["AmplNLWriter", "BenchmarkTools", "ControlSystemsBase", "DataInterpolations", "DelayDiffEq", "NonlinearSolve", "ForwardDiff", "Ipopt", "Ipopt_jll", "ModelingToolkitStandardLibrary", "Optimization", "OptimizationOptimJL", "OptimizationMOI", "OrdinaryDiffEq", "REPL", "Random", "ReferenceTests", "SafeTestsets", "StableRNGs", "Statistics", "SteadyStateDiffEq", "Test", "StochasticDiffEq", "Sundials", "StochasticDelayDiffEq", "Pkg", "JET"] diff --git a/test/runtests.jl b/test/runtests.jl index 0a9cf3c4db..1c48956458 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,4 +1,6 @@ using SafeTestsets, Pkg, Test +# https://github.com/JuliaLang/julia/issues/54664 +import REPL const GROUP = get(ENV, "GROUP", "All") From 422230bf30f1a2223c4ace11a9b4fd8a22ab469d Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 9 Oct 2024 14:50:02 +0530 Subject: [PATCH 0088/1337] test: remove ODAEProblem test --- test/structural_transformation/tearing.jl | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/test/structural_transformation/tearing.jl b/test/structural_transformation/tearing.jl index 60edcb76c5..a0ccc7355d 100644 --- a/test/structural_transformation/tearing.jl +++ b/test/structural_transformation/tearing.jl @@ -169,22 +169,6 @@ infprob = ODEProblem(structural_simplify(sys), [x => 1.0], (0, 1.0), [p => 0.2]) infprob.f(du, infprob.u0, pr, tt) @test any(isnan, du) -sol1 = solve(prob, RosShamp4(), reltol = 2e-4) -sol2 = solve(ODEProblem{false}((u, p, t) -> [-asin(u[1] - pr * t)], - [1.0], - (0, 1.0), - 0.2), - Tsit5(), tstops = sol1.t, adaptive = false) -@test Array(sol1[x])≈Array(sol2[1, :]) atol=1e-5 - -@test sol1[x] == first.(sol1.u) -@test sol1[y] == first.(sol1.u) -@test sin.(sol1[z]) .+ sol1[y]≈pr[1] * sol1.t atol=8e-4 -@test sol1[sin(z) + y]≈sin.(sol1[z]) .+ sol1[y] rtol=1e-12 - -@test sol1[y, :] == sol1[x, :] -@test (@. sin(sol1[z, :]) + sol1[y, :])≈pr * sol1.t atol=8e-4 - # 1426 function Translational_Mass(; name, m = 1.0) sts = @variables s(t) v(t) a(t) From 8ce64bf34899e5beb5aebbf577128f60880bc9a2 Mon Sep 17 00:00:00 2001 From: Christopher Rackauckas Date: Wed, 9 Oct 2024 08:09:20 -0400 Subject: [PATCH 0089/1337] Update Project.toml --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 16b71b9c91..1947ea7ce6 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "ModelingToolkit" uuid = "961ee093-0014-501f-94e3-6117800e7a78" authors = ["Yingbo Ma ", "Chris Rackauckas and contributors"] -version = "9.43.0" +version = "9.44.0" [deps] AbstractTrees = "1520ce14-60c1-5f80-bbc7-55ef81b5835c" From b2dbf919fa7a38bda16b45b0e1b98345e3596c49 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 6 Aug 2024 10:57:25 +0530 Subject: [PATCH 0090/1337] feat: allow parameters to be unknowns in the initialization system --- src/systems/abstractsystem.jl | 9 ++ src/systems/diffeqs/abstractodesystem.jl | 102 +++++++++++++++++++-- src/systems/nonlinear/initializesystem.jl | 106 ++++++++++++++++++++-- src/systems/parameter_buffer.jl | 3 + src/utils.jl | 4 +- 5 files changed, 207 insertions(+), 17 deletions(-) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index aa02a5fb73..661a5fd4b4 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -736,6 +736,15 @@ function has_observed_with_lhs(sys, sym) end end +function has_parameter_dependency_with_lhs(sys, sym) + has_parameter_dependencies(sys) || return false + if has_index_cache(sys) && (ic = get_index_cache(sys)) !== nothing + return any(isequal(sym), ic.dependent_pars) + else + return any(isequal(sym), [eq.lhs for eq in parameter_dependencies(sys)]) + end +end + function _all_ts_idxs!(ts_idxs, ::NotSymbolic, sys, sym) if is_variable(sys, sym) || is_independent_variable(sys, sym) push!(ts_idxs, ContinuousTimeseries()) diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index a7ae411def..623b2b47af 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -357,7 +357,9 @@ function DiffEqBase.ODEFunction{iip, specialize}(sys::AbstractODESystem, analytic = nothing, split_idxs = nothing, initializeprob = nothing, + update_initializeprob! = nothing, initializeprobmap = nothing, + initializeprobpmap = nothing, kwargs...) where {iip, specialize} if !iscomplete(sys) error("A completed system is required. Call `complete` or `structural_simplify` on the system before creating an `ODEFunction`") @@ -459,7 +461,9 @@ function DiffEqBase.ODEFunction{iip, specialize}(sys::AbstractODESystem, sparsity = sparsity ? jacobian_sparsity(sys) : nothing, analytic = analytic, initializeprob = initializeprob, - initializeprobmap = initializeprobmap) + update_initializeprob! = update_initializeprob!, + initializeprobmap = initializeprobmap, + initializeprobpmap = initializeprobpmap) end """ @@ -789,6 +793,45 @@ function get_u0( return u0, defs end +struct GetUpdatedMTKParameters{G, S} + # `getu` functor which gets parameters that are unknowns during initialization + getpunknowns::G + # `setu` functor which returns a modified MTKParameters using those parameters + setpunknowns::S +end + +function (f::GetUpdatedMTKParameters)(prob, initializesol) + mtkp = copy(parameter_values(prob)) + f.setpunknowns(mtkp, f.getpunknowns(initializesol)) + mtkp +end + +struct UpdateInitializeprob{G, S} + # `getu` functor which gets all values from prob + getvals::G + # `setu` functor which updates initializeprob with values + setvals::S +end + +function (f::UpdateInitializeprob)(initializeprob, prob) + f.setvals(initializeprob, f.getvals(prob)) +end + +function get_temporary_value(p) + stype = symtype(unwrap(p)) + return if stype == Real + zero(Float64) + elseif stype <: AbstractArray{Real} + zeros(Float64, size(p)) + elseif stype <: Real + zero(stype) + elseif stype <: AbstractArray + zeros(eltype(stype), size(p)) + else + error("Nonnumeric parameter $p with symtype $stype cannot be solved for during initialization") + end +end + function process_DEProblem(constructor, sys::AbstractODESystem, u0map, parammap; implicit_dae = false, du0map = nothing, version = nothing, tgrad = false, @@ -829,18 +872,38 @@ function process_DEProblem(constructor, sys::AbstractODESystem, u0map, parammap; end if eltype(parammap) <: Pair - parammap = Dict(unwrap(k) => v for (k, v) in todict(parammap)) + parammap = Dict{Any, Any}(unwrap(k) => v for (k, v) in parammap) elseif parammap isa AbstractArray if isempty(parammap) parammap = SciMLBase.NullParameters() else - parammap = Dict(unwrap.(parameters(sys)) .=> parammap) + parammap = Dict{Any, Any}(unwrap.(parameters(sys)) .=> parammap) end end - + defs = defaults(sys) + if has_guesses(sys) + guesses = merge( + ModelingToolkit.guesses(sys), isempty(guesses) ? Dict() : todict(guesses)) + solvablepars = [p + for p in parameters(sys) + if is_parameter_solvable(p, parammap, defs, guesses)] + + pvarmap = if parammap === nothing || parammap == SciMLBase.NullParameters() || !(eltype(parammap) <: Pair) && isempty(parammap) + defs + else + merge(defs, todict(parammap)) + end + setparobserved = filter(keys(pvarmap)) do var + has_parameter_dependency_with_lhs(sys, var) + end + else + solvablepars = () + setparobserved = () + end # ModelingToolkit.get_tearing_state(sys) !== nothing => Requires structural_simplify first if sys isa ODESystem && build_initializeprob && - (((implicit_dae || !isempty(missingvars) || !isempty(setobserved)) && + (((implicit_dae || !isempty(missingvars) || !isempty(solvablepars) || + !isempty(setobserved) || !isempty(setparobserved)) && ModelingToolkit.get_tearing_state(sys) !== nothing) || !isempty(initialization_equations(sys))) && t !== nothing if eltype(u0map) <: Number @@ -854,14 +917,32 @@ function process_DEProblem(constructor, sys::AbstractODESystem, u0map, parammap; sys, t, u0map, parammap; guesses, warn_initialize_determined, initialization_eqs, eval_expression, eval_module, fully_determined, check_units) initializeprobmap = getu(initializeprob, unknowns(sys)) + punknowns = [p + for p in all_variable_symbols(initializeprob) if is_parameter(sys, p)] + getpunknowns = getu(initializeprob, punknowns) + setpunknowns = setp(sys, punknowns) + initializeprobpmap = GetUpdatedMTKParameters(getpunknowns, setpunknowns) + reqd_syms = parameter_symbols(initializeprob) + update_initializeprob! = UpdateInitializeprob( + getu(sys, reqd_syms), setu(initializeprob, reqd_syms)) zerovars = Dict(setdiff(unknowns(sys), keys(defaults(sys))) .=> 0.0) + if parammap isa SciMLBase.NullParameters + parammap = Dict() + end + for p in punknowns + p = unwrap(p) + stype = symtype(p) + parammap[p] = get_temporary_value(p) + end trueinit = collect(merge(zerovars, eltype(u0map) <: Pair ? todict(u0map) : u0map)) u0map isa StaticArraysCore.StaticArray && (trueinit = SVector{length(trueinit)}(trueinit)) else initializeprob = nothing + update_initializeprob! = nothing initializeprobmap = nothing + initializeprobpmap = nothing trueinit = u0map end @@ -909,7 +990,9 @@ function process_DEProblem(constructor, sys::AbstractODESystem, u0map, parammap; sparse = sparse, eval_expression = eval_expression, eval_module = eval_module, initializeprob = initializeprob, + update_initializeprob! = update_initializeprob!, initializeprobmap = initializeprobmap, + initializeprobpmap = initializeprobpmap, kwargs...) implicit_dae ? (f, du0, u0, p) : (f, u0, p) end @@ -1471,10 +1554,12 @@ function InitializationProblem{iip, specialize}(sys::AbstractODESystem, isys = get_initializesystem(sys; initialization_eqs, check_units) elseif isempty(u0map) && get_initializesystem(sys) === nothing isys = structural_simplify( - generate_initializesystem(sys; initialization_eqs, check_units); fully_determined) + generate_initializesystem( + sys; initialization_eqs, check_units, pmap = parammap); fully_determined) else isys = structural_simplify( - generate_initializesystem(sys; u0map, initialization_eqs, check_units); fully_determined) + generate_initializesystem( + sys; u0map, initialization_eqs, check_units, pmap = parammap); fully_determined) end uninit = setdiff(unknowns(sys), [unknowns(isys); getfield.(observed(isys), :lhs)]) @@ -1498,6 +1583,7 @@ function InitializationProblem{iip, specialize}(sys::AbstractODESystem, parammap = parammap isa DiffEqBase.NullParameters || isempty(parammap) ? [get_iv(sys) => t] : merge(todict(parammap), Dict(get_iv(sys) => t)) + parammap = Dict(k => v for (k, v) in parammap if v !== missing) if isempty(u0map) u0map = Dict() end @@ -1505,7 +1591,7 @@ function InitializationProblem{iip, specialize}(sys::AbstractODESystem, guesses = Dict() end - u0map = merge(todict(guesses), todict(u0map)) + u0map = merge(ModelingToolkit.guesses(sys), todict(guesses), todict(u0map)) if neqs == nunknown NonlinearProblem(isys, u0map, parammap; kwargs...) else diff --git a/src/systems/nonlinear/initializesystem.jl b/src/systems/nonlinear/initializesystem.jl index 2097e2955e..d4253afda5 100644 --- a/src/systems/nonlinear/initializesystem.jl +++ b/src/systems/nonlinear/initializesystem.jl @@ -5,6 +5,7 @@ Generate `NonlinearSystem` which initializes an ODE problem from specified initi """ function generate_initializesystem(sys::ODESystem; u0map = Dict(), + pmap = Dict(), initialization_eqs = [], guesses = Dict(), default_dd_guess = 0.0, @@ -74,12 +75,103 @@ function generate_initializesystem(sys::ODESystem; end end - pars = [parameters(sys); get_iv(sys)] # include independent variable as pseudo-parameter - eqs_ics = [eqs_ics; observed(sys)] - return NonlinearSystem( - eqs_ics, vars, pars; - defaults = defs, parameter_dependencies = parameter_dependencies(sys), - checks = check_units, - name, kwargs... + # 4) process parameters as initialization unknowns + paramsubs = Dict() + if pmap isa SciMLBase.NullParameters + pmap = Dict() + end + pmap = todict(pmap) + for p in parameters(sys) + if is_parameter_solvable(p, pmap, defs, guesses) + # If either of them are `missing` the parameter is an unknown + # But if the parameter is passed a value, use that as an additional + # equation in the system + _val1 = get(pmap, p, nothing) + _val2 = get(defs, p, nothing) + _val3 = get(guesses, p, nothing) + varp = tovar(p) + paramsubs[p] = varp + # Has a default of `missing`, and (either an equation using the value passed to `ODEProblem` or a guess) + if _val2 === missing + if _val1 !== nothing && _val1 !== missing + push!(eqs_ics, varp ~ _val1) + push!(defs, varp => _val1) + elseif _val3 !== nothing + # assuming an equation exists (either via algebraic equations or initialization_eqs) + push!(defs, varp => _val3) + elseif check_defguess + error("Invalid setup: parameter $(p) has no default value, initial value, or guess") + end + # `missing` passed to `ODEProblem`, and (either an equation using default or a guess) + elseif _val1 === missing + if _val2 !== nothing && _val2 !== missing + push!(eqs_ics, varp ~ _val2) + push!(defs, varp => _val2) + elseif _val3 !== nothing + push!(defs, varp => _val3) + elseif check_defguess + error("Invalid setup: parameter $(p) has no default value, initial value, or guess") + end + # given a symbolic value to ODEProblem + elseif symbolic_type(_val1) != NotSymbolic() + push!(eqs_ics, varp ~ _val1) + push!(defs, varp => _val3) + # No value passed to `ODEProblem`, but a default and a guess are present + # _val2 !== missing is implied by it falling this far in the elseif chain + elseif _val1 === nothing && _val2 !== nothing + push!(eqs_ics, varp ~ _val2) + push!(defs, varp => _val3) + else + # _val1 !== missing and _val1 !== nothing, so a value was provided to ODEProblem + # This would mean `is_parameter_solvable` returned `false`, so we never end up + # here + error("This should never be reached") + end + end + end + + # 5) parameter dependencies become equations, their LHS become unknowns + for eq in parameter_dependencies(sys) + varp = tovar(eq.lhs) + paramsubs[eq.lhs] = varp + push!(eqs_ics, eq) + guessval = get(guesses, eq.lhs, eq.rhs) + push!(defs, varp => guessval) + end + + # 6) handle values provided for dependent parameters similar to values for observed variables + for (k, v) in merge(defaults(sys), pmap) + if is_variable_floatingpoint(k) && has_parameter_dependency_with_lhs(sys, k) + push!(eqs_ics, paramsubs[k] ~ v) + end + end + + # parameters do not include ones that became initialization unknowns + pars = vcat( + [get_iv(sys)], # include independent variable as pseudo-parameter + [p for p in parameters(sys) if !haskey(paramsubs, p)] ) + + eqs_ics = Symbolics.substitute.([eqs_ics; observed(sys)], (paramsubs,)) + vars = [vars; collect(values(paramsubs))] + for k in keys(defs) + defs[k] = substitute(defs[k], paramsubs) + end + return NonlinearSystem(eqs_ics, + vars, + pars; + defaults = defs, + checks = check_units, + name, + kwargs...) +end + +function is_parameter_solvable(p, pmap, defs, guesses) + _val1 = pmap isa AbstractDict ? get(pmap, p, nothing) : nothing + _val2 = get(defs, p, nothing) + _val3 = get(guesses, p, nothing) + # either (missing is a default or was passed to the ODEProblem) or (nothing was passed to + # the ODEProblem and it has a default and a guess) + return ((_val1 === missing || _val2 === missing) || + (_val1 === nothing && _val2 !== nothing)) && _val3 !== nothing end diff --git a/src/systems/parameter_buffer.jl b/src/systems/parameter_buffer.jl index 20ba3d731a..67b26ea50c 100644 --- a/src/systems/parameter_buffer.jl +++ b/src/systems/parameter_buffer.jl @@ -522,6 +522,9 @@ function _remake_buffer(indp, oldbuf::MTKParameters, idxs, vals; validate = true ic = get_index_cache(indp_to_system(indp)) for (idx, val) in zip(idxs, vals) sym = nothing + if val === missing + val = get_temporary_value(idx) + end if symbolic_type(idx) == ScalarSymbolic() sym = idx idx = parameter_index(ic, sym) diff --git a/src/utils.jl b/src/utils.jl index 25731f495a..a3f99731af 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -494,8 +494,8 @@ function collect_var!(unknowns, parameters, var, iv) push!(unknowns, var) end # Add also any parameters that appear only as defaults in the var - if hasdefault(var) - collect_vars!(unknowns, parameters, getdefault(var), iv) + if hasdefault(var) && (def = getdefault(var)) !== missing + collect_vars!(unknowns, parameters, def, iv) end return nothing end From 8ec6d3e467dede8db4c5c97fefb994b38c6a3af0 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 21 Aug 2024 12:36:44 +0530 Subject: [PATCH 0091/1337] feat: collect guesses from parameter metadata in ODESystem --- src/systems/diffeqs/odesystem.jl | 15 ++++++++++----- src/variables.jl | 10 ++++++++++ 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/src/systems/diffeqs/odesystem.jl b/src/systems/diffeqs/odesystem.jl index 784b071ef1..48c00a933b 100644 --- a/src/systems/diffeqs/odesystem.jl +++ b/src/systems/diffeqs/odesystem.jl @@ -249,11 +249,16 @@ function ODESystem(deqs::AbstractVector{<:Equation}, iv, dvs, ps; process_variables!(var_to_name, defaults, dvs′) process_variables!(var_to_name, defaults, ps′) - sysguesses = [ModelingToolkit.getguess(st) for st in dvs′] - hasaguess = findall(!isnothing, sysguesses) - var_guesses = dvs′[hasaguess] .=> sysguesses[hasaguess] - sysguesses = isempty(var_guesses) ? Dict() : todict(var_guesses) - guesses = merge(sysguesses, todict(guesses)) + sysdvsguesses = [ModelingToolkit.getguess(st) for st in dvs′] + hasaguess = findall(!isnothing, sysdvsguesses) + var_guesses = dvs′[hasaguess] .=> sysdvsguesses[hasaguess] + sysdvsguesses = isempty(var_guesses) ? Dict() : todict(var_guesses) + syspsguesses = [ModelingToolkit.getguess(st) for st in ps′] + hasaguess = findall(!isnothing, syspsguesses) + ps_guesses = ps′[hasaguess] .=> syspsguesses[hasaguess] + syspsguesses = isempty(ps_guesses) ? Dict() : todict(ps_guesses) + + guesses = merge(sysdvsguesses, syspsguesses, todict(guesses)) guesses = Dict{Any, Any}(value(k) => value(v) for (k, v) in pairs(guesses)) isempty(observed) || collect_var_to_name!(var_to_name, (eq.lhs for eq in observed)) diff --git a/src/variables.jl b/src/variables.jl index 4e5f860513..1c22540c63 100644 --- a/src/variables.jl +++ b/src/variables.jl @@ -491,6 +491,16 @@ function getguess(x) Symbolics.getmetadata(x, VariableGuess, nothing) end +""" + setguess(x, v) + +Set the guess for the initial value associated with symbolic variable `x` to `v`. +See also [`hasguess`](@ref). +""" +function setguess(x, v) + Symbolics.setmetadata(x, VariableGuess, v) +end + """ hasguess(x) From 6d63ffdfef27fef051390106f3bdabf09a002bb2 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 6 Aug 2024 16:19:26 +0530 Subject: [PATCH 0092/1337] test: test parameters as initialization unknowns --- test/initializationsystem.jl | 130 +++++++++++++++++++++++++++++++++ test/parameter_dependencies.jl | 4 +- 2 files changed, 132 insertions(+), 2 deletions(-) diff --git a/test/initializationsystem.jl b/test/initializationsystem.jl index e0caf22ba4..e6192144c8 100644 --- a/test/initializationsystem.jl +++ b/test/initializationsystem.jl @@ -1,4 +1,5 @@ using ModelingToolkit, OrdinaryDiffEq, NonlinearSolve, Test +using SymbolicIndexingInterface using ModelingToolkit: t_nounits as t, D_nounits as D @parameters g @@ -576,3 +577,132 @@ sol = solve(oprob_2nd_order_2, Rosenbrock23()) # retcode: Success sol = solve(prob, Tsit5(), reltol = 1e-4) @test all(sol(1.0, idxs = sys.x) .≈ +exp(1)) && all(sol(1.0, idxs = sys.y) .≈ -exp(1)) end + +@testset "Initialization of parameters" begin + function test_parameter(prob, sym, val, initialval = zero(val)) + @test prob.ps[sym] ≈ initialval + @test init(prob, Tsit5()).ps[sym] ≈ val + @test solve(prob, Tsit5()).ps[sym] ≈ val + end + function test_initializesystem(sys, u0map, pmap, p, equation) + isys = ModelingToolkit.generate_initializesystem( + sys; u0map, pmap, guesses = ModelingToolkit.guesses(sys)) + @test is_variable(isys, p) + @test equation in equations(isys) || (0 ~ -equation.rhs) in equations(isys) + end + @variables x(t) y(t) + @parameters p q + u0map = Dict(x => 1.0, y => 1.0) + pmap = Dict() + pmap[q] = 1.0 + # `missing` default, equation from ODEProblem + @mtkbuild sys = ODESystem( + [D(x) ~ x * q, D(y) ~ y * p], t; defaults = [p => missing], guesses = [p => 1.0]) + pmap[p] = 2q + prob = ODEProblem(sys, u0map, (0.0, 1.0), pmap) + test_parameter(prob, p, 2.0) + # `missing` default, provided guess + @mtkbuild sys = ODESystem( + [D(x) ~ x, p ~ x + y], t; defaults = [p => missing], guesses = [p => 0.0]) + prob = ODEProblem(sys, u0map, (0.0, 1.0)) + test_parameter(prob, p, 2.0) + test_initializesystem(sys, u0map, pmap, p, 0 ~ p - x - y) + + # `missing` to ODEProblem, equation from default + @mtkbuild sys = ODESystem( + [D(x) ~ x * q, D(y) ~ y * p], t; defaults = [p => 2q], guesses = [p => 1.0]) + pmap[p] = missing + prob = ODEProblem(sys, u0map, (0.0, 1.0), pmap) + test_parameter(prob, p, 2.0) + test_initializesystem(sys, u0map, pmap, p, 0 ~ 2q - p) + test_parameter(prob2, p, 2.0) + # `missing` to ODEProblem, provided guess + @mtkbuild sys = ODESystem( + [D(x) ~ x, p ~ x + y], t; guesses = [p => 0.0]) + prob = ODEProblem(sys, u0map, (0.0, 1.0), pmap) + test_parameter(prob, p, 2.0) + test_initializesystem(sys, u0map, pmap, p, 0 ~ x + y - p) + + # No `missing`, default and guess + @mtkbuild sys = ODESystem( + [D(x) ~ x * q, D(y) ~ y * p], t; defaults = [p => 2q], guesses = [p => 0.0]) + delete!(pmap, p) + prob = ODEProblem(sys, u0map, (0.0, 1.0), pmap) + test_parameter(prob, p, 2.0) + test_initializesystem(sys, u0map, pmap, p, 0 ~ 2q - p) + + # Should not be solved for: + + # ODEProblem value with guess, no `missing` + @mtkbuild sys = ODESystem([D(x) ~ x * q, D(y) ~ y * p], t; guesses = [p => 0.0]) + _pmap = merge(pmap, Dict(p => 3q)) + prob = ODEProblem(sys, u0map, (0.0, 1.0), _pmap) + @test prob.ps[p] ≈ 3.0 + @test prob.f.initializeprob === nothing + # Default overridden by ODEProblem, guess provided + @mtkbuild sys = ODESystem( + [D(x) ~ q * x, D(y) ~ y * p], t; defaults = [p => 2q], guesses = [p => 1.0]) + prob = ODEProblem(sys, u0map, (0.0, 1.0), _pmap) + @test prob.ps[p] ≈ 3.0 + @test prob.f.initializeprob === nothing + + @mtkbuild sys = ODESystem([D(x) ~ x, p ~ x + y], t; guesses = [p => 0.0]) + @test_throws ModelingToolkit.MissingParametersError ODEProblem( + sys, [x => 1.0, y => 1.0], (0.0, 1.0)) + + @testset "Null system" begin + @variables x(t) y(t) s(t) + @parameters x0 y0 + @mtkbuild sys = ODESystem([x ~ x0, y ~ y0, s ~ x + y], t; guesses = [y0 => 0.0]) + prob = ODEProblem(sys, [s => 1.0], (0.0, 1.0), [x0 => 0.3, y0 => missing]) + test_parameter(prob, y0, 0.7) + end + + using ModelingToolkitStandardLibrary.Mechanical.TranslationalModelica: Fixed, Mass, + Spring, Force, + Damper + using ModelingToolkitStandardLibrary.Mechanical: TranslationalModelica as TM + using ModelingToolkitStandardLibrary.Blocks: Constant + + @named mass = TM.Mass(; m = 1.0, s = 1.0, v = 0.0, a = 0.0) + @named fixed = Fixed(; s0 = 0.0) + @named spring = Spring(; c = 2.0, s_rel0 = nothing) + @named gravity = Force() + @named constant = Constant(; k = 9.81) + @named damper = TM.Damper(; d = 0.1) + @mtkbuild sys = ODESystem( + [connect(fixed.flange, spring.flange_a), connect(spring.flange_b, mass.flange_a), + connect(mass.flange_a, gravity.flange), connect(constant.output, gravity.f), + connect(fixed.flange, damper.flange_a), connect(damper.flange_b, mass.flange_a)], + t; + systems = [fixed, spring, mass, gravity, constant, damper], + guesses = [spring.s_rel0 => 1.0]) + prob = ODEProblem(sys, [], (0.0, 1.0), [spring.s_rel0 => missing]) + test_parameter(prob, spring.s_rel0, -3.905) +end + +@testset "Update initializeprob parameters" begin + @variables x(t) y(t) + @parameters p q + @mtkbuild sys = ODESystem( + [D(x) ~ x, p ~ x + y], t; guesses = [x => 0.0, p => 0.0]) + prob = ODEProblem(sys, [y => 1.0], (0.0, 1.0), [p => 3.0]) + @test prob.f.initializeprob.ps[p] ≈ 3.0 + @test init(prob, Tsit5())[x] ≈ 2.0 + prob.ps[p] = 2.0 + @test prob.f.initializeprob.ps[p] ≈ 3.0 + @test init(prob, Tsit5())[x] ≈ 1.0 + ModelingToolkit.defaults(prob.f.sys)[p] = missing +end + +@testset "Equations for dependent parameters" begin + @variables x(t) + @parameters p q=5 r + @mtkbuild sys = ODESystem( + D(x) ~ 2x + r, t; parameter_dependencies = [r ~ p + 2q, q ~ p + 3], + guesses = [p => 1.0]) + prob = ODEProblem(sys, [x => 1.0], (0.0, 1.0), [p => missing]) + @test length(equations(ModelingToolkit.get_parent(prob.f.initializeprob.f.sys))) == 4 + integ = init(prob, Tsit5()) + @test integ.ps[p] ≈ 2 +end diff --git a/test/parameter_dependencies.jl b/test/parameter_dependencies.jl index 9cfd4ca5c5..1f1aab9953 100644 --- a/test/parameter_dependencies.jl +++ b/test/parameter_dependencies.jl @@ -10,7 +10,7 @@ using SymbolicIndexingInterface using NonlinearSolve @testset "ODESystem with callbacks" begin - @parameters p1=1.0 p2=1.0 + @parameters p1=1.0 p2 @variables x(t) cb1 = [x ~ 2.0] => [p1 ~ 2.0] # triggers at t=-2+√6 function affect1!(integ, u, p, ctx) @@ -31,7 +31,7 @@ using NonlinearSolve prob = ODEProblem(sys, [x => 1.0], (0.0, 1.5), jac = true) @test prob.ps[p1] == 1.0 @test prob.ps[p2] == 2.0 - @test_nowarn solve(prob, Tsit5()) + @test SciMLBase.successful_retcode(solve(prob, Tsit5())) prob = ODEProblem(sys, [x => 1.0], (0.0, 1.5), [p1 => 1.0], jac = true) @test prob.ps[p1] == 1.0 @test prob.ps[p2] == 2.0 From 4682f8d2d968d2ed7c44a1fc87d3a864783ce5a7 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 6 Aug 2024 17:53:15 +0530 Subject: [PATCH 0093/1337] fix: check symbolic_type in `namespace_expr` --- src/systems/abstractsystem.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index 661a5fd4b4..bad83133a1 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -1356,6 +1356,7 @@ end function namespace_expr( O, sys, n = nameof(sys); ivs = independent_variables(sys)) O = unwrap(O) + symbolic_type(O) == NotSymbolic() && return O if any(isequal(O), ivs) return O elseif iscall(O) From 8425bb83060d9d57dfe61fc5860438c7d233b6bc Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 6 Aug 2024 17:53:31 +0530 Subject: [PATCH 0094/1337] refactor: allow parameter default to be `missing` in `@mtkmodel` --- src/systems/model_parsing.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/systems/model_parsing.jl b/src/systems/model_parsing.jl index fce2add6b7..70fab5a7cc 100644 --- a/src/systems/model_parsing.jl +++ b/src/systems/model_parsing.jl @@ -154,9 +154,9 @@ function update_kwargs_and_metadata!(dict, kwargs, a, def, indices, type, var, if !isnothing(meta) && haskey(meta, VariableUnit) uvar = gensym() push!(where_types, uvar) - push!(kwargs, Expr(:kw, :($a::Union{Nothing, $uvar}), nothing)) + push!(kwargs, Expr(:kw, :($a::Union{Nothing, Missing, $uvar}), nothing)) else - push!(kwargs, Expr(:kw, :($a::Union{Nothing, $type}), nothing)) + push!(kwargs, Expr(:kw, :($a::Union{Nothing, Missing, $type}), nothing)) end dict[:kwargs][getname(var)] = Dict(:value => def, :type => type) else From 5a538c3a7807f1744abeebcd60d98c1b6cd354e3 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 5 Jul 2024 19:56:35 +0530 Subject: [PATCH 0095/1337] feat: support `SciMLBase.remake_initializeprob` --- src/systems/nonlinear/initializesystem.jl | 53 +++++++++++++++++++++++ test/initializationsystem.jl | 5 +++ 2 files changed, 58 insertions(+) diff --git a/src/systems/nonlinear/initializesystem.jl b/src/systems/nonlinear/initializesystem.jl index d4253afda5..a4a29f223b 100644 --- a/src/systems/nonlinear/initializesystem.jl +++ b/src/systems/nonlinear/initializesystem.jl @@ -175,3 +175,56 @@ function is_parameter_solvable(p, pmap, defs, guesses) return ((_val1 === missing || _val2 === missing) || (_val1 === nothing && _val2 !== nothing)) && _val3 !== nothing end + +function SciMLBase.remake_initializeprob(sys::ODESystem, odefn, u0, t0, p) + if (u0 === missing || !(eltype(u0) <: Pair) || isempty(u0)) && + (p === missing || !(eltype(p) <: Pair) || isempty(p)) + return odefn.initializeprob, odefn.update_initializeprob!, odefn.initializeprobmap, + odefn.initializeprobpmap + end + if u0 === missing || isempty(u0) + u0 = Dict() + elseif !(eltype(u0) <: Pair) + u0 = Dict(unknowns(sys) .=> u0) + end + if p === missing + p = Dict() + end + if t0 === nothing + t0 = 0.0 + end + u0 = todict(u0) + defs = defaults(sys) + varmap = merge(defs, u0) + varmap = canonicalize_varmap(varmap) + missingvars = setdiff(unknowns(sys), collect(keys(varmap))) + setobserved = filter(keys(varmap)) do var + has_observed_with_lhs(sys, var) || has_observed_with_lhs(sys, default_toterm(var)) + end + p = todict(p) + guesses = ModelingToolkit.guesses(sys) + solvablepars = [par + for par in parameters(sys) + if is_parameter_solvable(par, p, defs, guesses)] + pvarmap = merge(defs, p) + setparobserved = filter(keys(pvarmap)) do var + has_parameter_dependency_with_lhs(sys, var) + end + if (((!isempty(missingvars) || !isempty(solvablepars) || + !isempty(setobserved) || !isempty(setparobserved)) && + ModelingToolkit.get_tearing_state(sys) !== nothing) || + !isempty(initialization_equations(sys))) + initprob = InitializationProblem(sys, t0, u0, p) + initprobmap = getu(initprob, unknowns(sys)) + punknowns = [p for p in all_variable_symbols(initprob) if is_parameter(sys, p)] + getpunknowns = getu(initprob, punknowns) + setpunknowns = setp(sys, punknowns) + initprobpmap = GetUpdatedMTKParameters(getpunknowns, setpunknowns) + reqd_syms = parameter_symbols(initprob) + update_initializeprob! = UpdateInitializeprob( + getu(sys, reqd_syms), setu(initprob, reqd_syms)) + return initprob, update_initializeprob!, initprobmap, initprobpmap + else + return nothing, nothing, nothing, nothing + end +end diff --git a/test/initializationsystem.jl b/test/initializationsystem.jl index e6192144c8..353eaa24e2 100644 --- a/test/initializationsystem.jl +++ b/test/initializationsystem.jl @@ -693,6 +693,11 @@ end @test prob.f.initializeprob.ps[p] ≈ 3.0 @test init(prob, Tsit5())[x] ≈ 1.0 ModelingToolkit.defaults(prob.f.sys)[p] = missing + prob2 = remake(prob; u0 = [y => 1.0], p = [p => 3x]) + @test !is_variable(prob2.f.initializeprob, p) && + !is_parameter(prob2.f.initializeprob, p) + @test init(prob2, Tsit5())[x] ≈ 0.5 + @test_nowarn solve(prob2, Tsit5()) end @testset "Equations for dependent parameters" begin From f783cb105f9e88117bf637151551f004e42a6c5a Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 21 Aug 2024 14:19:08 +0530 Subject: [PATCH 0096/1337] test: test remaking of initialization problem --- test/initializationsystem.jl | 35 +++++++++++++++++++++++++++++++++++ test/mtkparameters.jl | 2 +- test/split_parameters.jl | 5 +++-- 3 files changed, 39 insertions(+), 3 deletions(-) diff --git a/test/initializationsystem.jl b/test/initializationsystem.jl index 353eaa24e2..d9757ffc5b 100644 --- a/test/initializationsystem.jl +++ b/test/initializationsystem.jl @@ -601,12 +601,18 @@ end pmap[p] = 2q prob = ODEProblem(sys, u0map, (0.0, 1.0), pmap) test_parameter(prob, p, 2.0) + prob2 = remake(prob; u0 = u0map, p = pmap) + prob2.ps[p] = 0.0 + test_parameter(prob2, p, 2.0) # `missing` default, provided guess @mtkbuild sys = ODESystem( [D(x) ~ x, p ~ x + y], t; defaults = [p => missing], guesses = [p => 0.0]) prob = ODEProblem(sys, u0map, (0.0, 1.0)) test_parameter(prob, p, 2.0) test_initializesystem(sys, u0map, pmap, p, 0 ~ p - x - y) + prob2 = remake(prob; u0 = u0map) + prob2.ps[p] = 0.0 + test_parameter(prob2, p, 2.0) # `missing` to ODEProblem, equation from default @mtkbuild sys = ODESystem( @@ -615,6 +621,8 @@ end prob = ODEProblem(sys, u0map, (0.0, 1.0), pmap) test_parameter(prob, p, 2.0) test_initializesystem(sys, u0map, pmap, p, 0 ~ 2q - p) + prob2 = remake(prob; u0 = u0map, p = pmap) + prob2.ps[p] = 0.0 test_parameter(prob2, p, 2.0) # `missing` to ODEProblem, provided guess @mtkbuild sys = ODESystem( @@ -622,6 +630,9 @@ end prob = ODEProblem(sys, u0map, (0.0, 1.0), pmap) test_parameter(prob, p, 2.0) test_initializesystem(sys, u0map, pmap, p, 0 ~ x + y - p) + prob2 = remake(prob; u0 = u0map, p = pmap) + prob2.ps[p] = 0.0 + test_parameter(prob2, p, 2.0) # No `missing`, default and guess @mtkbuild sys = ODESystem( @@ -630,6 +641,9 @@ end prob = ODEProblem(sys, u0map, (0.0, 1.0), pmap) test_parameter(prob, p, 2.0) test_initializesystem(sys, u0map, pmap, p, 0 ~ 2q - p) + prob2 = remake(prob; u0 = u0map, p = pmap) + prob2.ps[p] = 0.0 + test_parameter(prob2, p, 2.0) # Should not be solved for: @@ -711,3 +725,24 @@ end integ = init(prob, Tsit5()) @test integ.ps[p] ≈ 2 end + +@testset "Re-creating initialization problem on remake" begin + @variables x(t) y(t) + @parameters p q + @mtkbuild sys = ODESystem( + [D(x) ~ x, p ~ x + y], t; defaults = [p => missing], guesses = [x => 0.0, p => 0.0]) + prob = ODEProblem(sys, [x => 1.0, y => 1.0], (0.0, 1.0)) + @test init(prob, Tsit5()).ps[p] ≈ 2.0 + # nonsensical value for y just to test that equations work + prob2 = remake(prob; u0 = [x => 1.0, y => 2x + exp(t)]) + @test init(prob2, Tsit5()).ps[p] ≈ 4.0 + # solve for `x` given `p` and `y` + prob3 = remake(prob; u0 = [x => nothing, y => 1.0], p = [p => 2x + exp(t)]) + @test init(prob3, Tsit5())[x] ≈ 0.0 + @test_logs (:warn, r"overdetermined") remake( + prob; u0 = [x => 1.0, y => 2.0], p = [p => 4.0]) + prob4 = remake(prob; u0 = [x => 1.0, y => 2.0], p = [p => 4.0]) + @test solve(prob4, Tsit5()).retcode == ReturnCode.InitialFailure + prob5 = remake(prob) + @test init(prob, Tsit5()).ps[p] ≈ 2.0 +end diff --git a/test/mtkparameters.jl b/test/mtkparameters.jl index e0aee8c289..b4fc1a9677 100644 --- a/test/mtkparameters.jl +++ b/test/mtkparameters.jl @@ -293,7 +293,7 @@ end ps_vec = [k => [2.0, 3.0, 4.0, 5.0]] ps_scal = [k[1] => 1.0, k[2] => 2.0, k[3] => 3.0, k[4] => 4.0] oprob_scal_scal = ODEProblem(osys_scal, u0, 1.0, ps_scal) - newoprob = remake(oprob_scal_scal; p = ps_vec) + newoprob = remake(oprob_scal_scal; p = ps_vec, build_initializeprob = false) @test newoprob.ps[k] == [2.0, 3.0, 4.0, 5.0] end diff --git a/test/split_parameters.jl b/test/split_parameters.jl index 9f124ef45d..b8651238ea 100644 --- a/test/split_parameters.jl +++ b/test/split_parameters.jl @@ -114,7 +114,7 @@ eqs = [D(y) ~ dy * a sys = structural_simplify(model; split = false) tspan = (0.0, t_end) -prob = ODEProblem(sys, [], tspan, []) +prob = ODEProblem(sys, [], tspan, []; build_initializeprob = false) @test prob.p isa Vector{Float64} sol = solve(prob, ImplicitEuler()); @@ -122,7 +122,8 @@ sol = solve(prob, ImplicitEuler()); # ------------------------ Mixed Type Conserved -prob = ODEProblem(sys, [], tspan, []; tofloat = false, use_union = true) +prob = ODEProblem( + sys, [], tspan, []; tofloat = false, use_union = true, build_initializeprob = false) @test prob.p isa Tuple{Vector{Float64}, Vector{Int64}} sol = solve(prob, ImplicitEuler()); From 26c3f3acc61516be334109f4e882dc0431f97837 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 4 Sep 2024 19:27:52 +0530 Subject: [PATCH 0097/1337] fix: handle edge cases in namespacing --- src/systems/abstractsystem.jl | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index bad83133a1..9908d6f1e3 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -1353,10 +1353,21 @@ function namespace_assignment(eq::Assignment, sys) Assignment(_lhs, _rhs) end +function is_array_of_symbolics(x) + symbolic_type(x) == ArraySymbolic() && return true + symbolic_type(x) == ScalarSymbolic() && return false + x isa AbstractArray && + any(y -> symbolic_type(y) != NotSymbolic() || is_array_of_symbolics(y), x) +end + function namespace_expr( O, sys, n = nameof(sys); ivs = independent_variables(sys)) O = unwrap(O) - symbolic_type(O) == NotSymbolic() && return O + # Exceptions for arrays of symbolic and Ref of a symbolic, the latter + # of which shows up in broadcasts + if symbolic_type(O) == NotSymbolic() && !(O isa AbstractArray) && !(O isa Ref) + return O + end if any(isequal(O), ivs) return O elseif iscall(O) @@ -1378,7 +1389,7 @@ function namespace_expr( end elseif isvariable(O) renamespace(n, O) - elseif O isa Array + elseif O isa AbstractArray && is_array_of_symbolics(O) let sys = sys, n = n map(o -> namespace_expr(o, sys, n; ivs), O) end From de94dc1a724225c53fba2073f7874d6b9a484f1d Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 6 Sep 2024 17:01:45 +0530 Subject: [PATCH 0098/1337] fix: allow passing `nothing` to override default for `@mtkmodel` models --- src/systems/model_parsing.jl | 18 ++++++++++++------ test/model_parsing.jl | 2 +- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/systems/model_parsing.jl b/src/systems/model_parsing.jl index 70fab5a7cc..7b6705bbd5 100644 --- a/src/systems/model_parsing.jl +++ b/src/systems/model_parsing.jl @@ -148,15 +148,20 @@ end pop_structure_dict!(dict, key) = length(dict[key]) == 0 && pop!(dict, key) +struct NoValue end +const NO_VALUE = NoValue() + function update_kwargs_and_metadata!(dict, kwargs, a, def, indices, type, var, varclass, where_types, meta) if indices isa Nothing if !isnothing(meta) && haskey(meta, VariableUnit) uvar = gensym() push!(where_types, uvar) - push!(kwargs, Expr(:kw, :($a::Union{Nothing, Missing, $uvar}), nothing)) + push!(kwargs, + Expr(:kw, :($a::Union{Nothing, Missing, $NoValue, $uvar}), NO_VALUE)) else - push!(kwargs, Expr(:kw, :($a::Union{Nothing, Missing, $type}), nothing)) + push!(kwargs, + Expr(:kw, :($a::Union{Nothing, Missing, $NoValue, $type}), NO_VALUE)) end dict[:kwargs][getname(var)] = Dict(:value => def, :type => type) else @@ -164,8 +169,9 @@ function update_kwargs_and_metadata!(dict, kwargs, a, def, indices, type, var, push!(kwargs, Expr(:kw, Expr(:(::), a, - Expr(:curly, :Union, :Nothing, Expr(:curly, :AbstractArray, vartype))), - nothing)) + Expr(:curly, :Union, :Nothing, :Missing, NoValue, + Expr(:curly, :AbstractArray, vartype))), + NO_VALUE)) if !isnothing(meta) && haskey(meta, VariableUnit) push!(where_types, vartype) else @@ -679,7 +685,7 @@ function parse_variable_arg(dict, mod, arg, varclass, kwargs, where_types) varexpr = if haskey(metadata_with_exprs, VariableUnit) unit = metadata_with_exprs[VariableUnit] quote - $name = if $name === nothing + $name = if $name === $NO_VALUE $setdefault($vv, $def) else try @@ -699,7 +705,7 @@ function parse_variable_arg(dict, mod, arg, varclass, kwargs, where_types) end else quote - $name = if $name === nothing + $name = if $name === $NO_VALUE $setdefault($vv, $def) else $setdefault($vv, $name) diff --git a/test/model_parsing.jl b/test/model_parsing.jl index 6332dc12a5..f80dc77dc4 100644 --- a/test/model_parsing.jl +++ b/test/model_parsing.jl @@ -840,7 +840,7 @@ end n[1:3] = if flag [2, 2, 2] else - 1 + [1, 1, 1] end end end From 5874f97ed519a8bb18c83d9ec9b8bb8f794e4f74 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 6 Sep 2024 17:02:02 +0530 Subject: [PATCH 0099/1337] fix: fix `nothing` default overrides being ignored --- src/systems/diffeqs/odesystem.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/systems/diffeqs/odesystem.jl b/src/systems/diffeqs/odesystem.jl index 48c00a933b..539fd1a975 100644 --- a/src/systems/diffeqs/odesystem.jl +++ b/src/systems/diffeqs/odesystem.jl @@ -242,12 +242,12 @@ function ODESystem(deqs::AbstractVector{<:Equation}, iv, dvs, ps; "`default_u0` and `default_p` are deprecated. Use `defaults` instead.", :ODESystem, force = true) end - defaults = todict(defaults) - defaults = Dict{Any, Any}(value(k) => value(v) - for (k, v) in pairs(defaults) if value(v) !== nothing) + defaults = Dict{Any, Any}(todict(defaults)) var_to_name = Dict() process_variables!(var_to_name, defaults, dvs′) process_variables!(var_to_name, defaults, ps′) + defaults = Dict{Any, Any}(value(k) => value(v) + for (k, v) in pairs(defaults) if v !== nothing) sysdvsguesses = [ModelingToolkit.getguess(st) for st in dvs′] hasaguess = findall(!isnothing, sysdvsguesses) From d7755bfb9464469cd983f437fc9ab045c4398b08 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 26 Sep 2024 19:24:33 +0530 Subject: [PATCH 0100/1337] feat: track parameter dependency defaults and guesses --- src/systems/diffeqs/odesystem.jl | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/systems/diffeqs/odesystem.jl b/src/systems/diffeqs/odesystem.jl index 539fd1a975..bda5690a67 100644 --- a/src/systems/diffeqs/odesystem.jl +++ b/src/systems/diffeqs/odesystem.jl @@ -237,6 +237,8 @@ function ODESystem(deqs::AbstractVector{<:Equation}, iv, dvs, ps; ctrl′ = value.(controls) dvs′ = value.(dvs) dvs′ = filter(x -> !isdelay(x, iv), dvs′) + parameter_dependencies, ps′ = process_parameter_dependencies( + parameter_dependencies, ps′) if !(isempty(default_u0) && isempty(default_p)) Base.depwarn( "`default_u0` and `default_p` are deprecated. Use `defaults` instead.", @@ -246,6 +248,8 @@ function ODESystem(deqs::AbstractVector{<:Equation}, iv, dvs, ps; var_to_name = Dict() process_variables!(var_to_name, defaults, dvs′) process_variables!(var_to_name, defaults, ps′) + process_variables!(var_to_name, defaults, [eq.lhs for eq in parameter_dependencies]) + process_variables!(var_to_name, defaults, [eq.rhs for eq in parameter_dependencies]) defaults = Dict{Any, Any}(value(k) => value(v) for (k, v) in pairs(defaults) if v !== nothing) @@ -257,9 +261,15 @@ function ODESystem(deqs::AbstractVector{<:Equation}, iv, dvs, ps; hasaguess = findall(!isnothing, syspsguesses) ps_guesses = ps′[hasaguess] .=> syspsguesses[hasaguess] syspsguesses = isempty(ps_guesses) ? Dict() : todict(ps_guesses) + syspdepguesses = [ModelingToolkit.getguess(eq.lhs) for eq in parameter_dependencies] + hasaguess = findall(!isnothing, syspdepguesses) + pdep_guesses = [eq.lhs for eq in parameter_dependencies][hasaguess] .=> + syspdepguesses[hasaguess] + syspdepguesses = isempty(pdep_guesses) ? Dict() : todict(pdep_guesses) - guesses = merge(sysdvsguesses, syspsguesses, todict(guesses)) - guesses = Dict{Any, Any}(value(k) => value(v) for (k, v) in pairs(guesses)) + guesses = merge(sysdvsguesses, syspsguesses, syspdepguesses, todict(guesses)) + guesses = Dict{Any, Any}(value(k) => value(v) + for (k, v) in pairs(guesses) if v !== nothing) isempty(observed) || collect_var_to_name!(var_to_name, (eq.lhs for eq in observed)) @@ -274,8 +284,7 @@ function ODESystem(deqs::AbstractVector{<:Equation}, iv, dvs, ps; end cont_callbacks = SymbolicContinuousCallbacks(continuous_events) disc_callbacks = SymbolicDiscreteCallbacks(discrete_events) - parameter_dependencies, ps′ = process_parameter_dependencies( - parameter_dependencies, ps′) + if is_dde === nothing is_dde = _check_if_dde(deqs, iv′, systems) end From 96faa983161c677c33e351361238163b65a75dd8 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 1 Oct 2024 15:08:21 +0530 Subject: [PATCH 0101/1337] fix: handle type promotion in `InitializationProblem` with observed --- src/systems/diffeqs/abstractodesystem.jl | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index 623b2b47af..8ebe6d93d0 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -888,7 +888,8 @@ function process_DEProblem(constructor, sys::AbstractODESystem, u0map, parammap; for p in parameters(sys) if is_parameter_solvable(p, parammap, defs, guesses)] - pvarmap = if parammap === nothing || parammap == SciMLBase.NullParameters() || !(eltype(parammap) <: Pair) && isempty(parammap) + pvarmap = if parammap === nothing || parammap == SciMLBase.NullParameters() || + !(eltype(parammap) <: Pair) && isempty(parammap) defs else merge(defs, todict(parammap)) @@ -1592,6 +1593,22 @@ function InitializationProblem{iip, specialize}(sys::AbstractODESystem, end u0map = merge(ModelingToolkit.guesses(sys), todict(guesses), todict(u0map)) + fullmap = merge(u0map, parammap) + u0T = Union{} + for sym in unknowns(isys) + haskey(fullmap, sym) || continue + symbolic_type(fullmap[sym]) == NotSymbolic() || continue + u0T = promote_type(u0T, typeof(fullmap[sym])) + end + for eq in observed(isys) + haskey(fullmap, eq.lhs) || continue + symbolic_type(fullmap[eq.lhs]) == NotSymbolic() || continue + u0T = promote_type(u0T, typeof(fullmap[eq.lhs])) + end + if u0T != Union{} + u0map = Dict(k => symbolic_type(v) == NotSymbolic() ? u0T(v) : v + for (k, v) in u0map) + end if neqs == nunknown NonlinearProblem(isys, u0map, parammap; kwargs...) else From 8836abec485bd0d2d7504c9960b37bfa73fee3d5 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 1 Oct 2024 15:08:44 +0530 Subject: [PATCH 0102/1337] fix: improve type promotion in `remake_initializeprob` --- src/systems/nonlinear/initializesystem.jl | 45 +++++++++++++++++++++-- test/initializationsystem.jl | 36 +++++++++++++++++- 2 files changed, 77 insertions(+), 4 deletions(-) diff --git a/src/systems/nonlinear/initializesystem.jl b/src/systems/nonlinear/initializesystem.jl index a4a29f223b..5b465a677c 100644 --- a/src/systems/nonlinear/initializesystem.jl +++ b/src/systems/nonlinear/initializesystem.jl @@ -173,15 +173,54 @@ function is_parameter_solvable(p, pmap, defs, guesses) # either (missing is a default or was passed to the ODEProblem) or (nothing was passed to # the ODEProblem and it has a default and a guess) return ((_val1 === missing || _val2 === missing) || - (_val1 === nothing && _val2 !== nothing)) && _val3 !== nothing + (_val1 === nothing && _val2 !== nothing)) && _val3 !== nothing end function SciMLBase.remake_initializeprob(sys::ODESystem, odefn, u0, t0, p) - if (u0 === missing || !(eltype(u0) <: Pair) || isempty(u0)) && - (p === missing || !(eltype(p) <: Pair) || isempty(p)) + if u0 === missing && p === missing return odefn.initializeprob, odefn.update_initializeprob!, odefn.initializeprobmap, odefn.initializeprobpmap end + if !(eltype(u0) <: Pair) && !(eltype(p) <: Pair) + oldinitprob = odefn.initializeprob + if oldinitprob === nothing || !SciMLBase.has_sys(oldinitprob.f) || + !(oldinitprob.f.sys isa NonlinearSystem) + return oldinitprob, odefn.update_initializeprob!, odefn.initializeprobmap, + odefn.initializeprobpmap + end + pidxs = ParameterIndex[] + pvals = [] + u0idxs = Int[] + u0vals = [] + for sym in variable_symbols(oldinitprob) + if is_variable(sys, sym) + u0 !== missing || continue + idx = variable_index(oldinitprob, sym) + push!(u0idxs, idx) + push!(u0vals, eltype(u0)(state_values(oldinitprob, idx))) + else + p !== missing || continue + idx = variable_index(oldinitprob, sym) + push!(u0idxs, idx) + push!(u0vals, typeof(getp(sys, sym)(p))(state_values(oldinitprob, idx))) + end + end + if p !== missing + for sym in parameter_symbols(oldinitprob) + push!(pidxs, parameter_index(oldinitprob, sym)) + if isequal(sym, get_iv(sys)) + push!(pvals, t0) + else + push!(pvals, getp(sys, sym)(p)) + end + end + end + newu0 = remake_buffer(oldinitprob.f.sys, state_values(oldinitprob), u0idxs, u0vals) + newp = remake_buffer(oldinitprob.f.sys, parameter_values(oldinitprob), pidxs, pvals) + initprob = remake(oldinitprob; u0 = newu0, p = newp) + return initprob, odefn.update_initializeprob!, odefn.initializeprobmap, + odefn.initializeprobpmap + end if u0 === missing || isempty(u0) u0 = Dict() elseif !(eltype(u0) <: Pair) diff --git a/test/initializationsystem.jl b/test/initializationsystem.jl index d9757ffc5b..f30dc5fa3a 100644 --- a/test/initializationsystem.jl +++ b/test/initializationsystem.jl @@ -1,5 +1,7 @@ using ModelingToolkit, OrdinaryDiffEq, NonlinearSolve, Test -using SymbolicIndexingInterface +using ForwardDiff +using SymbolicIndexingInterface, SciMLStructures +using SciMLStructures: Tunable using ModelingToolkit: t_nounits as t, D_nounits as D @parameters g @@ -746,3 +748,35 @@ end prob5 = remake(prob) @test init(prob, Tsit5()).ps[p] ≈ 2.0 end + +@testset "`remake` changes initialization problem types" begin + @variables x(t) y(t) z(t) + @parameters p q + @mtkbuild sys = ODESystem( + [D(x) ~ x * p + y * q, y^2 * q + q^2 * x ~ 0, z * p - p^2 * x * z ~ 0], + t; guesses = [x => 0.0, y => 0.0, z => 0.0, p => 0.0, q => 0.0]) + prob = ODEProblem(sys, [x => 1.0], (0.0, 1.0), [p => 1.0, q => missing]) + @test is_variable(prob.f.initializeprob, q) + ps = prob.p + newps = SciMLStructures.replace(Tunable(), ps, ForwardDiff.Dual.(ps.tunable)) + prob2 = remake(prob; p = newps) + @test eltype(prob2.f.initializeprob.u0) <: ForwardDiff.Dual + @test eltype(prob2.f.initializeprob.p.tunable) <: ForwardDiff.Dual + @test prob2.f.initializeprob.u0 ≈ prob.f.initializeprob.u0 + + prob2 = remake(prob; u0 = ForwardDiff.Dual.(prob.u0)) + @test eltype(prob2.f.initializeprob.u0) <: ForwardDiff.Dual + @test eltype(prob2.f.initializeprob.p.tunable) <: Float64 + @test prob2.f.initializeprob.u0 ≈ prob.f.initializeprob.u0 + + prob2 = remake(prob; u0 = ForwardDiff.Dual.(prob.u0), p = newps) + @test eltype(prob2.f.initializeprob.u0) <: ForwardDiff.Dual + @test eltype(prob2.f.initializeprob.p.tunable) <: ForwardDiff.Dual + @test prob2.f.initializeprob.u0 ≈ prob.f.initializeprob.u0 + + prob2 = remake(prob; u0 = [x => ForwardDiff.Dual(1.0)], + p = [p => ForwardDiff.Dual(1.0), q => missing]) + @test eltype(prob2.f.initializeprob.u0) <: ForwardDiff.Dual + @test eltype(prob2.f.initializeprob.p.tunable) <: ForwardDiff.Dual + @test prob2.f.initializeprob.u0 ≈ prob.f.initializeprob.u0 +end From 015ce60b2d528f69dad83317100647985a817b02 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 1 Oct 2024 17:30:51 +0530 Subject: [PATCH 0103/1337] feat: preserve the `u0map` passed to `ODEProblem` and use it in `remake_initializeprob` --- src/systems/nonlinear/initializesystem.jl | 16 ++++++++++++++++ test/initializationsystem.jl | 16 ++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/src/systems/nonlinear/initializesystem.jl b/src/systems/nonlinear/initializesystem.jl index 5b465a677c..f0a07bfc25 100644 --- a/src/systems/nonlinear/initializesystem.jl +++ b/src/systems/nonlinear/initializesystem.jl @@ -157,15 +157,22 @@ function generate_initializesystem(sys::ODESystem; for k in keys(defs) defs[k] = substitute(defs[k], paramsubs) end + meta = InitializationSystemMetadata(Dict{Any, Any}(u0map), Dict{Any, Any}(pmap)) return NonlinearSystem(eqs_ics, vars, pars; defaults = defs, checks = check_units, name, + metadata = meta, kwargs...) end +struct InitializationSystemMetadata + u0map::Dict{Any, Any} + pmap::Dict{Any, Any} +end + function is_parameter_solvable(p, pmap, defs, guesses) _val1 = pmap isa AbstractDict ? get(pmap, p, nothing) : nothing _val2 = get(defs, p, nothing) @@ -253,6 +260,15 @@ function SciMLBase.remake_initializeprob(sys::ODESystem, odefn, u0, t0, p) !isempty(setobserved) || !isempty(setparobserved)) && ModelingToolkit.get_tearing_state(sys) !== nothing) || !isempty(initialization_equations(sys))) + if SciMLBase.has_initializeprob(odefn) + oldsys = odefn.initializeprob.f.sys + meta = get_metadata(oldsys) + if meta isa InitializationSystemMetadata + u0 = merge(meta.u0map, u0) + p = merge(meta.pmap, p) + end + end + initprob = InitializationProblem(sys, t0, u0, p) initprobmap = getu(initprob, unknowns(sys)) punknowns = [p for p in all_variable_symbols(initprob) if is_parameter(sys, p)] diff --git a/test/initializationsystem.jl b/test/initializationsystem.jl index f30dc5fa3a..77c015144e 100644 --- a/test/initializationsystem.jl +++ b/test/initializationsystem.jl @@ -780,3 +780,19 @@ end @test eltype(prob2.f.initializeprob.p.tunable) <: ForwardDiff.Dual @test prob2.f.initializeprob.u0 ≈ prob.f.initializeprob.u0 end + +@testset "`remake` preserves old u0map and pmap" begin + @variables x(t) y(t) + @parameters p + @mtkbuild sys = ODESystem( + [D(x) ~ x + p * y, y^2 + 4y * p^2 ~ x], t; guesses = [y => 1.0, p => 1.0]) + prob = ODEProblem(sys, [x => 1.0], (0.0, 1.0), [p => 1.0]) + @test is_variable(prob.f.initializeprob, y) + prob2 = @test_nowarn remake(prob; p = [p => 3.0]) # ensure no over/under-determined warning + @test is_variable(prob.f.initializeprob, y) + + prob = ODEProblem(sys, [y => 1.0, x => 2.0], (0.0, 1.0), [p => missing]) + @test is_variable(prob.f.initializeprob, p) + prob2 = @test_nowarn remake(prob; u0 = [y => 0.5]) + @test is_variable(prob.f.initializeprob, p) +end From cab11eb594002aea5ece2f5948c21a103878ccf1 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 7 Oct 2024 18:42:32 +0530 Subject: [PATCH 0104/1337] feat: validate parameter type and allow dependent initial values in param init --- src/systems/nonlinear/initializesystem.jl | 5 +++- src/utils.jl | 7 ++++++ test/initializationsystem.jl | 30 ++++++++++++++++++----- 3 files changed, 35 insertions(+), 7 deletions(-) diff --git a/src/systems/nonlinear/initializesystem.jl b/src/systems/nonlinear/initializesystem.jl index f0a07bfc25..6a21e9a6f0 100644 --- a/src/systems/nonlinear/initializesystem.jl +++ b/src/systems/nonlinear/initializesystem.jl @@ -174,13 +174,16 @@ struct InitializationSystemMetadata end function is_parameter_solvable(p, pmap, defs, guesses) + p = unwrap(p) + is_variable_floatingpoint(p) || return false _val1 = pmap isa AbstractDict ? get(pmap, p, nothing) : nothing _val2 = get(defs, p, nothing) _val3 = get(guesses, p, nothing) # either (missing is a default or was passed to the ODEProblem) or (nothing was passed to # the ODEProblem and it has a default and a guess) return ((_val1 === missing || _val2 === missing) || - (_val1 === nothing && _val2 !== nothing)) && _val3 !== nothing + (symbolic_type(_val1) != NotSymbolic() || + _val1 === nothing && _val2 !== nothing)) && _val3 !== nothing end function SciMLBase.remake_initializeprob(sys::ODESystem, odefn, u0, t0, p) diff --git a/src/utils.jl b/src/utils.jl index a3f99731af..29e61ede69 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -885,3 +885,10 @@ end diff2term_with_unit(x, t) = _with_unit(diff2term, x, t) lower_varname_with_unit(var, iv, order) = _with_unit(lower_varname, var, iv, iv, order) + +function is_variable_floatingpoint(sym) + sym = unwrap(sym) + T = symtype(sym) + return T == Real || T <: AbstractFloat || T <: AbstractArray{Real} || + T <: AbstractArray{<:AbstractFloat} +end diff --git a/test/initializationsystem.jl b/test/initializationsystem.jl index 77c015144e..08b653c22e 100644 --- a/test/initializationsystem.jl +++ b/test/initializationsystem.jl @@ -647,19 +647,37 @@ end prob2.ps[p] = 0.0 test_parameter(prob2, p, 2.0) - # Should not be solved for: + # Default overridden by ODEProblem, guess provided + @mtkbuild sys = ODESystem( + [D(x) ~ q * x, D(y) ~ y * p], t; defaults = [p => 2q], guesses = [p => 1.0]) + _pmap = merge(pmap, Dict(p => q)) + prob = ODEProblem(sys, u0map, (0.0, 1.0), _pmap) + test_parameter(prob, p, _pmap[q]) + test_initializesystem(sys, u0map, _pmap, p, 0 ~ q - p) - # ODEProblem value with guess, no `missing` + # ODEProblem dependent value with guess, no `missing` @mtkbuild sys = ODESystem([D(x) ~ x * q, D(y) ~ y * p], t; guesses = [p => 0.0]) _pmap = merge(pmap, Dict(p => 3q)) prob = ODEProblem(sys, u0map, (0.0, 1.0), _pmap) - @test prob.ps[p] ≈ 3.0 - @test prob.f.initializeprob === nothing - # Default overridden by ODEProblem, guess provided + test_parameter(prob, p, 3pmap[q]) + + # Should not be solved for: + + # Override dependent default with direct value @mtkbuild sys = ODESystem( [D(x) ~ q * x, D(y) ~ y * p], t; defaults = [p => 2q], guesses = [p => 1.0]) + _pmap = merge(pmap, Dict(p => 1.0)) prob = ODEProblem(sys, u0map, (0.0, 1.0), _pmap) - @test prob.ps[p] ≈ 3.0 + @test prob.ps[p] ≈ 1.0 + @test prob.f.initializeprob === nothing + + # Non-floating point + @parameters r::Int s::Int + @mtkbuild sys = ODESystem( + [D(x) ~ s * x, D(y) ~ y * r], t; defaults = [s => 2r], guesses = [s => 1.0]) + prob = ODEProblem(sys, u0map, (0.0, 1.0), [r => 1]) + @test prob.ps[r] == 1 + @test prob.ps[s] == 2 @test prob.f.initializeprob === nothing @mtkbuild sys = ODESystem([D(x) ~ x, p ~ x + y], t; guesses = [p => 0.0]) From 62c2f09d11c72dfeaf212270640fc7ff84000455 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 7 Oct 2024 22:14:57 +0530 Subject: [PATCH 0105/1337] fix: only allow numeric pdeps to be initialization equations --- src/systems/nonlinear/initializesystem.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/src/systems/nonlinear/initializesystem.jl b/src/systems/nonlinear/initializesystem.jl index 6a21e9a6f0..1641d66dc0 100644 --- a/src/systems/nonlinear/initializesystem.jl +++ b/src/systems/nonlinear/initializesystem.jl @@ -132,6 +132,7 @@ function generate_initializesystem(sys::ODESystem; # 5) parameter dependencies become equations, their LHS become unknowns for eq in parameter_dependencies(sys) + is_variable_floatingpoint(eq.lhs) || continue varp = tovar(eq.lhs) paramsubs[eq.lhs] = varp push!(eqs_ics, eq) From ba82445d0de255f6340348c019c675a47a65609f Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 7 Oct 2024 22:15:11 +0530 Subject: [PATCH 0106/1337] feat: allow `nothing` to override retained values in `remake_initializeprob` --- src/systems/nonlinear/initializesystem.jl | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/systems/nonlinear/initializesystem.jl b/src/systems/nonlinear/initializesystem.jl index 1641d66dc0..38a3095b5c 100644 --- a/src/systems/nonlinear/initializesystem.jl +++ b/src/systems/nonlinear/initializesystem.jl @@ -246,6 +246,11 @@ function SciMLBase.remake_initializeprob(sys::ODESystem, odefn, u0, t0, p) u0 = todict(u0) defs = defaults(sys) varmap = merge(defs, u0) + for k in collect(keys(varmap)) + if varmap[k] === nothing + delete!(varmap, k) + end + end varmap = canonicalize_varmap(varmap) missingvars = setdiff(unknowns(sys), collect(keys(varmap))) setobserved = filter(keys(varmap)) do var @@ -272,6 +277,16 @@ function SciMLBase.remake_initializeprob(sys::ODESystem, odefn, u0, t0, p) p = merge(meta.pmap, p) end end + for k in collect(keys(u0)) + if u0[k] === nothing + delete!(u0, k) + end + end + for k in collect(keys(p)) + if p[k] === nothing + delete!(p, k) + end + end initprob = InitializationProblem(sys, t0, u0, p) initprobmap = getu(initprob, unknowns(sys)) From ec75ebee36aff1aefcd8dcc2bfc0ac6d3e196649 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 6 Aug 2024 18:45:41 +0530 Subject: [PATCH 0107/1337] docs: document parameter initialization in the tutorial --- docs/src/tutorials/initialization.md | 134 +++++++++++++++++++++++++++ 1 file changed, 134 insertions(+) diff --git a/docs/src/tutorials/initialization.md b/docs/src/tutorials/initialization.md index f40c28991f..6451832dbc 100644 --- a/docs/src/tutorials/initialization.md +++ b/docs/src/tutorials/initialization.md @@ -201,6 +201,87 @@ long enough you will see that `λ = 0` is required for this equation, but since problem constructor. Additionally, any warning about not being fully determined can be suppressed via passing `warn_initialize_determined = false`. +## Initialization of parameters + +Parameters may also be treated as unknowns in the initialization system. Doing so works +almost identically to the standard case. For a parameter to be an initialization unknown +(henceforth referred to as "solved parameter") it must represent a floating point number +(have a `symtype` of `Real` or `<:AbstractFloat`) or an array of such numbers. Additionally, +it must have a guess and one of the following conditions must be satisfied: + + 1. The value of the parameter as passed to `ODEProblem` is an expression involving other + variables/parameters. For example, if `[p => 2q + x]` is passed to `ODEProblem`. In + this case, `p ~ 2q + x` is used as an equation during initialization. + 2. The parameter has a default (and no value for it is given to `ODEProblem`, since + that is condition 1). The default will be used as an equation during initialization. + 3. The parameter has a default of `missing`. If `ODEProblem` is given a value for this + parameter, it is used as an equation during initialization (whether the value is an + expression or not). + 4. `ODEProblem` is given a value of `missing` for the parameter. If the parameter has a + default, it will be used as an equation during initialization. + +All parameter dependencies (where the dependent parameter is a floating point number or +array thereof) also become equations during initialization, and the dependent parameters +become unknowns. + +`remake` will reconstruct the initialization system and problem, given the new +constraints provided to it. The new values will be combined with the original +variable-value mapping provided to `ODEProblem` and used to construct the initialization +problem. + +### Parameter initialization by example + +Consider the following system, where the sum of two unknowns is a constant parameter +`total`. + +```@example paraminit +using ModelingToolkit, OrdinaryDiffEq # hidden +using ModelingToolkit: t_nounits as t, D_nounits as D # hidden + +@variables x(t) y(t) +@parameters total +@mtkbuild sys = ODESystem([D(x) ~ -x, total ~ x + y], t; + defaults = [total => missing], guesses = [total => 1.0]) +``` + +Given any two of `x`, `y` and `total` we can determine the remaining variable. + +```@example paraminit +prob = ODEProblem(sys, [x => 1.0, y => 2.0], (0.0, 1.0)) +integ = init(prob, Tsit5()) +@assert integ.ps[total] ≈ 3.0 # hide +integ.ps[total] +``` + +Suppose we want to re-create this problem, but now solve for `x` given `total` and `y`: + +```@example paraminit +prob2 = remake(prob; u0 = [y => 1.0], p = [total => 4.0]) +initsys = prob2.f.initializeprob.f.sys +``` + +The system is now overdetermined. In fact: + +```@example paraminit +[equations(initsys); observed(initsys)] +``` + +The system can never be satisfied and will always lead to an `InitialFailure`. This is +due to the aforementioned behavior of retaining the original variable-value mapping +provided to `ODEProblem`. To fix this, we pass `x => nothing` to `remake` to remove its +retained value. + +```@example paraminit +prob2 = remake(prob; u0 = [y => 1.0, x => nothing], p = [total => 4.0]) +initsys = prob2.f.initializeprob.f.sys +``` + +The system is fully determined, and the equations are solvable. + +```@example +[equations(initsys); observed(initsys)] +``` + ## Diving Deeper: Constructing the Initialization System To get a better sense of the initialization system and to help debug it, you can construct @@ -383,3 +464,56 @@ sol[α * x - β * x * y] ```@example init plot(sol) ``` + +## Solving for parameters during initialization + +Sometimes, it is necessary to solve for a parameter during initialization. For example, +given a spring-mass system we want to find the un-stretched length of the spring given +that the initial condition of the system is its steady state. + +```@example init +using ModelingToolkitStandardLibrary.Mechanical.TranslationalModelica: Fixed, Mass, Spring, + Force, Damper +using ModelingToolkitStandardLibrary.Blocks: Constant + +@named mass = Mass(; m = 1.0, s = 1.0, v = 0.0, a = 0.0) +@named fixed = Fixed(; s0 = 0.0) +@named spring = Spring(; c = 2.0, s_rel0 = missing) +@named gravity = Force() +@named constant = Constant(; k = 9.81) +@named damper = Damper(; d = 0.1) +@mtkbuild sys = ODESystem( + [connect(fixed.flange, spring.flange_a), connect(spring.flange_b, mass.flange_a), + connect(mass.flange_a, gravity.flange), connect(constant.output, gravity.f), + connect(fixed.flange, damper.flange_a), connect(damper.flange_b, mass.flange_a)], + t; + systems = [fixed, spring, mass, gravity, constant, damper], + guesses = [spring.s_rel0 => 1.0]) +``` + +Note that we explicitly provide `s_rel0 = missing` to the spring. Parameters are only +solved for during initialization if their value (either default, or explicitly passed +to the `ODEProblem` constructor) is `missing`. We also need to provide a guess for the +parameter. + +If a parameter is not given a value of `missing`, and does not have a default or initial +value, the `ODEProblem` constructor will throw an error. If the parameter _does_ have a +value of `missing`, it must be given a guess. + +```@example init +prob = ODEProblem(sys, [], (0.0, 1.0)) +prob.ps[spring.s_rel0] +``` + +Note that the value of the parameter in the problem is zero, similar to unknowns that +are solved for during initialization. + +```@example init +integ = init(prob) +integ.ps[spring.s_rel0] +``` + +The un-stretched length of the spring is now correctly calculated. The same result can be +achieved if `s_rel0 = missing` is omitted when constructing `spring`, and instead +`spring.s_rel0 => missing` is passed to the `ODEProblem` constructor along with values +of other parameters. From 84a1f2e233624a4fd2a60420d03f6349e3c74aa6 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 9 Oct 2024 17:54:17 +0530 Subject: [PATCH 0108/1337] build: bump compats for SciMLBase, DiffEqBase, OrdinaryDiffEqCore --- Project.toml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Project.toml b/Project.toml index 1947ea7ce6..cfd424ef8b 100644 --- a/Project.toml +++ b/Project.toml @@ -81,7 +81,7 @@ ConstructionBase = "1" DataInterpolations = "6.4" DataStructures = "0.17, 0.18" DeepDiffs = "1" -DiffEqBase = "6.103.0" +DiffEqBase = "6.157" DiffEqCallbacks = "2.16, 3, 4" DiffEqNoiseProcess = "5" DiffRules = "0.1, 1.0" @@ -110,12 +110,13 @@ NonlinearSolve = "3.14" OffsetArrays = "1" OrderedCollections = "1" OrdinaryDiffEq = "6.82.0" +OrdinaryDiffEqCore = "1.7.0" PrecompileTools = "1" REPL = "1" RecursiveArrayTools = "3.26" Reexport = "0.2, 1" RuntimeGeneratedFunctions = "0.5.9" -SciMLBase = "2.55" +SciMLBase = "2.56.1" SciMLStructures = "1.0" Serialization = "1" Setfield = "0.7, 0.8, 1" @@ -148,6 +149,7 @@ Optimization = "7f7a1694-90dd-40f0-9382-eb1efda571ba" OptimizationMOI = "fd9f6733-72f4-499f-8506-86b2bdd0dea1" OptimizationOptimJL = "36348300-93cb-4f02-beb5-3c3902f8871e" OrdinaryDiffEq = "1dea7af3-3e70-54e6-95c3-0bf5283fa5ed" +OrdinaryDiffEqCore = "bbf590c4-e513-4bbe-9b18-05decba2e5d8" Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" REPL = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" @@ -162,4 +164,4 @@ Sundials = "c3572dad-4567-51f8-b174-8c6c989267f4" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [targets] -test = ["AmplNLWriter", "BenchmarkTools", "ControlSystemsBase", "DataInterpolations", "DelayDiffEq", "NonlinearSolve", "ForwardDiff", "Ipopt", "Ipopt_jll", "ModelingToolkitStandardLibrary", "Optimization", "OptimizationOptimJL", "OptimizationMOI", "OrdinaryDiffEq", "REPL", "Random", "ReferenceTests", "SafeTestsets", "StableRNGs", "Statistics", "SteadyStateDiffEq", "Test", "StochasticDiffEq", "Sundials", "StochasticDelayDiffEq", "Pkg", "JET"] +test = ["AmplNLWriter", "BenchmarkTools", "ControlSystemsBase", "DataInterpolations", "DelayDiffEq", "NonlinearSolve", "ForwardDiff", "Ipopt", "Ipopt_jll", "ModelingToolkitStandardLibrary", "Optimization", "OptimizationOptimJL", "OptimizationMOI", "OrdinaryDiffEq", "OrdinaryDiffEqCore", "REPL", "Random", "ReferenceTests", "SafeTestsets", "StableRNGs", "Statistics", "SteadyStateDiffEq", "Test", "StochasticDiffEq", "Sundials", "StochasticDelayDiffEq", "Pkg", "JET"] From 816fde74ed572cbb0201e67f42abd32afc4594d0 Mon Sep 17 00:00:00 2001 From: Christopher Rackauckas Date: Thu, 10 Oct 2024 04:02:36 -0400 Subject: [PATCH 0109/1337] Update Project.toml --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index cfd424ef8b..23ae9f8d13 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "ModelingToolkit" uuid = "961ee093-0014-501f-94e3-6117800e7a78" authors = ["Yingbo Ma ", "Chris Rackauckas and contributors"] -version = "9.44.0" +version = "9.45.0" [deps] AbstractTrees = "1520ce14-60c1-5f80-bbc7-55ef81b5835c" From ae22c7127fef1210f2d4c17210d45d60a5d58933 Mon Sep 17 00:00:00 2001 From: Venkateshprasad <32921645+ven-k@users.noreply.github.com> Date: Tue, 8 Oct 2024 14:06:52 +0000 Subject: [PATCH 0110/1337] feat: add helpers to update symbolic array metadata --- src/systems/model_parsing.jl | 64 +++++++++++++++++++++++++++++++++--- test/model_parsing.jl | 6 ++-- 2 files changed, 64 insertions(+), 6 deletions(-) diff --git a/src/systems/model_parsing.jl b/src/systems/model_parsing.jl index 7b6705bbd5..8a0f5165e4 100644 --- a/src/systems/model_parsing.jl +++ b/src/systems/model_parsing.jl @@ -163,7 +163,7 @@ function update_kwargs_and_metadata!(dict, kwargs, a, def, indices, type, var, push!(kwargs, Expr(:kw, :($a::Union{Nothing, Missing, $NoValue, $type}), NO_VALUE)) end - dict[:kwargs][getname(var)] = Dict(:value => def, :type => type) + dict[:kwargs][a] = Dict(:value => def, :type => type) else vartype = gensym(:T) push!(kwargs, @@ -177,15 +177,71 @@ function update_kwargs_and_metadata!(dict, kwargs, a, def, indices, type, var, else push!(where_types, :($vartype <: $type)) end - dict[:kwargs][getname(var)] = Dict(:value => def, :type => AbstractArray{type}) + dict[:kwargs][a] = Dict(:value => def, :type => AbstractArray{type}) end if dict[varclass] isa Vector - dict[varclass][1][getname(var)][:type] = AbstractArray{type} + dict[varclass][1][a][:type] = AbstractArray{type} else - dict[varclass][getname(var)][:type] = type + dict[varclass][a][:type] = type end end +function update_readable_metadata!(varclass_dict, meta::Dict, varname) + metatypes = [(:connection_type, VariableConnectType), + (:description, VariableDescription), + (:unit, VariableUnit), + (:bounds, VariableBounds), + (:noise, VariableNoiseType), + (:input, VariableInput), + (:output, VariableOutput), + (:irreducible, VariableIrreducible), + (:state_priority, VariableStatePriority), + (:misc, VariableMisc), + (:disturbance, VariableDisturbance), + (:tunable, VariableTunable), + (:dist, VariableDistribution)] + + var_dict = get!(varclass_dict, varname) do + Dict{Symbol, Any}() + end + + for (type, key) in metatypes + if (mt = get(meta, key, nothing)) !== nothing + key == VariableConnectType && (mt = nameof(mt)) + var_dict[type] = mt + end + end +end + +function push_array_kwargs_and_metadata!( + dict, indices, meta, type, varclass, varname, varval) + dict[varclass] = get!(dict, varclass) do + Dict{Symbol, Dict{Symbol, Any}}() + end + varclass_dict = dict[varclass] isa Vector ? Ref(dict[varclass][1]) : Ref(dict[varclass]) + + merge!(varclass_dict[], + Dict(varname => Dict( + :size => tuple([index_arg.args[end] for index_arg in indices]...), + :value => varval, + :type => type + ))) + + # Useful keys for kwargs entry are: value, type and size. + dict[:kwargs][varname] = varclass_dict[][varname] + + meta !== nothing && update_readable_metadata!(varclass_dict[], meta, varname) +end + +function unit_handled_variable_value(meta, varname) + varval = if meta isa Nothing || get(meta, VariableUnit, nothing) isa Nothing + varname + else + :($convert_units($(meta[VariableUnit]), $varname)) + end + return varval +end + function parse_variable_def!(dict, mod, arg, varclass, kwargs, where_types; def = nothing, indices::Union{Vector{UnitRange{Int}}, Nothing} = nothing, type::Type = Real, meta = Dict{DataType, Expr}()) diff --git a/test/model_parsing.jl b/test/model_parsing.jl index f80dc77dc4..727954a00d 100644 --- a/test/model_parsing.jl +++ b/test/model_parsing.jl @@ -259,7 +259,8 @@ end @test all(collect(hasmetadata.(model.l, ModelingToolkit.VariableDescription))) @test all(lastindex.([model.a2, model.b2, model.d2, model.e2, model.h2]) .== 2) - @test size(model.l) == MockModel.structure[:parameters][:l][:size] == (2, 3) + @test size(model.l) == (2, 3) + @test MockModel.structure[:parameters][:l][:size] == (2, 3) model = complete(model) @test getdefault(model.cval) == 1 @@ -474,7 +475,8 @@ using ModelingToolkit: getdefault, scalarize @named model_with_component_array = ModelWithComponentArray() - @test eval(ModelWithComponentArray.structure[:parameters][:r][:unit]) == eval(u"Ω") + @test eval(ModelWithComponentArray.structure[:parameters][:r][:unit]) == + eval(u"Ω") @test lastindex(parameters(model_with_component_array)) == 3 # Test the constant `k`. Manually k's value should be kept in sync here From b0ab722aa5e9412a615b3e73d78bd2048a9755a2 Mon Sep 17 00:00:00 2001 From: Venkateshprasad <32921645+ven-k@users.noreply.github.com> Date: Tue, 8 Oct 2024 14:09:58 +0000 Subject: [PATCH 0111/1337] feat: support arbitrary length arrays with metadata and default Update metadata dict and kwargs for symbolic arrays --- src/systems/model_parsing.jl | 207 +++++++++++++++++++++-------------- 1 file changed, 127 insertions(+), 80 deletions(-) diff --git a/src/systems/model_parsing.jl b/src/systems/model_parsing.jl index 8a0f5165e4..04b133ba87 100644 --- a/src/systems/model_parsing.jl +++ b/src/systems/model_parsing.jl @@ -245,20 +245,6 @@ end function parse_variable_def!(dict, mod, arg, varclass, kwargs, where_types; def = nothing, indices::Union{Vector{UnitRange{Int}}, Nothing} = nothing, type::Type = Real, meta = Dict{DataType, Expr}()) - metatypes = [(:connection_type, VariableConnectType), - (:description, VariableDescription), - (:unit, VariableUnit), - (:bounds, VariableBounds), - (:noise, VariableNoiseType), - (:input, VariableInput), - (:output, VariableOutput), - (:irreducible, VariableIrreducible), - (:state_priority, VariableStatePriority), - (:misc, VariableMisc), - (:disturbance, VariableDisturbance), - (:tunable, VariableTunable), - (:dist, VariableDistribution)] - arg isa LineNumberNode && return MLStyle.@match arg begin a::Symbol => begin @@ -284,27 +270,86 @@ function parse_variable_def!(dict, mod, arg, varclass, kwargs, where_types; varclass, where_types, meta) return var, def, Dict() end + Expr(:tuple, Expr(:(::), Expr(:ref, a, indices...), type), meta_val) || + Expr(:tuple, Expr(:ref, a, indices...), meta_val) => begin + (@isdefined type) || (type = Real) + varname = Meta.isexpr(a, :call) ? a.args[1] : a + push!(kwargs, Expr(:kw, varname, nothing)) + meta = parse_metadata(mod, meta_val) + varval = (@isdefined default_val) ? default_val : + unit_handled_variable_value(meta, varname) + if varclass == :parameters + var = :($varname = $first(@parameters ($a[$(indices...)]::$type = $varval), + $meta_val)) + else + var = :($varname = $first(@variables ($a[$(indices)]::$type = $varval), + $meta_val)) + end + push_array_kwargs_and_metadata!( + dict, indices, meta, type, varclass, varname, varval) + (:($varname...), var), nothing, Dict() + end + Expr(:(=), Expr(:(::), Expr(:ref, a, indices...), type), def_n_meta) || + Expr(:(=), Expr(:ref, a, indices...), def_n_meta) => begin + (@isdefined type) || (type = Real) + varname = Meta.isexpr(a, :call) ? a.args[1] : a + if Meta.isexpr(def_n_meta, :tuple) + meta = parse_metadata(mod, def_n_meta) + varval = unit_handled_variable_value(meta, varname) + val, def_n_meta = (def_n_meta.args[1], def_n_meta.args[2:end]) + push!(kwargs, Expr(:kw, varname, nothing)) + if varclass == :parameters + var = :($varname = $varname === nothing ? $val : $varname; + $varname = $first(@parameters ($a[$(indices...)]::$type = $varval), + $(def_n_meta...))) + else + var = :($varname = $varname === nothing ? $val : $varname; + $varname = $first(@variables $a[$(indices...)]::$type = ( + $varval), + $(def_n_meta...))) + end + else + push!(kwargs, Expr(:kw, varname, nothing)) + if varclass == :parameters + var = :($varname = $varname === nothing ? $def_n_meta : $varname; + $varname = $first(@parameters $a[$(indices...)]::$type = $varname)) + else + var = :($varname = $varname === nothing ? $def_n_meta : $varname; + $varname = $first(@variables $a[$(indices...)]::$type = $varname)) + end + varval, meta = def_n_meta, nothing + end + push_array_kwargs_and_metadata!( + dict, indices, meta, type, varclass, varname, varval) + (:($varname...), var), nothing, Dict() + end + Expr(:(::), Expr(:ref, a, indices...), type) || + Expr(:ref, a, indices...) => begin + (@isdefined type) || (type = Real) + varname = a isa Expr && a.head == :call ? a.args[1] : a + push!(kwargs, Expr(:kw, varname, nothing)) + if varclass == :parameters + var = :($varname = $first(@parameters $a[$(indices...)]::$type = $varname)) + elseif varclass == :variables + var = :($varname = $first(@variables $a[$(indices...)]::$type = $varname)) + else + throw("Symbolic array with arbitrary length is not handled for $varclass. + Please open an issue with an example.") + end + push_array_kwargs_and_metadata!( + dict, indices, nothing, type, varclass, varname, nothing) + (:($varname...), var), nothing, Dict() + end Expr(:(=), a, b) => begin Base.remove_linenums!(b) def, meta = parse_default(mod, b) var, def, _ = parse_variable_def!( dict, mod, a, varclass, kwargs, where_types; def, type, meta) - if dict[varclass] isa Vector - dict[varclass][1][getname(var)][:default] = def - else - dict[varclass][getname(var)][:default] = def - end + varclass_dict = dict[varclass] isa Vector ? Ref(dict[varclass][1]) : + Ref(dict[varclass]) + varclass_dict[][getname(var)][:default] = def if meta !== nothing - for (type, key) in metatypes - if (mt = get(meta, key, nothing)) !== nothing - key == VariableConnectType && (mt = nameof(mt)) - if dict[varclass] isa Vector - dict[varclass][1][getname(var)][type] = mt - else - dict[varclass][getname(var)][type] = mt - end - end - end + update_readable_metadata!(varclass_dict[], meta, getname(var)) var, metadata_with_exprs = set_var_metadata(var, meta) return var, def, metadata_with_exprs end @@ -314,27 +359,15 @@ function parse_variable_def!(dict, mod, arg, varclass, kwargs, where_types; meta = parse_metadata(mod, b) var, def, _ = parse_variable_def!( dict, mod, a, varclass, kwargs, where_types; type, meta) + varclass_dict = dict[varclass] isa Vector ? Ref(dict[varclass][1]) : + Ref(dict[varclass]) if meta !== nothing - for (type, key) in metatypes - if (mt = get(meta, key, nothing)) !== nothing - key == VariableConnectType && (mt = nameof(mt)) - if dict[varclass] isa Vector - dict[varclass][1][getname(var)][type] = mt - else - dict[varclass][getname(var)][type] = mt - end - end - end + update_readable_metadata!(varclass_dict[], meta, getname(var)) var, metadata_with_exprs = set_var_metadata(var, meta) return var, def, metadata_with_exprs end return var, def, Dict() end - Expr(:ref, a, b...) => begin - indices = map(i -> UnitRange(i.args[2], i.args[end]), b) - parse_variable_def!(dict, mod, a, varclass, kwargs, where_types; - def, indices, type, meta) - end _ => error("$arg cannot be parsed") end end @@ -442,14 +475,23 @@ function parse_default(mod, a) end end -function parse_metadata(mod, a) +function parse_metadata(mod, a::Expr) MLStyle.@match a begin - Expr(:vect, eles...) => Dict(parse_metadata(mod, e) for e in eles) + Expr(:vect, b...) => Dict(parse_metadata(mod, m) for m in b) + Expr(:tuple, a, b...) => parse_metadata(mod, b) Expr(:(=), a, b) => Symbolics.option_to_metadata_type(Val(a)) => get_var(mod, b) _ => error("Cannot parse metadata $a") end end +function parse_metadata(mod, metadata::AbstractArray) + ret = Dict() + for m in metadata + merge!(ret, parse_metadata(mod, m)) + end + ret +end + function _set_var_metadata!(metadata_with_exprs, a, m, v::Expr) push!(metadata_with_exprs, m => v) a @@ -707,6 +749,7 @@ function parse_variable_arg!(exprs, vs, dict, mod, arg, varclass, kwargs, where_ end function convert_units(varunits::DynamicQuantities.Quantity, value) + value isa Nothing && return nothing DynamicQuantities.ustrip(DynamicQuantities.uconvert( DynamicQuantities.SymbolicUnits.as_quantity(varunits), value)) end @@ -718,6 +761,7 @@ function convert_units( end function convert_units(varunits::Unitful.FreeUnits, value) + value isa Nothing && return nothing Unitful.ustrip(varunits, value) end @@ -736,47 +780,50 @@ end function parse_variable_arg(dict, mod, arg, varclass, kwargs, where_types) vv, def, metadata_with_exprs = parse_variable_def!( dict, mod, arg, varclass, kwargs, where_types) - name = getname(vv) - - varexpr = if haskey(metadata_with_exprs, VariableUnit) - unit = metadata_with_exprs[VariableUnit] - quote - $name = if $name === $NO_VALUE - $setdefault($vv, $def) - else - try - $setdefault($vv, $convert_units($unit, $name)) - catch e - if isa(e, $(DynamicQuantities.DimensionError)) || - isa(e, $(Unitful.DimensionError)) - error("Unable to convert units for \'" * string(:($$vv)) * "\'") - elseif isa(e, MethodError) - error("No or invalid units provided for \'" * string(:($$vv)) * - "\'") - else - rethrow(e) + if !(vv isa Tuple) + name = getname(vv) + varexpr = if haskey(metadata_with_exprs, VariableUnit) + unit = metadata_with_exprs[VariableUnit] + quote + $name = if $name === $NO_VALUE + $setdefault($vv, $def) + else + try + $setdefault($vv, $convert_units($unit, $name)) + catch e + if isa(e, $(DynamicQuantities.DimensionError)) || + isa(e, $(Unitful.DimensionError)) + error("Unable to convert units for \'" * string(:($$vv)) * "\'") + elseif isa(e, MethodError) + error("No or invalid units provided for \'" * string(:($$vv)) * + "\'") + else + rethrow(e) + end end end end - end - else - quote - $name = if $name === $NO_VALUE - $setdefault($vv, $def) - else - $setdefault($vv, $name) + else + quote + $name = if $name === $NO_VALUE + $setdefault($vv, $def) + else + $setdefault($vv, $name) + end end end - end - metadata_expr = Expr(:block) - for (k, v) in metadata_with_exprs - push!(metadata_expr.args, - :($name = $wrap($set_scalar_metadata($unwrap($name), $k, $v)))) - end + metadata_expr = Expr(:block) + for (k, v) in metadata_with_exprs + push!(metadata_expr.args, + :($name = $wrap($set_scalar_metadata($unwrap($name), $k, $v)))) + end - push!(varexpr.args, metadata_expr) - return vv isa Num ? name : :($name...), varexpr + push!(varexpr.args, metadata_expr) + return vv isa Num ? name : :($name...), varexpr + else + return vv + end end function handle_conditional_vars!( From a403880cc92493fd3bd980fc1c72bba6e50eb1a5 Mon Sep 17 00:00:00 2001 From: Venkateshprasad <32921645+ven-k@users.noreply.github.com> Date: Mon, 23 Sep 2024 20:32:56 +0000 Subject: [PATCH 0112/1337] docs: use symbolic array with arbitray length in ModelC example --- docs/src/basics/MTKLanguage.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/src/basics/MTKLanguage.md b/docs/src/basics/MTKLanguage.md index a2fb7d0870..a656b24096 100644 --- a/docs/src/basics/MTKLanguage.md +++ b/docs/src/basics/MTKLanguage.md @@ -63,13 +63,14 @@ end @structural_parameters begin f = sin N = 2 + M = 3 end begin v_var = 1.0 end @variables begin v(t) = v_var - v_array(t)[1:2, 1:3] + v_array(t)[1:N, 1:M] v_for_defaults(t) end @extend ModelB(; p1) @@ -324,10 +325,10 @@ For example, the structure of `ModelC` is: julia> ModelC.structure Dict{Symbol, Any} with 10 entries: :components => Any[Union{Expr, Symbol}[:model_a, :ModelA], Union{Expr, Symbol}[:model_array_a, :ModelA, :(1:N)], Union{Expr, Symbol}[:model_array_b, :ModelA, :(1:N)]] - :variables => Dict{Symbol, Dict{Symbol, Any}}(:v=>Dict(:default=>:v_var, :type=>Real), :v_array=>Dict(:type=>Real, :size=>(2, 3)), :v_for_defaults=>Dict(:type=>Real)) + :variables => Dict{Symbol, Dict{Symbol, Any}}(:v=>Dict(:default=>:v_var, :type=>Real), :v_for_defaults=>Dict(:type=>Real)) :icon => URI("https://github.com/SciML/SciMLDocs/blob/main/docs/src/assets/logo.png") - :kwargs => Dict{Symbol, Dict}(:f=>Dict(:value=>:sin), :N=>Dict(:value=>2), :v=>Dict{Symbol, Any}(:value=>:v_var, :type=>Real), :v_array=>Dict{Symbol, Union{Nothing, UnionAll}}(:value=>nothing, :type=>AbstractArray{Real}), :v_for_defaults=>Dict{Symbol, Union{Nothing, DataType}}(:value=>nothing, :type=>Real), :p1=>Dict(:value=>nothing)) - :structural_parameters => Dict{Symbol, Dict}(:f=>Dict(:value=>:sin), :N=>Dict(:value=>2)) + :kwargs => Dict{Symbol, Dict}(:f => Dict(:value => :sin), :N => Dict(:value => 2), :M => Dict(:value => 3), :v => Dict{Symbol, Any}(:value => :v_var, :type => Real), :v_for_defaults => Dict{Symbol, Union{Nothing, DataType}}(:value => nothing, :type => Real), :p1 => Dict(:value => nothing)), + :structural_parameters => Dict{Symbol, Dict}(:f => Dict(:value => :sin), :N => Dict(:value => 2), :M => Dict(:value => 3)) :independent_variable => t :constants => Dict{Symbol, Dict}(:c=>Dict{Symbol, Any}(:value=>1, :type=>Int64, :description=>"Example constant.")) :extend => Any[[:p2, :p1], Symbol("#mtkmodel__anonymous__ModelB"), :ModelB] From 9f4e7b8f92d2efa50c2c359d41dac5e95ce8c860 Mon Sep 17 00:00:00 2001 From: Venkateshprasad <32921645+ven-k@users.noreply.github.com> Date: Tue, 8 Oct 2024 14:12:20 +0000 Subject: [PATCH 0113/1337] feat: parity in symbolic-array definition with vanilla `@variables` --- src/systems/model_parsing.jl | 2 ++ test/model_parsing.jl | 15 +++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/src/systems/model_parsing.jl b/src/systems/model_parsing.jl index 04b133ba87..3e011b044a 100644 --- a/src/systems/model_parsing.jl +++ b/src/systems/model_parsing.jl @@ -270,6 +270,8 @@ function parse_variable_def!(dict, mod, arg, varclass, kwargs, where_types; varclass, where_types, meta) return var, def, Dict() end + Expr(:tuple, Expr(:(=), Expr(:ref, a, indices...), default_val), meta_val) || + Expr(:tuple, Expr(:(=), Expr(:(::), Expr(:ref, a, indices...), type), default_val), meta_val) || Expr(:tuple, Expr(:(::), Expr(:ref, a, indices...), type), meta_val) || Expr(:tuple, Expr(:ref, a, indices...), meta_val) => begin (@isdefined type) || (type = Real) diff --git a/test/model_parsing.jl b/test/model_parsing.jl index 727954a00d..c108b88625 100644 --- a/test/model_parsing.jl +++ b/test/model_parsing.jl @@ -279,6 +279,21 @@ end @test MockModel.structure[:defaults] == Dict(:n => 1.0, :n2 => "g()") end +@testset "Arrays using vanilla-@variable syntax" begin + @mtkmodel TupleInArrayDef begin + @parameters begin + (l(t)[1:2, 1:3] = 1), [description = "l is more than 1D"] + (l2(t)[1:3] = 2), [description = "l2 is 1D"] + (l3(t)[1:3]::Int = 3), [description = "l3 is 1D and has a type"] + end + end + + @named arr = TupleInArrayDef() + @test getdefault(arr.l) == 1 + @test getdefault(arr.l2) == 2 + @test getdefault(arr.l3) == 3 +end + @testset "Type annotation" begin @mtkmodel TypeModel begin @structural_parameters begin From d7d82bf1ce82626de29be9c2bf1aae1349c184eb Mon Sep 17 00:00:00 2001 From: Venkateshprasad <32921645+ven-k@users.noreply.github.com> Date: Tue, 8 Oct 2024 14:46:08 +0000 Subject: [PATCH 0114/1337] feat: check independent-var usage in symbolic vars - Ensure `@variables` are variables of an independent var. - In both `@variables` and `@parameters` case, ensure a single indpendent var is used --- src/systems/model_parsing.jl | 34 ++++++++++++++++++++++++++++++---- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/src/systems/model_parsing.jl b/src/systems/model_parsing.jl index 3e011b044a..94469308e4 100644 --- a/src/systems/model_parsing.jl +++ b/src/systems/model_parsing.jl @@ -281,9 +281,13 @@ function parse_variable_def!(dict, mod, arg, varclass, kwargs, where_types; varval = (@isdefined default_val) ? default_val : unit_handled_variable_value(meta, varname) if varclass == :parameters + Meta.isexpr(a, :call) && assert_unique_independent_var(dict, a.args[end]) var = :($varname = $first(@parameters ($a[$(indices...)]::$type = $varval), $meta_val)) else + Meta.isexpr(a, :call) || + throw("$a is not a variable of the independent variable") + assert_unique_independent_var(dict, a.args[end]) var = :($varname = $first(@variables ($a[$(indices)]::$type = $varval), $meta_val)) end @@ -301,10 +305,15 @@ function parse_variable_def!(dict, mod, arg, varclass, kwargs, where_types; val, def_n_meta = (def_n_meta.args[1], def_n_meta.args[2:end]) push!(kwargs, Expr(:kw, varname, nothing)) if varclass == :parameters + Meta.isexpr(a, :call) && + assert_unique_independent_var(dict, a.args[end]) var = :($varname = $varname === nothing ? $val : $varname; $varname = $first(@parameters ($a[$(indices...)]::$type = $varval), $(def_n_meta...))) else + Meta.isexpr(a, :call) || + throw("$a is not a variable of the independent variable") + assert_unique_independent_var(dict, a.args[end]) var = :($varname = $varname === nothing ? $val : $varname; $varname = $first(@variables $a[$(indices...)]::$type = ( $varval), @@ -313,9 +322,14 @@ function parse_variable_def!(dict, mod, arg, varclass, kwargs, where_types; else push!(kwargs, Expr(:kw, varname, nothing)) if varclass == :parameters + Meta.isexpr(a, :call) && + assert_unique_independent_var(dict, a.args[end]) var = :($varname = $varname === nothing ? $def_n_meta : $varname; $varname = $first(@parameters $a[$(indices...)]::$type = $varname)) else + Meta.isexpr(a, :call) || + throw("$a is not a variable of the independent variable") + assert_unique_independent_var(dict, a.args[end]) var = :($varname = $varname === nothing ? $def_n_meta : $varname; $varname = $first(@variables $a[$(indices...)]::$type = $varname)) end @@ -331,8 +345,12 @@ function parse_variable_def!(dict, mod, arg, varclass, kwargs, where_types; varname = a isa Expr && a.head == :call ? a.args[1] : a push!(kwargs, Expr(:kw, varname, nothing)) if varclass == :parameters + Meta.isexpr(a, :call) && assert_unique_independent_var(dict, a.args[end]) var = :($varname = $first(@parameters $a[$(indices...)]::$type = $varname)) elseif varclass == :variables + Meta.isexpr(a, :call) || + throw("$a is not a variable of the independent variable") + assert_unique_independent_var(dict, a.args[end]) var = :($varname = $first(@variables $a[$(indices...)]::$type = $varname)) else throw("Symbolic array with arbitrary length is not handled for $varclass. @@ -411,14 +429,22 @@ function generate_var!(dict, a, varclass; generate_var(a, varclass; indices, type) end +function assert_unique_independent_var(dict, iv::Num) + assert_unique_independent_var(dict, nameof(iv)) +end +function assert_unique_independent_var(dict, iv) + prev_iv = get!(dict, :independent_variable) do + iv + end + prev_iv isa Num && (prev_iv = nameof(prev_iv)) + @assert isequal(iv, prev_iv) "Multiple independent variables are used in the model $(typeof(iv)) $(typeof(prev_iv))" +end + function generate_var!(dict, a, b, varclass, mod; indices::Union{Vector{UnitRange{Int}}, Nothing} = nothing, type = Real) iv = b == :t ? get_t(mod, b) : generate_var(b, :independent_variables) - prev_iv = get!(dict, :independent_variable) do - iv - end - @assert isequal(iv, prev_iv) "Multiple independent variables are used in the model" + assert_unique_independent_var(dict, iv) check_name_uniqueness(dict, a, varclass) vd = get!(dict, varclass) do Dict{Symbol, Dict{Symbol, Any}}() From 84bf015a4541037ea141c8dc861c529b1e057a80 Mon Sep 17 00:00:00 2001 From: Venkateshprasad <32921645+ven-k@users.noreply.github.com> Date: Tue, 8 Oct 2024 15:08:53 +0000 Subject: [PATCH 0115/1337] docs: add a dedicated section to showcase symbolic-array definition With many ways to define, this feature demands a dedicated section. --- docs/src/basics/MTKLanguage.md | 42 +++++++++++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/docs/src/basics/MTKLanguage.md b/docs/src/basics/MTKLanguage.md index a656b24096..685c549429 100644 --- a/docs/src/basics/MTKLanguage.md +++ b/docs/src/basics/MTKLanguage.md @@ -302,7 +302,7 @@ end For more examples of usage, checkout [ModelingToolkitStandardLibrary.jl](https://github.com/SciML/ModelingToolkitStandardLibrary.jl/) -## More on `Model.structure` +## [More on `Model.structure`](@id model_structure) `structure` stores metadata that describes composition of a model. It includes: @@ -336,6 +336,46 @@ Dict{Symbol, Any} with 10 entries: :equations => Any["model_a.k ~ f(v)"] ``` +### Different ways to define symbolics arrays: + +`@mtkmodel` supports symbolics arrays in both `@parameters` and `@variables`. +Using a structural parameters, symbolic arrays of arbitrary lengths can be defined. +Refer the following example for different ways to define symbolic arrays. + +```@example mtkmodel-example +@mtkmodel ModelWithArrays begin + @structural_parameters begin + N = 2 + M = 3 + end + @parameters begin + p1[1:4] + p2[1:N] + p3[1:N, 1:M] = 10, + [description = "A multi-dimensional array of arbitrary length with description"] + (p4[1:N, 1:M] = 10), + [description = "An alternate syntax for p3 to match the syntax of vanilla parameters macro"] + end + @variables begin + v1(t)[1:2] = 10, [description = "An array of variable `v1`"] + v2(t)[1:3] = [1, 2, 3] + end +end +``` + +The size of symbolic array can be accessed via `:size` key, along with other metadata (refer [More on `Model.structure`](@ref model_structure)) +of the symbolic variable. + +```julia +julia> ModelWithArrays.structure +Dict{Symbol, Any} with 5 entries: + :variables => Dict{Symbol, Dict{Symbol, Any}}(:v2 => Dict(:value => :([1, 2, 3]), :type => Real, :size => (3,)), :v1 => Dict(:value => :v1, :type => Real, :description => "An array of variable `v1`", :size => (2,))) + :kwargs => Dict{Symbol, Dict}(:p2 => Dict{Symbol, Any}(:value => nothing, :type => Real, :size => (:N,)), :v1 => Dict{Symbol, Any}(:value => :v1, :type => Real, :description => "An array of variable `v1`", :size => (2,)), :N => Dict(:value => 2), :M => Dict(:value => 3), :p4 => Dict{Symbol, Any}(:value => 10, :type => Real, :description => "An alternate syntax for p3 to match the syntax of vanilla parameters macro", :size => (:N, :M)), :v2 => Dict{Symbol, Any}(:value => :([1, 2, 3]), :type => Real, :size => (3,)), :p1 => Dict{Symbol, Any}(:value => nothing, :type => Real, :size => (4,)), :p3 => Dict{Symbol, Any}(:value => :p3, :type => Real, :description => "A multi-dimensional array of arbitrary length with description", :size => (:N, :M))) + :structural_parameters => Dict{Symbol, Dict}(:N => Dict(:value => 2), :M => Dict(:value => 3)) + :independent_variable => :t + :parameters => Dict{Symbol, Dict{Symbol, Any}}(:p2 => Dict(:value => nothing, :type => Real, :size => (:N,)), :p4 => Dict(:value => 10, :type => Real, :description => "An alternate syntax for p3 to match the syntax of vanilla parameters macro", :size => (:N, :M)), :p1 => Dict(:value => nothing, :type => Real, :size => (4,)), :p3 => Dict(:value => :p3, :type => Real, :description => "A multi-dimensional array of arbitrary length with description", :size => (:N, :M)))), false) +``` + ### Using conditional statements #### Conditional elements of the system From 7b9a234c7a850424f44b784e80b59744554588cf Mon Sep 17 00:00:00 2001 From: Venkateshprasad <32921645+ven-k@users.noreply.github.com> Date: Tue, 8 Oct 2024 18:15:49 +0000 Subject: [PATCH 0116/1337] feat: update the vartype of kwargs rel. to symbolic-array While here, removes passing `indices` around to generate_var, update_kwargs_and_metadata! and parse_variable_def! as these no longer handles symbolic-arrays. --- src/systems/model_parsing.jl | 84 ++++++++++++++++-------------------- 1 file changed, 36 insertions(+), 48 deletions(-) diff --git a/src/systems/model_parsing.jl b/src/systems/model_parsing.jl index 94469308e4..7e8eed9900 100644 --- a/src/systems/model_parsing.jl +++ b/src/systems/model_parsing.jl @@ -151,34 +151,18 @@ pop_structure_dict!(dict, key) = length(dict[key]) == 0 && pop!(dict, key) struct NoValue end const NO_VALUE = NoValue() -function update_kwargs_and_metadata!(dict, kwargs, a, def, indices, type, var, +function update_kwargs_and_metadata!(dict, kwargs, a, def, type, varclass, where_types, meta) - if indices isa Nothing - if !isnothing(meta) && haskey(meta, VariableUnit) - uvar = gensym() - push!(where_types, uvar) - push!(kwargs, - Expr(:kw, :($a::Union{Nothing, Missing, $NoValue, $uvar}), NO_VALUE)) - else - push!(kwargs, - Expr(:kw, :($a::Union{Nothing, Missing, $NoValue, $type}), NO_VALUE)) - end - dict[:kwargs][a] = Dict(:value => def, :type => type) + if !isnothing(meta) && haskey(meta, VariableUnit) + uvar = gensym() + push!(where_types, uvar) + push!(kwargs, + Expr(:kw, :($a::Union{Nothing, Missing, $NoValue, $uvar}), NO_VALUE)) else - vartype = gensym(:T) push!(kwargs, - Expr(:kw, - Expr(:(::), a, - Expr(:curly, :Union, :Nothing, :Missing, NoValue, - Expr(:curly, :AbstractArray, vartype))), - NO_VALUE)) - if !isnothing(meta) && haskey(meta, VariableUnit) - push!(where_types, vartype) - else - push!(where_types, :($vartype <: $type)) - end - dict[:kwargs][a] = Dict(:value => def, :type => AbstractArray{type}) + Expr(:kw, :($a::Union{Nothing, Missing, $NoValue, $type}), NO_VALUE)) end + dict[:kwargs][a] = Dict(:value => def, :type => type) if dict[varclass] isa Vector dict[varclass][1][a][:type] = AbstractArray{type} else @@ -213,8 +197,8 @@ function update_readable_metadata!(varclass_dict, meta::Dict, varname) end end -function push_array_kwargs_and_metadata!( - dict, indices, meta, type, varclass, varname, varval) +function update_array_kwargs_and_metadata!( + dict, indices, kwargs, meta, type, varclass, varname, varval, where_types) dict[varclass] = get!(dict, varclass) do Dict{Symbol, Dict{Symbol, Any}}() end @@ -227,6 +211,18 @@ function push_array_kwargs_and_metadata!( :type => type ))) + vartype = gensym(:T) + push!(kwargs, + Expr(:kw, + Expr(:(::), varname, + Expr(:curly, :Union, :Nothing, Expr(:curly, :AbstractArray, vartype))), + nothing)) + if !isnothing(meta) && haskey(meta, VariableUnit) + push!(where_types, vartype) + else + push!(where_types, :($vartype <: $type)) + end + # Useful keys for kwargs entry are: value, type and size. dict[:kwargs][varname] = varclass_dict[][varname] @@ -243,13 +239,12 @@ function unit_handled_variable_value(meta, varname) end function parse_variable_def!(dict, mod, arg, varclass, kwargs, where_types; - def = nothing, indices::Union{Vector{UnitRange{Int}}, Nothing} = nothing, - type::Type = Real, meta = Dict{DataType, Expr}()) + def = nothing, type::Type = Real, meta = Dict{DataType, Expr}()) arg isa LineNumberNode && return MLStyle.@match arg begin a::Symbol => begin - var = generate_var!(dict, a, varclass; indices, type) - update_kwargs_and_metadata!(dict, kwargs, a, def, indices, type, var, + var = generate_var!(dict, a, varclass; type) + update_kwargs_and_metadata!(dict, kwargs, a, def, type, varclass, where_types, meta) return var, def, Dict() end @@ -265,8 +260,8 @@ function parse_variable_def!(dict, mod, arg, varclass, kwargs, where_types; dict, mod, a, varclass, kwargs, where_types; def, type, meta) end Expr(:call, a, b) => begin - var = generate_var!(dict, a, b, varclass, mod; indices, type) - update_kwargs_and_metadata!(dict, kwargs, a, def, indices, type, var, + var = generate_var!(dict, a, b, varclass, mod; type) + update_kwargs_and_metadata!(dict, kwargs, a, def, type, varclass, where_types, meta) return var, def, Dict() end @@ -276,7 +271,6 @@ function parse_variable_def!(dict, mod, arg, varclass, kwargs, where_types; Expr(:tuple, Expr(:ref, a, indices...), meta_val) => begin (@isdefined type) || (type = Real) varname = Meta.isexpr(a, :call) ? a.args[1] : a - push!(kwargs, Expr(:kw, varname, nothing)) meta = parse_metadata(mod, meta_val) varval = (@isdefined default_val) ? default_val : unit_handled_variable_value(meta, varname) @@ -291,8 +285,8 @@ function parse_variable_def!(dict, mod, arg, varclass, kwargs, where_types; var = :($varname = $first(@variables ($a[$(indices)]::$type = $varval), $meta_val)) end - push_array_kwargs_and_metadata!( - dict, indices, meta, type, varclass, varname, varval) + update_array_kwargs_and_metadata!( + dict, indices, kwargs, meta, type, varclass, varname, varval, where_types) (:($varname...), var), nothing, Dict() end Expr(:(=), Expr(:(::), Expr(:ref, a, indices...), type), def_n_meta) || @@ -303,7 +297,6 @@ function parse_variable_def!(dict, mod, arg, varclass, kwargs, where_types; meta = parse_metadata(mod, def_n_meta) varval = unit_handled_variable_value(meta, varname) val, def_n_meta = (def_n_meta.args[1], def_n_meta.args[2:end]) - push!(kwargs, Expr(:kw, varname, nothing)) if varclass == :parameters Meta.isexpr(a, :call) && assert_unique_independent_var(dict, a.args[end]) @@ -320,7 +313,6 @@ function parse_variable_def!(dict, mod, arg, varclass, kwargs, where_types; $(def_n_meta...))) end else - push!(kwargs, Expr(:kw, varname, nothing)) if varclass == :parameters Meta.isexpr(a, :call) && assert_unique_independent_var(dict, a.args[end]) @@ -335,15 +327,14 @@ function parse_variable_def!(dict, mod, arg, varclass, kwargs, where_types; end varval, meta = def_n_meta, nothing end - push_array_kwargs_and_metadata!( - dict, indices, meta, type, varclass, varname, varval) + update_array_kwargs_and_metadata!( + dict, indices, kwargs, meta, type, varclass, varname, varval, where_types) (:($varname...), var), nothing, Dict() end Expr(:(::), Expr(:ref, a, indices...), type) || Expr(:ref, a, indices...) => begin (@isdefined type) || (type = Real) varname = a isa Expr && a.head == :call ? a.args[1] : a - push!(kwargs, Expr(:kw, varname, nothing)) if varclass == :parameters Meta.isexpr(a, :call) && assert_unique_independent_var(dict, a.args[end]) var = :($varname = $first(@parameters $a[$(indices...)]::$type = $varname)) @@ -356,8 +347,8 @@ function parse_variable_def!(dict, mod, arg, varclass, kwargs, where_types; throw("Symbolic array with arbitrary length is not handled for $varclass. Please open an issue with an example.") end - push_array_kwargs_and_metadata!( - dict, indices, nothing, type, varclass, varname, nothing) + update_array_kwargs_and_metadata!( + dict, indices, kwargs, nothing, type, varclass, varname, nothing, where_types) (:($varname...), var), nothing, Dict() end Expr(:(=), a, b) => begin @@ -392,11 +383,8 @@ function parse_variable_def!(dict, mod, arg, varclass, kwargs, where_types; end end -function generate_var(a, varclass; - indices::Union{Vector{UnitRange{Int}}, Nothing} = nothing, - type = Real) - var = indices === nothing ? Symbolics.variable(a; T = type) : - first(@variables $a[indices...]::type) +function generate_var(a, varclass; type = Real) + var = Symbolics.variable(a; T = type) if varclass == :parameters var = toparam(var) elseif varclass == :independent_variables @@ -426,7 +414,7 @@ function generate_var!(dict, a, varclass; vd isa Vector && (vd = first(vd)) vd[a] = Dict{Symbol, Any}() indices !== nothing && (vd[a][:size] = Tuple(lastindex.(indices))) - generate_var(a, varclass; indices, type) + generate_var(a, varclass; type) end function assert_unique_independent_var(dict, iv::Num) From da4429283b657e107fd631e3b1ba6346e6319032 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 11 Oct 2024 12:52:55 +0530 Subject: [PATCH 0117/1337] feat: better discover scoped parameters in parent systems --- src/systems/abstractsystem.jl | 17 +++++++++ src/systems/diffeqs/odesystem.jl | 15 +++++--- src/utils.jl | 61 +++++++++++++++++++++++++++++--- test/variable_scope.jl | 38 ++++++++++++++++++++ 4 files changed, 121 insertions(+), 10 deletions(-) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index 9908d6f1e3..00a003e06d 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -917,6 +917,15 @@ Mark a system as completed. If a system is complete, the system will no longer namespace its subsystems or variables, i.e. `isequal(complete(sys).v.i, v.i)`. """ function complete(sys::AbstractSystem; split = true) + if !(sys isa JumpSystem) + newunknowns = OrderedSet() + newparams = OrderedSet() + iv = has_iv(sys) ? get_iv(sys) : nothing + collect_scoped_vars!(newunknowns, newparams, sys, iv; depth = -1) + # don't update unknowns to not disturb `structural_simplify` order + # `GlobalScope`d unknowns will be picked up and added there + @set! sys.ps = unique!(vcat(get_ps(sys), collect(newparams))) + end if split && has_index_cache(sys) @set! sys.index_cache = IndexCache(sys) all_ps = parameters(sys) @@ -3011,6 +3020,14 @@ function compose(sys::AbstractSystem, systems::AbstractArray; name = nameof(sys) if has_is_dde(sys) @set! sys.is_dde = _check_if_dde(equations(sys), get_iv(sys), get_systems(sys)) end + newunknowns = OrderedSet() + newparams = OrderedSet() + iv = has_iv(sys) ? get_iv(sys) : nothing + for ssys in systems + collect_scoped_vars!(newunknowns, newparams, ssys, iv) + end + @set! sys.unknowns = unique!(vcat(get_unknowns(sys), collect(newunknowns))) + @set! sys.ps = unique!(vcat(get_ps(sys), collect(newparams))) return sys end function compose(syss...; name = nameof(first(syss))) diff --git a/src/systems/diffeqs/odesystem.jl b/src/systems/diffeqs/odesystem.jl index bda5690a67..68ea0b48e3 100644 --- a/src/systems/diffeqs/odesystem.jl +++ b/src/systems/diffeqs/odesystem.jl @@ -323,11 +323,13 @@ function ODESystem(eqs, iv; kwargs...) collect_vars!(allunknowns, ps, eq.rhs, iv) if isdiffeq(eq) diffvar, _ = var_from_nested_derivative(eq.lhs) - isequal(iv, iv_from_nested_derivative(eq.lhs)) || - throw(ArgumentError("An ODESystem can only have one independent variable.")) - diffvar in diffvars && - throw(ArgumentError("The differential variable $diffvar is not unique in the system of equations.")) - push!(diffvars, diffvar) + if check_scope_depth(getmetadata(diffvar, SymScope, LocalScope()), 0) + isequal(iv, iv_from_nested_derivative(eq.lhs)) || + throw(ArgumentError("An ODESystem can only have one independent variable.")) + diffvar in diffvars && + throw(ArgumentError("The differential variable $diffvar is not unique in the system of equations.")) + push!(diffvars, diffvar) + end push!(diffeq, eq) else push!(algeeq, eq) @@ -342,6 +344,9 @@ function ODESystem(eqs, iv; kwargs...) collect_vars!(allunknowns, ps, eq.rhs, iv) end end + for ssys in get(kwargs, :systems, ODESystem[]) + collect_scoped_vars!(allunknowns, ps, ssys, iv) + end for v in allunknowns isdelay(v, iv) || continue collect_vars!(allunknowns, ps, arguments(v)[1], iv) diff --git a/src/utils.jl b/src/utils.jl index 29e61ede69..3e347bd405 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -467,23 +467,56 @@ function find_derivatives!(vars, expr, f) return vars end -function collect_vars!(unknowns, parameters, expr, iv; op = Differential) +""" + $(TYPEDSIGNATURES) + +Search through equations and parameter dependencies of `sys`, where sys is at a depth of +`depth` from the root system, looking for variables scoped to the root system. Also +recursively searches through all subsystems of `sys`, increasing the depth if it is not +`-1`. A depth of `-1` indicates searching for variables with `GlobalScope`. +""" +function collect_scoped_vars!(unknowns, parameters, sys, iv; depth = 1, op = Differential) + if has_eqs(sys) + for eq in get_eqs(sys) + eq.lhs isa Union{Symbolic, Number} || continue + collect_vars!(unknowns, parameters, eq.lhs, iv; depth, op) + collect_vars!(unknowns, parameters, eq.rhs, iv; depth, op) + end + end + if has_parameter_dependencies(sys) + for eq in get_parameter_dependencies(sys) + if eq isa Pair + collect_vars!(unknowns, parameters, eq[1], iv; depth, op) + collect_vars!(unknowns, parameters, eq[2], iv; depth, op) + else + collect_vars!(unknowns, parameters, eq.lhs, iv; depth, op) + collect_vars!(unknowns, parameters, eq.rhs, iv; depth, op) + end + end + end + newdepth = depth == -1 ? depth : depth + 1 + for ssys in get_systems(sys) + collect_scoped_vars!(unknowns, parameters, ssys, iv; depth = newdepth, op) + end +end + +function collect_vars!(unknowns, parameters, expr, iv; depth = 0, op = Differential) if issym(expr) - collect_var!(unknowns, parameters, expr, iv) + collect_var!(unknowns, parameters, expr, iv; depth) else for var in vars(expr; op) if iscall(var) && operation(var) isa Differential var, _ = var_from_nested_derivative(var) end - collect_var!(unknowns, parameters, var, iv) + collect_var!(unknowns, parameters, var, iv; depth) end end return nothing end -function collect_var!(unknowns, parameters, var, iv) +function collect_var!(unknowns, parameters, var, iv; depth = 0) isequal(var, iv) && return nothing - getmetadata(var, SymScope, LocalScope()) == LocalScope() || return nothing + check_scope_depth(getmetadata(var, SymScope, LocalScope()), depth) || return nothing if iscalledparameter(var) callable = getcalledparameter(var) push!(parameters, callable) @@ -500,6 +533,24 @@ function collect_var!(unknowns, parameters, var, iv) return nothing end +""" + $(TYPEDSIGNATURES) + +Check if the given `scope` is at a depth of `depth` from the root system. Only +returns `true` for `scope::GlobalScope` if `depth == -1`. +""" +function check_scope_depth(scope, depth) + if scope isa LocalScope + return depth == 0 + elseif scope isa ParentScope + return depth > 0 && check_scope_depth(scope.parent, depth - 1) + elseif scope isa DelayParentScope + return depth >= scope.N && check_scope_depth(scope.parent, depth - scope.N) + elseif scope isa GlobalScope + return depth == -1 + end +end + """ Find all the symbolic constants of some equations or terms and return them as a vector. """ diff --git a/test/variable_scope.jl b/test/variable_scope.jl index 853d5ce757..e90f7e6fbf 100644 --- a/test/variable_scope.jl +++ b/test/variable_scope.jl @@ -101,3 +101,41 @@ bar = complete(bar) defs = ModelingToolkit.defaults(bar) @test defs[bar.p] == 2 @test isequal(defs[bar.foo.p], bar.p) + +# Issue#3101 +@variables x1(t) x2(t) x3(t) x4(t) x5(t) +x2 = ParentScope(x2) +x3 = ParentScope(ParentScope(x3)) +x4 = DelayParentScope(x4, 2) +x5 = GlobalScope(x5) +@parameters p1 p2 p3 p4 p5 +p2 = ParentScope(p2) +p3 = ParentScope(ParentScope(p3)) +p4 = DelayParentScope(p4, 2) +p5 = GlobalScope(p5) + +@named sys1 = ODESystem([D(x1) ~ p1, D(x2) ~ p2, D(x3) ~ p3, D(x4) ~ p4, D(x5) ~ p5], t) +@test isequal(x1, only(unknowns(sys1))) +@test isequal(p1, only(parameters(sys1))) +@named sys2 = ODESystem(Equation[], t; systems = [sys1]) +@test length(unknowns(sys2)) == 2 +@test any(isequal(x2), unknowns(sys2)) +@test length(parameters(sys2)) == 2 +@test any(isequal(p2), parameters(sys2)) +@named sys3 = ODESystem(Equation[], t) +sys3 = sys3 ∘ sys2 +@test length(unknowns(sys3)) == 4 +@test any(isequal(x3), unknowns(sys3)) +@test any(isequal(x4), unknowns(sys3)) +@test length(parameters(sys3)) == 4 +@test any(isequal(p3), parameters(sys3)) +@test any(isequal(p4), parameters(sys3)) +sys4 = complete(sys3) +@test length(unknowns(sys3)) == 4 +@test length(parameters(sys4)) == 5 +@test any(isequal(p5), parameters(sys4)) +sys5 = structural_simplify(sys3) +@test length(unknowns(sys5)) == 5 +@test any(isequal(x5), unknowns(sys5)) +@test length(parameters(sys5)) == 5 +@test any(isequal(p5), parameters(sys5)) From 1290985bfdce26b2c8010e45314a1cf110fde273 Mon Sep 17 00:00:00 2001 From: Venkateshprasad <32921645+ven-k@users.noreply.github.com> Date: Fri, 11 Oct 2024 09:04:51 +0000 Subject: [PATCH 0118/1337] refactor: set default value of the symbolic-array-kwargs to NO_VALUE --- src/systems/model_parsing.jl | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/src/systems/model_parsing.jl b/src/systems/model_parsing.jl index 7e8eed9900..bf70d06b79 100644 --- a/src/systems/model_parsing.jl +++ b/src/systems/model_parsing.jl @@ -215,8 +215,9 @@ function update_array_kwargs_and_metadata!( push!(kwargs, Expr(:kw, Expr(:(::), varname, - Expr(:curly, :Union, :Nothing, Expr(:curly, :AbstractArray, vartype))), - nothing)) + Expr(:curly, :Union, :Nothing, :Missing, NoValue, + Expr(:curly, :AbstractArray, vartype))), + NO_VALUE)) if !isnothing(meta) && haskey(meta, VariableUnit) push!(where_types, vartype) else @@ -300,14 +301,14 @@ function parse_variable_def!(dict, mod, arg, varclass, kwargs, where_types; if varclass == :parameters Meta.isexpr(a, :call) && assert_unique_independent_var(dict, a.args[end]) - var = :($varname = $varname === nothing ? $val : $varname; + var = :($varname = $varname === $NO_VALUE ? $val : $varname; $varname = $first(@parameters ($a[$(indices...)]::$type = $varval), $(def_n_meta...))) else Meta.isexpr(a, :call) || throw("$a is not a variable of the independent variable") assert_unique_independent_var(dict, a.args[end]) - var = :($varname = $varname === nothing ? $val : $varname; + var = :($varname = $varname === $NO_VALUE ? $val : $varname; $varname = $first(@variables $a[$(indices...)]::$type = ( $varval), $(def_n_meta...))) @@ -316,13 +317,13 @@ function parse_variable_def!(dict, mod, arg, varclass, kwargs, where_types; if varclass == :parameters Meta.isexpr(a, :call) && assert_unique_independent_var(dict, a.args[end]) - var = :($varname = $varname === nothing ? $def_n_meta : $varname; + var = :($varname = $varname === $NO_VALUE ? $def_n_meta : $varname; $varname = $first(@parameters $a[$(indices...)]::$type = $varname)) else Meta.isexpr(a, :call) || throw("$a is not a variable of the independent variable") assert_unique_independent_var(dict, a.args[end]) - var = :($varname = $varname === nothing ? $def_n_meta : $varname; + var = :($varname = $varname === $NO_VALUE ? $def_n_meta : $varname; $varname = $first(@variables $a[$(indices...)]::$type = $varname)) end varval, meta = def_n_meta, nothing @@ -765,11 +766,12 @@ function parse_variable_arg!(exprs, vs, dict, mod, arg, varclass, kwargs, where_ end function convert_units(varunits::DynamicQuantities.Quantity, value) - value isa Nothing && return nothing DynamicQuantities.ustrip(DynamicQuantities.uconvert( DynamicQuantities.SymbolicUnits.as_quantity(varunits), value)) end +convert_units(::DynamicQuantities.Quantity, value::NoValue) = NO_VALUE + function convert_units( varunits::DynamicQuantities.Quantity, value::AbstractArray{T}) where {T} DynamicQuantities.ustrip.(DynamicQuantities.uconvert.( @@ -777,21 +779,18 @@ function convert_units( end function convert_units(varunits::Unitful.FreeUnits, value) - value isa Nothing && return nothing Unitful.ustrip(varunits, value) end +convert_units(::Unitful.FreeUnits, value::NoValue) = NO_VALUE + function convert_units(varunits::Unitful.FreeUnits, value::AbstractArray{T}) where {T} Unitful.ustrip.(varunits, value) end -function convert_units(varunits::Unitful.FreeUnits, value::Num) - value -end +convert_units(::Unitful.FreeUnits, value::Num) = value -function convert_units(varunits::DynamicQuantities.Quantity, value::Num) - value -end +convert_units(::DynamicQuantities.Quantity, value::Num) = value function parse_variable_arg(dict, mod, arg, varclass, kwargs, where_types) vv, def, metadata_with_exprs = parse_variable_def!( From b83a251a33434dd2ccdd6aea73a69959119720b1 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 11 Oct 2024 14:39:56 +0530 Subject: [PATCH 0119/1337] fix: make `vars` search through arrays of symbolics --- src/utils.jl | 6 ++++++ test/variable_utils.jl | 10 +++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/utils.jl b/src/utils.jl index 29e61ede69..71b7bc3cd4 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -385,6 +385,12 @@ function vars!(vars, O; op = Differential) if isvariable(O) return push!(vars, O) end + if symbolic_type(O) == NotSymbolic() && O isa AbstractArray + for arg in O + vars!(vars, arg; op) + end + return vars + end !iscall(O) && return vars operation(O) isa op && return push!(vars, O) diff --git a/test/variable_utils.jl b/test/variable_utils.jl index 8f3178f453..d76f2f1209 100644 --- a/test/variable_utils.jl +++ b/test/variable_utils.jl @@ -1,5 +1,5 @@ using ModelingToolkit, Test -using ModelingToolkit: value +using ModelingToolkit: value, vars using SymbolicUtils: <ₑ @parameters α β δ expr = (((1 / β - 1) + δ) / α)^(1 / (α - 1)) @@ -33,3 +33,11 @@ aov = ModelingToolkit.collect_applied_operators(eq, Differential) ts = collect_ivs([eq]) @test ts == Set([t]) + +@testset "vars searching through array of symbolics" begin + fn(x, y) = sum(x) + y + @register_symbolic fn(x::AbstractArray, y) + @variables x y z + res = vars(fn([x, y], z)) + @test length(res) == 3 +end From 5d7d97daf58ca4d7beeb3ba496e4c492b9a6f8b4 Mon Sep 17 00:00:00 2001 From: Christopher Rackauckas Date: Fri, 11 Oct 2024 08:05:40 -0400 Subject: [PATCH 0120/1337] Update abstractsystem.jl --- src/systems/abstractsystem.jl | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index 00a003e06d..efec8b7fd8 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -913,7 +913,12 @@ end """ $(TYPEDSIGNATURES) -Mark a system as completed. If a system is complete, the system will no longer +Mark a system as completed. A completed system is a system which is done being +defined/modified and is ready for structural analysis or other transformations. +This allows for analyses and optimizations to be performed which require knowing +the global structure of the system. + +One property to note is that if a system is complete, the system will no longer namespace its subsystems or variables, i.e. `isequal(complete(sys).v.i, v.i)`. """ function complete(sys::AbstractSystem; split = true) From 09de19020c99b92d51ba577d13b4bae6b3202d1c Mon Sep 17 00:00:00 2001 From: Venkateshprasad <32921645+ven-k@users.noreply.github.com> Date: Fri, 11 Oct 2024 14:44:05 +0000 Subject: [PATCH 0121/1337] test: for vanilla-`@variable` syntax with arbitrary length --- test/model_parsing.jl | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/test/model_parsing.jl b/test/model_parsing.jl index c108b88625..3e2bd2045c 100644 --- a/test/model_parsing.jl +++ b/test/model_parsing.jl @@ -281,17 +281,34 @@ end @testset "Arrays using vanilla-@variable syntax" begin @mtkmodel TupleInArrayDef begin + @structural_parameters begin + N + M + end @parameters begin (l(t)[1:2, 1:3] = 1), [description = "l is more than 1D"] - (l2(t)[1:3] = 2), [description = "l2 is 1D"] - (l3(t)[1:3]::Int = 3), [description = "l3 is 1D and has a type"] + (l2(t)[1:N, 1:M] = 2), + [description = "l is more than 1D, with arbitrary length"] + (l3(t)[1:3] = 3), [description = "l2 is 1D"] + (l4(t)[1:N] = 4), [description = "l2 is 1D, with arbitrary length"] + (l5(t)[1:3]::Int = 5), [description = "l3 is 1D and has a type"] + (l6(t)[1:N]::Int = 6), + [description = "l3 is 1D and has a type, with arbitrary length"] end end - @named arr = TupleInArrayDef() + N, M = 4, 5 + @named arr = TupleInArrayDef(; N, M) @test getdefault(arr.l) == 1 @test getdefault(arr.l2) == 2 @test getdefault(arr.l3) == 3 + @test getdefault(arr.l4) == 4 + @test getdefault(arr.l5) == 5 + @test getdefault(arr.l6) == 6 + + @test size(arr.l2) == (N, M) + @test size(arr.l4) == (N,) + @test size(arr.l6) == (N,) end @testset "Type annotation" begin From 0d4c97d6a015fa76c1e80bda64a21bd1ab6a9974 Mon Sep 17 00:00:00 2001 From: Venkateshprasad <32921645+ven-k@users.noreply.github.com> Date: Fri, 11 Oct 2024 16:10:46 +0000 Subject: [PATCH 0122/1337] fix: remove an unused parsing block This is handled in two steps and this block is never used --- src/systems/model_parsing.jl | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/systems/model_parsing.jl b/src/systems/model_parsing.jl index bf70d06b79..51b7c6eb69 100644 --- a/src/systems/model_parsing.jl +++ b/src/systems/model_parsing.jl @@ -254,12 +254,6 @@ function parse_variable_def!(dict, mod, arg, varclass, kwargs, where_types; parse_variable_def!( dict, mod, a, varclass, kwargs, where_types; def, type, meta) end - Expr(:(::), Expr(:call, a, b), type) => begin - type = getfield(mod, type) - def = _type_check!(def, a, type, varclass) - parse_variable_def!( - dict, mod, a, varclass, kwargs, where_types; def, type, meta) - end Expr(:call, a, b) => begin var = generate_var!(dict, a, b, varclass, mod; type) update_kwargs_and_metadata!(dict, kwargs, a, def, type, From 4c0b1edee4f4cae0b05eec9f206e491f372c6955 Mon Sep 17 00:00:00 2001 From: Venkateshprasad <32921645+ven-k@users.noreply.github.com> Date: Fri, 11 Oct 2024 16:12:24 +0000 Subject: [PATCH 0123/1337] docs: document the syntax parsed by a particular block --- src/systems/model_parsing.jl | 62 ++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/src/systems/model_parsing.jl b/src/systems/model_parsing.jl index 51b7c6eb69..1298d72506 100644 --- a/src/systems/model_parsing.jl +++ b/src/systems/model_parsing.jl @@ -239,27 +239,65 @@ function unit_handled_variable_value(meta, varname) return varval end +# This function parses various variable/parameter definitions. +# +# The comments indicate the syntax matched by a block; either when parsed directly +# when it is called recursively for parsing a part of an expression. +# These variable definitions are part of test suite in `test/model_parsing.jl` function parse_variable_def!(dict, mod, arg, varclass, kwargs, where_types; def = nothing, type::Type = Real, meta = Dict{DataType, Expr}()) arg isa LineNumberNode && return MLStyle.@match arg begin + # Parses: `a` + # Recursively called by: `c(t) = cval + jval` + # Recursively called by: `d = 2` + # Recursively called by: `e, [description = "e"]` + # Recursively called by: `f = 3, [description = "f"]` + # Recursively called by: `k = kval, [description = "k"]` + # Recursively called by: `par0::Bool = true` a::Symbol => begin var = generate_var!(dict, a, varclass; type) update_kwargs_and_metadata!(dict, kwargs, a, def, type, varclass, where_types, meta) return var, def, Dict() end + # Parses: `par5[1:3]::BigFloat` + # Parses: `par6(t)[1:3]::BigFloat` + # Recursively called by: `par2(t)::Int` + # Recursively called by: `par3(t)::BigFloat = 1.0` Expr(:(::), a, type) => begin type = getfield(mod, type) parse_variable_def!( dict, mod, a, varclass, kwargs, where_types; def, type, meta) end + # Recursively called by: `i(t) = 4, [description = "i(t)"]` + # Recursively called by: `h(t), [description = "h(t)"]` + # Recursively called by: `j(t) = jval, [description = "j(t)"]` + # Recursively called by: `par2(t)::Int` + # Recursively called by: `par3(t)::BigFloat = 1.0` Expr(:call, a, b) => begin var = generate_var!(dict, a, b, varclass, mod; type) update_kwargs_and_metadata!(dict, kwargs, a, def, type, varclass, where_types, meta) return var, def, Dict() end + # Condition 1 parses: + # `(l(t)[1:2, 1:3] = 1), [description = "l is more than 1D"]` + # `(l2(t)[1:N, 1:M] = 2), [description = "l is more than 1D, with arbitrary length"]` + # `(l3(t)[1:3] = 3), [description = "l2 is 1D"]` + # `(l4(t)[1:N] = 4), [description = "l2 is 1D, with arbitrary length"]` + # + # Condition 2 parses: + # `(l5(t)[1:3]::Int = 5), [description = "l3 is 1D and has a type"]` + # `(l6(t)[1:N]::Int = 6), [description = "l3 is 1D and has a type, with arbitrary length"]` + # + # Condition 3 parses: + # `e2[1:2]::Int, [description = "e2"]` + # `h2(t)[1:2]::Int, [description = "h2(t)"]` + # + # Condition 4 parses: + # `e2[1:2], [description = "e2"]` + # `h2(t)[1:2], [description = "h2(t)"]` Expr(:tuple, Expr(:(=), Expr(:ref, a, indices...), default_val), meta_val) || Expr(:tuple, Expr(:(=), Expr(:(::), Expr(:ref, a, indices...), type), default_val), meta_val) || Expr(:tuple, Expr(:(::), Expr(:ref, a, indices...), type), meta_val) || @@ -284,6 +322,12 @@ function parse_variable_def!(dict, mod, arg, varclass, kwargs, where_types; dict, indices, kwargs, meta, type, varclass, varname, varval, where_types) (:($varname...), var), nothing, Dict() end + # Condition 1 parses: + # `par7(t)[1:3, 1:3]::BigFloat = 1.0, [description = "with description"]` + # + # Condition 2 parses: + # `d2[1:2] = 2` + # `l(t)[1:2, 1:3] = 2, [description = "l is more than 1D"]` Expr(:(=), Expr(:(::), Expr(:ref, a, indices...), type), def_n_meta) || Expr(:(=), Expr(:ref, a, indices...), def_n_meta) => begin (@isdefined type) || (type = Real) @@ -326,6 +370,13 @@ function parse_variable_def!(dict, mod, arg, varclass, kwargs, where_types; dict, indices, kwargs, meta, type, varclass, varname, varval, where_types) (:($varname...), var), nothing, Dict() end + # Condition 1 is recursively called by: + # `par5[1:3]::BigFloat` + # `par6(t)[1:3]::BigFloat` + # + # Condition 2 parses: + # `b2(t)[1:2]` + # `a2[1:2]` Expr(:(::), Expr(:ref, a, indices...), type) || Expr(:ref, a, indices...) => begin (@isdefined type) || (type = Real) @@ -346,6 +397,14 @@ function parse_variable_def!(dict, mod, arg, varclass, kwargs, where_types; dict, indices, kwargs, nothing, type, varclass, varname, nothing, where_types) (:($varname...), var), nothing, Dict() end + # Parses: `c(t) = cval + jval` + # Parses: `d = 2` + # Parses: `f = 3, [description = "f"]` + # Parses: `i(t) = 4, [description = "i(t)"]` + # Parses: `j(t) = jval, [description = "j(t)"]` + # Parses: `k = kval, [description = "k"]` + # Parses: `par0::Bool = true` + # Parses: `par3(t)::BigFloat = 1.0` Expr(:(=), a, b) => begin Base.remove_linenums!(b) def, meta = parse_default(mod, b) @@ -361,6 +420,9 @@ function parse_variable_def!(dict, mod, arg, varclass, kwargs, where_types; end return var, def, Dict() end + # Parses: `e, [description = "e"]` + # Parses: `h(t), [description = "h(t)"]` + # Parses: `par2(t)::Int` Expr(:tuple, a, b) => begin meta = parse_metadata(mod, b) var, def, _ = parse_variable_def!( From 31efe04cd025c5a0a67ae7331c2f8964407428e2 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 8 Oct 2024 11:33:57 +0530 Subject: [PATCH 0124/1337] fix: handle unknowns inside callable parameters when topsorting equations --- src/utils.jl | 8 ++++++++ test/structural_transformation/utils.jl | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/src/utils.jl b/src/utils.jl index dadf31ebcc..d38ab822d4 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -383,6 +383,14 @@ function vars!(vars, eq::Equation; op = Differential) end function vars!(vars, O; op = Differential) if isvariable(O) + if iscalledparameter(O) + f = getcalledparameter(O) + push!(vars, f) + for arg in arguments(O) + vars!(vars, arg; op) + end + return vars + end return push!(vars, O) end if symbolic_type(O) == NotSymbolic() && O isa AbstractArray diff --git a/test/structural_transformation/utils.jl b/test/structural_transformation/utils.jl index 6d4a5a7506..8644d96945 100644 --- a/test/structural_transformation/utils.jl +++ b/test/structural_transformation/utils.jl @@ -32,3 +32,11 @@ se = collect(StructuralTransformations.edges(graph)) @test se == mapreduce(vcat, enumerate(graph.fadjlist)) do (s, d) StructuralTransformations.BipartiteEdge.(s, d) end + +@testset "observed2graph handles unknowns inside callable parameters" begin + @variables x(t) y(t) + @parameters p(..) + g, _ = ModelingToolkit.observed2graph([y ~ p(x), x ~ 0], [y, x]) + @test ModelingToolkit.𝑠neighbors(g, 1) == [2] + @test ModelingToolkit.𝑑neighbors(g, 2) == [1] +end From c3deaf96c6d2e4e5c4529ebdc4fc0258ad64fae6 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Sat, 12 Oct 2024 17:42:49 +0530 Subject: [PATCH 0125/1337] build: bump Symbolics compat --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 23ae9f8d13..5025ce3017 100644 --- a/Project.toml +++ b/Project.toml @@ -126,7 +126,7 @@ SpecialFunctions = "0.7, 0.8, 0.9, 0.10, 1.0, 2" StaticArrays = "0.10, 0.11, 0.12, 1.0" SymbolicIndexingInterface = "0.3.31" SymbolicUtils = "3.7" -Symbolics = "6.12" +Symbolics = "6.14" URIs = "1" UnPack = "0.1, 1.0" Unitful = "1.1" From 84ae858e33da996622f28dfcdda852e1910df81b Mon Sep 17 00:00:00 2001 From: Christopher Rackauckas Date: Sat, 12 Oct 2024 10:16:54 -0400 Subject: [PATCH 0126/1337] Update Project.toml --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 5025ce3017..0c152b1d6f 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "ModelingToolkit" uuid = "961ee093-0014-501f-94e3-6117800e7a78" authors = ["Yingbo Ma ", "Chris Rackauckas and contributors"] -version = "9.45.0" +version = "9.46.0" [deps] AbstractTrees = "1520ce14-60c1-5f80-bbc7-55ef81b5835c" From 375a78d4339e58fe879e446c9e0274feca3e86c8 Mon Sep 17 00:00:00 2001 From: ArnoStrouwen Date: Sun, 29 Sep 2024 19:08:07 +0200 Subject: [PATCH 0127/1337] Rewrite of SDE tutorial --- docs/src/tutorials/stochastic_diffeq.md | 87 ++++++++++++++++++------- 1 file changed, 64 insertions(+), 23 deletions(-) diff --git a/docs/src/tutorials/stochastic_diffeq.md b/docs/src/tutorials/stochastic_diffeq.md index fd03d51810..c53c74b8b3 100644 --- a/docs/src/tutorials/stochastic_diffeq.md +++ b/docs/src/tutorials/stochastic_diffeq.md @@ -1,38 +1,79 @@ # Modeling with Stochasticity -All models with `ODESystem` are deterministic. `SDESystem` adds another element -to the model: randomness. This is a +All previous differential equations tutorials deal with deterministic `ODESystem`s. +In this tutorial, we add randomness. +In particular, we show how to represent a [stochastic differential equation](https://en.wikipedia.org/wiki/Stochastic_differential_equation) -which has a deterministic (drift) component and a stochastic (diffusion) -component. Let's take the Lorenz equation from the first tutorial and extend -it to have multiplicative noise by creating `@brownian` variables in the equations. +as a `SDESystem`. + +!!! note + + The high level `@mtkmodel` macro used in the + [getting started tutorial](@ref getting_started) + is not yet compatible with `SDESystem`. + We thus have to use a lower level interface to define stochastic differential equations. + For an introduction to this interface, read the + [programmatically generating ODESystems tutorial](@ref programmatically). + +Let's take the Lorenz equation and add noise to each of the states. +To show the flexibility of ModelingToolkit, +we do not use homogeneous noise, with constant variance, +but instead use heterogeneous noise, +where the magnitude of the noise scales with (0.1 times) the magnitude of each of the states: + +```math +\begin{aligned} +dx &= (\sigma (y-x))dt &+ 0.1xdB \\ +dy &= (x(\rho-z) - y)dt &+ 0.1ydB \\ +dz &= (xy - \beta z)dt &+ 0.1zdB \\ +\end{aligned} +``` + +Where $B$, is standard Brownian motion, also called the +[Wiener process](https://en.wikipedia.org/wiki/Wiener_process). +In ModelingToolkit this can be done by `@brownian` variables. ```@example SDE using ModelingToolkit, StochasticDiffEq using ModelingToolkit: t_nounits as t, D_nounits as D +using Plots -# Define some variables -@parameters σ ρ β -@variables x(t) y(t) z(t) -@brownian a -eqs = [D(x) ~ σ * (y - x) + 0.1a * x, - D(y) ~ x * (ρ - z) - y + 0.1a * y, - D(z) ~ x * y - β * z + 0.1a * z] +@parameters σ=10.0 ρ=2.33 β=26.0 +@variables x(t)=5.0 y(t)=5.0 z(t)=1.0 +@brownian B +eqs = [D(x) ~ σ * (y - x) + 0.1B * x, + D(y) ~ x * (ρ - z) - y + 0.1B * y, + D(z) ~ x * y - β * z + 0.1B * z] @mtkbuild de = System(eqs, t) +``` + +Even though we did not explicitly use `SDESystem`, ModelingToolkit can still infer this from the equations. -u0map = [ - x => 1.0, - y => 0.0, - z => 0.0 -] +```@example SDE +typeof(de) +``` -parammap = [ - σ => 10.0, - β => 26.0, - ρ => 2.33 -] +We continue by solving and plotting the SDE. -prob = SDEProblem(de, u0map, (0.0, 100.0), parammap) +```@example SDE +prob = SDEProblem(de, [], (0.0, 100.0), []) sol = solve(prob, LambaEulerHeun()) +plot(sol, idxs = [(1, 2, 3)]) +``` + +The noise present in all 3 equations is correlated, as can be seen on the below figure. +If you want uncorrelated noise for each equation, +multiple `@brownian` variables have to be declared. + +```@example SDE +@brownian Bx By Bz +``` + +The figure also shows the multiplicative nature of the noise. +Because states `x` and `y` generally take on larger values, +the noise also takes on a more pronounced effect on these states compared to the state `z`. + +```@example SDE +plot(sol) ``` From b144e15611ee5a354b1d1c43122a6a9f33b614ee Mon Sep 17 00:00:00 2001 From: Christopher Rackauckas Date: Sat, 12 Oct 2024 11:28:42 -0400 Subject: [PATCH 0128/1337] Update Project.toml --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 73c2fa4c8a..381354b493 100644 --- a/Project.toml +++ b/Project.toml @@ -72,7 +72,7 @@ MTKLabelledArraysExt = "LabelledArrays" [compat] AbstractTrees = "0.3, 0.4" ArrayInterface = "6, 7" -BifurcationKit = "0.3, 0.4" +BifurcationKit = "0.4" BlockArrays = "1.1" ChainRulesCore = "1" Combinatorics = "1" From 87cbf0e72592fc91331a004dfa12deab1c0f5f79 Mon Sep 17 00:00:00 2001 From: ArnoStrouwen Date: Sat, 12 Oct 2024 17:49:12 +0200 Subject: [PATCH 0129/1337] Finish rewrite of SDE tutorial --- docs/src/tutorials/stochastic_diffeq.md | 45 ++++++++++++++++--------- 1 file changed, 30 insertions(+), 15 deletions(-) diff --git a/docs/src/tutorials/stochastic_diffeq.md b/docs/src/tutorials/stochastic_diffeq.md index c53c74b8b3..a6543dc977 100644 --- a/docs/src/tutorials/stochastic_diffeq.md +++ b/docs/src/tutorials/stochastic_diffeq.md @@ -19,19 +19,27 @@ Let's take the Lorenz equation and add noise to each of the states. To show the flexibility of ModelingToolkit, we do not use homogeneous noise, with constant variance, but instead use heterogeneous noise, -where the magnitude of the noise scales with (0.1 times) the magnitude of each of the states: +where the magnitude of the noise scales with (0.3 times) the magnitude of each of the states: ```math \begin{aligned} -dx &= (\sigma (y-x))dt &+ 0.1xdB \\ -dy &= (x(\rho-z) - y)dt &+ 0.1ydB \\ -dz &= (xy - \beta z)dt &+ 0.1zdB \\ +\frac{dx}{dt} &= (\sigma (y-x)) &+ 0.1x\frac{dB}{dt} \\ +\frac{dy}{dt} &= (x(\rho-z) - y) &+ 0.1y\frac{dB}{dt} \\ +\frac{dz}{dt} &= (xy - \beta z) &+ 0.1z\frac{dB}{dt} \\ \end{aligned} ``` Where $B$, is standard Brownian motion, also called the [Wiener process](https://en.wikipedia.org/wiki/Wiener_process). -In ModelingToolkit this can be done by `@brownian` variables. +We use notation similar to the +[Langevin equation](https://en.wikipedia.org/wiki/Stochastic_differential_equation#Use_in_physics), +often used in physics. +By "multiplying" the equations by $dt$, the notation used in +[probability theory](https://en.wikipedia.org/wiki/Stochastic_differential_equation#Use_in_probability_and_mathematical_finance) +can be recovered. + +We use this Langevin-like notation because it allows us to extend MTK modeling capacity from ODEs to SDEs, +using only a single new concept, `@brownian` variables, which represent $\frac{dB}{dt}$ in the above equation. ```@example SDE using ModelingToolkit, StochasticDiffEq @@ -41,9 +49,9 @@ using Plots @parameters σ=10.0 ρ=2.33 β=26.0 @variables x(t)=5.0 y(t)=5.0 z(t)=1.0 @brownian B -eqs = [D(x) ~ σ * (y - x) + 0.1B * x, - D(y) ~ x * (ρ - z) - y + 0.1B * y, - D(z) ~ x * y - β * z + 0.1B * z] +eqs = [D(x) ~ σ * (y - x) + 0.3x * B, + D(y) ~ x * (ρ - z) - y + 0.3y * B, + D(z) ~ x * y - β * z + 0.3z * B] @mtkbuild de = System(eqs, t) ``` @@ -58,22 +66,29 @@ We continue by solving and plotting the SDE. ```@example SDE prob = SDEProblem(de, [], (0.0, 100.0), []) -sol = solve(prob, LambaEulerHeun()) +sol = solve(prob, SRIW1()) plot(sol, idxs = [(1, 2, 3)]) ``` The noise present in all 3 equations is correlated, as can be seen on the below figure. -If you want uncorrelated noise for each equation, -multiple `@brownian` variables have to be declared. +The figure also shows the multiplicative nature of the noise. +Because states `x` and `y` generally take on larger values, +the noise also takes on a more pronounced effect on these states compared to the state `z`. ```@example SDE -@brownian Bx By Bz +plot(sol) ``` -The figure also shows the multiplicative nature of the noise. -Because states `x` and `y` generally take on larger values, -the noise also takes on a more pronounced effect on these states compared to the state `z`. +If you want uncorrelated noise for each equation, +multiple `@brownian` variables have to be declared. ```@example SDE +@brownian Bx By Bz +eqs = [D(x) ~ σ * (y - x) + 0.3x * Bx, + D(y) ~ x * (ρ - z) - y + 0.3y * By, + D(z) ~ x * y - β * z + 0.3z * Bz] +@mtkbuild de = System(eqs, t) +prob = SDEProblem(de, [], (0.0, 100.0), []) +sol = solve(prob, SRIW1()) plot(sol) ``` From e0d5e15736b079c5e0120fd177422059cd181204 Mon Sep 17 00:00:00 2001 From: Christopher Rackauckas Date: Sat, 12 Oct 2024 14:37:32 -0400 Subject: [PATCH 0130/1337] Update bifurcationkit.jl --- test/extensions/bifurcationkit.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/extensions/bifurcationkit.jl b/test/extensions/bifurcationkit.jl index 698dd085c8..85ac28765f 100644 --- a/test/extensions/bifurcationkit.jl +++ b/test/extensions/bifurcationkit.jl @@ -1,4 +1,4 @@ -using BifurcationKit, ModelingToolkit, Test +using BifurcationKit, ModelingToolkit, SetField, Test using ModelingToolkit: t_nounits as t, D_nounits as D # Simple pitchfork diagram, compares solution to native BifurcationKit, checks they are identical. # Checks using `jac=false` option. @@ -36,7 +36,7 @@ let bprob_BK = BifurcationProblem(f_BK, [1.0, 1.0], [-1.0, 1.0], - (@lens _[1]); + (SetField.@lens _[1]); record_from_solution = (x, p) -> x[1]) bif_dia_BK = bifurcationdiagram(bprob_BK, PALC(), From 37e8063f503b0d4095bded50ecc0261c4828ff51 Mon Sep 17 00:00:00 2001 From: Christopher Rackauckas Date: Sat, 12 Oct 2024 14:43:24 -0400 Subject: [PATCH 0131/1337] Fix initialization.md --- docs/src/tutorials/initialization.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/tutorials/initialization.md b/docs/src/tutorials/initialization.md index 6451832dbc..0cdc2d312b 100644 --- a/docs/src/tutorials/initialization.md +++ b/docs/src/tutorials/initialization.md @@ -278,7 +278,7 @@ initsys = prob2.f.initializeprob.f.sys The system is fully determined, and the equations are solvable. -```@example +```@example paraminit [equations(initsys); observed(initsys)] ``` From 0a479785f7366cff5386111caa0d8b994bbf1d12 Mon Sep 17 00:00:00 2001 From: Christopher Rackauckas Date: Sat, 12 Oct 2024 14:49:26 -0400 Subject: [PATCH 0132/1337] Update Project.toml --- test/extensions/Project.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/test/extensions/Project.toml b/test/extensions/Project.toml index 81097d98d2..3e9ad1557e 100644 --- a/test/extensions/Project.toml +++ b/test/extensions/Project.toml @@ -8,5 +8,6 @@ ModelingToolkit = "961ee093-0014-501f-94e3-6117800e7a78" OrdinaryDiffEq = "1dea7af3-3e70-54e6-95c3-0bf5283fa5ed" SciMLSensitivity = "1ed8b502-d754-442c-8d5d-10ac956f44a1" SciMLStructures = "53ae85a6-f571-4167-b2af-e1d143709226" +SetField = "efcf1570-3423-57d1-acb7-fd33fddbac46" SymbolicIndexingInterface = "2efcf032-c050-4f8e-a9bb-153293bab1f5" Zygote = "e88e6eb3-aa80-5325-afca-941959d7151f" From d52828ebc89cc66ad4ca8903191f2e346be57036 Mon Sep 17 00:00:00 2001 From: Christopher Rackauckas Date: Sat, 12 Oct 2024 14:58:56 -0400 Subject: [PATCH 0133/1337] Update test/extensions/Project.toml --- test/extensions/Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/extensions/Project.toml b/test/extensions/Project.toml index 3e9ad1557e..0e6ba6bc4a 100644 --- a/test/extensions/Project.toml +++ b/test/extensions/Project.toml @@ -8,6 +8,6 @@ ModelingToolkit = "961ee093-0014-501f-94e3-6117800e7a78" OrdinaryDiffEq = "1dea7af3-3e70-54e6-95c3-0bf5283fa5ed" SciMLSensitivity = "1ed8b502-d754-442c-8d5d-10ac956f44a1" SciMLStructures = "53ae85a6-f571-4167-b2af-e1d143709226" -SetField = "efcf1570-3423-57d1-acb7-fd33fddbac46" +Setfield = "efcf1570-3423-57d1-acb7-fd33fddbac46" SymbolicIndexingInterface = "2efcf032-c050-4f8e-a9bb-153293bab1f5" Zygote = "e88e6eb3-aa80-5325-afca-941959d7151f" From 0437dd5449e1cc43920eb205b4dffc0164a25da5 Mon Sep 17 00:00:00 2001 From: Christopher Rackauckas Date: Sat, 12 Oct 2024 15:12:18 -0400 Subject: [PATCH 0134/1337] Update test/extensions/bifurcationkit.jl --- test/extensions/bifurcationkit.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/extensions/bifurcationkit.jl b/test/extensions/bifurcationkit.jl index 85ac28765f..a81a754e0b 100644 --- a/test/extensions/bifurcationkit.jl +++ b/test/extensions/bifurcationkit.jl @@ -36,7 +36,7 @@ let bprob_BK = BifurcationProblem(f_BK, [1.0, 1.0], [-1.0, 1.0], - (SetField.@lens _[1]); + (Setfield.@lens _[1]); record_from_solution = (x, p) -> x[1]) bif_dia_BK = bifurcationdiagram(bprob_BK, PALC(), From fed60d9796c7db9746f61c13c604ebac6366ef01 Mon Sep 17 00:00:00 2001 From: Christopher Rackauckas Date: Sat, 12 Oct 2024 15:12:23 -0400 Subject: [PATCH 0135/1337] Update test/extensions/bifurcationkit.jl --- test/extensions/bifurcationkit.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/extensions/bifurcationkit.jl b/test/extensions/bifurcationkit.jl index a81a754e0b..724a3ebe64 100644 --- a/test/extensions/bifurcationkit.jl +++ b/test/extensions/bifurcationkit.jl @@ -1,4 +1,4 @@ -using BifurcationKit, ModelingToolkit, SetField, Test +using BifurcationKit, ModelingToolkit, Setfield, Test using ModelingToolkit: t_nounits as t, D_nounits as D # Simple pitchfork diagram, compares solution to native BifurcationKit, checks they are identical. # Checks using `jac=false` option. From f288c470abf7116333974c08d1be4b1ae28c93a0 Mon Sep 17 00:00:00 2001 From: Christopher Rackauckas Date: Sat, 12 Oct 2024 16:08:32 -0400 Subject: [PATCH 0136/1337] Remove extra initial condition Fixes https://github.com/SciML/ModelingToolkit.jl/issues/3088 --- docs/src/tutorials/acausal_components.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/src/tutorials/acausal_components.md b/docs/src/tutorials/acausal_components.md index 982629595c..c91ba29670 100644 --- a/docs/src/tutorials/acausal_components.md +++ b/docs/src/tutorials/acausal_components.md @@ -311,8 +311,7 @@ DAE solver](https://docs.sciml.ai/DiffEqDocs/stable/solvers/dae_solve/#OrdinaryD This is done as follows: ```@example acausal -u0 = [rc_model.capacitor.v => 0.0 - rc_model.capacitor.p.i => 0.0] +u0 = [rc_model.capacitor.v => 0.0] prob = ODEProblem(rc_model, u0, (0, 10.0)) sol = solve(prob) From 8380d6b13887cc4af7287291aee515230e156e4e Mon Sep 17 00:00:00 2001 From: Christopher Rackauckas Date: Sun, 13 Oct 2024 02:29:19 -0400 Subject: [PATCH 0137/1337] Backwards compatibility of scope detection for Catalyst Attempt to fix https://github.com/SciML/Catalyst.jl/issues/1075 --- src/utils.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/src/utils.jl b/src/utils.jl index d38ab822d4..39d5d18ffd 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -492,6 +492,7 @@ recursively searches through all subsystems of `sys`, increasing the depth if it function collect_scoped_vars!(unknowns, parameters, sys, iv; depth = 1, op = Differential) if has_eqs(sys) for eq in get_eqs(sys) + hasfield(eq, :lhs) || continue eq.lhs isa Union{Symbolic, Number} || continue collect_vars!(unknowns, parameters, eq.lhs, iv; depth, op) collect_vars!(unknowns, parameters, eq.rhs, iv; depth, op) From d0014f62d6776b760e29a50ffc19582613127487 Mon Sep 17 00:00:00 2001 From: Christopher Rackauckas Date: Sun, 13 Oct 2024 03:33:32 -0400 Subject: [PATCH 0138/1337] Update src/utils.jl Co-authored-by: Sam Isaacson --- src/utils.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils.jl b/src/utils.jl index 39d5d18ffd..e8ed131d78 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -492,7 +492,7 @@ recursively searches through all subsystems of `sys`, increasing the depth if it function collect_scoped_vars!(unknowns, parameters, sys, iv; depth = 1, op = Differential) if has_eqs(sys) for eq in get_eqs(sys) - hasfield(eq, :lhs) || continue + eq isa Equation || continue eq.lhs isa Union{Symbolic, Number} || continue collect_vars!(unknowns, parameters, eq.lhs, iv; depth, op) collect_vars!(unknowns, parameters, eq.rhs, iv; depth, op) From 1e41add2e0439da2c304fb063b654e4c8ea4478e Mon Sep 17 00:00:00 2001 From: Christopher Rackauckas Date: Sun, 13 Oct 2024 04:35:17 -0400 Subject: [PATCH 0139/1337] Update Project.toml --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 0c152b1d6f..808d68af06 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "ModelingToolkit" uuid = "961ee093-0014-501f-94e3-6117800e7a78" authors = ["Yingbo Ma ", "Chris Rackauckas and contributors"] -version = "9.46.0" +version = "9.46.1" [deps] AbstractTrees = "1520ce14-60c1-5f80-bbc7-55ef81b5835c" From 281b82312379b1941a4c7b27a6eabb1982348086 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 14 Oct 2024 10:15:41 +0530 Subject: [PATCH 0140/1337] docs: remove redundant parameter initialization section --- docs/src/tutorials/initialization.md | 53 ---------------------------- 1 file changed, 53 deletions(-) diff --git a/docs/src/tutorials/initialization.md b/docs/src/tutorials/initialization.md index 0cdc2d312b..c3dbf8bbb0 100644 --- a/docs/src/tutorials/initialization.md +++ b/docs/src/tutorials/initialization.md @@ -464,56 +464,3 @@ sol[α * x - β * x * y] ```@example init plot(sol) ``` - -## Solving for parameters during initialization - -Sometimes, it is necessary to solve for a parameter during initialization. For example, -given a spring-mass system we want to find the un-stretched length of the spring given -that the initial condition of the system is its steady state. - -```@example init -using ModelingToolkitStandardLibrary.Mechanical.TranslationalModelica: Fixed, Mass, Spring, - Force, Damper -using ModelingToolkitStandardLibrary.Blocks: Constant - -@named mass = Mass(; m = 1.0, s = 1.0, v = 0.0, a = 0.0) -@named fixed = Fixed(; s0 = 0.0) -@named spring = Spring(; c = 2.0, s_rel0 = missing) -@named gravity = Force() -@named constant = Constant(; k = 9.81) -@named damper = Damper(; d = 0.1) -@mtkbuild sys = ODESystem( - [connect(fixed.flange, spring.flange_a), connect(spring.flange_b, mass.flange_a), - connect(mass.flange_a, gravity.flange), connect(constant.output, gravity.f), - connect(fixed.flange, damper.flange_a), connect(damper.flange_b, mass.flange_a)], - t; - systems = [fixed, spring, mass, gravity, constant, damper], - guesses = [spring.s_rel0 => 1.0]) -``` - -Note that we explicitly provide `s_rel0 = missing` to the spring. Parameters are only -solved for during initialization if their value (either default, or explicitly passed -to the `ODEProblem` constructor) is `missing`. We also need to provide a guess for the -parameter. - -If a parameter is not given a value of `missing`, and does not have a default or initial -value, the `ODEProblem` constructor will throw an error. If the parameter _does_ have a -value of `missing`, it must be given a guess. - -```@example init -prob = ODEProblem(sys, [], (0.0, 1.0)) -prob.ps[spring.s_rel0] -``` - -Note that the value of the parameter in the problem is zero, similar to unknowns that -are solved for during initialization. - -```@example init -integ = init(prob) -integ.ps[spring.s_rel0] -``` - -The un-stretched length of the spring is now correctly calculated. The same result can be -achieved if `s_rel0 = missing` is omitted when constructing `spring`, and instead -`spring.s_rel0 => missing` is passed to the `ODEProblem` constructor along with values -of other parameters. From 82d815c4c2eaaf6ec28f2a257c2cf512723bb094 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 2 Oct 2024 16:14:56 +0530 Subject: [PATCH 0141/1337] refactor: major cleanup of `*Problem` construction --- src/ModelingToolkit.jl | 1 + src/systems/diffeqs/abstractodesystem.jl | 222 +------- src/systems/diffeqs/sdesystem.jl | 5 +- .../discrete_system/discrete_system.jl | 60 +-- src/systems/nonlinear/nonlinearsystem.jl | 40 +- src/systems/problem_utils.jl | 503 ++++++++++++++++++ src/variables.jl | 2 +- test/initializationsystem.jl | 3 +- test/split_parameters.jl | 2 +- 9 files changed, 541 insertions(+), 297 deletions(-) create mode 100644 src/systems/problem_utils.jl diff --git a/src/ModelingToolkit.jl b/src/ModelingToolkit.jl index 2237ba8952..2f57bb1765 100644 --- a/src/ModelingToolkit.jl +++ b/src/ModelingToolkit.jl @@ -144,6 +144,7 @@ include("systems/abstractsystem.jl") include("systems/model_parsing.jl") include("systems/connectors.jl") include("systems/callbacks.jl") +include("systems/problem_utils.jl") include("systems/nonlinear/nonlinearsystem.jl") include("systems/diffeqs/odesystem.jl") diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index 8ebe6d93d0..48770e7a52 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -793,211 +793,6 @@ function get_u0( return u0, defs end -struct GetUpdatedMTKParameters{G, S} - # `getu` functor which gets parameters that are unknowns during initialization - getpunknowns::G - # `setu` functor which returns a modified MTKParameters using those parameters - setpunknowns::S -end - -function (f::GetUpdatedMTKParameters)(prob, initializesol) - mtkp = copy(parameter_values(prob)) - f.setpunknowns(mtkp, f.getpunknowns(initializesol)) - mtkp -end - -struct UpdateInitializeprob{G, S} - # `getu` functor which gets all values from prob - getvals::G - # `setu` functor which updates initializeprob with values - setvals::S -end - -function (f::UpdateInitializeprob)(initializeprob, prob) - f.setvals(initializeprob, f.getvals(prob)) -end - -function get_temporary_value(p) - stype = symtype(unwrap(p)) - return if stype == Real - zero(Float64) - elseif stype <: AbstractArray{Real} - zeros(Float64, size(p)) - elseif stype <: Real - zero(stype) - elseif stype <: AbstractArray - zeros(eltype(stype), size(p)) - else - error("Nonnumeric parameter $p with symtype $stype cannot be solved for during initialization") - end -end - -function process_DEProblem(constructor, sys::AbstractODESystem, u0map, parammap; - implicit_dae = false, du0map = nothing, - version = nothing, tgrad = false, - jac = false, - checkbounds = false, sparse = false, - simplify = false, - linenumbers = true, parallel = SerialForm(), - eval_expression = false, - eval_module = @__MODULE__, - use_union = false, - tofloat = true, - symbolic_u0 = false, - u0_constructor = identity, - guesses = Dict(), - t = nothing, - warn_initialize_determined = true, - build_initializeprob = true, - initialization_eqs = [], - fully_determined = false, - check_units = true, - kwargs...) - eqs = equations(sys) - dvs = unknowns(sys) - ps = parameters(sys) - iv = get_iv(sys) - - check_array_equations_unknowns(eqs, dvs) - # TODO: Pass already computed information to varmap_to_vars call - # in process_u0? That would just be a small optimization - varmap = u0map === nothing || isempty(u0map) || eltype(u0map) <: Number ? - defaults(sys) : - merge(defaults(sys), todict(u0map)) - varmap = canonicalize_varmap(varmap) - varlist = collect(map(unwrap, dvs)) - missingvars = setdiff(varlist, collect(keys(varmap))) - setobserved = filter(keys(varmap)) do var - has_observed_with_lhs(sys, var) || has_observed_with_lhs(sys, default_toterm(var)) - end - - if eltype(parammap) <: Pair - parammap = Dict{Any, Any}(unwrap(k) => v for (k, v) in parammap) - elseif parammap isa AbstractArray - if isempty(parammap) - parammap = SciMLBase.NullParameters() - else - parammap = Dict{Any, Any}(unwrap.(parameters(sys)) .=> parammap) - end - end - defs = defaults(sys) - if has_guesses(sys) - guesses = merge( - ModelingToolkit.guesses(sys), isempty(guesses) ? Dict() : todict(guesses)) - solvablepars = [p - for p in parameters(sys) - if is_parameter_solvable(p, parammap, defs, guesses)] - - pvarmap = if parammap === nothing || parammap == SciMLBase.NullParameters() || - !(eltype(parammap) <: Pair) && isempty(parammap) - defs - else - merge(defs, todict(parammap)) - end - setparobserved = filter(keys(pvarmap)) do var - has_parameter_dependency_with_lhs(sys, var) - end - else - solvablepars = () - setparobserved = () - end - # ModelingToolkit.get_tearing_state(sys) !== nothing => Requires structural_simplify first - if sys isa ODESystem && build_initializeprob && - (((implicit_dae || !isempty(missingvars) || !isempty(solvablepars) || - !isempty(setobserved) || !isempty(setparobserved)) && - ModelingToolkit.get_tearing_state(sys) !== nothing) || - !isempty(initialization_equations(sys))) && t !== nothing - if eltype(u0map) <: Number - u0map = unknowns(sys) .=> vec(u0map) - end - if u0map === nothing || isempty(u0map) - u0map = Dict() - end - - initializeprob = ModelingToolkit.InitializationProblem( - sys, t, u0map, parammap; guesses, warn_initialize_determined, - initialization_eqs, eval_expression, eval_module, fully_determined, check_units) - initializeprobmap = getu(initializeprob, unknowns(sys)) - punknowns = [p - for p in all_variable_symbols(initializeprob) if is_parameter(sys, p)] - getpunknowns = getu(initializeprob, punknowns) - setpunknowns = setp(sys, punknowns) - initializeprobpmap = GetUpdatedMTKParameters(getpunknowns, setpunknowns) - reqd_syms = parameter_symbols(initializeprob) - update_initializeprob! = UpdateInitializeprob( - getu(sys, reqd_syms), setu(initializeprob, reqd_syms)) - - zerovars = Dict(setdiff(unknowns(sys), keys(defaults(sys))) .=> 0.0) - if parammap isa SciMLBase.NullParameters - parammap = Dict() - end - for p in punknowns - p = unwrap(p) - stype = symtype(p) - parammap[p] = get_temporary_value(p) - end - trueinit = collect(merge(zerovars, eltype(u0map) <: Pair ? todict(u0map) : u0map)) - u0map isa StaticArraysCore.StaticArray && - (trueinit = SVector{length(trueinit)}(trueinit)) - else - initializeprob = nothing - update_initializeprob! = nothing - initializeprobmap = nothing - initializeprobpmap = nothing - trueinit = u0map - end - - if has_index_cache(sys) && get_index_cache(sys) !== nothing - u0, defs = get_u0(sys, trueinit, parammap; symbolic_u0, - t0 = constructor <: Union{DDEFunction, SDDEFunction} ? nothing : t, use_union) - check_eqs_u0(eqs, dvs, u0; kwargs...) - p = if parammap === nothing || - parammap == SciMLBase.NullParameters() && isempty(defs) - nothing - else - MTKParameters(sys, parammap, trueinit; t0 = t) - end - else - u0, p, defs = get_u0_p(sys, - trueinit, - parammap; - tofloat, - use_union, - t0 = constructor <: Union{DDEFunction, SDDEFunction} ? nothing : t, - symbolic_u0) - p, split_idxs = split_parameters_by_type(p) - if p isa Tuple - ps = Base.Fix1(getindex, parameters(sys)).(split_idxs) - ps = (ps...,) #if p is Tuple, ps should be Tuple - end - end - if u0 !== nothing - u0 = u0_constructor(u0) - end - - if implicit_dae && du0map !== nothing - ddvs = map(Differential(iv), dvs) - defs = mergedefaults(defs, du0map, ddvs) - du0 = varmap_to_vars(du0map, ddvs; defaults = defs, toterm = identity, - tofloat = true) - else - du0 = nothing - ddvs = nothing - end - check_eqs_u0(eqs, dvs, u0; kwargs...) - f = constructor(sys, dvs, ps, u0; ddvs = ddvs, tgrad = tgrad, jac = jac, - checkbounds = checkbounds, p = p, - linenumbers = linenumbers, parallel = parallel, simplify = simplify, - sparse = sparse, eval_expression = eval_expression, - eval_module = eval_module, - initializeprob = initializeprob, - update_initializeprob! = update_initializeprob!, - initializeprobmap = initializeprobmap, - initializeprobpmap = initializeprobpmap, - kwargs...) - implicit_dae ? (f, du0, u0, p) : (f, u0, p) -end - function ODEFunctionExpr(sys::AbstractODESystem, args...; kwargs...) ODEFunctionExpr{true}(sys, args...; kwargs...) end @@ -1104,7 +899,7 @@ function DiffEqBase.ODEProblem{iip, specialize}(sys::AbstractODESystem, u0map = if !iscomplete(sys) error("A completed system is required. Call `complete` or `structural_simplify` on the system before creating an `ODEProblem`") end - f, u0, p = process_DEProblem(ODEFunction{iip, specialize}, sys, u0map, parammap; + f, u0, p = process_SciMLProblem(ODEFunction{iip, specialize}, sys, u0map, parammap; t = tspan !== nothing ? tspan[1] : tspan, check_length, warn_initialize_determined, eval_expression, eval_module, kwargs...) cbs = process_events(sys; callback, eval_expression, eval_module, kwargs...) @@ -1147,7 +942,7 @@ function DiffEqBase.DAEProblem{iip}(sys::AbstractODESystem, du0map, u0map, tspan if !iscomplete(sys) error("A completed system is required. Call `complete` or `structural_simplify` on the system before creating a `DAEProblem`") end - f, du0, u0, p = process_DEProblem(DAEFunction{iip}, sys, u0map, parammap; + f, du0, u0, p = process_SciMLProblem(DAEFunction{iip}, sys, u0map, parammap; implicit_dae = true, du0map = du0map, check_length, t = tspan !== nothing ? tspan[1] : tspan, warn_initialize_determined, kwargs...) @@ -1179,7 +974,7 @@ function DiffEqBase.DDEProblem{iip}(sys::AbstractODESystem, u0map = [], if !iscomplete(sys) error("A completed system is required. Call `complete` or `structural_simplify` on the system before creating a `DDEProblem`") end - f, u0, p = process_DEProblem(DDEFunction{iip}, sys, u0map, parammap; + f, u0, p = process_SciMLProblem(DDEFunction{iip}, sys, u0map, parammap; t = tspan !== nothing ? tspan[1] : tspan, symbolic_u0 = true, check_length, eval_expression, eval_module, kwargs...) @@ -1214,7 +1009,7 @@ function DiffEqBase.SDDEProblem{iip}(sys::AbstractODESystem, u0map = [], if !iscomplete(sys) error("A completed system is required. Call `complete` or `structural_simplify` on the system before creating a `SDDEProblem`") end - f, u0, p = process_DEProblem(SDDEFunction{iip}, sys, u0map, parammap; + f, u0, p = process_SciMLProblem(SDDEFunction{iip}, sys, u0map, parammap; t = tspan !== nothing ? tspan[1] : tspan, symbolic_u0 = true, eval_expression, eval_module, check_length, kwargs...) @@ -1274,7 +1069,8 @@ function ODEProblemExpr{iip}(sys::AbstractODESystem, u0map, tspan, if !iscomplete(sys) error("A completed system is required. Call `complete` or `structural_simplify` on the system before creating a `ODEProblemExpr`") end - f, u0, p = process_DEProblem(ODEFunctionExpr{iip}, sys, u0map, parammap; check_length, + f, u0, p = process_SciMLProblem( + ODEFunctionExpr{iip}, sys, u0map, parammap; check_length, t = tspan !== nothing ? tspan[1] : tspan, kwargs...) linenumbers = get(kwargs, :linenumbers, true) @@ -1320,7 +1116,7 @@ function DAEProblemExpr{iip}(sys::AbstractODESystem, du0map, u0map, tspan, if !iscomplete(sys) error("A completed system is required. Call `complete` or `structural_simplify` on the system before creating a `DAEProblemExpr`") end - f, du0, u0, p = process_DEProblem(DAEFunctionExpr{iip}, sys, u0map, parammap; + f, du0, u0, p = process_SciMLProblem(DAEFunctionExpr{iip}, sys, u0map, parammap; t = tspan !== nothing ? tspan[1] : tspan, implicit_dae = true, du0map = du0map, check_length, kwargs...) @@ -1372,7 +1168,7 @@ function DiffEqBase.SteadyStateProblem{iip}(sys::AbstractODESystem, u0map, if !iscomplete(sys) error("A completed system is required. Call `complete` or `structural_simplify` on the system before creating a `SteadyStateProblem`") end - f, u0, p = process_DEProblem(ODEFunction{iip}, sys, u0map, parammap; + f, u0, p = process_SciMLProblem(ODEFunction{iip}, sys, u0map, parammap; steady_state = true, check_length, kwargs...) kwargs = filter_kwargs(kwargs) @@ -1404,7 +1200,7 @@ function SteadyStateProblemExpr{iip}(sys::AbstractODESystem, u0map, if !iscomplete(sys) error("A completed system is required. Call `complete` or `structural_simplify` on the system before creating a `SteadyStateProblemExpr`") end - f, u0, p = process_DEProblem(ODEFunctionExpr{iip}, sys, u0map, parammap; + f, u0, p = process_SciMLProblem(ODEFunctionExpr{iip}, sys, u0map, parammap; steady_state = true, check_length, kwargs...) linenumbers = get(kwargs, :linenumbers, true) diff --git a/src/systems/diffeqs/sdesystem.jl b/src/systems/diffeqs/sdesystem.jl index 1b368f7df3..0d73aaf313 100644 --- a/src/systems/diffeqs/sdesystem.jl +++ b/src/systems/diffeqs/sdesystem.jl @@ -659,7 +659,7 @@ function DiffEqBase.SDEProblem{iip, specialize}( if !iscomplete(sys) error("A completed `SDESystem` is required. Call `complete` or `structural_simplify` on the system before creating an `SDEProblem`") end - f, u0, p = process_DEProblem( + f, u0, p = process_SciMLProblem( SDEFunction{iip, specialize}, sys, u0map, parammap; check_length, kwargs...) cbs = process_events(sys; callback, kwargs...) @@ -745,7 +745,8 @@ function SDEProblemExpr{iip}(sys::SDESystem, u0map, tspan, if !iscomplete(sys) error("A completed `SDESystem` is required. Call `complete` or `structural_simplify` on the system before creating an `SDEProblemExpr`") end - f, u0, p = process_DEProblem(SDEFunctionExpr{iip}, sys, u0map, parammap; check_length, + f, u0, p = process_SciMLProblem( + SDEFunctionExpr{iip}, sys, u0map, parammap; check_length, kwargs...) linenumbers = get(kwargs, :linenumbers, true) sparsenoise === nothing && (sparsenoise = get(kwargs, :sparse, false)) diff --git a/src/systems/discrete_system/discrete_system.jl b/src/systems/discrete_system/discrete_system.jl index 7103cfca80..bf7879be62 100644 --- a/src/systems/discrete_system/discrete_system.jl +++ b/src/systems/discrete_system/discrete_system.jl @@ -236,55 +236,25 @@ function generate_function( generate_custom_function(sys, exprs, dvs, ps; wrap_code, kwargs...) end -function process_DiscreteProblem(constructor, sys::DiscreteSystem, u0map, parammap; - linenumbers = true, parallel = SerialForm(), - use_union = false, - tofloat = !use_union, - eval_expression = false, eval_module = @__MODULE__, - kwargs...) +function shift_u0map_forward(sys::DiscreteSystem, u0map, defs) iv = get_iv(sys) - eqs = equations(sys) - dvs = unknowns(sys) - ps = parameters(sys) - - if eltype(u0map) <: Number - u0map = unknowns(sys) .=> vec(u0map) - end - if u0map === nothing || isempty(u0map) - u0map = Dict() - end - - trueu0map = Dict() - for (k, v) in u0map - k = unwrap(k) + updated = AnyDict() + for k in collect(keys(u0map)) + v = u0map[k] if !((op = operation(k)) isa Shift) 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)).") end - trueu0map[Shift(iv, op.steps + 1)(arguments(k)[1])] = v - end - defs = ModelingToolkit.get_defaults(sys) - for var in dvs - if (op = operation(var)) isa Shift && !haskey(trueu0map, var) - root = arguments(var)[1] - haskey(defs, root) || error("Initial condition for $var not provided.") - trueu0map[var] = defs[root] - end + updated[Shift(iv, op.steps + 1)(arguments(k)[1])] = v end - if has_index_cache(sys) && get_index_cache(sys) !== nothing - u0, defs = get_u0(sys, trueu0map, parammap) - p = MTKParameters(sys, parammap, trueu0map) - else - u0, p, defs = get_u0_p(sys, trueu0map, parammap; tofloat, use_union) + for var in unknowns(sys) + op = operation(var) + op isa Shift || continue + haskey(updated, var) && continue + root = first(arguments(var)) + haskey(defs, root) || error("Initial condition for $var not provided.") + updated[var] = defs[root] end - - check_eqs_u0(eqs, dvs, u0; kwargs...) - - f = constructor(sys, dvs, ps, u0; - linenumbers = linenumbers, parallel = parallel, - syms = Symbol.(dvs), paramsyms = Symbol.(ps), - eval_expression = eval_expression, eval_module = eval_module, - kwargs...) - return f, u0, p + return updated end """ @@ -307,7 +277,9 @@ function SciMLBase.DiscreteProblem( eqs = equations(sys) iv = get_iv(sys) - f, u0, p = process_DiscreteProblem( + u0map = to_varmap(u0map, dvs) + u0map = shift_u0map_forward(sys, u0map, defaults(sys)) + f, u0, p = process_SciMLProblem( DiscreteFunction, sys, u0map, parammap; eval_expression, eval_module) u0 = f(u0, p, tspan[1]) DiscreteProblem(f, u0, tspan, p; kwargs...) diff --git a/src/systems/nonlinear/nonlinearsystem.jl b/src/systems/nonlinear/nonlinearsystem.jl index 46bf032d6f..a0cd636753 100644 --- a/src/systems/nonlinear/nonlinearsystem.jl +++ b/src/systems/nonlinear/nonlinearsystem.jl @@ -293,7 +293,7 @@ function SciMLBase.NonlinearFunction(sys::NonlinearSystem, args...; kwargs...) end function SciMLBase.NonlinearFunction{iip}(sys::NonlinearSystem, dvs = unknowns(sys), - ps = parameters(sys), u0 = nothing, p = nothing; + ps = parameters(sys), u0 = nothing; p = nothing, version = nothing, jac = false, eval_expression = false, @@ -408,36 +408,6 @@ function NonlinearFunctionExpr{iip}(sys::NonlinearSystem, dvs = unknowns(sys), !linenumbers ? Base.remove_linenums!(ex) : ex end -function process_NonlinearProblem(constructor, sys::NonlinearSystem, u0map, parammap; - version = nothing, - jac = false, - checkbounds = false, sparse = false, - simplify = false, - linenumbers = true, parallel = SerialForm(), - eval_expression = false, - eval_module = @__MODULE__, - use_union = false, - tofloat = !use_union, - kwargs...) - eqs = equations(sys) - dvs = unknowns(sys) - ps = parameters(sys) - if has_index_cache(sys) && get_index_cache(sys) !== nothing - u0, defs = get_u0(sys, u0map, parammap) - check_eqs_u0(eqs, dvs, u0; kwargs...) - p = MTKParameters(sys, parammap, u0map) - else - u0, p, defs = get_u0_p(sys, u0map, parammap; tofloat, use_union) - check_eqs_u0(eqs, dvs, u0; kwargs...) - end - - f = constructor(sys, dvs, ps, u0, p; jac = jac, checkbounds = checkbounds, - linenumbers = linenumbers, parallel = parallel, simplify = simplify, - sparse = sparse, eval_expression = eval_expression, eval_module = eval_module, - kwargs...) - return f, u0, p -end - """ ```julia DiffEqBase.NonlinearProblem{iip}(sys::NonlinearSystem, u0map, @@ -461,7 +431,7 @@ function DiffEqBase.NonlinearProblem{iip}(sys::NonlinearSystem, u0map, if !iscomplete(sys) error("A completed `NonlinearSystem` is required. Call `complete` or `structural_simplify` on the system before creating a `NonlinearProblem`") end - f, u0, p = process_NonlinearProblem(NonlinearFunction{iip}, sys, u0map, parammap; + f, u0, p = process_SciMLProblem(NonlinearFunction{iip}, sys, u0map, parammap; check_length, kwargs...) pt = something(get_metadata(sys), StandardNonlinearProblem()) NonlinearProblem{iip}(f, u0, p, pt; filter_kwargs(kwargs)...) @@ -490,7 +460,7 @@ function DiffEqBase.NonlinearLeastSquaresProblem{iip}(sys::NonlinearSystem, u0ma if !iscomplete(sys) error("A completed `NonlinearSystem` is required. Call `complete` or `structural_simplify` on the system before creating a `NonlinearLeastSquaresProblem`") end - f, u0, p = process_NonlinearProblem(NonlinearFunction{iip}, sys, u0map, parammap; + f, u0, p = process_SciMLProblem(NonlinearFunction{iip}, sys, u0map, parammap; check_length, kwargs...) pt = something(get_metadata(sys), StandardNonlinearProblem()) NonlinearLeastSquaresProblem{iip}(f, u0, p; filter_kwargs(kwargs)...) @@ -523,7 +493,7 @@ function NonlinearProblemExpr{iip}(sys::NonlinearSystem, u0map, if !iscomplete(sys) error("A completed `NonlinearSystem` is required. Call `complete` or `structural_simplify` on the system before creating a `NonlinearProblemExpr`") end - f, u0, p = process_NonlinearProblem(NonlinearFunctionExpr{iip}, sys, u0map, parammap; + f, u0, p = process_SciMLProblem(NonlinearFunctionExpr{iip}, sys, u0map, parammap; check_length, kwargs...) linenumbers = get(kwargs, :linenumbers, true) @@ -563,7 +533,7 @@ function NonlinearLeastSquaresProblemExpr{iip}(sys::NonlinearSystem, u0map, if !iscomplete(sys) error("A completed `NonlinearSystem` is required. Call `complete` or `structural_simplify` on the system before creating a `NonlinearProblemExpr`") end - f, u0, p = process_NonlinearProblem(NonlinearFunctionExpr{iip}, sys, u0map, parammap; + f, u0, p = process_SciMLProblem(NonlinearFunctionExpr{iip}, sys, u0map, parammap; check_length, kwargs...) linenumbers = get(kwargs, :linenumbers, true) diff --git a/src/systems/problem_utils.jl b/src/systems/problem_utils.jl new file mode 100644 index 0000000000..b5de7b64d6 --- /dev/null +++ b/src/systems/problem_utils.jl @@ -0,0 +1,503 @@ +const AnyDict = Dict{Any, Any} + +""" + $(TYPEDSIGNATURES) + +If called without arguments, return `Dict{Any, Any}`. Otherwise, interpret the input +as a symbolic map and turn it into a `Dict{Any, Any}`. Handles `SciMLBase.NullParameters` +and `nothing`. +""" +anydict() = AnyDict() +anydict(::SciMLBase.NullParameters) = AnyDict() +anydict(::Nothing) = AnyDict() +anydict(x::AnyDict) = x +anydict(x) = AnyDict(x) + +""" + $(TYPEDSIGNATURES) + +Check if `x` is a symbolic with known size. Assumes `Symbolics.shape(unwrap(x))` +is a valid operation. +""" +is_sized_array_symbolic(x) = Symbolics.shape(unwrap(x)) != Symbolics.Unknown() + +""" + $(TYPEDSIGNATURES) + +Check if the system is in split form (has an `IndexCache`). +""" +is_split(sys::AbstractSystem) = has_index_cache(sys) && get_index_cache(sys) !== nothing + +""" + $(TYPEDSIGNATURES) + +Given a variable-value mapping, add mappings for the `toterm` of each of the keys. +""" +function add_toterms!(varmap::AbstractDict; toterm = default_toterm) + for k in collect(keys(varmap)) + varmap[toterm(k)] = varmap[k] + end + return nothing +end + +""" + $(TYPEDSIGNATURES) + +Out-of-place version of [`add_toterms!`](@ref). +""" +function add_toterms(varmap::AbstractDict; toterm = default_toterm) + cp = copy(varmap) + add_toterms!(cp; toterm) + return cp +end + +""" + $(TYPEDSIGNATURES) + +Ensure `varmap` contains entries for all variables in `vars` by using values from +`fallbacks` if they don't already exist in `varmap`. Return the set of all variables in +`vars` not present in `varmap` or `fallbacks`. If an array variable in `vars` does not +exist in `varmap` or `fallbacks`, each of its scalarized elements will be searched for. +In case none of the scalarized elements exist, the array variable will be reported as +missing. In case some of the scalarized elements exist, the missing elements will be +reported as missing. If `fallbacks` contains both the scalarized and non-scalarized forms, +the latter will take priority. + +Variables as they are specified in `vars` will take priority over their `toterm` forms. +""" +function add_fallbacks!( + varmap::AnyDict, vars::Vector, fallbacks::Dict; toterm = default_toterm) + missingvars = Set() + for var in vars + haskey(varmap, var) && continue + ttvar = toterm(var) + haskey(varmap, ttvar) && continue + + # array symbolics with a defined size may be present in the scalarized form + if Symbolics.isarraysymbolic(var) && is_sized_array_symbolic(var) + val = map(eachindex(var)) do idx + # @something is lazy and saves from writing a massive if-elseif-else + @something(get(varmap, var[idx], nothing), + get(varmap, ttvar[idx], nothing), get(fallbacks, var, nothing)[idx], + get(fallbacks, ttvar, nothing)[idx], get(fallbacks, var[idx], nothing), + get(fallbacks, ttvar[idx], nothing), Some(nothing)) + end + # only push the missing entries + mask = map(x -> x === nothing, val) + if all(mask) + push!(missingvars, var) + elseif any(mask) + for i in eachindex(var) + if mask[i] + push!(missingvars, var) + else + varmap[var[i]] = val[i] + end + end + else + varmap[var] = val + end + else + if iscall(var) && operation(var) == getindex + args = arguments(var) + arrvar = args[1] + ttarrvar = toterm(arrvar) + idxs = args[2:end] + val = @something get(varmap, arrvar, nothing) get(varmap, ttarrvar, nothing) get( + fallbacks, arrvar, nothing) get(fallbacks, ttarrvar, nothing) Some(nothing) + if val !== nothing + val = val[idxs...] + end + else + val = nothing + end + val = @something val get(fallbacks, var, nothing) get(fallbacks, ttvar, nothing) Some(nothing) + if val === nothing + push!(missingvars, var) + else + varmap[var] = val + end + end + end + + return missingvars +end + +""" + $(TYPEDSIGNATURES) + +Return the list of variables in `varlist` not present in `varmap`. Uses the same criteria +for missing array variables and `toterm` forms as [`add_fallbacks!`](@ref). +""" +function missingvars( + varmap::AbstractDict, varlist::Vector; toterm = default_toterm) + missingvars = Set() + for var in varlist + haskey(varmap, var) && continue + ttsym = toterm(var) + haskey(varmap, ttsym) && continue + + if Symbolics.isarraysymbolic(var) && is_sized_array_symbolic(var) + mask = map(eachindex(var)) do idx + !haskey(varmap, var[idx]) && !haskey(varmap, ttsym[idx]) + end + if all(mask) + push!(missingvars, var) + else + for i in eachindex(var) + mask[i] && push!(missingvars, var[i]) + end + end + else + push!(missingvars, var) + end + end + return missingvars +end + +""" + $(TYPEDSIGNATURES) + +Attempt to interpret `vals` as a symbolic map of variables in `varlist` to values. Return +the result as a `Dict{Any, Any}`. In case `vals` is already an iterable of pairs, convert +it to a `Dict{Any, Any}` and return. If `vals` is an array (whose `eltype` is not `Pair`) +with the same length as `varlist`, assume the `i`th element of `varlist` is mapped to the +`i`th element of `vals`. Automatically `unwrap`s all keys and values in the mapping. Also +handles `SciMLBase.NullParameters` and `nothing`, both of which are interpreted as empty +maps. +""" +function to_varmap(vals, varlist::Vector) + if vals isa AbstractArray && !(eltype(vals) <: Pair) && !isempty(vals) + check_eqs_u0(varlist, varlist, vals) + vals = vec(varlist) .=> vec(vals) + end + return anydict(unwrap(k) => unwrap(v) for (k, v) in anydict(vals)) +end + +""" + $(TYPEDSIGNATURES) + +Return the appropriate zero value for a symbolic variable representing a number or array of +numbers. Sized array symbolics return a zero-filled array of matching size. Unsized array +symbolics return an empty array of the appropriate `eltype`. +""" +function zero_var(x::Symbolic{T}) where {V <: Number, T <: Union{V, AbstractArray{V}}} + if Symbolics.isarraysymbolic(x) + if is_sized_array_symbolic(x) + return zeros(T, size(x)) + else + return T[] + end + else + return zero(T) + end +end + +""" + $(TYPEDSIGNATURES) + +Add equations `eqs` to `varmap`. Assumes each element in `eqs` maps a single symbolic +variable to an expression representing its value. In case `varmap` already contains an +entry for `eq.lhs`, insert the reverse mapping if `eq.rhs` is not a number. +""" +function add_observed_equations!(varmap::AbstractDict, eqs) + for eq in eqs + if haskey(varmap, eq.lhs) + eq.rhs isa Number && continue + haskey(varmap, eq.rhs) && continue + !iscall(eq.rhs) || issym(operation(eq.rhs)) || continue + varmap[eq.rhs] = eq.lhs + else + varmap[eq.lhs] = eq.rhs + end + end +end + +""" + $(TYPEDSIGNATURES) + +Add all equations in `observed(sys)` to `varmap` using [`add_observed_equations!`](@ref). +""" +function add_observed!(sys::AbstractSystem, varmap::AbstractDict) + add_observed_equations!(varmap, observed(sys)) +end + +""" + $(TYPEDSIGNATURES) + +Add all equations in `parameter_dependencies(sys)` to `varmap` using +[`add_observed_equations!`](@ref). +""" +function add_parameter_dependencies!(sys::AbstractSystem, varmap::AbstractDict) + has_parameter_dependencies(sys) || return nothing + add_observed_equations!(varmap, parameter_dependencies(sys)) +end + +""" + $(TYPEDSIGNATURES) + +Return an array of values where the `i`th element corresponds to the value of `vars[i]` +in `varmap`. Does not perform symbolic substitution in the values of `varmap`. + +Keyword arguments: +- `tofloat`: Convert values to floating point numbers using `float`. +- `use_union`: Use a `Union`-typed array if the values have heterogeneous types. +- `container_type`: The type of container to use for the values. +- `toterm`: The `toterm` method to use for converting symbolics. +- `promotetoconcrete`: whether the promote to a concrete buffer (respecting + `tofloat` and `use_union`). Defaults to `container_type <: AbstractArray`. +- `check`: Error if any variables in `vars` do not have a mapping in `varmap`. Uses + [`missingvars`](@ref) to perform the check. +- `allow_symbolic` allows the returned array to contain symbolic values. If this is `true`, + `promotetoconcrete` is set to `false`. +""" +function better_varmap_to_vars(varmap::AbstractDict, vars::Vector; + tofloat = true, use_union = true, container_type = Array, + toterm = default_toterm, promotetoconcrete = nothing, check = true, allow_symbolic = false) + isempty(vars) && return nothing + + if check + missing_vars = missingvars(varmap, vars; toterm) + isempty(missing_vars) || throw(MissingVariablesError(missing_vars)) + end + vals = map(x -> varmap[x], vars) + + if container_type <: Union{AbstractDict, Tuple, Nothing} + container_type = Array + end + + promotetoconcrete === nothing && (promotetoconcrete = container_type <: AbstractArray) + if promotetoconcrete && !allow_symbolic + vals = promote_to_concrete(vals; tofloat = tofloat, use_union = use_union) + end + + if isempty(vals) + return nothing + elseif container_type <: Tuple + return (vals...,) + else + return SymbolicUtils.Code.create_array(container_type, eltype(vals), Val{1}(), + Val(length(vals)), vals...) + end +end + +""" + $(TYPEDSIGNATURES) + +Performs symbolic substitution on the values in `varmap`, using `varmap` itself as the +set of substitution rules. +""" +function evaluate_varmap!(varmap::AbstractDict) + for (k, v) in varmap + varmap[k] = fixpoint_sub(v, varmap) + end +end + +struct GetUpdatedMTKParameters{G, S} + # `getu` functor which gets parameters that are unknowns during initialization + getpunknowns::G + # `setu` functor which returns a modified MTKParameters using those parameters + setpunknowns::S +end + +function (f::GetUpdatedMTKParameters)(prob, initializesol) + mtkp = copy(parameter_values(prob)) + f.setpunknowns(mtkp, f.getpunknowns(initializesol)) + mtkp +end + +struct UpdateInitializeprob{G, S} + # `getu` functor which gets all values from prob + getvals::G + # `setu` functor which updates initializeprob with values + setvals::S +end + +function (f::UpdateInitializeprob)(initializeprob, prob) + f.setvals(initializeprob, f.getvals(prob)) +end + +function get_temporary_value(p) + stype = symtype(unwrap(p)) + return if stype == Real + zero(Float64) + elseif stype <: AbstractArray{Real} + zeros(Float64, size(p)) + elseif stype <: Real + zero(stype) + elseif stype <: AbstractArray + zeros(eltype(stype), size(p)) + else + error("Nonnumeric parameter $p with symtype $stype cannot be solved for during initialization") + end +end + +""" + $(TYPEDSIGNATURES) + +Return the SciMLFunction created via calling `constructor`, the initial conditions `u0` +and parameter object `p` given the system `sys`, and user-provided initial values `u0map` +and `pmap`. `u0map` and `pmap` are converted into variable maps via [`to_varmap`](@ref). + +The order of unknowns is determined by `unknowns(sys)`. If the system is split +[`is_split`](@ref) create an [`MTKParameters`](@ref) object. Otherwise, a parameter vector. +Initial values provided in terms of other variables will be symbolically evaluated using +[`evaluate_varmap!`](@ref). The type of `u0map` and `pmap` will be used to determine the +type of the containers (if parameters are not in an `MTKParameters` object). `Dict`s will be +turned into `Array`s. + +If `sys isa ODESystem`, this will also build the initialization problem and related objects +and pass them to the SciMLFunction as keyword arguments. + +Keyword arguments: +- `build_initializeprob`: If `false`, avoids building the initialization problem for an + `ODESystem`. +- `t`: The initial time of the `ODEProblem`. If this is not provided, the initialization + problem cannot be built. +- `implicit_dae`: Also build a mapping of derivatives of states to values for implicit DAEs, + using `du0map`. Changes the return value of this function to `(f, du0, u0, p)` instead of + `(f, u0, p)`. +- `guesses`: The guesses for variables in the system, used as initial values for the + initialization problem. +- `warn_initialize_determined`: Warn if the initialization system is under/over-determined. +- `initialization_eqs`: Extra equations to use in the initialization problem. +- `eval_expression`: Whether to compile any functions via `eval` or `RuntimeGeneratedFunctions`. +- `eval_module`: If `eval_expression == true`, the module to `eval` into. Otherwise, the module + in which to generate the `RuntimeGeneratedFunction`. +- `fully_determined`: Override whether the initialization system is fully determined. +- `check_units`: Enable or disable unit checks. +- `tofloat`, `use_union`: Passed to [`better_varmap_to_vars`](@ref) for building `u0` (and + possibly `p`). +- `u0_constructor`: A function to apply to the `u0` value returned from `better_varmap_to_vars` + to construct the final `u0` value. +- `du0map`: A map of derivatives to values. See `implicit_dae`. +- `check_length`: Whether to check the number of equations along with number of unknowns and + length of `u0` vector for consistency. If `false`, do not check with equations. This is + forwarded to `check_eqs_u0` +- `symbolic_u0` allows the returned `u0` to be an array of symbolics. + +All other keyword arguments are passed as-is to `constructor`. +""" +function process_SciMLProblem( + constructor, sys::AbstractSystem, u0map, pmap; build_initializeprob = true, + implicit_dae = false, t = nothing, guesses = AnyDict(), + warn_initialize_determined = true, initialization_eqs = [], + eval_expression = false, eval_module = @__MODULE__, fully_determined = false, + check_units = true, tofloat = true, use_union = false, + u0_constructor = identity, du0map = nothing, check_length = true, symbolic_u0 = false, kwargs...) + dvs = unknowns(sys) + ps = parameters(sys) + iv = has_iv(sys) ? get_iv(sys) : nothing + eqs = equations(sys) + + check_array_equations_unknowns(eqs, dvs) + + u0Type = typeof(u0map) + pType = typeof(pmap) + _u0map = u0map + u0map = to_varmap(u0map, dvs) + _pmap = pmap + pmap = to_varmap(pmap, ps) + defs = add_toterms(defaults(sys)) + cmap, cs = get_cmap(sys) + kwargs = NamedTuple(kwargs) + + op = add_toterms(u0map) + missing_unknowns = add_fallbacks!(op, dvs, defs) + for (k, v) in defs + haskey(op, k) && continue + op[k] = v + end + merge!(op, pmap) + missing_pars = add_fallbacks!(op, ps, defs) + for eq in cmap + op[eq.lhs] = eq.rhs + end + if sys isa ODESystem + guesses = merge(ModelingToolkit.guesses(sys), todict(guesses)) + has_observed_u0s = any( + k -> has_observed_with_lhs(sys, k) || has_parameter_dependency_with_lhs(sys, k), + keys(op)) + solvablepars = [p + for p in parameters(sys) + if is_parameter_solvable(p, pmap, defs, guesses)] + if build_initializeprob && + (((implicit_dae || has_observed_u0s || !isempty(missing_unknowns) || + !isempty(solvablepars)) && + get_tearing_state(sys) !== nothing) || + !isempty(initialization_equations(sys))) && t !== nothing + initializeprob = ModelingToolkit.InitializationProblem( + sys, t, u0map, pmap; guesses, warn_initialize_determined, + initialization_eqs, eval_expression, eval_module, fully_determined, check_units) + initializeprobmap = getu(initializeprob, unknowns(sys)) + + punknowns = [p + for p in all_variable_symbols(initializeprob) + if is_parameter(sys, p)] + getpunknowns = getu(initializeprob, punknowns) + setpunknowns = setp(sys, punknowns) + initializeprobpmap = GetUpdatedMTKParameters(getpunknowns, setpunknowns) + + reqd_syms = parameter_symbols(initializeprob) + update_initializeprob! = UpdateInitializeprob( + getu(sys, reqd_syms), setu(initializeprob, reqd_syms)) + for p in punknowns + p = unwrap(p) + stype = symtype(p) + op[p] = get_temporary_value(p) + delete!(missing_pars, p) + end + + for v in missing_unknowns + op[v] = zero_var(v) + end + empty!(missing_unknowns) + kwargs = merge(kwargs, + (; initializeprob, initializeprobmap, + initializeprobpmap, update_initializeprob!)) + end + end + + if t !== nothing && !(constructor <: Union{DDEFunction, SDDEFunction}) + op[iv] = t + end + + add_observed!(sys, op) + add_parameter_dependencies!(sys, op) + + evaluate_varmap!(op) + + u0 = better_varmap_to_vars( + op, dvs; tofloat = true, use_union = false, + container_type = u0Type, allow_symbolic = symbolic_u0) + + if u0 !== nothing + u0 = u0_constructor(u0) + end + + check_eqs_u0(eqs, dvs, u0; check_length, kwargs...) + + if is_split(sys) + p = MTKParameters(sys, op) + else + p = better_varmap_to_vars(op, ps; tofloat, use_union, container_type = pType) + end + + if implicit_dae && du0map !== nothing + ddvs = map(Differential(iv), dvs) + du0map = to_varmap(du0map, ddvs) + merge!(op, du0map) + + du0 = varmap_to_vars(du0map, ddvs; toterm = identity, + tofloat = true) + kwargs = merge(kwargs, (; ddvs)) + else + du0 = nothing + end + + f = constructor(sys, dvs, ps, u0; p = p, + eval_expression = eval_expression, + eval_module = eval_module, + kwargs...) + implicit_dae ? (f, du0, u0, p) : (f, u0, p) +end diff --git a/src/variables.jl b/src/variables.jl index 1c22540c63..c0c875450c 100644 --- a/src/variables.jl +++ b/src/variables.jl @@ -140,7 +140,7 @@ function varmap_to_vars(varmap, varlist; defaults = Dict(), check = true, if is_incomplete_initialization || isempty(varmap) if isempty(defaults) if !is_incomplete_initialization && check - isempty(varlist) || throw_missingvars(varlist) + isempty(varlist) || throw(MissingVariablesError(varlist)) end return nothing else diff --git a/test/initializationsystem.jl b/test/initializationsystem.jl index 08b653c22e..e819980752 100644 --- a/test/initializationsystem.jl +++ b/test/initializationsystem.jl @@ -487,7 +487,8 @@ sys = extend(sysx, sysy) @variables x(t) y(t) @named sys = ODESystem([x^2 + y^2 ~ 25, D(x) ~ 1], t) ssys = structural_simplify(sys) - @test_throws ArgumentError ODEProblem(ssys, [x => 3], (0, 1), []) # y should have a guess + @test_throws ModelingToolkit.MissingVariablesError ODEProblem( + ssys, [x => 3], (0, 1), []) # y should have a guess end # https://github.com/SciML/ModelingToolkit.jl/issues/3025 diff --git a/test/split_parameters.jl b/test/split_parameters.jl index b8651238ea..0c317cb154 100644 --- a/test/split_parameters.jl +++ b/test/split_parameters.jl @@ -125,7 +125,7 @@ sol = solve(prob, ImplicitEuler()); prob = ODEProblem( sys, [], tspan, []; tofloat = false, use_union = true, build_initializeprob = false) -@test prob.p isa Tuple{Vector{Float64}, Vector{Int64}} +@test prob.p isa Union{Float64, Int64} sol = solve(prob, ImplicitEuler()); @test sol.retcode == ReturnCode.Success From 3c43431e6f2688e8c87bbe59ce86adb2a1bb7cb5 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 2 Oct 2024 16:17:07 +0530 Subject: [PATCH 0142/1337] refactor: move `get_u0_p` and `get_u0` to `problem_utils.jl` --- src/systems/diffeqs/abstractodesystem.jl | 91 ----------------------- src/systems/problem_utils.jl | 95 ++++++++++++++++++++++++ 2 files changed, 95 insertions(+), 91 deletions(-) diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index 48770e7a52..e0d4c72f2b 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -702,97 +702,6 @@ function ODEFunctionExpr{iip}(sys::AbstractODESystem, dvs = unknowns(sys), !linenumbers ? Base.remove_linenums!(ex) : ex end -""" - u0, p, defs = get_u0_p(sys, u0map, parammap; use_union=true, tofloat=true) - -Take dictionaries with initial conditions and parameters and convert them to numeric arrays `u0` and `p`. Also return the merged dictionary `defs` containing the entire operating point. -""" -function get_u0_p(sys, - u0map, - parammap = nothing; - t0 = nothing, - use_union = true, - tofloat = true, - symbolic_u0 = false) - dvs = unknowns(sys) - ps = parameters(sys) - - defs = defaults(sys) - if t0 !== nothing - defs[get_iv(sys)] = t0 - end - if parammap !== nothing - defs = mergedefaults(defs, parammap, ps) - end - if u0map isa Vector && eltype(u0map) <: Pair - u0map = Dict(u0map) - end - if u0map isa Dict - allobs = Set(getproperty.(observed(sys), :lhs)) - if any(in(allobs), keys(u0map)) - u0s_in_obs = filter(in(allobs), keys(u0map)) - @warn "Observed variables cannot be assigned initial values. Initial values for $u0s_in_obs will be ignored." - end - end - obs = filter!(x -> !(x[1] isa Number), map(x -> x.rhs => x.lhs, observed(sys))) - observedmap = isempty(obs) ? Dict() : todict(obs) - defs = mergedefaults(defs, observedmap, u0map, dvs) - for (k, v) in defs - if Symbolics.isarraysymbolic(k) - ks = scalarize(k) - length(ks) == length(v) || error("$k has default value $v with unmatched size") - for (kk, vv) in zip(ks, v) - if !haskey(defs, kk) - defs[kk] = vv - end - end - end - end - - if symbolic_u0 - u0 = varmap_to_vars(u0map, dvs; defaults = defs, tofloat = false, use_union = false) - else - u0 = varmap_to_vars(u0map, dvs; defaults = defs, tofloat = true, use_union) - end - p = varmap_to_vars(parammap, ps; defaults = defs, tofloat, use_union) - p = p === nothing ? SciMLBase.NullParameters() : p - t0 !== nothing && delete!(defs, get_iv(sys)) - u0, p, defs -end - -function get_u0( - sys, u0map, parammap = nothing; symbolic_u0 = false, - toterm = default_toterm, t0 = nothing, use_union = true) - dvs = unknowns(sys) - ps = parameters(sys) - defs = defaults(sys) - if t0 !== nothing - defs[get_iv(sys)] = t0 - end - if parammap !== nothing - defs = mergedefaults(defs, parammap, ps) - end - - # Convert observed equations "lhs ~ rhs" into defaults. - # Use the order "lhs => rhs" by default, but flip it to "rhs => lhs" - # if "lhs" is known by other means (parameter, another default, ...) - # TODO: Is there a better way to determine which equations to flip? - obs = map(x -> x.lhs => x.rhs, observed(sys)) - obs = map(x -> x[1] in keys(defs) ? reverse(x) : x, obs) - obs = filter!(x -> !(x[1] isa Number), obs) # exclude e.g. "0 => x^2 + y^2 - 25" - obsmap = isempty(obs) ? Dict() : todict(obs) - - defs = mergedefaults(defs, obsmap, u0map, dvs) - if symbolic_u0 - u0 = varmap_to_vars( - u0map, dvs; defaults = defs, tofloat = false, use_union = false, toterm) - else - u0 = varmap_to_vars(u0map, dvs; defaults = defs, tofloat = true, use_union, toterm) - end - t0 !== nothing && delete!(defs, get_iv(sys)) - return u0, defs -end - function ODEFunctionExpr(sys::AbstractODESystem, args...; kwargs...) ODEFunctionExpr{true}(sys, args...; kwargs...) end diff --git a/src/systems/problem_utils.jl b/src/systems/problem_utils.jl index b5de7b64d6..2b894eb7ee 100644 --- a/src/systems/problem_utils.jl +++ b/src/systems/problem_utils.jl @@ -501,3 +501,98 @@ function process_SciMLProblem( kwargs...) implicit_dae ? (f, du0, u0, p) : (f, u0, p) end + +############## +# Legacy functions for backward compatibility +############## + +""" + u0, p, defs = get_u0_p(sys, u0map, parammap; use_union=true, tofloat=true) + +Take dictionaries with initial conditions and parameters and convert them to numeric arrays `u0` and `p`. Also return the merged dictionary `defs` containing the entire operating point. +""" +function get_u0_p(sys, + u0map, + parammap = nothing; + t0 = nothing, + use_union = true, + tofloat = true, + symbolic_u0 = false) + dvs = unknowns(sys) + ps = parameters(sys) + + defs = defaults(sys) + if t0 !== nothing + defs[get_iv(sys)] = t0 + end + if parammap !== nothing + defs = mergedefaults(defs, parammap, ps) + end + if u0map isa Vector && eltype(u0map) <: Pair + u0map = Dict(u0map) + end + if u0map isa Dict + allobs = Set(getproperty.(observed(sys), :lhs)) + if any(in(allobs), keys(u0map)) + u0s_in_obs = filter(in(allobs), keys(u0map)) + @warn "Observed variables cannot be assigned initial values. Initial values for $u0s_in_obs will be ignored." + end + end + obs = filter!(x -> !(x[1] isa Number), map(x -> x.rhs => x.lhs, observed(sys))) + observedmap = isempty(obs) ? Dict() : todict(obs) + defs = mergedefaults(defs, observedmap, u0map, dvs) + for (k, v) in defs + if Symbolics.isarraysymbolic(k) + ks = scalarize(k) + length(ks) == length(v) || error("$k has default value $v with unmatched size") + for (kk, vv) in zip(ks, v) + if !haskey(defs, kk) + defs[kk] = vv + end + end + end + end + + if symbolic_u0 + u0 = varmap_to_vars(u0map, dvs; defaults = defs, tofloat = false, use_union = false) + else + u0 = varmap_to_vars(u0map, dvs; defaults = defs, tofloat = true, use_union) + end + p = varmap_to_vars(parammap, ps; defaults = defs, tofloat, use_union) + p = p === nothing ? SciMLBase.NullParameters() : p + t0 !== nothing && delete!(defs, get_iv(sys)) + u0, p, defs +end + +function get_u0( + sys, u0map, parammap = nothing; symbolic_u0 = false, + toterm = default_toterm, t0 = nothing, use_union = true) + dvs = unknowns(sys) + ps = parameters(sys) + defs = defaults(sys) + if t0 !== nothing + defs[get_iv(sys)] = t0 + end + if parammap !== nothing + defs = mergedefaults(defs, parammap, ps) + end + + # Convert observed equations "lhs ~ rhs" into defaults. + # Use the order "lhs => rhs" by default, but flip it to "rhs => lhs" + # if "lhs" is known by other means (parameter, another default, ...) + # TODO: Is there a better way to determine which equations to flip? + obs = map(x -> x.lhs => x.rhs, observed(sys)) + obs = map(x -> x[1] in keys(defs) ? reverse(x) : x, obs) + obs = filter!(x -> !(x[1] isa Number), obs) # exclude e.g. "0 => x^2 + y^2 - 25" + obsmap = isempty(obs) ? Dict() : todict(obs) + + defs = mergedefaults(defs, obsmap, u0map, dvs) + if symbolic_u0 + u0 = varmap_to_vars( + u0map, dvs; defaults = defs, tofloat = false, use_union = false, toterm) + else + u0 = varmap_to_vars(u0map, dvs; defaults = defs, tofloat = true, use_union, toterm) + end + t0 !== nothing && delete!(defs, get_iv(sys)) + return u0, defs +end From b0f2132ea2b0eccff1a7b8335a86204d48703220 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 14 Oct 2024 20:32:54 +0530 Subject: [PATCH 0143/1337] fix: handle array dummy derivatives in generate_initializesystem --- src/systems/nonlinear/initializesystem.jl | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/systems/nonlinear/initializesystem.jl b/src/systems/nonlinear/initializesystem.jl index 38a3095b5c..99e4e19d09 100644 --- a/src/systems/nonlinear/initializesystem.jl +++ b/src/systems/nonlinear/initializesystem.jl @@ -37,7 +37,7 @@ function generate_initializesystem(sys::ODESystem; # set dummy derivatives to default_dd_guess unless specified push!(defs, x[1] => get(guesses, x[1], default_dd_guess)) end - for (y, x) in u0map + function process_u0map_with_dummysubs(y, x) y = get(schedule.dummy_sub, y, y) y = fixpoint_sub(y, diffmap) if y ∈ vars_set @@ -53,6 +53,13 @@ function generate_initializesystem(sys::ODESystem; error("Initialization expression $y is currently not supported. If its a higher order derivative expression, then only the dummy derivative expressions are supported.") end end + for (y, x) in u0map + if Symbolics.isarraysymbolic(y) + process_u0map_with_dummysubs.(collect(y), collect(x)) + else + process_u0map_with_dummysubs(y, x) + end + end end # 2) process other variables From 49bcc8263586997bd3083321ad5a5e9112666a2e Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 14 Oct 2024 20:40:32 +0530 Subject: [PATCH 0144/1337] test: test initial values dependent on constants --- test/constants.jl | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/test/constants.jl b/test/constants.jl index bfdc83bafc..f2c4fdaa86 100644 --- a/test/constants.jl +++ b/test/constants.jl @@ -37,3 +37,16 @@ eqs = [D(x) ~ β] simp = structural_simplify(sys) @test isempty(MT.collect_constants(nothing)) + +@testset "Issue#3044" begin + @constants h = 1 + @parameters τ = 0.5 * h + @variables x(MT.t_nounits) = h + eqs = [MT.D_nounits(x) ~ (h - x) / τ] + + @mtkbuild fol_model = ODESystem(eqs, MT.t_nounits) + + prob = ODEProblem(fol_model, [], (0.0, 10.0)) + @test prob[x] ≈ 1 + @test prob.ps[τ] ≈ 0.5 +end From e67763ace865ac33b562086178bb361a40a501d6 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 15 Oct 2024 16:55:43 +0530 Subject: [PATCH 0145/1337] test: remove test for creating incomplete `SDEProblem` --- test/sdesystem.jl | 4 ---- 1 file changed, 4 deletions(-) diff --git a/test/sdesystem.jl b/test/sdesystem.jl index cae9ec9376..c258a4142b 100644 --- a/test/sdesystem.jl +++ b/test/sdesystem.jl @@ -36,10 +36,6 @@ solexpr = solve(eval(probexpr), SRIW1(), seed = 1) @test all(x -> x == 0, Array(sol - solexpr)) -# Test no error -@test_nowarn SDEProblem(de, nothing, (0, 10.0)) -@test SDEProblem(de, nothing).tspan == (0.0, 10.0) - noiseeqs_nd = [0.01*x 0.01*x*y 0.02*x*z σ 0.01*y 0.02*x*z ρ β 0.01*z] From 03e294f46c5fd7bca6ea9175ac57d54d96d1dc92 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 15 Oct 2024 16:55:57 +0530 Subject: [PATCH 0146/1337] test: refactor test for old parameter splitting --- test/split_parameters.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/split_parameters.jl b/test/split_parameters.jl index 0c317cb154..22c90edf7a 100644 --- a/test/split_parameters.jl +++ b/test/split_parameters.jl @@ -125,7 +125,7 @@ sol = solve(prob, ImplicitEuler()); prob = ODEProblem( sys, [], tspan, []; tofloat = false, use_union = true, build_initializeprob = false) -@test prob.p isa Union{Float64, Int64} +@test prob.p isa Vector{Union{Float64, Int64}} sol = solve(prob, ImplicitEuler()); @test sol.retcode == ReturnCode.Success From c69d28e0d829e0f9a21a56ea8c2364c052f0a367 Mon Sep 17 00:00:00 2001 From: Sam Isaacson Date: Tue, 15 Oct 2024 10:59:51 -0400 Subject: [PATCH 0147/1337] add collect_vars! equation dispatch --- src/systems/diffeqs/odesystem.jl | 9 +++---- .../discrete_system/discrete_system.jl | 9 +++---- src/systems/nonlinear/nonlinearsystem.jl | 9 +++---- src/utils.jl | 24 +++++++++++++------ 4 files changed, 26 insertions(+), 25 deletions(-) diff --git a/src/systems/diffeqs/odesystem.jl b/src/systems/diffeqs/odesystem.jl index 68ea0b48e3..d1cd01ce7b 100644 --- a/src/systems/diffeqs/odesystem.jl +++ b/src/systems/diffeqs/odesystem.jl @@ -319,8 +319,7 @@ function ODESystem(eqs, iv; kwargs...) compressed_eqs = Equation[] # equations that need to be expanded later, like `connect(a, b)` for eq in eqs eq.lhs isa Union{Symbolic, Number} || (push!(compressed_eqs, eq); continue) - collect_vars!(allunknowns, ps, eq.lhs, iv) - collect_vars!(allunknowns, ps, eq.rhs, iv) + collect_vars!(allunknowns, ps, eq, iv) if isdiffeq(eq) diffvar, _ = var_from_nested_derivative(eq.lhs) if check_scope_depth(getmetadata(diffvar, SymScope, LocalScope()), 0) @@ -337,11 +336,9 @@ function ODESystem(eqs, iv; kwargs...) end for eq in get(kwargs, :parameter_dependencies, Equation[]) if eq isa Pair - collect_vars!(allunknowns, ps, eq[1], iv) - collect_vars!(allunknowns, ps, eq[2], iv) + collect_vars!(allunknowns, ps, eq, iv) else - collect_vars!(allunknowns, ps, eq.lhs, iv) - collect_vars!(allunknowns, ps, eq.rhs, iv) + collect_vars!(allunknowns, ps, eq, iv) end end for ssys in get(kwargs, :systems, ODESystem[]) diff --git a/src/systems/discrete_system/discrete_system.jl b/src/systems/discrete_system/discrete_system.jl index 7103cfca80..99f76a8ce9 100644 --- a/src/systems/discrete_system/discrete_system.jl +++ b/src/systems/discrete_system/discrete_system.jl @@ -175,8 +175,7 @@ function DiscreteSystem(eqs, iv; kwargs...) ps = OrderedSet() iv = value(iv) for eq in eqs - collect_vars!(allunknowns, ps, eq.lhs, iv; op = Shift) - collect_vars!(allunknowns, ps, eq.rhs, iv; op = Shift) + collect_vars!(allunknowns, ps, eq, iv; op = Shift) if iscall(eq.lhs) && operation(eq.lhs) isa Shift isequal(iv, operation(eq.lhs).t) || throw(ArgumentError("A DiscreteSystem can only have one independent variable.")) @@ -187,11 +186,9 @@ function DiscreteSystem(eqs, iv; kwargs...) end for eq in get(kwargs, :parameter_dependencies, Equation[]) if eq isa Pair - collect_vars!(allunknowns, ps, eq[1], iv) - collect_vars!(allunknowns, ps, eq[2], iv) + collect_vars!(allunknowns, ps, eq, iv) else - collect_vars!(allunknowns, ps, eq.lhs, iv) - collect_vars!(allunknowns, ps, eq.rhs, iv) + collect_vars!(allunknowns, ps, eq, iv) end end new_ps = OrderedSet() diff --git a/src/systems/nonlinear/nonlinearsystem.jl b/src/systems/nonlinear/nonlinearsystem.jl index 46bf032d6f..c649b9b287 100644 --- a/src/systems/nonlinear/nonlinearsystem.jl +++ b/src/systems/nonlinear/nonlinearsystem.jl @@ -166,16 +166,13 @@ function NonlinearSystem(eqs; kwargs...) allunknowns = OrderedSet() ps = OrderedSet() for eq in eqs - collect_vars!(allunknowns, ps, eq.lhs, nothing) - collect_vars!(allunknowns, ps, eq.rhs, nothing) + collect_vars!(allunknowns, ps, eq, nothing) end for eq in get(kwargs, :parameter_dependencies, Equation[]) if eq isa Pair - collect_vars!(allunknowns, ps, eq[1], nothing) - collect_vars!(allunknowns, ps, eq[2], nothing) + collect_vars!(allunknowns, ps, eq, nothing) else - collect_vars!(allunknowns, ps, eq.lhs, nothing) - collect_vars!(allunknowns, ps, eq.rhs, nothing) + collect_vars!(allunknowns, ps, eq, nothing) end end new_ps = OrderedSet() diff --git a/src/utils.jl b/src/utils.jl index e8ed131d78..62445d5814 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -493,19 +493,16 @@ function collect_scoped_vars!(unknowns, parameters, sys, iv; depth = 1, op = Dif if has_eqs(sys) for eq in get_eqs(sys) eq isa Equation || continue - eq.lhs isa Union{Symbolic, Number} || continue - collect_vars!(unknowns, parameters, eq.lhs, iv; depth, op) - collect_vars!(unknowns, parameters, eq.rhs, iv; depth, op) + (eq isa Equation && eq.lhs isa Union{Symbolic, Number}) || continue + collect_vars!(unknowns, parameters, eq, iv; depth, op) end end if has_parameter_dependencies(sys) for eq in get_parameter_dependencies(sys) if eq isa Pair - collect_vars!(unknowns, parameters, eq[1], iv; depth, op) - collect_vars!(unknowns, parameters, eq[2], iv; depth, op) + collect_vars!(unknowns, parameters, eq, iv; depth, op) else - collect_vars!(unknowns, parameters, eq.lhs, iv; depth, op) - collect_vars!(unknowns, parameters, eq.rhs, iv; depth, op) + collect_vars!(unknowns, parameters, eq, iv; depth, op) end end end @@ -529,6 +526,19 @@ function collect_vars!(unknowns, parameters, expr, iv; depth = 0, op = Different return nothing end +function collect_vars!(unknowns, parameters, eq::Equation, iv; + depth = 0, op = Differential) + collect_vars!(unknowns, parameters, eq.lhs, iv; depth, op) + collect_vars!(unknowns, parameters, eq.rhs, iv; depth, op) + return nothing +end + +function collect_vars!(unknowns, parameters, p::Pair, iv; depth = 0, op = Differential) + collect_vars!(unknowns, parameters, p[1], iv; depth, op) + collect_vars!(unknowns, parameters, p[2], iv; depth, op) + return nothing +end + function collect_var!(unknowns, parameters, var, iv; depth = 0) isequal(var, iv) && return nothing check_scope_depth(getmetadata(var, SymScope, LocalScope()), depth) || return nothing From dbe211d80c4008726329d15ed029c8f21d4ec45e Mon Sep 17 00:00:00 2001 From: Sam Isaacson Date: Tue, 15 Oct 2024 11:07:17 -0400 Subject: [PATCH 0148/1337] add collect_vars equation dispatch --- src/utils.jl | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/utils.jl b/src/utils.jl index 62445d5814..1eaa2fa8eb 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -492,7 +492,7 @@ recursively searches through all subsystems of `sys`, increasing the depth if it function collect_scoped_vars!(unknowns, parameters, sys, iv; depth = 1, op = Differential) if has_eqs(sys) for eq in get_eqs(sys) - eq isa Equation || continue + eqtype_supports_collect_vars(eq) || continue (eq isa Equation && eq.lhs isa Union{Symbolic, Number}) || continue collect_vars!(unknowns, parameters, eq, iv; depth, op) end @@ -526,6 +526,16 @@ function collect_vars!(unknowns, parameters, expr, iv; depth = 0, op = Different return nothing end +""" + $(TYPEDSIGNATURES) + +Indicate whether the given equation type (Equation, Pair, etc) supports `collect_vars!`. Can +be dispatched by higher-level libraries to indicate support. +""" +eqtype_supports_collect_vars(eq) = false +eqtype_supports_collect_vars(eq::Equation) = true +eqtype_supports_collect_vars(eq::Pair) = true + function collect_vars!(unknowns, parameters, eq::Equation, iv; depth = 0, op = Differential) collect_vars!(unknowns, parameters, eq.lhs, iv; depth, op) From 1209b1e0fd0d9678520a5ee2c4a2b605ecd8179a Mon Sep 17 00:00:00 2001 From: Sam Isaacson Date: Tue, 15 Oct 2024 11:14:37 -0400 Subject: [PATCH 0149/1337] comment tweak --- src/utils.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utils.jl b/src/utils.jl index 1eaa2fa8eb..5eb411a1e5 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -529,8 +529,8 @@ end """ $(TYPEDSIGNATURES) -Indicate whether the given equation type (Equation, Pair, etc) supports `collect_vars!`. Can -be dispatched by higher-level libraries to indicate support. +Indicate whether the given equation type (Equation, Pair, etc) supports `collect_vars!`. +Can be dispatched by higher-level libraries to indicate support. """ eqtype_supports_collect_vars(eq) = false eqtype_supports_collect_vars(eq::Equation) = true From 9cd326bc9b64654a2b7a39f73dedde1d259b896c Mon Sep 17 00:00:00 2001 From: Sam Isaacson Date: Tue, 15 Oct 2024 11:19:44 -0400 Subject: [PATCH 0150/1337] format --- src/utils.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils.jl b/src/utils.jl index 5eb411a1e5..1072c37353 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -536,7 +536,7 @@ eqtype_supports_collect_vars(eq) = false eqtype_supports_collect_vars(eq::Equation) = true eqtype_supports_collect_vars(eq::Pair) = true -function collect_vars!(unknowns, parameters, eq::Equation, iv; +function collect_vars!(unknowns, parameters, eq::Equation, iv; depth = 0, op = Differential) collect_vars!(unknowns, parameters, eq.lhs, iv; depth, op) collect_vars!(unknowns, parameters, eq.rhs, iv; depth, op) From 557e20d194af7970142e98951887bcbf711cbb8b Mon Sep 17 00:00:00 2001 From: Sam Isaacson Date: Tue, 15 Oct 2024 11:24:11 -0400 Subject: [PATCH 0151/1337] fix check --- src/utils.jl | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/utils.jl b/src/utils.jl index 1072c37353..67074246b9 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -493,7 +493,9 @@ function collect_scoped_vars!(unknowns, parameters, sys, iv; depth = 1, op = Dif if has_eqs(sys) for eq in get_eqs(sys) eqtype_supports_collect_vars(eq) || continue - (eq isa Equation && eq.lhs isa Union{Symbolic, Number}) || continue + if eq isa Equation + eq.lhs isa Union{Symbolic, Number} || continue + end collect_vars!(unknowns, parameters, eq, iv; depth, op) end end From a8c09306c5bb4a2198a6936d461d2b4cc157faaa Mon Sep 17 00:00:00 2001 From: Sam Isaacson Date: Tue, 15 Oct 2024 12:15:25 -0400 Subject: [PATCH 0152/1337] format --- src/utils.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils.jl b/src/utils.jl index 67074246b9..c13d8a480a 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -493,7 +493,7 @@ function collect_scoped_vars!(unknowns, parameters, sys, iv; depth = 1, op = Dif if has_eqs(sys) for eq in get_eqs(sys) eqtype_supports_collect_vars(eq) || continue - if eq isa Equation + if eq isa Equation eq.lhs isa Union{Symbolic, Number} || continue end collect_vars!(unknowns, parameters, eq, iv; depth, op) From 18617272fab3c7c022870c7d0a077124b96471bc Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 16 Oct 2024 11:25:41 +0530 Subject: [PATCH 0153/1337] refactor: use `process_SciMLProblem` in `jumpsystem.jl` --- src/systems/abstractsystem.jl | 2 +- src/systems/jumps/jumpsystem.jl | 41 +++++---------------------------- src/systems/problem_utils.jl | 12 ++++++++++ src/utils.jl | 11 ++++++++- 4 files changed, 29 insertions(+), 37 deletions(-) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index efec8b7fd8..b6e2a79c3e 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -2914,7 +2914,7 @@ function Base.eltype(::Type{<:TreeIterator{ModelingToolkit.AbstractSystem}}) end function check_array_equations_unknowns(eqs, dvs) - if any(eq -> Symbolics.isarraysymbolic(eq.lhs), eqs) + if any(eq -> eq isa Equation && Symbolics.isarraysymbolic(eq.lhs), eqs) throw(ArgumentError("The system has array equations. Call `structural_simplify` to handle such equations or scalarize them manually.")) end if any(x -> Symbolics.isarraysymbolic(x), dvs) diff --git a/src/systems/jumps/jumpsystem.jl b/src/systems/jumps/jumpsystem.jl index 60b061b1da..a714d4b364 100644 --- a/src/systems/jumps/jumpsystem.jl +++ b/src/systems/jumps/jumpsystem.jl @@ -348,20 +348,8 @@ function DiffEqBase.DiscreteProblem(sys::JumpSystem, u0map, tspan::Union{Tuple, if !iscomplete(sys) error("A completed `JumpSystem` is required. Call `complete` or `structural_simplify` on the system before creating a `DiscreteProblem`") end - dvs = unknowns(sys) - ps = parameters(sys) - - defs = defaults(sys) - defs = mergedefaults(defs, parammap, ps) - defs = mergedefaults(defs, u0map, dvs) - - u0 = varmap_to_vars(u0map, dvs; defaults = defs, tofloat = false) - if has_index_cache(sys) && get_index_cache(sys) !== nothing - p = MTKParameters(sys, parammap, u0map) - else - p = varmap_to_vars(parammap, ps; defaults = defs, tofloat = false, use_union) - end - + _, u0, p = process_SciMLProblem(EmptySciMLFunction, sys, u0map, parammap; + t = tspan === nothing ? nothing : tspan[1], use_union, tofloat = false, check_length = false) f = DiffEqBase.DISCRETE_INPLACE_DEFAULT observedfun = ObservedFunctionCache(sys; eval_expression, eval_module) @@ -399,16 +387,9 @@ function DiscreteProblemExpr{iip}(sys::JumpSystem, u0map, tspan::Union{Tuple, No if !iscomplete(sys) error("A completed `JumpSystem` is required. Call `complete` or `structural_simplify` on the system before creating a `DiscreteProblemExpr`") end - dvs = unknowns(sys) - ps = parameters(sys) - defs = defaults(sys) - u0 = varmap_to_vars(u0map, dvs; defaults = defs, tofloat = false) - if has_index_cache(sys) && get_index_cache(sys) !== nothing - p = MTKParameters(sys, parammap, u0map) - else - p = varmap_to_vars(parammap, ps; defaults = defs, tofloat = false, use_union) - end + _, u0, p = process_SciMLProblem(EmptySciMLFunction, sys, u0map, parammap; + t = tspan === nothing ? nothing : tspan[1], use_union, tofloat = false, check_length = false) # identity function to make syms works quote f = DiffEqBase.DISCRETE_INPLACE_DEFAULT @@ -454,19 +435,9 @@ function DiffEqBase.ODEProblem(sys::JumpSystem, u0map, tspan::Union{Tuple, Nothi if !iscomplete(sys) error("A completed `JumpSystem` is required. Call `complete` or `structural_simplify` on the system before creating a `DiscreteProblem`") end - dvs = unknowns(sys) - ps = parameters(sys) - - defs = defaults(sys) - defs = mergedefaults(defs, parammap, ps) - defs = mergedefaults(defs, u0map, dvs) - u0 = varmap_to_vars(u0map, dvs; defaults = defs, tofloat = false) - if has_index_cache(sys) && get_index_cache(sys) !== nothing - p = MTKParameters(sys, parammap, u0map) - else - p = varmap_to_vars(parammap, ps; defaults = defs, tofloat = false, use_union) - end + _, u0, p = process_SciMLProblem(EmptySciMLFunction, sys, u0map, parammap; + t = tspan === nothing ? nothing : tspan[1], use_union, tofloat = false, check_length = false) observedfun = ObservedFunctionCache(sys; eval_expression, eval_module) diff --git a/src/systems/problem_utils.jl b/src/systems/problem_utils.jl index 2b894eb7ee..e530e62eed 100644 --- a/src/systems/problem_utils.jl +++ b/src/systems/problem_utils.jl @@ -332,6 +332,18 @@ function get_temporary_value(p) end end +""" + $(TYPEDEF) + +A simple utility meant to be used as the `constructor` passed to `process_SciMLProblem` in +case constructing a SciMLFunction is not required. +""" +struct EmptySciMLFunction end + +function EmptySciMLFunction(args...; kwargs...) + return nothing +end + """ $(TYPEDSIGNATURES) diff --git a/src/utils.jl b/src/utils.jl index e8ed131d78..1e54e7047b 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -610,6 +610,15 @@ function collect_constants!(constants, expr::Symbolic) end end +function collect_constants!(constants, expr::Union{ConstantRateJump, VariableRateJump}) + collect_constants!(constants, expr.rate) + collect_constants!(constants, expr.affect!) +end + +function collect_constants!(constants, ::MassActionJump) + return constants +end + """ Replace symbolic constants with their literal values """ @@ -667,7 +676,7 @@ end function get_cmap(sys, exprs = nothing) #Inject substitutions for constants => values - cs = collect_constants([get_eqs(sys); get_observed(sys)]) #ctrls? what else? + cs = collect_constants([collect(get_eqs(sys)); get_observed(sys)]) #ctrls? what else? if !empty_substitutions(sys) cs = [cs; collect_constants(get_substitutions(sys).subs)] end From 713c1cb3588af44e10081988413050a01c0e4b80 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 16 Oct 2024 19:34:51 +0530 Subject: [PATCH 0154/1337] fix: handle parameter dependencies with constant RHS --- src/systems/diffeqs/odesystem.jl | 6 +----- src/utils.jl | 2 ++ test/odesystem.jl | 5 +++++ 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/systems/diffeqs/odesystem.jl b/src/systems/diffeqs/odesystem.jl index d1cd01ce7b..ec2c5f8157 100644 --- a/src/systems/diffeqs/odesystem.jl +++ b/src/systems/diffeqs/odesystem.jl @@ -335,11 +335,7 @@ function ODESystem(eqs, iv; kwargs...) end end for eq in get(kwargs, :parameter_dependencies, Equation[]) - if eq isa Pair - collect_vars!(allunknowns, ps, eq, iv) - else - collect_vars!(allunknowns, ps, eq, iv) - end + collect_vars!(allunknowns, ps, eq, iv) end for ssys in get(kwargs, :systems, ODESystem[]) collect_scoped_vars!(allunknowns, ps, ssys, iv) diff --git a/src/utils.jl b/src/utils.jl index 2ff6af2231..d2e8a3ea38 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -252,6 +252,7 @@ end function collect_defaults!(defs, vars) for v in vars + symbolic_type(v) == NotSymbolic() && continue if haskey(defs, v) || !hasdefault(unwrap(v)) || (def = getdefault(v)) === nothing continue end @@ -262,6 +263,7 @@ end function collect_var_to_name!(vars, xs) for x in xs + symbolic_type(x) == NotSymbolic() && continue x = unwrap(x) if hasmetadata(x, Symbolics.GetindexParent) xarr = getmetadata(x, Symbolics.GetindexParent) diff --git a/test/odesystem.jl b/test/odesystem.jl index d00d9228a5..9446d105e0 100644 --- a/test/odesystem.jl +++ b/test/odesystem.jl @@ -1442,3 +1442,8 @@ end end end end + +@testset "Parameter dependencies with constant RHS" begin + @parameters p + @test_nowarn ODESystem(Equation[], t; parameter_dependencies = [p ~ 1.0], name = :a) +end From 51af9ae121f62c21b670ba9d1c470d6e1e518fe7 Mon Sep 17 00:00:00 2001 From: Anant Thazhemadam Date: Thu, 17 Oct 2024 10:34:15 +0530 Subject: [PATCH 0155/1337] ci: test with `1`, `lts` and `pre` versions of julia --- .github/workflows/Tests.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/Tests.yml b/.github/workflows/Tests.yml index 93db3ac518..609ab2fb3d 100644 --- a/.github/workflows/Tests.yml +++ b/.github/workflows/Tests.yml @@ -25,6 +25,10 @@ jobs: strategy: fail-fast: false matrix: + version: + - "1" + - "lts" + - "pre" group: - InterfaceI - InterfaceII @@ -35,5 +39,6 @@ jobs: - RegressionI uses: "SciML/.github/.github/workflows/tests.yml@v1" with: + julia-version: "${{ matrix.version }}" group: "${{ matrix.group }}" secrets: "inherit" From 101d4dfc9d122b777d61974b9701d96d5f39f9d0 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 17 Oct 2024 17:42:25 +0530 Subject: [PATCH 0156/1337] docs: add tutorial for callable parameters --- docs/pages.jl | 3 +- docs/src/tutorials/callable_params.md | 91 +++++++++++++++++++++++++++ 2 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 docs/src/tutorials/callable_params.md diff --git a/docs/pages.jl b/docs/pages.jl index d772c32471..2af487adf8 100644 --- a/docs/pages.jl +++ b/docs/pages.jl @@ -12,7 +12,8 @@ pages = [ "tutorials/parameter_identifiability.md", "tutorials/bifurcation_diagram_computation.md", "tutorials/SampledData.md", - "tutorials/domain_connections.md"], + "tutorials/domain_connections.md", + "tutorials/callable_params.md"], "Examples" => Any[ "Basic Examples" => Any["examples/higher_order.md", "examples/spring_mass.md", diff --git a/docs/src/tutorials/callable_params.md b/docs/src/tutorials/callable_params.md new file mode 100644 index 0000000000..74e7ea87fa --- /dev/null +++ b/docs/src/tutorials/callable_params.md @@ -0,0 +1,91 @@ +# Callable parameters and interpolating data + +ModelingToolkit.jl allows creating parameters that represent functions to be called. This +is especially useful for including interpolants and/or lookup tables inside ODEs. In this +tutorial we will create an `ODESystem` which employs callable parameters to interpolate data +inside an ODE and go over the various syntax options and their implications. + +## Callable parameter syntax + +The syntax for callable parameters declared via `@parameters` must be one of the following + + 1. `(fn::FType)(..)` + 2. `fn(::argType1, ::argType2, ...)` + +In the first case, the parameter is callable with any number/combination of arguments, and +has a type of `FType` (the callable must be a subtype of `FType`). In the second case, +the parameter is callable with as many arguments as declared, and all must match the +declared types. + +By default, the return type of the callable symbolic is inferred to be `Real`. To change +this, a `::retType` annotation can be added at the end. + +To declare a function that returns an array of values, the same array syntax can be used +as for normal variables: + +```julia +@parameters (foo::FType)(..)[1:3]::retType +@parameters foo(::argType1, ::argType2)[1:3]::retType +``` + +`retType` here is the `eltype` of the returned array. + +## Storage of callable parameters + +Callable parameters declared with the `::FType` syntax will be stored in a `Vector{FType}`. +Thus, if `FType` is non-concrete, the buffer will also be non-concrete. This is sometimes +necessary to allow the value of the callable to be switched out for a different type without +rebuilding the model. Typically this syntax is preferable when `FType` is concrete or +a small union. + +Callable parameters declared with the `::argType1, ...` syntax will be stored in a +`Vector{FunctionWrappers.FunctionWrapper{retType, Tuple{argType1, ...}}}`. This suffers +the small overhead of a `FunctionWrapper` and restricts the signature of the callable, +symbolic, but allows storing the parameter in a type-stable manner and swapping it out. +This is preferable when the values that the callable can take do not share a common +subtype. For example, when a callable can represent the activation of a neural network +and can be `tanh`, `sigmoid`, etc. which have a common ancestor of `Function`. + +If both `::FType` and `::argType`s are specified, `::FType` takes priority. For example, + +```julia +@parameters (p::LinearInterpolation)(::Real) +``` + +`p` will be stored in a `Vector{LinearInterpolation}`. If `::LinearInterpolation` was not +specified, it would be stored in a `Vector{FunctionWrapper{Real, Tuple{Real}}}`. + +## Example using interpolations + +```@example callable +using ModelingToolkit +using OrdinaryDiffEq +using DataInterpolations +using ModelingToolkit: t_nounits as t, D_nounits as D + +ts = collect(0.0:0.1:10.0) +spline = LinearInterpolation(ts .^ 2, ts) +Tspline = typeof(spline) +@variables x(t) +@parameters (interp::Tspline)(..) + +@mtkbuild sys = ODESystem(D(x) ~ interp(t), t) +``` + +The derivative of `x` is obtained via an interpolation from DataInterpolations.jl. Note +the parameter syntax. The `(..)` marks the parameter as callable. `(interp::Tspline)` +indicates that the parameter is of type `Tspline`. + +```@example callable +prob = ODEProblem(sys, [x => 0.0], (0.0, 1.0), [interp => spline]) +solve(prob) +``` + +Note that the the following will not work: + +```julia +ODEProblem( + sys; [x => 0.0], (0.0, 1.0), [interp => LinearInterpolation(0.0:0.1:1.0, 0.0:0.1:1.0)]) +``` + +Since the type of the spline doesn't match. From ce739406c461ee55aa939da18f6377ce0cce82d0 Mon Sep 17 00:00:00 2001 From: Venkateshprasad <32921645+ven-k@users.noreply.github.com> Date: Tue, 15 Oct 2024 12:00:17 +0000 Subject: [PATCH 0157/1337] feat: extend all arguments of a base sys to sys --- docs/src/basics/MTKLanguage.md | 26 ++++++++------- src/systems/model_parsing.jl | 59 +++++++++++++++++++++++++--------- test/model_parsing.jl | 30 ++++++++++++++--- 3 files changed, 84 insertions(+), 31 deletions(-) diff --git a/docs/src/basics/MTKLanguage.md b/docs/src/basics/MTKLanguage.md index 685c549429..537e1b0349 100644 --- a/docs/src/basics/MTKLanguage.md +++ b/docs/src/basics/MTKLanguage.md @@ -73,7 +73,7 @@ end v_array(t)[1:N, 1:M] v_for_defaults(t) end - @extend ModelB(; p1) + @extend ModelB(p1 = 1) @components begin model_a = ModelA(; k_array) model_array_a = [ModelA(; k = i) for i in 1:N] @@ -149,14 +149,18 @@ julia> ModelingToolkit.getdefault(model_c1.v) #### `@extend` begin block - - Partial systems can be extended in a higher system as `@extend PartialSystem(; kwargs)`. - - Keyword arguments pf partial system in the `@extend` definition are added as the keyword arguments of the base system. - - Note that in above example, `p1` is promoted as an argument of `ModelC`. Users can set the value of `p1`. However, as `p2` isn't listed in the model definition, its initial guess can't be specified while creating an instance of `ModelC`. +Partial systems can be extended in a higher system in two ways: -```julia -julia> @mtkbuild model_c2 = ModelC(; p1 = 2.0) + - `@extend PartialSystem(var1 = value1)` + + + This is the recommended way of extending a base system. + + The default values for the arguments of the base system can be declared in the `@extend` statement. + + Note that all keyword arguments of the base system are added as the keyword arguments of the main system. -``` + - `@extend var_to_unpack1, var_to_unpack2 = partial_sys = PartialSystem(var1 = value1)` + + + In this method: explicitly list the variables that should be unpacked, provide a name for the partial system and declare the base system. + + Note that only the arguments listed out in the declaration of the base system (here: `var1`) are added as the keyword arguments of the higher system. #### `@components` begin block @@ -325,11 +329,11 @@ For example, the structure of `ModelC` is: julia> ModelC.structure Dict{Symbol, Any} with 10 entries: :components => Any[Union{Expr, Symbol}[:model_a, :ModelA], Union{Expr, Symbol}[:model_array_a, :ModelA, :(1:N)], Union{Expr, Symbol}[:model_array_b, :ModelA, :(1:N)]] - :variables => Dict{Symbol, Dict{Symbol, Any}}(:v=>Dict(:default=>:v_var, :type=>Real), :v_for_defaults=>Dict(:type=>Real)) + :variables => Dict{Symbol, Dict{Symbol, Any}}(:v=>Dict(:default=>:v_var, :type=>Real), :v_array=>Dict(:value=>nothing, :type=>Real, :size=>(:N, :M)), :v_for_defaults=>Dict(:type=>Real)) :icon => URI("https://github.com/SciML/SciMLDocs/blob/main/docs/src/assets/logo.png") - :kwargs => Dict{Symbol, Dict}(:f => Dict(:value => :sin), :N => Dict(:value => 2), :M => Dict(:value => 3), :v => Dict{Symbol, Any}(:value => :v_var, :type => Real), :v_for_defaults => Dict{Symbol, Union{Nothing, DataType}}(:value => nothing, :type => Real), :p1 => Dict(:value => nothing)), - :structural_parameters => Dict{Symbol, Dict}(:f => Dict(:value => :sin), :N => Dict(:value => 2), :M => Dict(:value => 3)) - :independent_variable => t + :kwargs => Dict{Symbol, Dict}(:f=>Dict(:value=>:sin), :p2=>Dict(:value=>NoValue()), :N=>Dict(:value=>2), :M=>Dict(:value=>3), :v=>Dict{Symbol, Any}(:value=>:v_var, :type=>Real), :v_array=>Dict{Symbol, Any}(:value=>nothing, :type=>Real, :size=>(:N, :M)), :v_for_defaults=>Dict{Symbol, Union{Nothing, DataType}}(:value=>nothing, :type=>Real), :p1=>Dict(:value=>1)) + :structural_parameters => Dict{Symbol, Dict}(:f=>Dict(:value=>:sin), :N=>Dict(:value=>2), :M=>Dict(:value=>3)) + :independent_variable => :t :constants => Dict{Symbol, Dict}(:c=>Dict{Symbol, Any}(:value=>1, :type=>Int64, :description=>"Example constant.")) :extend => Any[[:p2, :p1], Symbol("#mtkmodel__anonymous__ModelB"), :ModelB] :defaults => Dict{Symbol, Any}(:v_for_defaults=>2.0) diff --git a/src/systems/model_parsing.jl b/src/systems/model_parsing.jl index 1298d72506..ab70646824 100644 --- a/src/systems/model_parsing.jl +++ b/src/systems/model_parsing.jl @@ -717,7 +717,7 @@ function parse_structural_parameters!(exprs, sps, dict, mod, body, kwargs) end end -function extend_args!(a, b, dict, expr, kwargs, varexpr, has_param = false) +function extend_args!(a, b, dict, expr, kwargs, has_param = false) # Whenever `b` is a function call, skip the first arg aka the function name. # Whenever it is a kwargs list, include it. start = b.head == :call ? 2 : 1 @@ -738,18 +738,18 @@ function extend_args!(a, b, dict, expr, kwargs, varexpr, has_param = false) dict[:kwargs][x] = Dict(:value => nothing) end Expr(:kw, x) => begin + b.args[i] = Expr(:kw, x, x) push!(kwargs, Expr(:kw, x, nothing)) dict[:kwargs][x] = Dict(:value => nothing) end Expr(:kw, x, y) => begin b.args[i] = Expr(:kw, x, x) - push!(varexpr.args, :($x = $x === nothing ? $y : $x)) - push!(kwargs, Expr(:kw, x, nothing)) - dict[:kwargs][x] = Dict(:value => nothing) + push!(kwargs, Expr(:kw, x, y)) + dict[:kwargs][x] = Dict(:value => y) end Expr(:parameters, x...) => begin has_param = true - extend_args!(a, arg, dict, expr, kwargs, varexpr, has_param) + extend_args!(a, arg, dict, expr, kwargs, has_param) end _ => error("Could not parse $arg of component $a") end @@ -758,17 +758,40 @@ end const EMPTY_DICT = Dict() const EMPTY_VoVoSYMBOL = Vector{Symbol}[] +const EMPTY_VoVoVoSYMBOL = Vector{Symbol}[[]] -function Base.names(model::Model) +function _arguments(model::Model) vars = keys(get(model.structure, :variables, EMPTY_DICT)) vars = union(vars, keys(get(model.structure, :parameters, EMPTY_DICT))) - vars = union(vars, - map(first, get(model.structure, :components, EMPTY_VoVoSYMBOL))) + vars = union(vars, first(get(model.structure, :extend, EMPTY_VoVoVoSYMBOL))) collect(vars) end -function _parse_extend!(ext, a, b, dict, expr, kwargs, varexpr, vars) - extend_args!(a, b, dict, expr, kwargs, varexpr) +function Base.names(model::Model) + collect(union(_arguments(model), + map(first, get(model.structure, :components, EMPTY_VoVoSYMBOL)))) +end + +function _parse_extend!(ext, a, b, dict, expr, kwargs, vars, additional_args) + extend_args!(a, b, dict, expr, kwargs) + + # `additional_args` doubles as a flag to check the mode of `@extend`. It is + # `nothing` for explicit destructuring. + # The following block modifies the arguments of both base and higher systems + # for the implicit extend statements. + if additional_args !== nothing + b.args = [b.args[1]] + allvars = [additional_args.args..., vars.args...] + push!(b.args, Expr(:parameters)) + for var in allvars + push!(b.args[end].args, var) + if !haskey(dict[:kwargs], var) + push!(dict[:kwargs], var => Dict(:value => NO_VALUE)) + push!(kwargs, Expr(:kw, var, NO_VALUE)) + end + end + end + ext[] = a push!(b.args, Expr(:kw, :name, Meta.quot(a))) push!(expr.args, :($a = $b)) @@ -780,8 +803,6 @@ end function parse_extend!(exprs, ext, dict, mod, body, kwargs) expr = Expr(:block) - varexpr = Expr(:block) - push!(exprs, varexpr) push!(exprs, expr) body = deepcopy(body) MLStyle.@match body begin @@ -792,7 +813,9 @@ function parse_extend!(exprs, ext, dict, mod, body, kwargs) error("`@extend` destructuring only takes an tuple as LHS. Got $body") end a, b = b.args - _parse_extend!(ext, a, b, dict, expr, kwargs, varexpr, vars) + # This doubles as a flag to identify the mode of `@extend` + additional_args = nothing + _parse_extend!(ext, a, b, dict, expr, kwargs, vars, additional_args) else error("When explicitly destructing in `@extend` please use the syntax: `@extend a, b = oneport = OnePort()`.") end @@ -802,8 +825,11 @@ function parse_extend!(exprs, ext, dict, mod, body, kwargs) b = body if (model = getproperty(mod, b.args[1])) isa Model vars = Expr(:tuple) - append!(vars.args, names(model)) - _parse_extend!(ext, a, b, dict, expr, kwargs, varexpr, vars) + append!(vars.args, _arguments(model)) + additional_args = Expr(:tuple) + append!(additional_args.args, + keys(get(model.structure, :structural_parameters, EMPTY_DICT))) + _parse_extend!(ext, a, b, dict, expr, kwargs, vars, additional_args) else error("Cannot infer the exact `Model` that `@extend $(body)` refers." * " Please specify the names that it brings into scope by:" * @@ -1104,7 +1130,7 @@ 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) URI("file:///" * abspath(body)) - elseif (iconpath = joinpath(icon_dir, body); isfile(iconpath)) + elseif (iconpath = abspath(joinpath(icon_dir, body)); isfile(iconpath)) URI("file:///" * abspath(iconpath)) elseif try Base.isvalid(URI(body)) @@ -1115,6 +1141,7 @@ function parse_icon!(body::String, dict, icon, mod) elseif (_body = lstrip(body); startswith(_body, r"<\?xml| Dict(:type => Real)) @test A.structure[:extend] == [[:e], :extended_e, :E] @test A.structure[:equations] == ["e ~ 0"] - @test A.structure[:kwargs] == - Dict{Symbol, Dict}(:p => Dict(:value => nothing, :type => Real), - :v => Dict(:value => nothing, :type => Real)) + @test A.structure[:kwargs] == Dict{Symbol, Dict}( + :p => Dict{Symbol, Union{Nothing, DataType}}(:value => nothing, :type => Real), + :v => Dict{Symbol, Union{Nothing, DataType}}(:value => nothing, :type => Real)) @test A.structure[:components] == [[:cc, :C]] end @@ -910,3 +910,25 @@ end end), false) end + +@mtkmodel BaseSys begin + @parameters begin + p1 + p2 + end + @variables begin + v1(t) + end +end + +@testset "Arguments of base system" begin + @mtkmodel MainSys begin + @extend BaseSys(p1 = 1) + end + + @test names(MainSys) == [:p2, :p1, :v1] + @named main_sys = MainSys(p1 = 11, p2 = 12, v1 = 13) + @test getdefault(main_sys.p1) == 11 + @test getdefault(main_sys.p2) == 12 + @test getdefault(main_sys.v1) == 13 +end From a6f781d45cbbc885edf9167695682124cd9655bb Mon Sep 17 00:00:00 2001 From: Ben Chung Date: Thu, 1 Aug 2024 20:36:50 -0700 Subject: [PATCH 0158/1337] First pass at MutatingFunctionalAffect --- Project.toml | 1 + src/ModelingToolkit.jl | 1 + src/systems/callbacks.jl | 144 +++++++++++++++++++++++++++++-- src/systems/diffeqs/odesystem.jl | 27 +++++- test/symbolic_events.jl | 49 +++++++++++ 5 files changed, 215 insertions(+), 7 deletions(-) diff --git a/Project.toml b/Project.toml index 808d68af06..729966fde0 100644 --- a/Project.toml +++ b/Project.toml @@ -9,6 +9,7 @@ ArrayInterface = "4fba245c-0d91-5ea0-9b3e-6abc04ee57a9" BlockArrays = "8e7c35d0-a365-5155-bbbb-fb81a777f24e" Combinatorics = "861a8166-3701-5b0c-9a16-15d98fcdc6aa" Compat = "34da2185-b29b-5c13-b0c7-acf172513d20" +ComponentArrays = "b0b7db55-cfe3-40fc-9ded-d10e2dbeff66" ConstructionBase = "187b0558-2788-49d3-abe0-74a17ed4e7c9" DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8" DiffEqBase = "2b5f629d-d688-5b77-993f-72d75c75574e" diff --git a/src/ModelingToolkit.jl b/src/ModelingToolkit.jl index 2f57bb1765..f5262a1526 100644 --- a/src/ModelingToolkit.jl +++ b/src/ModelingToolkit.jl @@ -54,6 +54,7 @@ using Reexport using RecursiveArrayTools import Graphs: SimpleDiGraph, add_edge!, incidence_matrix import BlockArrays: BlockedArray, Block, blocksize, blocksizes +import ComponentArrays using RuntimeGeneratedFunctions using RuntimeGeneratedFunctions: drop_expr diff --git a/src/systems/callbacks.jl b/src/systems/callbacks.jl index 86cab57634..f7d4baa4cb 100644 --- a/src/systems/callbacks.jl +++ b/src/systems/callbacks.jl @@ -60,8 +60,6 @@ function Base.hash(a::FunctionalAffect, s::UInt) hash(a.ctx, s) end -has_functional_affect(cb) = affects(cb) isa FunctionalAffect - namespace_affect(affect, s) = namespace_equation(affect, s) function namespace_affect(affect::FunctionalAffect, s) FunctionalAffect(func(affect), @@ -73,6 +71,67 @@ function namespace_affect(affect::FunctionalAffect, s) context(affect)) end +""" +`MutatingFunctionalAffect` differs from `FunctionalAffect` in two key ways: +* First, insetad of the `u` vector passed to `f` being a vector of indices into `integ.u` it's instead the result of evaluating `obs` at the current state, named as specified in `obs_syms`. This allows affects to easily access observed states and decouples affect inputs from the system structure. +* Second, it abstracts the assignment back to system states away. Instead of writing `integ.u[u.myvar] = [whatever]`, you instead declare in `mod_params` that you want to modify `myvar` and then either (out of place) return a named tuple with `myvar` or (in place) modify the associated element in the ComponentArray that's given. +Initially, we only support "flat" states in `modified`; these states will be marked as irreducible in the overarching system and they will simply be bulk assigned at mutation. In the future, this will be extended to perform a nonlinear solve to further decouple the affect from the system structure. +""" +@kwdef struct MutatingFunctionalAffect + f::Any + obs::Vector + obs_syms::Vector{Symbol} + modified::Vector + mod_syms::Vector{Symbol} + ctx::Any +end + +MutatingFunctionalAffect(f::Function; + observed::NamedTuple = NamedTuple{()}(()), + modified::NamedTuple = NamedTuple{()}(()), + ctx=nothing) = MutatingFunctionalAffect(f, collect(values(observed)), collect(keys(observed)), collect(values(modified)), collect(keys(modified)), ctx) +MutatingFunctionalAffect(f::Function, observed::NamedTuple; modified::NamedTuple = NamedTuple{()}(()), ctx=nothing) = + MutatingFunctionalAffect(f, observed=observed, modified=modified, ctx=ctx) +MutatingFunctionalAffect(f::Function, observed::NamedTuple, modified::NamedTuple; ctx=nothing) = + MutatingFunctionalAffect(f, observed=observed, modified=modified, ctx=ctx) +MutatingFunctionalAffect(f::Function, observed::NamedTuple, modified::NamedTuple, ctx) = + MutatingFunctionalAffect(f, observed=observed, modified=modified, ctx=ctx) + +func(f::MutatingFunctionalAffect) = f.f +context(a::MutatingFunctionalAffect) = a.ctx +observed(a::MutatingFunctionalAffect) = a.obs +observed_syms(a::MutatingFunctionalAffect) = a.obs_syms +discretes(a::MutatingFunctionalAffect) = filter(ModelingToolkit.isparameter, a.modified) +modified(a::MutatingFunctionalAffect) = a.modified +modified_syms(a::MutatingFunctionalAffect) = a.mod_syms + +function Base.:(==)(a1::MutatingFunctionalAffect, a2::MutatingFunctionalAffect) + isequal(a1.f, a2.f) && isequal(a1.obs, a2.obs) && isequal(a1.modified, a2.modified) && + isequal(a1.obs_syms, a2.obs_syms) && isequal(a1.mod_syms, a2.mod_syms)&& isequal(a1.ctx, a2.ctx) +end + +function Base.hash(a::MutatingFunctionalAffect, s::UInt) + s = hash(a.f, s) + s = hash(a.obs, s) + s = hash(a.obs_syms, s) + s = hash(a.modified, s) + s = hash(a.mod_syms, s) + hash(a.ctx, s) +end + +function namespace_affect(affect::MutatingFunctionalAffect, s) + MutatingFunctionalAffect(func(affect), + renamespace.((s,), observed(affect)), + observed_syms(affect), + renamespace.((s,), modified(affect)), + modified_syms(affect), + context(affect)) +end + +function has_functional_affect(cb) + (affects(cb) isa FunctionalAffect || affects(cb) isa MutatingFunctionalAffect) +end + #################################### continuous events ##################################### const NULL_AFFECT = Equation[] @@ -109,8 +168,8 @@ Affects (i.e. `affect` and `affect_neg`) can be specified as either: """ struct SymbolicContinuousCallback eqs::Vector{Equation} - affect::Union{Vector{Equation}, FunctionalAffect} - affect_neg::Union{Vector{Equation}, FunctionalAffect, Nothing} + affect::Union{Vector{Equation}, FunctionalAffect, MutatingFunctionalAffect} + affect_neg::Union{Vector{Equation}, FunctionalAffect, MutatingFunctionalAffect, Nothing} rootfind::SciMLBase.RootfindOpt function SymbolicContinuousCallback(; eqs::Vector{Equation}, affect = NULL_AFFECT, affect_neg = affect, rootfind = SciMLBase.LeftRootFind) @@ -250,6 +309,7 @@ scalarize_affects(affects) = scalarize(affects) scalarize_affects(affects::Tuple) = FunctionalAffect(affects...) scalarize_affects(affects::NamedTuple) = FunctionalAffect(; affects...) scalarize_affects(affects::FunctionalAffect) = affects +scalarize_affects(affects::MutatingFunctionalAffect) = affects SymbolicDiscreteCallback(p::Pair) = SymbolicDiscreteCallback(p[1], p[2]) SymbolicDiscreteCallback(cb::SymbolicDiscreteCallback) = cb # passthrough @@ -257,7 +317,7 @@ 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 + if db.affects isa FunctionalAffect || db.affects isa MutatingFunctionalAffect # TODO println(io, " ", db.affects) else @@ -749,6 +809,80 @@ function compile_user_affect(affect::FunctionalAffect, cb, sys, dvs, ps; kwargs. end end +invalid_variables(sys, expr) = filter(x -> !any(isequal(x), all_symbols(sys)), vars(expr)) +function unassignable_variables(sys, expr) + assignable_syms = vcat(unknowns(sys), parameters(sys)) + return filter(x -> !any(isequal(x), assignable_syms), vars(expr)) +end + +function compile_user_affect(affect::MutatingFunctionalAffect, sys, dvs, ps; kwargs...) + #= + Implementation sketch: + generate observed function (oop), should save to a component array under obs_syms + do the same stuff as the normal FA for pars_syms + call the affect method - test if it's OOP or IP using applicable + unpack and apply the resulting values + =# + obs_exprs = observed(affect) + for oexpr in obs_exprs + invalid_vars = invalid_variables(sys, oexpr) + if length(invalid_vars) > 0 + error("Observed equation $(oexpr) in affect refers to missing variable(s) $(invalid_vars); the variables may not have been added (e.g. if a component is missing).") + end + end + obs_syms = observed_syms(affect) + obs_size = size.(obs_exprs) # we will generate a work buffer of a ComponentArray that maps obs_syms to arrays of size obs_size + + mod_exprs = modified(affect) + for mexpr in mod_exprs + if !is_observed(sys, mexpr) && parameter_index(sys, mexpr) === nothing + error("Expression $mexpr cannot be assigned to; currently only unknowns and parameters may be updated by an affect.") + end + invalid_vars = unassignable_variables(sys, mexpr) + if length(invalid_vars) > 0 + error("Observed equation $(mexpr) in affect refers to missing variable(s) $(invalid_vars); the variables may not have been added (e.g. if a component is missing) or they may have been reduced away.") + end + end + mod_syms = modified_syms(affect) + _, mod_og_val_fun = build_explicit_observed_function(sys, mod_exprs; return_inplace=true) + + # sanity checks done! now build the data and update function for observed values + mkzero(sz) = if sz === () 0.0 else zeros(sz) end + _, obs_fun = build_explicit_observed_function(sys, reduce(vcat, Symbolics.scalarize.(obs_exprs); init = []); return_inplace=true) + obs_component_array = ComponentArrays.ComponentArray(NamedTuple{(obs_syms..., )}(mkzero.(obs_size))) + + # okay so now to generate the stuff to assign it back into the system + # note that we reorder the componentarray to make the views coherent wrt the base array + mod_pairs = mod_exprs .=> mod_syms + mod_param_pairs = filter(v -> is_parameter(sys, v[1]), mod_pairs) + mod_unk_pairs = filter(v -> !is_parameter(sys, v[1]), mod_pairs) + _, mod_og_val_fun = build_explicit_observed_function(sys, reduce(vcat, [first.(mod_param_pairs); first.(mod_unk_pairs)]; init = []); return_inplace=true) + upd_params_fun = setu(sys, reduce(vcat, Symbolics.scalarize.(first.(mod_param_pairs)); init = [])) + upd_unk_fun = setu(sys, reduce(vcat, Symbolics.scalarize.(first.(mod_unk_pairs)); init = [])) + + upd_component_array = ComponentArrays.ComponentArray(NamedTuple{([last.(mod_param_pairs); last.(mod_unk_pairs)]...,)}( + [collect(mkzero(size(e)) for e in first.(mod_param_pairs)); + collect(mkzero(size(e)) for e in first.(mod_unk_pairs))])) + upd_params_view = view(upd_component_array, last.(mod_param_pairs)) + upd_unks_view = view(upd_component_array, last.(mod_unk_pairs)) + let user_affect = func(affect), ctx = context(affect) + function (integ) + # update the to-be-mutated values; this ensures that if you do a no-op then nothing happens + mod_og_val_fun(upd_component_array, integ.u, integ.p..., integ.t) + + # update the observed values + obs_fun(obs_component_array, integ.u, integ.p..., integ.t) + + # let the user do their thing + user_affect(upd_component_array, obs_component_array, integ, ctx) + + # write the new values back to the integrator + upd_params_fun(integ, upd_params_view) + upd_unk_fun(integ, upd_unks_view) + end + end +end + function compile_affect(affect::FunctionalAffect, cb, sys, dvs, ps; kwargs...) compile_user_affect(affect, cb, sys, dvs, ps; kwargs...) end diff --git a/src/systems/diffeqs/odesystem.jl b/src/systems/diffeqs/odesystem.jl index ec2c5f8157..daa4321ed0 100644 --- a/src/systems/diffeqs/odesystem.jl +++ b/src/systems/diffeqs/odesystem.jl @@ -404,8 +404,31 @@ ODESystem(eq::Equation, args...; kwargs...) = ODESystem([eq], args...; kwargs... """ $(SIGNATURES) -Build the observed function assuming the observed equations are all explicit, -i.e. there are no cycles. +Generates a function that computes the observed value(s) `ts` in the system `sys` assuming that there are no cycles in the equations. + +The return value will be either: +* a single function if the input is a scalar or if the input is a Vector but `return_inplace` is false +* the out of place and in-place functions `(ip, oop)` if `return_inplace` is true and the input is a `Vector` + +The function(s) will be: +* `RuntimeGeneratedFunction`s by default, +* A Julia `Expr` if `expression` is true, +* A directly evaluated Julia function in the module `eval_module` if `eval_expression` is true + +The signatures will be of the form `g(...)` with arguments: +* `output` for in-place functions +* `unknowns` if `params_only` is `false` +* `inputs` if `inputs` is an array of symbolic inputs that should be available in `ts` +* `p...` unconditionally; note that in the case of `MTKParameters` more than one parameters argument may be present, so it must be splatted +* `t` if the system is time-dependent; for example `NonlinearSystem` will not have `t` +For example, a function `g(op, unknowns, p, inputs, t)` will be the in-place function generated if `return_inplace` is true, `ts` is a vector, an array of inputs `inputs` is given, and `params_only` is false for a time-dependent system. + +Options not otherwise specified are: +* `output_type = Array` the type of the array generated by the out-of-place vector-valued function +* `checkbounds = true` checks bounds if true when destructuring parameters +* `op = Operator` sets the recursion terminator for the walk done by `vars` to identify the variables that appear in `ts`. See the documentation for `vars` for more detail. +* `throw = true` if true, throw an error when generating a function for `ts` that reference variables that do not exist +* `drop_expr` is deprecated. """ function build_explicit_observed_function(sys, ts; inputs = nothing, diff --git a/test/symbolic_events.jl b/test/symbolic_events.jl index e1d12814ef..351f8111ee 100644 --- a/test/symbolic_events.jl +++ b/test/symbolic_events.jl @@ -887,3 +887,52 @@ 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 + eqs = [ + D(temp) ~ furnace_on * furnace_power - temp^2 * leakage + ] + + furnace_off = ModelingToolkit.SymbolicContinuousCallback([temp ~ furnace_off_threshold], + ModelingToolkit.MutatingFunctionalAffect(modified=(; furnace_on)) do x, o, i, c + x.furnace_on = false + end) + furnace_enable = ModelingToolkit.SymbolicContinuousCallback([temp ~ furnace_on_threshold], + ModelingToolkit.MutatingFunctionalAffect(modified=(; furnace_on)) do x, o, i, c + 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) +end + +@testset "Quadrature" begin + @variables theta(t) omega(t) + params = @parameters qA=0 qB=0 + eqs = [ + D(theta) ~ omega + omega ~ sin(0.5*t) + ] + qAevt = ModelingToolkit.SymbolicContinuousCallback([cos(1000 * theta) ~ 0], + ModelingToolkit.MutatingFunctionalAffect(modified=(; qA)) do x, o, i, c + x.qA = 1 + end, + affect_neg = ModelingToolkit.MutatingFunctionalAffect(modified=(; qA)) do x, o, i, c + x.qA = 0 + end) + qBevt = ModelingToolkit.SymbolicContinuousCallback([cos(1000 * theta + π/2) ~ 0], + ModelingToolkit.MutatingFunctionalAffect(modified=(; qB)) do x, o, i, c + x.qB = 1 + end, + affect_neg = ModelingToolkit.MutatingFunctionalAffect(modified=(; qB)) do x, o, i, c + x.qB = 0 + end) + @named sys = ODESystem(eqs, t, [theta, omega], params; continuous_events = [qAevt, qBevt]) + ss = structural_simplify(sys) + prob = ODEProblem(ss, [theta => 0.0], (0.0, 1.0)) + sol = solve(prob, Tsit5(); dtmax=0.01) +end From f151e429cd9edb33b55ec019be069af12cb9d108 Mon Sep 17 00:00:00 2001 From: Ben Chung Date: Fri, 2 Aug 2024 16:19:51 -0700 Subject: [PATCH 0159/1337] Clarify documentation for SCC --- src/systems/callbacks.jl | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/src/systems/callbacks.jl b/src/systems/callbacks.jl index f7d4baa4cb..a57d5c006d 100644 --- a/src/systems/callbacks.jl +++ b/src/systems/callbacks.jl @@ -145,17 +145,26 @@ By default `affect_neg = affect`; to only get rising edges specify `affect_neg = Assume without loss of generality that the equation is of the form `c(u,p,t) ~ 0`; we denote the integrator state as `i.u`. For compactness, we define `prev_sign = sign(c(u[t-1], p[t-1], t-1))` and `cur_sign = sign(c(u[t], p[t], t))`. A condition edge will be detected and the callback will be invoked iff `prev_sign * cur_sign <= 0`. +The positive edge `affect` will be triggered iff an edge is detected and if `prev_sign < 0`; similarly, `affect_neg` will be +triggered iff an edge is detected and `prev_sign > 0`. + Inter-sample condition activation is not guaranteed; for example if we use the dirac delta function as `c` to insert a sharp discontinuity between integrator steps (which in this example would not normally be identified by adaptivity) then the condition is not guaranteed to be triggered. Once detected the integrator will "wind back" through a root-finding process to identify the point when the condition became active; the method used -is specified by `rootfind` from [`SciMLBase.RootfindOpt`](@ref). Multiple callbacks in the same system with different `rootfind` operations will be resolved -into separate VectorContinuousCallbacks in the enumeration order of `SciMLBase.RootfindOpt`, which may cause some callbacks to not fire if several become -active at the same instant. See the `SciMLBase` documentation for more information on the semantic rules. - -The positive edge `affect` will be triggered iff an edge is detected and if `prev_sign < 0`; similarly, `affect_neg` will be -triggered iff an edge is detected `prev_sign > 0`. +is specified by `rootfind` from [`SciMLBase.RootfindOpt`](@ref). If we denote the time when the condition becomes active at tc, +the value in the integrator after windback will be: +* `u[tc-epsilon], p[tc-epsilon], tc` if `LeftRootFind` is used, +* `u[tc+epsilon], p[tc+epsilon], tc` if `RightRootFind` is used, +* or `u[t], p[t], t` if `NoRootFind` is used. +For example, if we want to detect when an unknown variable `x` satisfies `x > 0` using the condition `x ~ 0` on a positive edge (that is, `D(x) > 0`), +then left root finding will get us `x=-epsilon`, right root finding `x=epsilon` and no root finding whatever the next step of the integrator was after +it passed through 0. + +Multiple callbacks in the same system with different `rootfind` operations will be grouped +by their `rootfind` value into separate VectorContinuousCallbacks in the enumeration order of `SciMLBase.RootfindOpt`. This may cause some callbacks to not fire if several become +active at the same instant. See the `SciMLBase` documentation for more information on the semantic rules. Affects (i.e. `affect` and `affect_neg`) can be specified as either: * A list of equations that should be applied when the callback is triggered (e.g. `x ~ 3, y ~ 7`) which must be of the form `unknown ~ observed value` where each `unknown` appears only once. Equations will be applied in the order that they appear in the vector; parameters and state updates will become immediately visible to following equations. From 49d48b833645a17f18f7581009f2a15a9c708161 Mon Sep 17 00:00:00 2001 From: Ben Chung Date: Fri, 2 Aug 2024 16:20:05 -0700 Subject: [PATCH 0160/1337] MutatingFunctionalAffect test cases --- test/symbolic_events.jl | 160 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 149 insertions(+), 11 deletions(-) diff --git a/test/symbolic_events.jl b/test/symbolic_events.jl index 351f8111ee..ca9f0ad9c3 100644 --- a/test/symbolic_events.jl +++ b/test/symbolic_events.jl @@ -227,6 +227,117 @@ affect_neg = [x ~ 1] @test e[].affect == affect end +@testset "MutatingFunctionalAffect constructors" begin + fmfa(o, x, i, c) = nothing + m = ModelingToolkit.MutatingFunctionalAffect(fmfa) + @test m isa ModelingToolkit.MutatingFunctionalAffect + @test m.f == fmfa + @test m.obs == [] + @test m.obs_syms == [] + @test m.modified == [] + @test m.mod_syms == [] + @test m.ctx === nothing + + m = ModelingToolkit.MutatingFunctionalAffect(fmfa, (;)) + @test m isa ModelingToolkit.MutatingFunctionalAffect + @test m.f == fmfa + @test m.obs == [] + @test m.obs_syms == [] + @test m.modified == [] + @test m.mod_syms == [] + @test m.ctx === nothing + + m = ModelingToolkit.MutatingFunctionalAffect(fmfa, (; x)) + @test m isa ModelingToolkit.MutatingFunctionalAffect + @test m.f == fmfa + @test isequal(m.obs, [x]) + @test m.obs_syms == [:x] + @test m.modified == [] + @test m.mod_syms == [] + @test m.ctx === nothing + + m = ModelingToolkit.MutatingFunctionalAffect(fmfa, (; y=x)) + @test m isa ModelingToolkit.MutatingFunctionalAffect + @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.MutatingFunctionalAffect(fmfa; observed=(; y=x)) + @test m isa ModelingToolkit.MutatingFunctionalAffect + @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.MutatingFunctionalAffect(fmfa; modified=(; x)) + @test m isa ModelingToolkit.MutatingFunctionalAffect + @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.MutatingFunctionalAffect(fmfa; modified=(; y=x)) + @test m isa ModelingToolkit.MutatingFunctionalAffect + @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.MutatingFunctionalAffect(fmfa, (; x), (; x)) + @test m isa ModelingToolkit.MutatingFunctionalAffect + @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.MutatingFunctionalAffect(fmfa, (; y=x), (; y=x)) + @test m isa ModelingToolkit.MutatingFunctionalAffect + @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.MutatingFunctionalAffect(fmfa; modified=(; y=x), observed=(; y=x)) + @test m isa ModelingToolkit.MutatingFunctionalAffect + @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.MutatingFunctionalAffect(fmfa; modified=(; y=x), observed=(; y=x), ctx=3) + @test m isa ModelingToolkit.MutatingFunctionalAffect + @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.MutatingFunctionalAffect(fmfa, (; x), (; x), 3) + @test m isa ModelingToolkit.MutatingFunctionalAffect + @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 + ## @named sys = ODESystem(eqs, t, continuous_events = [x ~ 1]) @@ -912,27 +1023,54 @@ end @testset "Quadrature" begin @variables theta(t) omega(t) - params = @parameters qA=0 qB=0 + params = @parameters qA=0 qB=0 hA=0 hB=0 cnt=0 eqs = [ D(theta) ~ omega - omega ~ sin(0.5*t) + omega ~ 1.0 ] - qAevt = ModelingToolkit.SymbolicContinuousCallback([cos(1000 * theta) ~ 0], - ModelingToolkit.MutatingFunctionalAffect(modified=(; qA)) do x, o, i, c + 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 + # todo: warn about dups + # todo: warn if a variable appears in both observed and modified + qAevt = ModelingToolkit.SymbolicContinuousCallback([cos(100 * theta) ~ 0], + ModelingToolkit.MutatingFunctionalAffect((; qB), (; qA, hA, hB, cnt)) do x, o, i, c + x.hA = x.qA + x.hB = o.qB x.qA = 1 + x.cnt += decoder(x.hA, x.hB, x.qA, o.qB) end, - affect_neg = ModelingToolkit.MutatingFunctionalAffect(modified=(; qA)) do x, o, i, c + affect_neg = ModelingToolkit.MutatingFunctionalAffect((; qB), (; qA, hA, hB, cnt)) do x, o, i, c + x.hA = x.qA + x.hB = o.qB x.qA = 0 - end) - qBevt = ModelingToolkit.SymbolicContinuousCallback([cos(1000 * theta + π/2) ~ 0], - ModelingToolkit.MutatingFunctionalAffect(modified=(; qB)) do x, o, i, c + x.cnt += decoder(x.hA, x.hB, x.qA, o.qB) + end; rootfind=SciMLBase.RightRootFind) + qBevt = ModelingToolkit.SymbolicContinuousCallback([cos(100 * theta - π/2) ~ 0], + ModelingToolkit.MutatingFunctionalAffect((; qA), (; qB, hA, hB, cnt)) do x, o, i, c + x.hA = o.qA + x.hB = x.qB x.qB = 1 + x.cnt += decoder(x.hA, x.hB, o.qA, x.qB) end, - affect_neg = ModelingToolkit.MutatingFunctionalAffect(modified=(; qB)) do x, o, i, c + affect_neg = ModelingToolkit.MutatingFunctionalAffect((; qA), (; qB, hA, hB, cnt)) do x, o, i, c + x.hA = o.qA + x.hB = x.qB x.qB = 0 - end) + x.cnt += decoder(x.hA, x.hB, o.qA, x.qB) + end; rootfind=SciMLBase.RightRootFind) @named sys = ODESystem(eqs, t, [theta, omega], params; continuous_events = [qAevt, qBevt]) ss = structural_simplify(sys) - prob = ODEProblem(ss, [theta => 0.0], (0.0, 1.0)) + prob = ODEProblem(ss, [theta => 0.0], (0.0, pi)) sol = solve(prob, Tsit5(); dtmax=0.01) + @test sol[cnt] == 197 # 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 From b96cd2eb273d640bf39d3a7f3c01a5e994cb7cc9 Mon Sep 17 00:00:00 2001 From: Ben Chung Date: Fri, 2 Aug 2024 16:42:55 -0700 Subject: [PATCH 0161/1337] More sanity checking --- src/systems/callbacks.jl | 28 ++++++++++++++++++++++++--- test/symbolic_events.jl | 42 ++++++++++++++++++++++++++++++++++++++-- 2 files changed, 65 insertions(+), 5 deletions(-) diff --git a/src/systems/callbacks.jl b/src/systems/callbacks.jl index a57d5c006d..612d80536c 100644 --- a/src/systems/callbacks.jl +++ b/src/systems/callbacks.jl @@ -818,10 +818,10 @@ function compile_user_affect(affect::FunctionalAffect, cb, sys, dvs, ps; kwargs. end end -invalid_variables(sys, expr) = filter(x -> !any(isequal(x), all_symbols(sys)), vars(expr)) +invalid_variables(sys, expr) = filter(x -> !any(isequal(x), all_symbols(sys)), reduce(vcat, vars(expr); init=[])) function unassignable_variables(sys, expr) assignable_syms = vcat(unknowns(sys), parameters(sys)) - return filter(x -> !any(isequal(x), assignable_syms), vars(expr)) + return filter(x -> !any(isequal(x), assignable_syms), reduce(vcat, vars(expr); init=[])) end function compile_user_affect(affect::MutatingFunctionalAffect, sys, dvs, ps; kwargs...) @@ -832,6 +832,21 @@ function compile_user_affect(affect::MutatingFunctionalAffect, sys, dvs, ps; kwa call the affect method - test if it's OOP or IP using applicable unpack and apply the resulting values =# + function check_dups(syms, exprs) # = (syms_dedup, exprs_dedup) + seen = Set{Symbol}() + syms_dedup = []; exprs_dedup = [] + for (sym, exp) in Iterators.zip(syms, exprs) + if !in(sym, seen) + push!(syms_dedup, sym) + push!(exprs_dedup, exp) + push!(seen, sym) + else + @warn "Expression $(expr) is aliased as $sym, which has already been used. The first definition will be used." + end + end + return (syms_dedup, exprs_dedup) + end + obs_exprs = observed(affect) for oexpr in obs_exprs invalid_vars = invalid_variables(sys, oexpr) @@ -840,6 +855,7 @@ function compile_user_affect(affect::MutatingFunctionalAffect, sys, dvs, ps; kwa end end obs_syms = observed_syms(affect) + obs_syms, obs_exprs = check_dups(obs_syms, obs_exprs) obs_size = size.(obs_exprs) # we will generate a work buffer of a ComponentArray that maps obs_syms to arrays of size obs_size mod_exprs = modified(affect) @@ -849,12 +865,18 @@ function compile_user_affect(affect::MutatingFunctionalAffect, sys, dvs, ps; kwa end invalid_vars = unassignable_variables(sys, mexpr) if length(invalid_vars) > 0 - error("Observed equation $(mexpr) in affect refers to missing variable(s) $(invalid_vars); the variables may not have been added (e.g. if a component is missing) or they may have been reduced away.") + error("Modified equation $(mexpr) in affect refers to missing variable(s) $(invalid_vars); the variables may not have been added (e.g. if a component is missing) or they may have been reduced away.") end end mod_syms = modified_syms(affect) + mod_syms, mod_exprs = check_dups(mod_syms, mod_exprs) _, mod_og_val_fun = build_explicit_observed_function(sys, mod_exprs; return_inplace=true) + overlapping_syms = intersect(mod_syms, obs_syms) + if length(overlapping_syms) > 0 + @warn "The symbols $overlapping_syms 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." + end + # sanity checks done! now build the data and update function for observed values mkzero(sz) = if sz === () 0.0 else zeros(sz) end _, obs_fun = build_explicit_observed_function(sys, reduce(vcat, Symbolics.scalarize.(obs_exprs); init = []); return_inplace=true) diff --git a/test/symbolic_events.jl b/test/symbolic_events.jl index ca9f0ad9c3..d24b590970 100644 --- a/test/symbolic_events.jl +++ b/test/symbolic_events.jl @@ -1021,6 +1021,46 @@ end @test all(sol[temp][sol.t .> 1.0] .<= 0.79) && all(sol[temp][sol.t .> 1.0] .>= 0.49) end +@testset "MutatingFunctionalAffect 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.MutatingFunctionalAffect(modified=(; furnace_on), observed=(; furnace_on)) do x, o, i, c + 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.MutatingFunctionalAffect(modified=(; furnace_on, tempsq), observed=(; furnace_on)) do x, o, i, c + 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.MutatingFunctionalAffect(modified=(; furnace_on), observed=(; furnace_on, not_actually_here)) do x, o, i, c + 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)) +end + @testset "Quadrature" begin @variables theta(t) omega(t) params = @parameters qA=0 qB=0 hA=0 hB=0 cnt=0 @@ -1040,8 +1080,6 @@ end return 0 # err is interpreted as no movement end end - # todo: warn about dups - # todo: warn if a variable appears in both observed and modified qAevt = ModelingToolkit.SymbolicContinuousCallback([cos(100 * theta) ~ 0], ModelingToolkit.MutatingFunctionalAffect((; qB), (; qA, hA, hB, cnt)) do x, o, i, c x.hA = x.qA From eec24cfb95546fb5b8a2415a8cd3a2be0d60a6a8 Mon Sep 17 00:00:00 2001 From: Ben Chung Date: Fri, 2 Aug 2024 16:56:56 -0700 Subject: [PATCH 0162/1337] Document MutatingFunctionalAffect --- src/systems/callbacks.jl | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/src/systems/callbacks.jl b/src/systems/callbacks.jl index 612d80536c..fefff94442 100644 --- a/src/systems/callbacks.jl +++ b/src/systems/callbacks.jl @@ -72,10 +72,29 @@ function namespace_affect(affect::FunctionalAffect, s) end """ -`MutatingFunctionalAffect` differs from `FunctionalAffect` in two key ways: -* First, insetad of the `u` vector passed to `f` being a vector of indices into `integ.u` it's instead the result of evaluating `obs` at the current state, named as specified in `obs_syms`. This allows affects to easily access observed states and decouples affect inputs from the system structure. -* Second, it abstracts the assignment back to system states away. Instead of writing `integ.u[u.myvar] = [whatever]`, you instead declare in `mod_params` that you want to modify `myvar` and then either (out of place) return a named tuple with `myvar` or (in place) modify the associated element in the ComponentArray that's given. -Initially, we only support "flat" states in `modified`; these states will be marked as irreducible in the overarching system and they will simply be bulk assigned at mutation. In the future, this will be extended to perform a nonlinear solve to further decouple the affect from the system structure. + MutatingFunctionalAffect(f::Function; observed::NamedTuple, modified::NamedTuple, ctx) + +`MutatingFunctionalAffect` is a helper for writing affect functions that will compute observed values and +ensure that modified values are correctly written back into the system. The affect function `f` needs to have +one of three signatures: +* `f(observed::ComponentArray)` if the function only reads observed values back from the system, +* `f(observed::ComponentArray, modified::ComponentArray)` if the function also writes values (unknowns or parameters) into the system, +* `f(observed::ComponentArray, modified::ComponentArray, ctx)` if the function needs the user-defined context, +* `f(observed::ComponentArray, modified::ComponentArray, ctx, integrator)` if the function needs the low-level integrator. + +The function `f` will be called with `observed` and `modified` `ComponentArray`s that are derived from their respective `NamedTuple` definitions. +Each `NamedTuple` should map an expression to a symbol; for example if we pass `observed=(; x = a + b)` this will alias the result of executing `a+b` in the system as `x` +so the value of `a + b` will be accessible as `observed.x` in `f`. `modified` currently restricts symbolic expressions to only bare variables, so only tuples of the form +`(; x = y)` or `(; x)` (which aliases `x` as itself) are allowed. + +Both `observed` and `modified` will be automatically populated with the current values of their corresponding expressions on function entry. +The values in `modified` will be written back to the system after `f` returns. For example, if we want to update the value of `x` to be the result of `x + y` we could write + + MutatingFunctionalAffect(observed=(; x_plus_y = x + y), modified=(; x)) do o, m + m.x = o.x_plus_y + end + +The affect function updates the value at `x` in `modified` to be the result of evaluating `x + y` as passed in the observed values. """ @kwdef struct MutatingFunctionalAffect f::Any @@ -174,6 +193,7 @@ Affects (i.e. `affect` and `affect_neg`) can be specified as either: + `read_parameters` is a vector of the parameters that are *used* by `f!`. Their indices are passed to `f` in `p` similarly to the indices of `unknowns` passed in `u`. + `modified_parameters` is a vector of the parameters that are *modified* by `f!`. Note that a parameter will not appear in `p` if it only appears in `modified_parameters`; it must appear in both `parameters` and `modified_parameters` if it is used in the affect definition. + `ctx` is a user-defined context object passed to `f!` when invoked. This value is aliased for each problem. +* A [`MutatingFunctionalAffect`](@ref); refer to its documentation for details. """ struct SymbolicContinuousCallback eqs::Vector{Equation} From fd0125d36715693c0b7e2c5d5fe130e402c137ae Mon Sep 17 00:00:00 2001 From: Ben Chung Date: Fri, 2 Aug 2024 17:15:38 -0700 Subject: [PATCH 0163/1337] Flip modified and observed order; write docstring --- src/systems/callbacks.jl | 34 +++++++++++++------ test/symbolic_events.jl | 73 +++++++++++++++++++++++++++++++--------- 2 files changed, 80 insertions(+), 27 deletions(-) diff --git a/src/systems/callbacks.jl b/src/systems/callbacks.jl index fefff94442..715b0e9a91 100644 --- a/src/systems/callbacks.jl +++ b/src/systems/callbacks.jl @@ -72,15 +72,16 @@ function namespace_affect(affect::FunctionalAffect, s) end """ - MutatingFunctionalAffect(f::Function; observed::NamedTuple, modified::NamedTuple, ctx) + MutatingFunctionalAffect(f::Function; modified::NamedTuple, observed::NamedTuple, ctx) `MutatingFunctionalAffect` is a helper for writing affect functions that will compute observed values and ensure that modified values are correctly written back into the system. The affect function `f` needs to have -one of three signatures: -* `f(observed::ComponentArray)` if the function only reads observed values back from the system, -* `f(observed::ComponentArray, modified::ComponentArray)` if the function also writes values (unknowns or parameters) into the system, -* `f(observed::ComponentArray, modified::ComponentArray, ctx)` if the function needs the user-defined context, -* `f(observed::ComponentArray, modified::ComponentArray, ctx, integrator)` if the function needs the low-level integrator. +one of four signatures: +* `f(modified::ComponentArray)` if the function only writes values (unknowns or parameters) to the system, +* `f(modified::ComponentArray, observed::ComponentArray)` if the function also reads observed values from the system, +* `f(modified::ComponentArray, observed::ComponentArray, ctx)` if the function needs the user-defined context, +* `f(modified::ComponentArray, observed::ComponentArray, ctx, integrator)` if the function needs the low-level integrator. +These will be checked in reverse order (that is, the four-argument version first, than the 3, etc). The function `f` will be called with `observed` and `modified` `ComponentArray`s that are derived from their respective `NamedTuple` definitions. Each `NamedTuple` should map an expression to a symbol; for example if we pass `observed=(; x = a + b)` this will alias the result of executing `a+b` in the system as `x` @@ -90,7 +91,7 @@ so the value of `a + b` will be accessible as `observed.x` in `f`. `modified` cu Both `observed` and `modified` will be automatically populated with the current values of their corresponding expressions on function entry. The values in `modified` will be written back to the system after `f` returns. For example, if we want to update the value of `x` to be the result of `x + y` we could write - MutatingFunctionalAffect(observed=(; x_plus_y = x + y), modified=(; x)) do o, m + MutatingFunctionalAffect(observed=(; x_plus_y = x + y), modified=(; x)) do m, o m.x = o.x_plus_y end @@ -109,11 +110,11 @@ MutatingFunctionalAffect(f::Function; observed::NamedTuple = NamedTuple{()}(()), modified::NamedTuple = NamedTuple{()}(()), ctx=nothing) = MutatingFunctionalAffect(f, collect(values(observed)), collect(keys(observed)), collect(values(modified)), collect(keys(modified)), ctx) -MutatingFunctionalAffect(f::Function, observed::NamedTuple; modified::NamedTuple = NamedTuple{()}(()), ctx=nothing) = +MutatingFunctionalAffect(f::Function, modified::NamedTuple; observed::NamedTuple = NamedTuple{()}(()), ctx=nothing) = MutatingFunctionalAffect(f, observed=observed, modified=modified, ctx=ctx) -MutatingFunctionalAffect(f::Function, observed::NamedTuple, modified::NamedTuple; ctx=nothing) = +MutatingFunctionalAffect(f::Function, modified::NamedTuple, observed::NamedTuple; ctx=nothing) = MutatingFunctionalAffect(f, observed=observed, modified=modified, ctx=ctx) -MutatingFunctionalAffect(f::Function, observed::NamedTuple, modified::NamedTuple, ctx) = +MutatingFunctionalAffect(f::Function, modified::NamedTuple, observed::NamedTuple, ctx) = MutatingFunctionalAffect(f, observed=observed, modified=modified, ctx=ctx) func(f::MutatingFunctionalAffect) = f.f @@ -925,7 +926,18 @@ function compile_user_affect(affect::MutatingFunctionalAffect, sys, dvs, ps; kwa obs_fun(obs_component_array, integ.u, integ.p..., integ.t) # let the user do their thing - user_affect(upd_component_array, obs_component_array, integ, ctx) + if applicable(user_affect, upd_component_array, obs_component_array, ctx, integ) + user_affect(upd_component_array, obs_component_array, ctx, integ) + elseif applicable(user_affect, upd_component_array, obs_component_array, ctx) + user_affect(upd_component_array, obs_component_array, ctx) + elseif applicable(user_affect, upd_component_array, obs_component_array) + user_affect(upd_component_array, obs_component_array) + elseif applicable(user_affect, upd_component_array) + user_affect(upd_component_array) + else + @error "User affect function $user_affect needs to implement one of the supported MutatingFunctionalAffect callback forms; see the MutatingFunctionalAffect docstring for more details" + user_affect(upd_component_array, obs_component_array, integ, ctx) # this WILL error but it'll give a more sensible message + end # write the new values back to the integrator upd_params_fun(integ, upd_params_view) diff --git a/test/symbolic_events.jl b/test/symbolic_events.jl index d24b590970..779f41471a 100644 --- a/test/symbolic_events.jl +++ b/test/symbolic_events.jl @@ -250,19 +250,19 @@ end m = ModelingToolkit.MutatingFunctionalAffect(fmfa, (; x)) @test m isa ModelingToolkit.MutatingFunctionalAffect @test m.f == fmfa - @test isequal(m.obs, [x]) - @test m.obs_syms == [:x] - @test m.modified == [] - @test m.mod_syms == [] + @test isequal(m.obs, []) + @test m.obs_syms == [] + @test isequal(m.modified, [x]) + @test m.mod_syms == [:x] @test m.ctx === nothing m = ModelingToolkit.MutatingFunctionalAffect(fmfa, (; y=x)) @test m isa ModelingToolkit.MutatingFunctionalAffect @test m.f == fmfa - @test isequal(m.obs, [x]) - @test m.obs_syms == [:y] - @test m.modified == [] - @test m.mod_syms == [] + @test isequal(m.obs, []) + @test m.obs_syms == [] + @test isequal(m.modified, [x]) + @test m.mod_syms == [:y] @test m.ctx === nothing m = ModelingToolkit.MutatingFunctionalAffect(fmfa; observed=(; y=x)) @@ -1013,7 +1013,48 @@ end ModelingToolkit.MutatingFunctionalAffect(modified=(; furnace_on)) do x, o, i, c 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.MutatingFunctionalAffect(modified=(; furnace_on)) do x, o, i + x.furnace_on = false + end) + furnace_enable = ModelingToolkit.SymbolicContinuousCallback([temp ~ furnace_on_threshold], + ModelingToolkit.MutatingFunctionalAffect(modified=(; furnace_on)) do x, o, i + 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.MutatingFunctionalAffect(modified=(; furnace_on)) do x, o + x.furnace_on = false + end) + furnace_enable = ModelingToolkit.SymbolicContinuousCallback([temp ~ furnace_on_threshold], + ModelingToolkit.MutatingFunctionalAffect(modified=(; furnace_on)) do x, o + 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.MutatingFunctionalAffect(modified=(; furnace_on)) do x + x.furnace_on = false + end) + furnace_enable = ModelingToolkit.SymbolicContinuousCallback([temp ~ furnace_on_threshold], + ModelingToolkit.MutatingFunctionalAffect(modified=(; furnace_on)) do x + 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)) @@ -1029,7 +1070,7 @@ end ] furnace_off = ModelingToolkit.SymbolicContinuousCallback([temp ~ furnace_off_threshold], - ModelingToolkit.MutatingFunctionalAffect(modified=(; furnace_on), observed=(; furnace_on)) do x, o, i, c + ModelingToolkit.MutatingFunctionalAffect(modified=(; furnace_on), observed=(; furnace_on)) do x, o, c, i x.furnace_on = false end) @named sys = ODESystem(eqs, t, [temp], params; continuous_events = [furnace_off]) @@ -1043,7 +1084,7 @@ end ] furnace_off = ModelingToolkit.SymbolicContinuousCallback([temp ~ furnace_off_threshold], - ModelingToolkit.MutatingFunctionalAffect(modified=(; furnace_on, tempsq), observed=(; furnace_on)) do x, o, i, c + ModelingToolkit.MutatingFunctionalAffect(modified=(; furnace_on, tempsq), observed=(; furnace_on)) do x, o, c, i x.furnace_on = false end) @named sys = ODESystem(eqs, t, [temp, tempsq], params; continuous_events = [furnace_off]) @@ -1053,7 +1094,7 @@ end @parameters not_actually_here furnace_off = ModelingToolkit.SymbolicContinuousCallback([temp ~ furnace_off_threshold], - ModelingToolkit.MutatingFunctionalAffect(modified=(; furnace_on), observed=(; furnace_on, not_actually_here)) do x, o, i, c + ModelingToolkit.MutatingFunctionalAffect(modified=(; furnace_on), observed=(; furnace_on, not_actually_here)) do x, o, c, i x.furnace_on = false end) @named sys = ODESystem(eqs, t, [temp, tempsq], params; continuous_events = [furnace_off]) @@ -1081,26 +1122,26 @@ end end end qAevt = ModelingToolkit.SymbolicContinuousCallback([cos(100 * theta) ~ 0], - ModelingToolkit.MutatingFunctionalAffect((; qB), (; qA, hA, hB, cnt)) do x, o, i, c + ModelingToolkit.MutatingFunctionalAffect((; qA, hA, hB, cnt), (; qB)) do x, o, i, c x.hA = x.qA x.hB = o.qB x.qA = 1 x.cnt += decoder(x.hA, x.hB, x.qA, o.qB) end, - affect_neg = ModelingToolkit.MutatingFunctionalAffect((; qB), (; qA, hA, hB, cnt)) do x, o, i, c + affect_neg = ModelingToolkit.MutatingFunctionalAffect((; qA, hA, hB, cnt), (; qB)) do x, o, c, i x.hA = x.qA x.hB = o.qB x.qA = 0 x.cnt += decoder(x.hA, x.hB, x.qA, o.qB) end; rootfind=SciMLBase.RightRootFind) qBevt = ModelingToolkit.SymbolicContinuousCallback([cos(100 * theta - π/2) ~ 0], - ModelingToolkit.MutatingFunctionalAffect((; qA), (; qB, hA, hB, cnt)) do x, o, i, c + ModelingToolkit.MutatingFunctionalAffect((; qB, hA, hB, cnt), (; qA)) do x, o, i, c x.hA = o.qA x.hB = x.qB x.qB = 1 x.cnt += decoder(x.hA, x.hB, o.qA, x.qB) end, - affect_neg = ModelingToolkit.MutatingFunctionalAffect((; qA), (; qB, hA, hB, cnt)) do x, o, i, c + affect_neg = ModelingToolkit.MutatingFunctionalAffect((; qB, hA, hB, cnt), (; qA)) do x, o, c, i x.hA = o.qA x.hB = x.qB x.qB = 0 From 9948de076b491a114b395f8a9168eaaebe0959b1 Mon Sep 17 00:00:00 2001 From: Ben Chung Date: Fri, 2 Aug 2024 17:17:12 -0700 Subject: [PATCH 0164/1337] Run formatter --- src/systems/callbacks.jl | 87 ++++++++++++------- test/symbolic_events.jl | 175 ++++++++++++++++++++++----------------- 2 files changed, 158 insertions(+), 104 deletions(-) diff --git a/src/systems/callbacks.jl b/src/systems/callbacks.jl index 715b0e9a91..5c6f9b2801 100644 --- a/src/systems/callbacks.jl +++ b/src/systems/callbacks.jl @@ -106,16 +106,25 @@ The affect function updates the value at `x` in `modified` to be the result of e ctx::Any end -MutatingFunctionalAffect(f::Function; - observed::NamedTuple = NamedTuple{()}(()), - modified::NamedTuple = NamedTuple{()}(()), - ctx=nothing) = MutatingFunctionalAffect(f, collect(values(observed)), collect(keys(observed)), collect(values(modified)), collect(keys(modified)), ctx) -MutatingFunctionalAffect(f::Function, modified::NamedTuple; observed::NamedTuple = NamedTuple{()}(()), ctx=nothing) = - MutatingFunctionalAffect(f, observed=observed, modified=modified, ctx=ctx) -MutatingFunctionalAffect(f::Function, modified::NamedTuple, observed::NamedTuple; ctx=nothing) = - MutatingFunctionalAffect(f, observed=observed, modified=modified, ctx=ctx) -MutatingFunctionalAffect(f::Function, modified::NamedTuple, observed::NamedTuple, ctx) = - MutatingFunctionalAffect(f, observed=observed, modified=modified, ctx=ctx) +function MutatingFunctionalAffect(f::Function; + observed::NamedTuple = NamedTuple{()}(()), + modified::NamedTuple = NamedTuple{()}(()), + ctx = nothing) + MutatingFunctionalAffect(f, collect(values(observed)), collect(keys(observed)), + collect(values(modified)), collect(keys(modified)), ctx) +end +function MutatingFunctionalAffect(f::Function, modified::NamedTuple; + observed::NamedTuple = NamedTuple{()}(()), ctx = nothing) + MutatingFunctionalAffect(f, observed = observed, modified = modified, ctx = ctx) +end +function MutatingFunctionalAffect( + f::Function, modified::NamedTuple, observed::NamedTuple; ctx = nothing) + MutatingFunctionalAffect(f, observed = observed, modified = modified, ctx = ctx) +end +function MutatingFunctionalAffect( + f::Function, modified::NamedTuple, observed::NamedTuple, ctx) + MutatingFunctionalAffect(f, observed = observed, modified = modified, ctx = ctx) +end func(f::MutatingFunctionalAffect) = f.f context(a::MutatingFunctionalAffect) = a.ctx @@ -126,8 +135,9 @@ modified(a::MutatingFunctionalAffect) = a.modified modified_syms(a::MutatingFunctionalAffect) = a.mod_syms function Base.:(==)(a1::MutatingFunctionalAffect, a2::MutatingFunctionalAffect) - isequal(a1.f, a2.f) && isequal(a1.obs, a2.obs) && isequal(a1.modified, a2.modified) && - isequal(a1.obs_syms, a2.obs_syms) && isequal(a1.mod_syms, a2.mod_syms)&& isequal(a1.ctx, a2.ctx) + isequal(a1.f, a2.f) && isequal(a1.obs, a2.obs) && isequal(a1.modified, a2.modified) && + isequal(a1.obs_syms, a2.obs_syms) && isequal(a1.mod_syms, a2.mod_syms) && + isequal(a1.ctx, a2.ctx) end function Base.hash(a::MutatingFunctionalAffect, s::UInt) @@ -839,10 +849,13 @@ function compile_user_affect(affect::FunctionalAffect, cb, sys, dvs, ps; kwargs. end end -invalid_variables(sys, expr) = filter(x -> !any(isequal(x), all_symbols(sys)), reduce(vcat, vars(expr); init=[])) -function unassignable_variables(sys, expr) +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 = vcat(unknowns(sys), parameters(sys)) - return filter(x -> !any(isequal(x), assignable_syms), reduce(vcat, vars(expr); init=[])) + return filter( + x -> !any(isequal(x), assignable_syms), reduce(vcat, vars(expr); init = [])) end function compile_user_affect(affect::MutatingFunctionalAffect, sys, dvs, ps; kwargs...) @@ -855,7 +868,8 @@ function compile_user_affect(affect::MutatingFunctionalAffect, sys, dvs, ps; kwa =# function check_dups(syms, exprs) # = (syms_dedup, exprs_dedup) seen = Set{Symbol}() - syms_dedup = []; exprs_dedup = [] + syms_dedup = [] + exprs_dedup = [] for (sym, exp) in Iterators.zip(syms, exprs) if !in(sym, seen) push!(syms_dedup, sym) @@ -869,7 +883,7 @@ function compile_user_affect(affect::MutatingFunctionalAffect, sys, dvs, ps; kwa end obs_exprs = observed(affect) - for oexpr in obs_exprs + for oexpr in obs_exprs invalid_vars = invalid_variables(sys, oexpr) if length(invalid_vars) > 0 error("Observed equation $(oexpr) in affect refers to missing variable(s) $(invalid_vars); the variables may not have been added (e.g. if a component is missing).") @@ -880,7 +894,7 @@ function compile_user_affect(affect::MutatingFunctionalAffect, sys, dvs, ps; kwa obs_size = size.(obs_exprs) # we will generate a work buffer of a ComponentArray that maps obs_syms to arrays of size obs_size mod_exprs = modified(affect) - for mexpr in mod_exprs + for mexpr in mod_exprs if !is_observed(sys, mexpr) && parameter_index(sys, mexpr) === nothing error("Expression $mexpr cannot be assigned to; currently only unknowns and parameters may be updated by an affect.") end @@ -891,7 +905,8 @@ function compile_user_affect(affect::MutatingFunctionalAffect, sys, dvs, ps; kwa end mod_syms = modified_syms(affect) mod_syms, mod_exprs = check_dups(mod_syms, mod_exprs) - _, mod_og_val_fun = build_explicit_observed_function(sys, mod_exprs; return_inplace=true) + _, mod_og_val_fun = build_explicit_observed_function( + sys, mod_exprs; return_inplace = true) overlapping_syms = intersect(mod_syms, obs_syms) if length(overlapping_syms) > 0 @@ -899,21 +914,33 @@ function compile_user_affect(affect::MutatingFunctionalAffect, sys, dvs, ps; kwa end # sanity checks done! now build the data and update function for observed values - mkzero(sz) = if sz === () 0.0 else zeros(sz) end - _, obs_fun = build_explicit_observed_function(sys, reduce(vcat, Symbolics.scalarize.(obs_exprs); init = []); return_inplace=true) - obs_component_array = ComponentArrays.ComponentArray(NamedTuple{(obs_syms..., )}(mkzero.(obs_size))) + mkzero(sz) = + if sz === () + 0.0 + else + zeros(sz) + end + _, obs_fun = build_explicit_observed_function( + sys, reduce(vcat, Symbolics.scalarize.(obs_exprs); init = []); + return_inplace = true) + obs_component_array = ComponentArrays.ComponentArray(NamedTuple{(obs_syms...,)}(mkzero.(obs_size))) # okay so now to generate the stuff to assign it back into the system # note that we reorder the componentarray to make the views coherent wrt the base array mod_pairs = mod_exprs .=> mod_syms mod_param_pairs = filter(v -> is_parameter(sys, v[1]), mod_pairs) mod_unk_pairs = filter(v -> !is_parameter(sys, v[1]), mod_pairs) - _, mod_og_val_fun = build_explicit_observed_function(sys, reduce(vcat, [first.(mod_param_pairs); first.(mod_unk_pairs)]; init = []); return_inplace=true) - upd_params_fun = setu(sys, reduce(vcat, Symbolics.scalarize.(first.(mod_param_pairs)); init = [])) - upd_unk_fun = setu(sys, reduce(vcat, Symbolics.scalarize.(first.(mod_unk_pairs)); init = [])) - - upd_component_array = ComponentArrays.ComponentArray(NamedTuple{([last.(mod_param_pairs); last.(mod_unk_pairs)]...,)}( - [collect(mkzero(size(e)) for e in first.(mod_param_pairs)); + _, mod_og_val_fun = build_explicit_observed_function( + sys, reduce(vcat, [first.(mod_param_pairs); first.(mod_unk_pairs)]; init = []); + return_inplace = true) + upd_params_fun = setu( + sys, reduce(vcat, Symbolics.scalarize.(first.(mod_param_pairs)); init = [])) + upd_unk_fun = setu( + sys, reduce(vcat, Symbolics.scalarize.(first.(mod_unk_pairs)); init = [])) + + upd_component_array = ComponentArrays.ComponentArray(NamedTuple{([last.(mod_param_pairs); + last.(mod_unk_pairs)]...,)}( + [collect(mkzero(size(e)) for e in first.(mod_param_pairs)); collect(mkzero(size(e)) for e in first.(mod_unk_pairs))])) upd_params_view = view(upd_component_array, last.(mod_param_pairs)) upd_unks_view = view(upd_component_array, last.(mod_unk_pairs)) @@ -921,7 +948,7 @@ function compile_user_affect(affect::MutatingFunctionalAffect, sys, dvs, ps; kwa function (integ) # update the to-be-mutated values; this ensures that if you do a no-op then nothing happens mod_og_val_fun(upd_component_array, integ.u, integ.p..., integ.t) - + # update the observed values obs_fun(obs_component_array, integ.u, integ.p..., integ.t) @@ -934,7 +961,7 @@ function compile_user_affect(affect::MutatingFunctionalAffect, sys, dvs, ps; kwa user_affect(upd_component_array, obs_component_array) elseif applicable(user_affect, upd_component_array) user_affect(upd_component_array) - else + else @error "User affect function $user_affect needs to implement one of the supported MutatingFunctionalAffect callback forms; see the MutatingFunctionalAffect docstring for more details" user_affect(upd_component_array, obs_component_array, integ, ctx) # this WILL error but it'll give a more sensible message end diff --git a/test/symbolic_events.jl b/test/symbolic_events.jl index 779f41471a..7aba69dc7f 100644 --- a/test/symbolic_events.jl +++ b/test/symbolic_events.jl @@ -237,7 +237,7 @@ end @test m.modified == [] @test m.mod_syms == [] @test m.ctx === nothing - + m = ModelingToolkit.MutatingFunctionalAffect(fmfa, (;)) @test m isa ModelingToolkit.MutatingFunctionalAffect @test m.f == fmfa @@ -246,7 +246,7 @@ end @test m.modified == [] @test m.mod_syms == [] @test m.ctx === nothing - + m = ModelingToolkit.MutatingFunctionalAffect(fmfa, (; x)) @test m isa ModelingToolkit.MutatingFunctionalAffect @test m.f == fmfa @@ -255,8 +255,8 @@ end @test isequal(m.modified, [x]) @test m.mod_syms == [:x] @test m.ctx === nothing - - m = ModelingToolkit.MutatingFunctionalAffect(fmfa, (; y=x)) + + m = ModelingToolkit.MutatingFunctionalAffect(fmfa, (; y = x)) @test m isa ModelingToolkit.MutatingFunctionalAffect @test m.f == fmfa @test isequal(m.obs, []) @@ -264,8 +264,8 @@ end @test isequal(m.modified, [x]) @test m.mod_syms == [:y] @test m.ctx === nothing - - m = ModelingToolkit.MutatingFunctionalAffect(fmfa; observed=(; y=x)) + + m = ModelingToolkit.MutatingFunctionalAffect(fmfa; observed = (; y = x)) @test m isa ModelingToolkit.MutatingFunctionalAffect @test m.f == fmfa @test isequal(m.obs, [x]) @@ -273,8 +273,8 @@ end @test m.modified == [] @test m.mod_syms == [] @test m.ctx === nothing - - m = ModelingToolkit.MutatingFunctionalAffect(fmfa; modified=(; x)) + + m = ModelingToolkit.MutatingFunctionalAffect(fmfa; modified = (; x)) @test m isa ModelingToolkit.MutatingFunctionalAffect @test m.f == fmfa @test isequal(m.obs, []) @@ -283,7 +283,7 @@ end @test m.mod_syms == [:x] @test m.ctx === nothing - m = ModelingToolkit.MutatingFunctionalAffect(fmfa; modified=(; y=x)) + m = ModelingToolkit.MutatingFunctionalAffect(fmfa; modified = (; y = x)) @test m isa ModelingToolkit.MutatingFunctionalAffect @test m.f == fmfa @test isequal(m.obs, []) @@ -291,7 +291,7 @@ end @test isequal(m.modified, [x]) @test m.mod_syms == [:y] @test m.ctx === nothing - + m = ModelingToolkit.MutatingFunctionalAffect(fmfa, (; x), (; x)) @test m isa ModelingToolkit.MutatingFunctionalAffect @test m.f == fmfa @@ -300,8 +300,8 @@ end @test isequal(m.modified, [x]) @test m.mod_syms == [:x] @test m.ctx === nothing - - m = ModelingToolkit.MutatingFunctionalAffect(fmfa, (; y=x), (; y=x)) + + m = ModelingToolkit.MutatingFunctionalAffect(fmfa, (; y = x), (; y = x)) @test m isa ModelingToolkit.MutatingFunctionalAffect @test m.f == fmfa @test isequal(m.obs, [x]) @@ -309,8 +309,9 @@ end @test isequal(m.modified, [x]) @test m.mod_syms == [:y] @test m.ctx === nothing - - m = ModelingToolkit.MutatingFunctionalAffect(fmfa; modified=(; y=x), observed=(; y=x)) + + m = ModelingToolkit.MutatingFunctionalAffect( + fmfa; modified = (; y = x), observed = (; y = x)) @test m isa ModelingToolkit.MutatingFunctionalAffect @test m.f == fmfa @test isequal(m.obs, [x]) @@ -318,8 +319,9 @@ end @test isequal(m.modified, [x]) @test m.mod_syms == [:y] @test m.ctx === nothing - - m = ModelingToolkit.MutatingFunctionalAffect(fmfa; modified=(; y=x), observed=(; y=x), ctx=3) + + m = ModelingToolkit.MutatingFunctionalAffect( + fmfa; modified = (; y = x), observed = (; y = x), ctx = 3) @test m isa ModelingToolkit.MutatingFunctionalAffect @test m.f == fmfa @test isequal(m.obs, [x]) @@ -327,7 +329,7 @@ end @test isequal(m.modified, [x]) @test m.mod_syms == [:y] @test m.ctx === 3 - + m = ModelingToolkit.MutatingFunctionalAffect(fmfa, (; x), (; x), 3) @test m isa ModelingToolkit.MutatingFunctionalAffect @test m.f == fmfa @@ -1005,151 +1007,176 @@ end D(temp) ~ furnace_on * furnace_power - temp^2 * leakage ] - furnace_off = ModelingToolkit.SymbolicContinuousCallback([temp ~ furnace_off_threshold], - ModelingToolkit.MutatingFunctionalAffect(modified=(; furnace_on)) do x, o, i, c + furnace_off = ModelingToolkit.SymbolicContinuousCallback( + [temp ~ furnace_off_threshold], + ModelingToolkit.MutatingFunctionalAffect(modified = (; furnace_on)) do x, o, i, c x.furnace_on = false end) - furnace_enable = ModelingToolkit.SymbolicContinuousCallback([temp ~ furnace_on_threshold], - ModelingToolkit.MutatingFunctionalAffect(modified=(; furnace_on)) do x, o, i, c + furnace_enable = ModelingToolkit.SymbolicContinuousCallback( + [temp ~ furnace_on_threshold], + ModelingToolkit.MutatingFunctionalAffect(modified = (; furnace_on)) do x, o, i, c x.furnace_on = true end) - @named sys = ODESystem(eqs, t, [temp], params; continuous_events = [furnace_off, furnace_enable]) + @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) + 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.MutatingFunctionalAffect(modified=(; furnace_on)) do x, o, i + furnace_off = ModelingToolkit.SymbolicContinuousCallback( + [temp ~ furnace_off_threshold], + ModelingToolkit.MutatingFunctionalAffect(modified = (; furnace_on)) do x, o, i x.furnace_on = false end) - furnace_enable = ModelingToolkit.SymbolicContinuousCallback([temp ~ furnace_on_threshold], - ModelingToolkit.MutatingFunctionalAffect(modified=(; furnace_on)) do x, o, i + furnace_enable = ModelingToolkit.SymbolicContinuousCallback( + [temp ~ furnace_on_threshold], + ModelingToolkit.MutatingFunctionalAffect(modified = (; furnace_on)) do x, o, i x.furnace_on = true end) - @named sys = ODESystem(eqs, t, [temp], params; continuous_events = [furnace_off, furnace_enable]) + @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) + 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.MutatingFunctionalAffect(modified=(; furnace_on)) do x, o + furnace_off = ModelingToolkit.SymbolicContinuousCallback( + [temp ~ furnace_off_threshold], + ModelingToolkit.MutatingFunctionalAffect(modified = (; furnace_on)) do x, o x.furnace_on = false end) - furnace_enable = ModelingToolkit.SymbolicContinuousCallback([temp ~ furnace_on_threshold], - ModelingToolkit.MutatingFunctionalAffect(modified=(; furnace_on)) do x, o + furnace_enable = ModelingToolkit.SymbolicContinuousCallback( + [temp ~ furnace_on_threshold], + ModelingToolkit.MutatingFunctionalAffect(modified = (; furnace_on)) do x, o x.furnace_on = true end) - @named sys = ODESystem(eqs, t, [temp], params; continuous_events = [furnace_off, furnace_enable]) + @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) + 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.MutatingFunctionalAffect(modified=(; furnace_on)) do x + furnace_off = ModelingToolkit.SymbolicContinuousCallback( + [temp ~ furnace_off_threshold], + ModelingToolkit.MutatingFunctionalAffect(modified = (; furnace_on)) do x x.furnace_on = false end) - furnace_enable = ModelingToolkit.SymbolicContinuousCallback([temp ~ furnace_on_threshold], - ModelingToolkit.MutatingFunctionalAffect(modified=(; furnace_on)) do x + furnace_enable = ModelingToolkit.SymbolicContinuousCallback( + [temp ~ furnace_on_threshold], + ModelingToolkit.MutatingFunctionalAffect(modified = (; furnace_on)) do x x.furnace_on = true end) - @named sys = ODESystem(eqs, t, [temp], params; continuous_events = [furnace_off, furnace_enable]) + @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) + 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) end -@testset "MutatingFunctionalAffect errors and warnings" begin +@testset "MutatingFunctionalAffect 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.MutatingFunctionalAffect(modified=(; furnace_on), observed=(; furnace_on)) do x, o, c, i + furnace_off = ModelingToolkit.SymbolicContinuousCallback( + [temp ~ furnace_off_threshold], + ModelingToolkit.MutatingFunctionalAffect( + modified = (; furnace_on), observed = (; furnace_on)) do x, o, c, i 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)) + @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 - ] + eqs = [tempsq ~ temp^2 + D(temp) ~ furnace_on * furnace_power - temp^2 * leakage] - furnace_off = ModelingToolkit.SymbolicContinuousCallback([temp ~ furnace_off_threshold], - ModelingToolkit.MutatingFunctionalAffect(modified=(; furnace_on, tempsq), observed=(; furnace_on)) do x, o, c, i + furnace_off = ModelingToolkit.SymbolicContinuousCallback( + [temp ~ furnace_off_threshold], + ModelingToolkit.MutatingFunctionalAffect( + modified = (; furnace_on, tempsq), observed = (; furnace_on)) do x, o, c, i x.furnace_on = false end) - @named sys = ODESystem(eqs, t, [temp, tempsq], params; continuous_events = [furnace_off]) + @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)) + @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.MutatingFunctionalAffect(modified=(; furnace_on), observed=(; furnace_on, not_actually_here)) do x, o, c, i + furnace_off = ModelingToolkit.SymbolicContinuousCallback( + [temp ~ furnace_off_threshold], + ModelingToolkit.MutatingFunctionalAffect(modified = (; furnace_on), + observed = (; furnace_on, not_actually_here)) do x, o, c, i x.furnace_on = false end) - @named sys = ODESystem(eqs, t, [temp, tempsq], params; continuous_events = [furnace_off]) + @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)) + @test_throws "refers to missing variable(s)" prob=ODEProblem( + ss, [temp => 0.0, furnace_on => true], (0.0, 100.0)) end -@testset "Quadrature" begin +@testset "Quadrature" begin @variables theta(t) omega(t) params = @parameters qA=0 qB=0 hA=0 hB=0 cnt=0 - eqs = [ - D(theta) ~ omega - omega ~ 1.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) + 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) + 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) + 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], + qAevt = ModelingToolkit.SymbolicContinuousCallback([cos(100 * theta) ~ 0], ModelingToolkit.MutatingFunctionalAffect((; qA, hA, hB, cnt), (; qB)) do x, o, i, c x.hA = x.qA x.hB = o.qB x.qA = 1 x.cnt += decoder(x.hA, x.hB, x.qA, o.qB) end, - affect_neg = ModelingToolkit.MutatingFunctionalAffect((; qA, hA, hB, cnt), (; qB)) do x, o, c, i + affect_neg = ModelingToolkit.MutatingFunctionalAffect( + (; qA, hA, hB, cnt), (; qB)) do x, o, c, i x.hA = x.qA x.hB = o.qB x.qA = 0 x.cnt += decoder(x.hA, x.hB, x.qA, o.qB) - end; rootfind=SciMLBase.RightRootFind) - qBevt = ModelingToolkit.SymbolicContinuousCallback([cos(100 * theta - π/2) ~ 0], + end; rootfind = SciMLBase.RightRootFind) + qBevt = ModelingToolkit.SymbolicContinuousCallback([cos(100 * theta - π / 2) ~ 0], ModelingToolkit.MutatingFunctionalAffect((; qB, hA, hB, cnt), (; qA)) do x, o, i, c x.hA = o.qA x.hB = x.qB x.qB = 1 x.cnt += decoder(x.hA, x.hB, o.qA, x.qB) end, - affect_neg = ModelingToolkit.MutatingFunctionalAffect((; qB, hA, hB, cnt), (; qA)) do x, o, c, i + affect_neg = ModelingToolkit.MutatingFunctionalAffect( + (; qB, hA, hB, cnt), (; qA)) do x, o, c, i x.hA = o.qA x.hB = x.qB x.qB = 0 x.cnt += decoder(x.hA, x.hB, o.qA, x.qB) - end; rootfind=SciMLBase.RightRootFind) - @named sys = ODESystem(eqs, t, [theta, omega], params; continuous_events = [qAevt, qBevt]) + end; rootfind = SciMLBase.RightRootFind) + @named sys = ODESystem( + eqs, t, [theta, omega], params; continuous_events = [qAevt, qBevt]) ss = structural_simplify(sys) prob = ODEProblem(ss, [theta => 0.0], (0.0, pi)) - sol = solve(prob, Tsit5(); dtmax=0.01) + sol = solve(prob, Tsit5(); dtmax = 0.01) @test sol[cnt] == 197 # 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 From 61cf6762c43e595448ed6f37f37bd889db663b10 Mon Sep 17 00:00:00 2001 From: Ben Chung Date: Fri, 2 Aug 2024 18:44:18 -0700 Subject: [PATCH 0165/1337] FIx SCC reconstruction --- src/systems/callbacks.jl | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/systems/callbacks.jl b/src/systems/callbacks.jl index 5c6f9b2801..8ea7d9f506 100644 --- a/src/systems/callbacks.jl +++ b/src/systems/callbacks.jl @@ -284,13 +284,15 @@ 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(af::MutatingFunctionalAffect, s) = namespace_affect(af, s) namespace_affects(::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)) + SymbolicContinuousCallback(; + eqs = namespace_equation.(equations(cb), (s,)), + affect = namespace_affects(affects(cb), s), + affect_neg = namespace_affects(affect_negs(cb), s), + rootfind = cb.rootfind) end """ From 8edef14b63637a4132e5139540d3ec89fa9d39fd Mon Sep 17 00:00:00 2001 From: Benjamin Chung Date: Mon, 19 Aug 2024 07:49:33 -0700 Subject: [PATCH 0166/1337] Implement initialize and finalize affects for symbolic callbacks --- src/systems/callbacks.jl | 206 +++++++++++++++++++++++++++++---------- 1 file changed, 154 insertions(+), 52 deletions(-) diff --git a/src/systems/callbacks.jl b/src/systems/callbacks.jl index 8ea7d9f506..d14fd50691 100644 --- a/src/systems/callbacks.jl +++ b/src/systems/callbacks.jl @@ -104,28 +104,38 @@ The affect function updates the value at `x` in `modified` to be the result of e modified::Vector mod_syms::Vector{Symbol} ctx::Any + skip_checks::Bool end function MutatingFunctionalAffect(f::Function; observed::NamedTuple = NamedTuple{()}(()), modified::NamedTuple = NamedTuple{()}(()), - ctx = nothing) - MutatingFunctionalAffect(f, collect(values(observed)), collect(keys(observed)), - collect(values(modified)), collect(keys(modified)), ctx) + ctx = nothing, + skip_checks = false) + MutatingFunctionalAffect(f, + collect(values(observed)), collect(keys(observed)), + collect(values(modified)), collect(keys(modified)), + ctx, skip_checks) end function MutatingFunctionalAffect(f::Function, modified::NamedTuple; - observed::NamedTuple = NamedTuple{()}(()), ctx = nothing) - MutatingFunctionalAffect(f, observed = observed, modified = modified, ctx = ctx) + observed::NamedTuple = NamedTuple{()}(()), ctx = nothing, skip_checks=false) + MutatingFunctionalAffect(f, observed = observed, modified = modified, ctx = ctx, skip_checks = skip_checks) end function MutatingFunctionalAffect( - f::Function, modified::NamedTuple, observed::NamedTuple; ctx = nothing) - MutatingFunctionalAffect(f, observed = observed, modified = modified, ctx = ctx) + f::Function, modified::NamedTuple, observed::NamedTuple; ctx = nothing, skip_checks=false) + MutatingFunctionalAffect(f, observed = observed, modified = modified, ctx = ctx, skip_checks = skip_checks) end function MutatingFunctionalAffect( - f::Function, modified::NamedTuple, observed::NamedTuple, ctx) - MutatingFunctionalAffect(f, observed = observed, modified = modified, ctx = ctx) + f::Function, modified::NamedTuple, observed::NamedTuple, ctx; skip_checks=false) + MutatingFunctionalAffect(f, observed = observed, modified = modified, ctx = ctx, skip_checks = skip_checks) end +function Base.show(io::IO, mfa::MutatingFunctionalAffect) + obs_vals = join(map((ob,nm) -> "$ob => $nm", mfa.obs, mfa.obs_syms), ", ") + mod_vals = join(map((md,nm) -> "$md => $nm", mfa.modified, mfa.mod_syms), ", ") + affect = mfa.f + print(io, "MutatingFunctionalAffect(observed: [$obs_vals], modified: [$mod_vals], affect:$affect)") +end func(f::MutatingFunctionalAffect) = f.f context(a::MutatingFunctionalAffect) = a.ctx observed(a::MutatingFunctionalAffect) = a.obs @@ -208,12 +218,19 @@ Affects (i.e. `affect` and `affect_neg`) can be specified as either: """ struct SymbolicContinuousCallback eqs::Vector{Equation} + initialize::Union{Vector{Equation}, FunctionalAffect, MutatingFunctionalAffect} + finalize::Union{Vector{Equation}, FunctionalAffect, MutatingFunctionalAffect} affect::Union{Vector{Equation}, FunctionalAffect, MutatingFunctionalAffect} affect_neg::Union{Vector{Equation}, FunctionalAffect, MutatingFunctionalAffect, Nothing} rootfind::SciMLBase.RootfindOpt - function SymbolicContinuousCallback(; eqs::Vector{Equation}, affect = NULL_AFFECT, - affect_neg = affect, rootfind = SciMLBase.LeftRootFind) - new(eqs, make_affect(affect), make_affect(affect_neg), rootfind) + function SymbolicContinuousCallback(; + eqs::Vector{Equation}, + affect = NULL_AFFECT, + affect_neg = affect, + rootfind = SciMLBase.LeftRootFind, + initialize=NULL_AFFECT, + finalize=NULL_AFFECT) + new(eqs, initialize, finalize, make_affect(affect), make_affect(affect_neg), rootfind) end # Default affect to nothing end make_affect(affect) = affect @@ -221,18 +238,81 @@ 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.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 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(cb.affect, s) s = foldr(hash, cb.eqs, init = s) - s = cb.affect isa AbstractVector ? foldr(hash, cb.affect, init = s) : hash(cb.affect, s) - s = cb.affect_neg isa AbstractVector ? foldr(hash, cb.affect_neg, init = s) : - hash(cb.affect_neg, 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) hash(cb.rootfind, s) end + +function Base.show(io::IO, cb::SymbolicContinuousCallback) + indent = get(io, :indent, 0) + iio = IOContext(io, :indent => indent+1) + print(io, "SymbolicContinuousCallback(") + print(iio, "Equations:") + show(iio, equations(cb)) + print(iio, "; ") + if affects(cb) != NULL_AFFECT + print(iio, "Affect:") + show(iio, affects(cb)) + print(iio, ", ") + end + if affect_negs(cb) != NULL_AFFECT + print(iio, "Negative-edge affect:") + show(iio, affect_negs(cb)) + print(iio, ", ") + end + if initialize_affects(cb) != NULL_AFFECT + print(iio, "Initialization affect:") + show(iio, initialize_affects(cb)) + print(iio, ", ") + end + if finalize_affects(cb) != NULL_AFFECT + print(iio, "Finalization affect:") + show(iio, finalize_affects(cb)) + end + print(iio, ")") +end + +function Base.show(io::IO, mime::MIME"text/plain", cb::SymbolicContinuousCallback) + indent = get(io, :indent, 0) + iio = IOContext(io, :indent => indent+1) + println(io, "SymbolicContinuousCallback:") + println(iio, "Equations:") + show(iio, mime, equations(cb)) + print(iio, "\n") + if affects(cb) != NULL_AFFECT + println(iio, "Affect:") + show(iio, mime, affects(cb)) + print(iio, "\n") + end + if affect_negs(cb) != NULL_AFFECT + println(iio, "Negative-edge affect:") + show(iio, mime, affect_negs(cb)) + print(iio, "\n") + end + if initialize_affects(cb) != NULL_AFFECT + println(iio, "Initialization affect:") + show(iio, mime, initialize_affects(cb)) + print(iio, "\n") + end + if finalize_affects(cb) != NULL_AFFECT + println(iio, "Finalization affect:") + show(iio, mime, finalize_affects(cb)) + print(iio, "\n") + end +end + to_equation_vector(eq::Equation) = [eq] to_equation_vector(eqs::Vector{Equation}) = eqs function to_equation_vector(eqs::Vector{Any}) @@ -246,14 +326,14 @@ 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; - affect_neg = affect, rootfind = SciMLBase.LeftRootFind) + affect_neg = affect, rootfind = SciMLBase.LeftRootFind, initialize = NULL_AFFECT, finalize = NULL_AFFECT) SymbolicContinuousCallback( - eqs = [eqs], affect = affect, affect_neg = affect_neg, rootfind = rootfind) + eqs = [eqs], affect = affect, affect_neg = affect_neg, rootfind = rootfind, initialize=initialize, finalize=finalize) end function SymbolicContinuousCallback(eqs::Vector{Equation}, affect = NULL_AFFECT; - affect_neg = affect, rootfind = SciMLBase.LeftRootFind) + affect_neg = affect, rootfind = SciMLBase.LeftRootFind, initialize = NULL_AFFECT, finalize = NULL_AFFECT) SymbolicContinuousCallback( - eqs = eqs, affect = affect, affect_neg = affect_neg, rootfind = rootfind) + eqs = eqs, affect = affect, affect_neg = affect_neg, rootfind = rootfind, initialize=initialize, finalize=finalize) end SymbolicContinuousCallbacks(cb::SymbolicContinuousCallback) = [cb] @@ -282,6 +362,16 @@ function affect_negs(cbs::Vector{SymbolicContinuousCallback}) mapreduce(affect_negs, vcat, cbs, init = Equation[]) end +initialize_affects(cb::SymbolicContinuousCallback) = cb.initialize +function initialize_affects(cbs::Vector{SymbolicContinuousCallback}) + mapreduce(initialize_affects, vcat, cbs, init = Equation[]) +end + +finalize_affects(cb::SymbolicContinuousCallback) = cb.initialize +function finalize_affects(cbs::Vector{SymbolicContinuousCallback}) + mapreduce(finalize_affects, vcat, cbs, init = Equation[]) +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(af::MutatingFunctionalAffect, s) = namespace_affect(af, s) @@ -292,6 +382,8 @@ function namespace_callback(cb::SymbolicContinuousCallback, s)::SymbolicContinuo 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) end @@ -681,8 +773,9 @@ function generate_single_rootfinding_callback( initfn = SciMLBase.INITIALIZE_DEFAULT end return ContinuousCallback( - cond, affect_function.affect, affect_function.affect_neg, - rootfind = cb.rootfind, initialize = initfn) + cond, affect_function.affect, affect_function.affect_neg, rootfind = cb.rootfind, + initialize = isnothing(affect_function.initialize) ? SciMLBase.INITIALIZE_DEFAULT : (c, u, t, i) -> affect_function.initialize(i), + finalize = isnothing(affect_function.finalize) ? SciMLBase.FINALIZE_DEFAULT : (c, u, t, i) -> affect_function.finalize(i)) end function generate_vector_rootfinding_callback( @@ -702,13 +795,12 @@ function generate_vector_rootfinding_callback( _, rf_ip = generate_custom_function( sys, rhss, dvs, ps; expression = Val{false}, kwargs...) - affect_functions = @NamedTuple{affect::Function, affect_neg::Union{Function, Nothing}}[compile_affect_fn( - cb, - sys, - dvs, - ps, - kwargs) - for cb in cbs] + 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 @@ -734,25 +826,27 @@ function generate_vector_rootfinding_callback( affect_neg(integ) end end - if has_index_cache(sys) && (ic = get_index_cache(sys)) !== nothing - save_idxs = mapreduce( - cb -> get(ic.callback_to_clocks, cb, Int[]), vcat, cbs; init = Int[]) - initfn = if isempty(save_idxs) - SciMLBase.INITIALIZE_DEFAULT + function handle_optional_setup_fn(funs, default) + if all(isnothing, funs) + return default else - let save_idxs = save_idxs - function (cb, u, t, integrator) - for idx in save_idxs - SciMLBase.save_discretes!(integrator, idx) + 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 - else - initfn = SciMLBase.INITIALIZE_DEFAULT end + initialize = handle_optional_setup_fn(map(fn -> fn.initialize, affect_functions), SciMLBase.INITIALIZE_DEFAULT) + 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 = initfn) + cond, affect, affect_neg, length(eqs), rootfind = rootfind, initialize = initialize, finalize = finalize) end """ @@ -762,15 +856,23 @@ function compile_affect_fn(cb, sys::AbstractODESystem, 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) + if isnothing(aff) + return nothing + else + affspr = compile_affect(aff, cb, sys, dvs, ps; expression = Val{true}, kwargs...) + @show affspr + return compile_affect(aff, cb, sys, dvs, ps; expression = Val{false}, kwargs...) + end + end if eq_neg_aff === eq_aff affect_neg = affect - elseif isnothing(eq_neg_aff) - affect_neg = nothing else - affect_neg = compile_affect( - eq_neg_aff, cb, sys, dvs, ps; expression = Val{false}, kwargs...) + affect_neg = compile_optional_affect(eq_neg_aff) end - (affect = affect, affect_neg = affect_neg) + initialize = compile_optional_affect(initialize_affects(cb)) + finalize = compile_optional_affect(finalize_affects(cb)) + (affect = affect, affect_neg = affect_neg, initialize = initialize, finalize = finalize) end function generate_rootfinding_callback(cbs, sys::AbstractODESystem, dvs = unknowns(sys), @@ -877,7 +979,7 @@ function compile_user_affect(affect::MutatingFunctionalAffect, sys, dvs, ps; kwa push!(syms_dedup, sym) push!(exprs_dedup, exp) push!(seen, sym) - else + elseif !affect.skip_checks @warn "Expression $(expr) is aliased as $sym, which has already been used. The first definition will be used." end end @@ -887,7 +989,7 @@ function compile_user_affect(affect::MutatingFunctionalAffect, sys, dvs, ps; kwa obs_exprs = observed(affect) for oexpr in obs_exprs invalid_vars = invalid_variables(sys, oexpr) - if length(invalid_vars) > 0 + if length(invalid_vars) > 0 && !affect.skip_checks error("Observed equation $(oexpr) in affect refers to missing variable(s) $(invalid_vars); the variables may not have been added (e.g. if a component is missing).") end end @@ -897,11 +999,11 @@ function compile_user_affect(affect::MutatingFunctionalAffect, sys, dvs, ps; kwa mod_exprs = modified(affect) for mexpr in mod_exprs - if !is_observed(sys, mexpr) && parameter_index(sys, mexpr) === nothing - error("Expression $mexpr cannot be assigned to; currently only unknowns and parameters may be updated by an affect.") + if !is_observed(sys, mexpr) && parameter_index(sys, mexpr) === nothing && !affect.skip_checks + @warn ("Expression $mexpr cannot be assigned to; currently only unknowns and parameters may be updated by an affect.") end invalid_vars = unassignable_variables(sys, mexpr) - if length(invalid_vars) > 0 + if length(invalid_vars) > 0 && !affect.skip_checks error("Modified equation $(mexpr) in affect refers to missing variable(s) $(invalid_vars); the variables may not have been added (e.g. if a component is missing) or they may have been reduced away.") end end @@ -911,7 +1013,7 @@ function compile_user_affect(affect::MutatingFunctionalAffect, sys, dvs, ps; kwa sys, mod_exprs; return_inplace = true) overlapping_syms = intersect(mod_syms, obs_syms) - if length(overlapping_syms) > 0 + if length(overlapping_syms) > 0 && !affect.skip_checks @warn "The symbols $overlapping_syms 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." end From 3c7afd8c40b526dd6f139aa2d119095ab6debeb7 Mon Sep 17 00:00:00 2001 From: Benjamin Chung Date: Mon, 19 Aug 2024 08:08:13 -0700 Subject: [PATCH 0167/1337] Test some simple initialization affects --- test/symbolic_events.jl | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/test/symbolic_events.jl b/test/symbolic_events.jl index 7aba69dc7f..b0f230d75e 100644 --- a/test/symbolic_events.jl +++ b/test/symbolic_events.jl @@ -1074,6 +1074,26 @@ end 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.MutatingFunctionalAffect(modified = (; furnace_on)) do x + x.furnace_on = false + end; initialize = ModelingToolkit.MutatingFunctionalAffect(modified = (; temp)) do x + x.temp = 0.2 + end) + furnace_enable = ModelingToolkit.SymbolicContinuousCallback( + [temp ~ furnace_on_threshold], + ModelingToolkit.MutatingFunctionalAffect(modified = (; furnace_on)) do x, o, c, i + 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 "MutatingFunctionalAffect errors and warnings" begin From 95956f363eaa8bbc5b492452946c6c2a21e35bbd Mon Sep 17 00:00:00 2001 From: Benjamin Chung Date: Thu, 22 Aug 2024 07:17:50 -0700 Subject: [PATCH 0168/1337] Properly pass skip_checks through --- src/systems/callbacks.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/systems/callbacks.jl b/src/systems/callbacks.jl index d14fd50691..ca0a64b452 100644 --- a/src/systems/callbacks.jl +++ b/src/systems/callbacks.jl @@ -165,7 +165,8 @@ function namespace_affect(affect::MutatingFunctionalAffect, s) observed_syms(affect), renamespace.((s,), modified(affect)), modified_syms(affect), - context(affect)) + context(affect), + affect.skip_checks) end function has_functional_affect(cb) From 4f928ae57e33b2d8bb5e52faed229941b5e589af Mon Sep 17 00:00:00 2001 From: Benjamin Chung Date: Thu, 22 Aug 2024 07:35:38 -0700 Subject: [PATCH 0169/1337] Support time-indexed parameters --- src/systems/callbacks.jl | 16 ++++++++++++++-- src/systems/index_cache.jl | 2 +- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/systems/callbacks.jl b/src/systems/callbacks.jl index ca0a64b452..279d31483e 100644 --- a/src/systems/callbacks.jl +++ b/src/systems/callbacks.jl @@ -963,7 +963,7 @@ function unassignable_variables(sys, expr) x -> !any(isequal(x), assignable_syms), reduce(vcat, vars(expr); init = [])) end -function compile_user_affect(affect::MutatingFunctionalAffect, sys, dvs, ps; kwargs...) +function compile_user_affect(affect::MutatingFunctionalAffect, cb, sys, dvs, ps; kwargs...) #= Implementation sketch: generate observed function (oop), should save to a component array under obs_syms @@ -1049,6 +1049,13 @@ function compile_user_affect(affect::MutatingFunctionalAffect, sys, dvs, ps; kwa collect(mkzero(size(e)) for e in first.(mod_unk_pairs))])) upd_params_view = view(upd_component_array, last.(mod_param_pairs)) upd_unks_view = view(upd_component_array, last.(mod_unk_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) # update the to-be-mutated values; this ensures that if you do a no-op then nothing happens @@ -1074,11 +1081,16 @@ function compile_user_affect(affect::MutatingFunctionalAffect, sys, dvs, ps; kwa # write the new values back to the integrator upd_params_fun(integ, upd_params_view) upd_unk_fun(integ, upd_unks_view) + + + for idx in save_idxs + SciMLBase.save_discretes!(integ, idx) + end end end end -function compile_affect(affect::FunctionalAffect, cb, sys, dvs, ps; kwargs...) +function compile_affect(affect::Union{FunctionalAffect, MutatingFunctionalAffect}, cb, sys, dvs, ps; kwargs...) compile_user_affect(affect, cb, sys, dvs, ps; kwargs...) end diff --git a/src/systems/index_cache.jl b/src/systems/index_cache.jl index 00f7837407..55d819990b 100644 --- a/src/systems/index_cache.jl +++ b/src/systems/index_cache.jl @@ -126,7 +126,7 @@ function IndexCache(sys::AbstractSystem) for affect in affs if affect isa Equation is_parameter(sys, affect.lhs) && push!(discs, affect.lhs) - elseif affect isa FunctionalAffect + elseif affect isa FunctionalAffect || affect isa MutatingFunctionalAffect union!(discs, unwrap.(discretes(affect))) else error("Unhandled affect type $(typeof(affect))") From 3eca9b96567008469ee6963745a6a4f2ea64ed50 Mon Sep 17 00:00:00 2001 From: Benjamin Chung Date: Tue, 27 Aug 2024 09:28:17 -0700 Subject: [PATCH 0170/1337] Fix bugs relating to array arguments to callbacks --- src/systems/callbacks.jl | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/systems/callbacks.jl b/src/systems/callbacks.jl index 279d31483e..884d1d87f3 100644 --- a/src/systems/callbacks.jl +++ b/src/systems/callbacks.jl @@ -958,9 +958,10 @@ 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 = vcat(unknowns(sys), parameters(sys)) + assignable_syms = reduce(vcat, Symbolics.scalarize.(vcat(unknowns(sys), parameters(sys))); init=[]) + written = reduce(vcat, Symbolics.scalarize.(vars(expr)); init = []) return filter( - x -> !any(isequal(x), assignable_syms), reduce(vcat, vars(expr); init = [])) + x -> !any(isequal(x), assignable_syms), written) end function compile_user_affect(affect::MutatingFunctionalAffect, cb, sys, dvs, ps; kwargs...) @@ -1000,7 +1001,10 @@ function compile_user_affect(affect::MutatingFunctionalAffect, cb, sys, dvs, ps; mod_exprs = modified(affect) for mexpr in mod_exprs - if !is_observed(sys, mexpr) && parameter_index(sys, mexpr) === nothing && !affect.skip_checks + if affect.skip_checks + continue + end + if !is_variable(sys, mexpr) && parameter_index(sys, mexpr) === nothing && !affect.skip_checks @warn ("Expression $mexpr cannot be assigned to; currently only unknowns and parameters may be updated by an affect.") end invalid_vars = unassignable_variables(sys, mexpr) @@ -1036,7 +1040,7 @@ function compile_user_affect(affect::MutatingFunctionalAffect, cb, sys, dvs, ps; mod_param_pairs = filter(v -> is_parameter(sys, v[1]), mod_pairs) mod_unk_pairs = filter(v -> !is_parameter(sys, v[1]), mod_pairs) _, mod_og_val_fun = build_explicit_observed_function( - sys, reduce(vcat, [first.(mod_param_pairs); first.(mod_unk_pairs)]; init = []); + sys, reduce(vcat, Symbolics.scalarize.([first.(mod_param_pairs); first.(mod_unk_pairs)]); init = []); return_inplace = true) upd_params_fun = setu( sys, reduce(vcat, Symbolics.scalarize.(first.(mod_param_pairs)); init = [])) From c41e7d4677077f69024c0f4f0a8e9ef6f83b2d38 Mon Sep 17 00:00:00 2001 From: Benjamin Chung Date: Tue, 27 Aug 2024 18:27:22 -0700 Subject: [PATCH 0171/1337] Remove debug logging --- src/systems/callbacks.jl | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/systems/callbacks.jl b/src/systems/callbacks.jl index 884d1d87f3..a44b5093c8 100644 --- a/src/systems/callbacks.jl +++ b/src/systems/callbacks.jl @@ -861,8 +861,6 @@ function compile_affect_fn(cb, sys::AbstractODESystem, dvs, ps, kwargs) if isnothing(aff) return nothing else - affspr = compile_affect(aff, cb, sys, dvs, ps; expression = Val{true}, kwargs...) - @show affspr return compile_affect(aff, cb, sys, dvs, ps; expression = Val{false}, kwargs...) end end From 95fa1ee554c0cf449c3c2ac9abdcff85546c80d4 Mon Sep 17 00:00:00 2001 From: Benjamin Chung Date: Mon, 9 Sep 2024 20:06:45 -0700 Subject: [PATCH 0172/1337] Fix the namespace operation used while namespacing MutatingFunctionalAffects --- 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 a44b5093c8..a7834d918e 100644 --- a/src/systems/callbacks.jl +++ b/src/systems/callbacks.jl @@ -161,7 +161,7 @@ end function namespace_affect(affect::MutatingFunctionalAffect, s) MutatingFunctionalAffect(func(affect), - renamespace.((s,), observed(affect)), + namespace_expr.(observed(affect), (s,)), observed_syms(affect), renamespace.((s,), modified(affect)), modified_syms(affect), From f57215ab42f5c4c3747744827b8793403c2e6761 Mon Sep 17 00:00:00 2001 From: Benjamin Chung Date: Tue, 10 Sep 2024 12:45:14 -0700 Subject: [PATCH 0173/1337] Add support for the initializealg argument in SciMLBase callbacks --- src/systems/callbacks.jl | 41 ++++++++++++++++++++++++++++++---------- test/symbolic_events.jl | 6 +++--- 2 files changed, 34 insertions(+), 13 deletions(-) diff --git a/src/systems/callbacks.jl b/src/systems/callbacks.jl index a7834d918e..d2657d2ced 100644 --- a/src/systems/callbacks.jl +++ b/src/systems/callbacks.jl @@ -216,6 +216,11 @@ Affects (i.e. `affect` and `affect_neg`) can be specified as either: + `modified_parameters` is a vector of the parameters that are *modified* by `f!`. Note that a parameter will not appear in `p` if it only appears in `modified_parameters`; it must appear in both `parameters` and `modified_parameters` if it is used in the affect definition. + `ctx` is a user-defined context object passed to `f!` when invoked. This value is aliased for each problem. * A [`MutatingFunctionalAffect`](@ref); refer to its documentation for details. + +Callbacks that impact a DAE are applied, then the DAE is reinitialized using `reinitializealg` (which defaults to `SciMLBase.CheckInit`). +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. """ struct SymbolicContinuousCallback eqs::Vector{Equation} @@ -224,14 +229,16 @@ struct SymbolicContinuousCallback affect::Union{Vector{Equation}, FunctionalAffect, MutatingFunctionalAffect} affect_neg::Union{Vector{Equation}, FunctionalAffect, MutatingFunctionalAffect, Nothing} rootfind::SciMLBase.RootfindOpt + reinitializealg::SciMLBase.DAEInitializationAlgorithm function SymbolicContinuousCallback(; eqs::Vector{Equation}, affect = NULL_AFFECT, affect_neg = affect, rootfind = SciMLBase.LeftRootFind, initialize=NULL_AFFECT, - finalize=NULL_AFFECT) - new(eqs, initialize, finalize, make_affect(affect), make_affect(affect_neg), rootfind) + finalize=NULL_AFFECT, + reinitializealg=SciMLBase.CheckInit()) + new(eqs, initialize, finalize, make_affect(affect), make_affect(affect_neg), rootfind, reinitializealg) end # Default affect to nothing end make_affect(affect) = affect @@ -373,6 +380,10 @@ function finalize_affects(cbs::Vector{SymbolicContinuousCallback}) mapreduce(finalize_affects, vcat, cbs, init = Equation[]) end +reinitialization_alg(cb::SymbolicContinuousCallback) = cb.reinitializealg +reinitialization_algs(cbs::Vector{SymbolicContinuousCallback}) = + mapreduce(reinitialization_alg, vcat, cbs, init = SciMLBase.DAEInitializationAlgorithm[]) + 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(af::MutatingFunctionalAffect, s) = namespace_affect(af, s) @@ -419,11 +430,12 @@ struct SymbolicDiscreteCallback # TODO: Iterative condition::Any affects::Any + reinitializealg::SciMLBase.DAEInitializationAlgorithm - function SymbolicDiscreteCallback(condition, affects = NULL_AFFECT) + function SymbolicDiscreteCallback(condition, affects = NULL_AFFECT, reinitializealg=SciMLBase.CheckInit()) c = scalarize_condition(condition) a = scalarize_affects(affects) - new(c, a) + new(c, a, reinitializealg) end # Default affect to nothing end @@ -481,6 +493,10 @@ function affects(cbs::Vector{SymbolicDiscreteCallback}) reduce(vcat, affects(cb) for cb in cbs; init = []) end +reinitialization_alg(cb::SymbolicDiscreteCallback) = cb.reinitializealg +reinitialization_algs(cbs::Vector{SymbolicDiscreteCallback}) = + mapreduce(reinitialization_alg, vcat, cbs, init = SciMLBase.DAEInitializationAlgorithm[]) + function namespace_callback(cb::SymbolicDiscreteCallback, s)::SymbolicDiscreteCallback af = affects(cb) af = af isa AbstractVector ? namespace_affect.(af, Ref(s)) : namespace_affect(af, s) @@ -776,12 +792,13 @@ function generate_single_rootfinding_callback( return ContinuousCallback( cond, affect_function.affect, affect_function.affect_neg, rootfind = cb.rootfind, initialize = isnothing(affect_function.initialize) ? SciMLBase.INITIALIZE_DEFAULT : (c, u, t, i) -> affect_function.initialize(i), - finalize = isnothing(affect_function.finalize) ? SciMLBase.FINALIZE_DEFAULT : (c, u, t, i) -> affect_function.finalize(i)) + 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::AbstractODESystem, dvs = unknowns(sys), - ps = parameters(sys); rootfind = SciMLBase.RightRootFind, kwargs...) + ps = parameters(sys); rootfind = SciMLBase.RightRootFind, reinitialization = SciMLBase.CheckInit(), kwargs...) eqs = map(cb -> flatten_equations(cb.eqs), cbs) num_eqs = length.(eqs) # fuse equations to create VectorContinuousCallback @@ -847,7 +864,7 @@ function generate_vector_rootfinding_callback( initialize = handle_optional_setup_fn(map(fn -> fn.initialize, affect_functions), SciMLBase.INITIALIZE_DEFAULT) 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) + cond, affect, affect_neg, length(eqs), rootfind = rootfind, initialize = initialize, finalize = finalize, initializealg = reinitialization) end """ @@ -893,10 +910,14 @@ function generate_rootfinding_callback(cbs, sys::AbstractODESystem, dvs = unknow # group the cbs by what rootfind op they use # groupby would be very useful here, but alas cb_classes = Dict{ - @NamedTuple{rootfind::SciMLBase.RootfindOpt}, Vector{SymbolicContinuousCallback}}() + @NamedTuple{ + rootfind::SciMLBase.RootfindOpt, + reinitialization::SciMLBase.DAEInitializationAlgorithm}, Vector{SymbolicContinuousCallback}}() for cb in cbs push!( - get!(() -> SymbolicContinuousCallback[], cb_classes, (rootfind = cb.rootfind,)), + get!(() -> SymbolicContinuousCallback[], cb_classes, ( + rootfind = cb.rootfind, + reinitialization = reinitialization_alg(cb))), cb) end @@ -904,7 +925,7 @@ function generate_rootfinding_callback(cbs, sys::AbstractODESystem, dvs = unknow 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, kwargs...) + cbs_in_class, sys, dvs, ps; rootfind = equiv_class.rootfind, reinitialization=equiv_class.reinitialization, kwargs...) end if length(compiled_callbacks) == 1 return compiled_callbacks[] diff --git a/test/symbolic_events.jl b/test/symbolic_events.jl index b0f230d75e..dd60b99003 100644 --- a/test/symbolic_events.jl +++ b/test/symbolic_events.jl @@ -996,8 +996,8 @@ end @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[a] == [-1.0] + @test sol[b] == [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 @@ -1198,5 +1198,5 @@ end ss = structural_simplify(sys) prob = ODEProblem(ss, [theta => 0.0], (0.0, pi)) sol = solve(prob, Tsit5(); dtmax = 0.01) - @test sol[cnt] == 197 # 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 + @test getp(sol, cnt)(sol) == 197 # 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 From d79d49de426ece41ab43df618c7960d88611b6c5 Mon Sep 17 00:00:00 2001 From: Benjamin Chung Date: Tue, 10 Sep 2024 16:23:58 -0700 Subject: [PATCH 0174/1337] Switch MutatingFunctionalAffect from using ComponentArrays to using NamedTuples for heterotyped operation support. --- src/systems/callbacks.jl | 78 +++++++++++++++--------------- src/systems/diffeqs/odesystem.jl | 10 ++-- test/symbolic_events.jl | 81 ++++++++++++++++++++------------ 3 files changed, 94 insertions(+), 75 deletions(-) diff --git a/src/systems/callbacks.jl b/src/systems/callbacks.jl index d2657d2ced..34acb5b2ae 100644 --- a/src/systems/callbacks.jl +++ b/src/systems/callbacks.jl @@ -77,25 +77,29 @@ end `MutatingFunctionalAffect` is a helper for writing affect functions that will compute observed values and ensure that modified values are correctly written back into the system. The affect function `f` needs to have one of four signatures: -* `f(modified::ComponentArray)` if the function only writes values (unknowns or parameters) to the system, -* `f(modified::ComponentArray, observed::ComponentArray)` if the function also reads observed values from the system, -* `f(modified::ComponentArray, observed::ComponentArray, ctx)` if the function needs the user-defined context, -* `f(modified::ComponentArray, observed::ComponentArray, ctx, integrator)` if the function needs the low-level integrator. +* `f(modified::NamedTuple)::NamedTuple` if the function only writes values (unknowns or parameters) to the system, +* `f(modified::NamedTuple, observed::NamedTuple)::NamedTuple` if the function also reads observed values from the system, +* `f(modified::NamedTuple, observed::NamedTuple, ctx)::NamedTuple` if the function needs the user-defined context, +* `f(modified::NamedTuple, observed::NamedTuple, ctx, integrator)::NamedTuple` if the function needs the low-level integrator. These will be checked in reverse order (that is, the four-argument version first, than the 3, etc). -The function `f` will be called with `observed` and `modified` `ComponentArray`s that are derived from their respective `NamedTuple` definitions. -Each `NamedTuple` should map an expression to a symbol; for example if we pass `observed=(; x = a + b)` this will alias the result of executing `a+b` in the system as `x` +The function `f` will be called with `observed` and `modified` `NamedTuple`s that are derived from their respective `NamedTuple` definitions. +Each declaration`NamedTuple` should map an expression to a symbol; for example if we pass `observed=(; x = a + b)` this will alias the result of executing `a+b` in the system as `x` so the value of `a + b` will be accessible as `observed.x` in `f`. `modified` currently restricts symbolic expressions to only bare variables, so only tuples of the form `(; x = y)` or `(; x)` (which aliases `x` as itself) are allowed. -Both `observed` and `modified` will be automatically populated with the current values of their corresponding expressions on function entry. -The values in `modified` will be written back to the system after `f` returns. For example, if we want to update the value of `x` to be the result of `x + y` we could write +The argument NamedTuples (for instance `(;x=y)`) will be populated with the declared values on function entry; if we require `(;x=y)` in `observed` and `y=2`, for example, +then the NamedTuple `(;x=2)` will be passed as `observed` to the affect function `f`. + +The NamedTuple returned from `f` includes the values to be written back to the system after `f` returns. For example, if we want to update the value of `x` to be the result of `x + y` we could write MutatingFunctionalAffect(observed=(; x_plus_y = x + y), modified=(; x)) do m, o - m.x = o.x_plus_y + @set! m.x = o.x_plus_y end -The affect function updates the value at `x` in `modified` to be the result of evaluating `x + y` as passed in the observed values. +Where we use Setfield to copy the tuple `m` with a new value for `x`, then return the modified value of `m`. All values updated by the tuple must have names originally declared in +`modified`; a runtime error will be produced if a value is written that does not appear in `modified`. The user can dynamically decide not to write a value back by not including it +in the returned tuple, in which case the associated field will not be updated. """ @kwdef struct MutatingFunctionalAffect f::Any @@ -983,6 +987,18 @@ function unassignable_variables(sys, expr) 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 compile_user_affect(affect::MutatingFunctionalAffect, cb, sys, dvs, ps; kwargs...) #= Implementation sketch: @@ -1016,7 +1032,6 @@ function compile_user_affect(affect::MutatingFunctionalAffect, cb, sys, dvs, ps; end obs_syms = observed_syms(affect) obs_syms, obs_exprs = check_dups(obs_syms, obs_exprs) - obs_size = size.(obs_exprs) # we will generate a work buffer of a ComponentArray that maps obs_syms to arrays of size obs_size mod_exprs = modified(affect) for mexpr in mod_exprs @@ -1033,8 +1048,6 @@ function compile_user_affect(affect::MutatingFunctionalAffect, cb, sys, dvs, ps; end mod_syms = modified_syms(affect) mod_syms, mod_exprs = check_dups(mod_syms, mod_exprs) - _, mod_og_val_fun = build_explicit_observed_function( - sys, mod_exprs; return_inplace = true) overlapping_syms = intersect(mod_syms, obs_syms) if length(overlapping_syms) > 0 && !affect.skip_checks @@ -1048,31 +1061,20 @@ function compile_user_affect(affect::MutatingFunctionalAffect, cb, sys, dvs, ps; else zeros(sz) end - _, obs_fun = build_explicit_observed_function( + obs_fun = build_explicit_observed_function( sys, reduce(vcat, Symbolics.scalarize.(obs_exprs); init = []); - return_inplace = true) - obs_component_array = ComponentArrays.ComponentArray(NamedTuple{(obs_syms...,)}(mkzero.(obs_size))) + array_type = :tuple) + obs_sym_tuple = (obs_syms...,) # okay so now to generate the stuff to assign it back into the system - # note that we reorder the componentarray to make the views coherent wrt the base array mod_pairs = mod_exprs .=> mod_syms - mod_param_pairs = filter(v -> is_parameter(sys, v[1]), mod_pairs) - mod_unk_pairs = filter(v -> !is_parameter(sys, v[1]), mod_pairs) - _, mod_og_val_fun = build_explicit_observed_function( - sys, reduce(vcat, Symbolics.scalarize.([first.(mod_param_pairs); first.(mod_unk_pairs)]); init = []); - return_inplace = true) - upd_params_fun = setu( - sys, reduce(vcat, Symbolics.scalarize.(first.(mod_param_pairs)); init = [])) - upd_unk_fun = setu( - sys, reduce(vcat, Symbolics.scalarize.(first.(mod_unk_pairs)); init = [])) - - upd_component_array = ComponentArrays.ComponentArray(NamedTuple{([last.(mod_param_pairs); - last.(mod_unk_pairs)]...,)}( - [collect(mkzero(size(e)) for e in first.(mod_param_pairs)); - collect(mkzero(size(e)) for e in first.(mod_unk_pairs))])) - upd_params_view = view(upd_component_array, last.(mod_param_pairs)) - upd_unks_view = view(upd_component_array, last.(mod_unk_pairs)) + mod_names = (mod_syms..., ) + mod_og_val_fun = build_explicit_observed_function( + sys, reduce(vcat, Symbolics.scalarize.(first.(mod_pairs)); init = []); + array_type = :tuple) + 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 @@ -1082,13 +1084,13 @@ function compile_user_affect(affect::MutatingFunctionalAffect, cb, sys, dvs, ps; let user_affect = func(affect), ctx = context(affect) function (integ) # update the to-be-mutated values; this ensures that if you do a no-op then nothing happens - mod_og_val_fun(upd_component_array, integ.u, integ.p..., integ.t) + upd_component_array = NamedTuple{mod_names}(mod_og_val_fun(integ.u, integ.p..., integ.t)) # update the observed values - obs_fun(obs_component_array, integ.u, integ.p..., integ.t) + obs_component_array = NamedTuple{obs_sym_tuple}(obs_fun(integ.u, integ.p..., integ.t)) # let the user do their thing - if applicable(user_affect, upd_component_array, obs_component_array, ctx, integ) + modvals = if applicable(user_affect, upd_component_array, obs_component_array, ctx, integ) user_affect(upd_component_array, obs_component_array, ctx, integ) elseif applicable(user_affect, upd_component_array, obs_component_array, ctx) user_affect(upd_component_array, obs_component_array, ctx) @@ -1102,9 +1104,7 @@ function compile_user_affect(affect::MutatingFunctionalAffect, cb, sys, dvs, ps; end # write the new values back to the integrator - upd_params_fun(integ, upd_params_view) - upd_unk_fun(integ, upd_unks_view) - + _generated_writeback(integ, upd_funs, modvals) for idx in save_idxs SciMLBase.save_discretes!(integ, idx) diff --git a/src/systems/diffeqs/odesystem.jl b/src/systems/diffeqs/odesystem.jl index daa4321ed0..e99911eec6 100644 --- a/src/systems/diffeqs/odesystem.jl +++ b/src/systems/diffeqs/odesystem.jl @@ -429,6 +429,7 @@ Options not otherwise specified are: * `op = Operator` sets the recursion terminator for the walk done by `vars` to identify the variables that appear in `ts`. See the documentation for `vars` for more detail. * `throw = true` if true, throw an error when generating a function for `ts` that reference variables that do not exist * `drop_expr` is deprecated. +* `array_type`; only used if the output is an array (that is, `!isscalar(ts)`). If `:array`, then it will generate an array, if `:tuple` then it will generate a tuple. """ function build_explicit_observed_function(sys, ts; inputs = nothing, @@ -442,7 +443,8 @@ function build_explicit_observed_function(sys, ts; return_inplace = false, param_only = false, op = Operator, - throw = true) + throw = true, + array_type=:array) if (isscalar = symbolic_type(ts) !== NotSymbolic()) ts = [ts] end @@ -587,12 +589,10 @@ function build_explicit_observed_function(sys, ts; oop_mtkp_wrapper = mtkparams_wrapper end + output_expr = isscalar ? ts[1] : (array_type == :array ? MakeArray(ts, output_type) : MakeTuple(ts)) # Need to keep old method of building the function since it uses `output_type`, # which can't be provided to `build_function` - oop_fn = Func(args, [], - pre(Let(obsexprs, - isscalar ? ts[1] : MakeArray(ts, output_type), - false))) |> array_wrapper[1] |> oop_mtkp_wrapper |> toexpr + oop_fn = Func(args, [], pre(Let(obsexprs, output_expr, false))) |> array_wrapper[1] |> oop_mtkp_wrapper |> toexpr oop_fn = expression ? oop_fn : eval_or_rgf(oop_fn; eval_expression, eval_module) if !isscalar diff --git a/test/symbolic_events.jl b/test/symbolic_events.jl index dd60b99003..bc455ec06e 100644 --- a/test/symbolic_events.jl +++ b/test/symbolic_events.jl @@ -8,6 +8,7 @@ using ModelingToolkit: SymbolicContinuousCallback, using StableRNGs import SciMLBase using SymbolicIndexingInterface +using Setfield rng = StableRNG(12345) @variables x(t) = 0 @@ -1010,12 +1011,12 @@ end furnace_off = ModelingToolkit.SymbolicContinuousCallback( [temp ~ furnace_off_threshold], ModelingToolkit.MutatingFunctionalAffect(modified = (; furnace_on)) do x, o, i, c - x.furnace_on = false + @set! x.furnace_on = false end) furnace_enable = ModelingToolkit.SymbolicContinuousCallback( [temp ~ furnace_on_threshold], ModelingToolkit.MutatingFunctionalAffect(modified = (; furnace_on)) do x, o, i, c - x.furnace_on = true + @set! x.furnace_on = true end) @named sys = ODESystem( eqs, t, [temp], params; continuous_events = [furnace_off, furnace_enable]) @@ -1027,12 +1028,12 @@ end furnace_off = ModelingToolkit.SymbolicContinuousCallback( [temp ~ furnace_off_threshold], ModelingToolkit.MutatingFunctionalAffect(modified = (; furnace_on)) do x, o, i - x.furnace_on = false + @set! x.furnace_on = false end) furnace_enable = ModelingToolkit.SymbolicContinuousCallback( [temp ~ furnace_on_threshold], ModelingToolkit.MutatingFunctionalAffect(modified = (; furnace_on)) do x, o, i - x.furnace_on = true + @set! x.furnace_on = true end) @named sys = ODESystem( eqs, t, [temp], params; continuous_events = [furnace_off, furnace_enable]) @@ -1044,12 +1045,12 @@ end furnace_off = ModelingToolkit.SymbolicContinuousCallback( [temp ~ furnace_off_threshold], ModelingToolkit.MutatingFunctionalAffect(modified = (; furnace_on)) do x, o - x.furnace_on = false + @set! x.furnace_on = false end) furnace_enable = ModelingToolkit.SymbolicContinuousCallback( [temp ~ furnace_on_threshold], ModelingToolkit.MutatingFunctionalAffect(modified = (; furnace_on)) do x, o - x.furnace_on = true + @set! x.furnace_on = true end) @named sys = ODESystem( eqs, t, [temp], params; continuous_events = [furnace_off, furnace_enable]) @@ -1061,12 +1062,12 @@ end furnace_off = ModelingToolkit.SymbolicContinuousCallback( [temp ~ furnace_off_threshold], ModelingToolkit.MutatingFunctionalAffect(modified = (; furnace_on)) do x - x.furnace_on = false + @set! x.furnace_on = false end) furnace_enable = ModelingToolkit.SymbolicContinuousCallback( [temp ~ furnace_on_threshold], ModelingToolkit.MutatingFunctionalAffect(modified = (; furnace_on)) do x - x.furnace_on = true + @set! x.furnace_on = true end) @named sys = ODESystem( eqs, t, [temp], params; continuous_events = [furnace_off, furnace_enable]) @@ -1078,14 +1079,14 @@ end furnace_off = ModelingToolkit.SymbolicContinuousCallback( [temp ~ furnace_off_threshold], ModelingToolkit.MutatingFunctionalAffect(modified = (; furnace_on)) do x - x.furnace_on = false + @set! x.furnace_on = false end; initialize = ModelingToolkit.MutatingFunctionalAffect(modified = (; temp)) do x - x.temp = 0.2 + @set! x.temp = 0.2 end) furnace_enable = ModelingToolkit.SymbolicContinuousCallback( [temp ~ furnace_on_threshold], ModelingToolkit.MutatingFunctionalAffect(modified = (; furnace_on)) do x, o, c, i - x.furnace_on = true + @set! x.furnace_on = true end) @named sys = ODESystem( eqs, t, [temp], params; continuous_events = [furnace_off, furnace_enable]) @@ -1107,7 +1108,7 @@ end [temp ~ furnace_off_threshold], ModelingToolkit.MutatingFunctionalAffect( modified = (; furnace_on), observed = (; furnace_on)) do x, o, c, i - x.furnace_on = false + @set! x.furnace_on = false end) @named sys = ODESystem(eqs, t, [temp], params; continuous_events = [furnace_off]) ss = structural_simplify(sys) @@ -1123,7 +1124,7 @@ end [temp ~ furnace_off_threshold], ModelingToolkit.MutatingFunctionalAffect( modified = (; furnace_on, tempsq), observed = (; furnace_on)) do x, o, c, i - x.furnace_on = false + @set! x.furnace_on = false end) @named sys = ODESystem( eqs, t, [temp, tempsq], params; continuous_events = [furnace_off]) @@ -1136,18 +1137,32 @@ end [temp ~ furnace_off_threshold], ModelingToolkit.MutatingFunctionalAffect(modified = (; furnace_on), observed = (; furnace_on, not_actually_here)) do x, o, c, i - x.furnace_on = false + @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.MutatingFunctionalAffect(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=0 + 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) @@ -1167,31 +1182,35 @@ end end qAevt = ModelingToolkit.SymbolicContinuousCallback([cos(100 * theta) ~ 0], ModelingToolkit.MutatingFunctionalAffect((; qA, hA, hB, cnt), (; qB)) do x, o, i, c - x.hA = x.qA - x.hB = o.qB - x.qA = 1 - x.cnt += decoder(x.hA, x.hB, x.qA, o.qB) + @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.MutatingFunctionalAffect( (; qA, hA, hB, cnt), (; qB)) do x, o, c, i - x.hA = x.qA - x.hB = o.qB - x.qA = 0 - x.cnt += decoder(x.hA, x.hB, x.qA, o.qB) + @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.MutatingFunctionalAffect((; qB, hA, hB, cnt), (; qA)) do x, o, i, c - x.hA = o.qA - x.hB = x.qB - x.qB = 1 - x.cnt += decoder(x.hA, x.hB, o.qA, x.qB) + @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.MutatingFunctionalAffect( (; qB, hA, hB, cnt), (; qA)) do x, o, c, i - x.hA = o.qA - x.hB = x.qB - x.qB = 0 - x.cnt += decoder(x.hA, x.hB, o.qA, x.qB) + @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]) From c940e5ed27eeed1cafdb3d2a408a245b31950c3e Mon Sep 17 00:00:00 2001 From: Benjamin Chung Date: Fri, 13 Sep 2024 09:10:20 -0700 Subject: [PATCH 0175/1337] Fix support for array forms in the NamedTuple version of MutatingFunctionalAffect --- src/systems/callbacks.jl | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/systems/callbacks.jl b/src/systems/callbacks.jl index 34acb5b2ae..ad41807ab8 100644 --- a/src/systems/callbacks.jl +++ b/src/systems/callbacks.jl @@ -1062,7 +1062,7 @@ function compile_user_affect(affect::MutatingFunctionalAffect, cb, sys, dvs, ps; zeros(sz) end obs_fun = build_explicit_observed_function( - sys, reduce(vcat, Symbolics.scalarize.(obs_exprs); init = []); + sys, Symbolics.scalarize.(obs_exprs); array_type = :tuple) obs_sym_tuple = (obs_syms...,) @@ -1070,7 +1070,7 @@ function compile_user_affect(affect::MutatingFunctionalAffect, cb, sys, dvs, ps; mod_pairs = mod_exprs .=> mod_syms mod_names = (mod_syms..., ) mod_og_val_fun = build_explicit_observed_function( - sys, reduce(vcat, Symbolics.scalarize.(first.(mod_pairs)); init = []); + sys, Symbolics.scalarize.(first.(mod_pairs)); array_type = :tuple) upd_funs = NamedTuple{mod_names}((setu.((sys,), first.(mod_pairs))...,)) @@ -1084,7 +1084,8 @@ function compile_user_affect(affect::MutatingFunctionalAffect, cb, sys, dvs, ps; let user_affect = func(affect), ctx = context(affect) function (integ) # update the to-be-mutated values; this ensures that if you do a no-op then nothing happens - upd_component_array = NamedTuple{mod_names}(mod_og_val_fun(integ.u, integ.p..., integ.t)) + modvals = mod_og_val_fun(integ.u, integ.p..., integ.t) + upd_component_array = NamedTuple{mod_names}(modvals) # update the observed values obs_component_array = NamedTuple{obs_sym_tuple}(obs_fun(integ.u, integ.p..., integ.t)) From 2206425cae06f57c2985607c1c18d740bb7d439c Mon Sep 17 00:00:00 2001 From: Benjamin Chung Date: Mon, 23 Sep 2024 08:18:16 -0700 Subject: [PATCH 0176/1337] Remove ComponentArrays dep, cleanup handling of skip_checks --- Project.toml | 1 - src/systems/callbacks.jl | 29 +++++++++++++++-------------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/Project.toml b/Project.toml index 729966fde0..808d68af06 100644 --- a/Project.toml +++ b/Project.toml @@ -9,7 +9,6 @@ ArrayInterface = "4fba245c-0d91-5ea0-9b3e-6abc04ee57a9" BlockArrays = "8e7c35d0-a365-5155-bbbb-fb81a777f24e" Combinatorics = "861a8166-3701-5b0c-9a16-15d98fcdc6aa" Compat = "34da2185-b29b-5c13-b0c7-acf172513d20" -ComponentArrays = "b0b7db55-cfe3-40fc-9ded-d10e2dbeff66" ConstructionBase = "187b0558-2788-49d3-abe0-74a17ed4e7c9" DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8" DiffEqBase = "2b5f629d-d688-5b77-993f-72d75c75574e" diff --git a/src/systems/callbacks.jl b/src/systems/callbacks.jl index ad41807ab8..cf12b9078b 100644 --- a/src/systems/callbacks.jl +++ b/src/systems/callbacks.jl @@ -1024,26 +1024,27 @@ function compile_user_affect(affect::MutatingFunctionalAffect, cb, sys, dvs, ps; end obs_exprs = observed(affect) - for oexpr in obs_exprs - invalid_vars = invalid_variables(sys, oexpr) - if length(invalid_vars) > 0 && !affect.skip_checks - error("Observed equation $(oexpr) in affect refers to missing variable(s) $(invalid_vars); the variables may not have been added (e.g. if a component is missing).") + if !affect.skip_checks + for oexpr in obs_exprs + invalid_vars = invalid_variables(sys, oexpr) + if length(invalid_vars) > 0 + error("Observed equation $(oexpr) in affect refers to missing variable(s) $(invalid_vars); the variables may not have been added (e.g. if a component is missing).") + end end end obs_syms = observed_syms(affect) obs_syms, obs_exprs = check_dups(obs_syms, obs_exprs) mod_exprs = modified(affect) - for mexpr in mod_exprs - if affect.skip_checks - continue - end - if !is_variable(sys, mexpr) && parameter_index(sys, mexpr) === nothing && !affect.skip_checks - @warn ("Expression $mexpr cannot be assigned to; currently only unknowns and parameters may be updated by an affect.") - end - invalid_vars = unassignable_variables(sys, mexpr) - if length(invalid_vars) > 0 && !affect.skip_checks - error("Modified equation $(mexpr) in affect refers to missing variable(s) $(invalid_vars); the variables may not have been added (e.g. if a component is missing) or they may have been reduced away.") + if !affect.skip_checks + for mexpr in mod_exprs + if !is_variable(sys, mexpr) && parameter_index(sys, mexpr) === nothing + @warn ("Expression $mexpr cannot be assigned to; currently only unknowns and parameters may be updated by an affect.") + end + invalid_vars = unassignable_variables(sys, mexpr) + if length(invalid_vars) > 0 + error("Modified equation $(mexpr) in affect refers to missing variable(s) $(invalid_vars); the variables may not have been added (e.g. if a component is missing) or they may have been reduced away.") + end end end mod_syms = modified_syms(affect) From 98dcd4ee1ff90a572c273977f780c4cd23f4ce0d Mon Sep 17 00:00:00 2001 From: Benjamin Chung Date: Mon, 23 Sep 2024 09:59:43 -0700 Subject: [PATCH 0177/1337] Improve detection of writeback values --- src/systems/callbacks.jl | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/systems/callbacks.jl b/src/systems/callbacks.jl index cf12b9078b..23b7ccac2c 100644 --- a/src/systems/callbacks.jl +++ b/src/systems/callbacks.jl @@ -999,6 +999,18 @@ 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::MutatingFunctionalAffect, cb, sys, dvs, ps; kwargs...) #= Implementation sketch: @@ -1038,7 +1050,7 @@ function compile_user_affect(affect::MutatingFunctionalAffect, cb, sys, dvs, ps; mod_exprs = modified(affect) if !affect.skip_checks for mexpr in mod_exprs - if !is_variable(sys, mexpr) && parameter_index(sys, mexpr) === nothing + if !check_assignable(sys, mexpr) @warn ("Expression $mexpr cannot be assigned to; currently only unknowns and parameters may be updated by an affect.") end invalid_vars = unassignable_variables(sys, mexpr) From 3fd4462d31d0c10b77fae3a0c6678d36f9fe0046 Mon Sep 17 00:00:00 2001 From: Benjamin Chung Date: Mon, 23 Sep 2024 10:06:16 -0700 Subject: [PATCH 0178/1337] Remove ComponentArrays dep --- src/ModelingToolkit.jl | 1 - 1 file changed, 1 deletion(-) diff --git a/src/ModelingToolkit.jl b/src/ModelingToolkit.jl index f5262a1526..2f57bb1765 100644 --- a/src/ModelingToolkit.jl +++ b/src/ModelingToolkit.jl @@ -54,7 +54,6 @@ using Reexport using RecursiveArrayTools import Graphs: SimpleDiGraph, add_edge!, incidence_matrix import BlockArrays: BlockedArray, Block, blocksize, blocksizes -import ComponentArrays using RuntimeGeneratedFunctions using RuntimeGeneratedFunctions: drop_expr From e6ce6ab65fe9e62b5c34f8adc546ceafa91f475f Mon Sep 17 00:00:00 2001 From: Benjamin Chung Date: Thu, 17 Oct 2024 12:00:44 -0700 Subject: [PATCH 0179/1337] Rename MutatingFunctionalAffect to ImperativeAffect --- src/systems/callbacks.jl | 74 +++++++++++++++---------------- src/systems/index_cache.jl | 2 +- test/symbolic_events.jl | 90 +++++++++++++++++++------------------- 3 files changed, 83 insertions(+), 83 deletions(-) diff --git a/src/systems/callbacks.jl b/src/systems/callbacks.jl index 23b7ccac2c..73f2a7a6cf 100644 --- a/src/systems/callbacks.jl +++ b/src/systems/callbacks.jl @@ -72,9 +72,9 @@ function namespace_affect(affect::FunctionalAffect, s) end """ - MutatingFunctionalAffect(f::Function; modified::NamedTuple, observed::NamedTuple, ctx) + ImperativeAffect(f::Function; modified::NamedTuple, observed::NamedTuple, ctx) -`MutatingFunctionalAffect` is a helper for writing affect functions that will compute observed values and +`ImperativeAffect` is a helper for writing affect functions that will compute observed values and ensure that modified values are correctly written back into the system. The affect function `f` needs to have one of four signatures: * `f(modified::NamedTuple)::NamedTuple` if the function only writes values (unknowns or parameters) to the system, @@ -93,7 +93,7 @@ then the NamedTuple `(;x=2)` will be passed as `observed` to the affect function The NamedTuple returned from `f` includes the values to be written back to the system after `f` returns. For example, if we want to update the value of `x` to be the result of `x + y` we could write - MutatingFunctionalAffect(observed=(; x_plus_y = x + y), modified=(; x)) do m, o + ImperativeAffect(observed=(; x_plus_y = x + y), modified=(; x)) do m, o @set! m.x = o.x_plus_y end @@ -101,7 +101,7 @@ Where we use Setfield to copy the tuple `m` with a new value for `x`, then retur `modified`; a runtime error will be produced if a value is written that does not appear in `modified`. The user can dynamically decide not to write a value back by not including it in the returned tuple, in which case the associated field will not be updated. """ -@kwdef struct MutatingFunctionalAffect +@kwdef struct ImperativeAffect f::Any obs::Vector obs_syms::Vector{Symbol} @@ -111,50 +111,50 @@ in the returned tuple, in which case the associated field will not be updated. skip_checks::Bool end -function MutatingFunctionalAffect(f::Function; +function ImperativeAffect(f::Function; observed::NamedTuple = NamedTuple{()}(()), modified::NamedTuple = NamedTuple{()}(()), ctx = nothing, skip_checks = false) - MutatingFunctionalAffect(f, + ImperativeAffect(f, collect(values(observed)), collect(keys(observed)), collect(values(modified)), collect(keys(modified)), ctx, skip_checks) end -function MutatingFunctionalAffect(f::Function, modified::NamedTuple; +function ImperativeAffect(f::Function, modified::NamedTuple; observed::NamedTuple = NamedTuple{()}(()), ctx = nothing, skip_checks=false) - MutatingFunctionalAffect(f, observed = observed, modified = modified, ctx = ctx, skip_checks = skip_checks) + ImperativeAffect(f, observed = observed, modified = modified, ctx = ctx, skip_checks = skip_checks) end -function MutatingFunctionalAffect( +function ImperativeAffect( f::Function, modified::NamedTuple, observed::NamedTuple; ctx = nothing, skip_checks=false) - MutatingFunctionalAffect(f, observed = observed, modified = modified, ctx = ctx, skip_checks = skip_checks) + ImperativeAffect(f, observed = observed, modified = modified, ctx = ctx, skip_checks = skip_checks) end -function MutatingFunctionalAffect( +function ImperativeAffect( f::Function, modified::NamedTuple, observed::NamedTuple, ctx; skip_checks=false) - MutatingFunctionalAffect(f, observed = observed, modified = modified, ctx = ctx, skip_checks = skip_checks) + ImperativeAffect(f, observed = observed, modified = modified, ctx = ctx, skip_checks = skip_checks) end -function Base.show(io::IO, mfa::MutatingFunctionalAffect) +function Base.show(io::IO, mfa::ImperativeAffect) obs_vals = join(map((ob,nm) -> "$ob => $nm", mfa.obs, mfa.obs_syms), ", ") mod_vals = join(map((md,nm) -> "$md => $nm", mfa.modified, mfa.mod_syms), ", ") affect = mfa.f - print(io, "MutatingFunctionalAffect(observed: [$obs_vals], modified: [$mod_vals], affect:$affect)") + print(io, "ImperativeAffect(observed: [$obs_vals], modified: [$mod_vals], affect:$affect)") end -func(f::MutatingFunctionalAffect) = f.f -context(a::MutatingFunctionalAffect) = a.ctx -observed(a::MutatingFunctionalAffect) = a.obs -observed_syms(a::MutatingFunctionalAffect) = a.obs_syms -discretes(a::MutatingFunctionalAffect) = filter(ModelingToolkit.isparameter, a.modified) -modified(a::MutatingFunctionalAffect) = a.modified -modified_syms(a::MutatingFunctionalAffect) = a.mod_syms +func(f::ImperativeAffect) = f.f +context(a::ImperativeAffect) = a.ctx +observed(a::ImperativeAffect) = a.obs +observed_syms(a::ImperativeAffect) = a.obs_syms +discretes(a::ImperativeAffect) = filter(ModelingToolkit.isparameter, a.modified) +modified(a::ImperativeAffect) = a.modified +modified_syms(a::ImperativeAffect) = a.mod_syms -function Base.:(==)(a1::MutatingFunctionalAffect, a2::MutatingFunctionalAffect) +function Base.:(==)(a1::ImperativeAffect, a2::ImperativeAffect) isequal(a1.f, a2.f) && isequal(a1.obs, a2.obs) && isequal(a1.modified, a2.modified) && isequal(a1.obs_syms, a2.obs_syms) && isequal(a1.mod_syms, a2.mod_syms) && isequal(a1.ctx, a2.ctx) end -function Base.hash(a::MutatingFunctionalAffect, s::UInt) +function Base.hash(a::ImperativeAffect, s::UInt) s = hash(a.f, s) s = hash(a.obs, s) s = hash(a.obs_syms, s) @@ -163,8 +163,8 @@ function Base.hash(a::MutatingFunctionalAffect, s::UInt) hash(a.ctx, s) end -function namespace_affect(affect::MutatingFunctionalAffect, s) - MutatingFunctionalAffect(func(affect), +function namespace_affect(affect::ImperativeAffect, s) + ImperativeAffect(func(affect), namespace_expr.(observed(affect), (s,)), observed_syms(affect), renamespace.((s,), modified(affect)), @@ -174,7 +174,7 @@ function namespace_affect(affect::MutatingFunctionalAffect, s) end function has_functional_affect(cb) - (affects(cb) isa FunctionalAffect || affects(cb) isa MutatingFunctionalAffect) + (affects(cb) isa FunctionalAffect || affects(cb) isa ImperativeAffect) end #################################### continuous events ##################################### @@ -219,7 +219,7 @@ Affects (i.e. `affect` and `affect_neg`) can be specified as either: + `read_parameters` is a vector of the parameters that are *used* by `f!`. Their indices are passed to `f` in `p` similarly to the indices of `unknowns` passed in `u`. + `modified_parameters` is a vector of the parameters that are *modified* by `f!`. Note that a parameter will not appear in `p` if it only appears in `modified_parameters`; it must appear in both `parameters` and `modified_parameters` if it is used in the affect definition. + `ctx` is a user-defined context object passed to `f!` when invoked. This value is aliased for each problem. -* A [`MutatingFunctionalAffect`](@ref); refer to its documentation for details. +* A [`ImperativeAffect`](@ref); refer to its documentation for details. Callbacks that impact a DAE are applied, then the DAE is reinitialized using `reinitializealg` (which defaults to `SciMLBase.CheckInit`). This reinitialization algorithm ensures that the DAE is satisfied after the callback runs. The default value of `CheckInit` will simply validate @@ -228,10 +228,10 @@ initialization. """ struct SymbolicContinuousCallback eqs::Vector{Equation} - initialize::Union{Vector{Equation}, FunctionalAffect, MutatingFunctionalAffect} - finalize::Union{Vector{Equation}, FunctionalAffect, MutatingFunctionalAffect} - affect::Union{Vector{Equation}, FunctionalAffect, MutatingFunctionalAffect} - affect_neg::Union{Vector{Equation}, FunctionalAffect, MutatingFunctionalAffect, Nothing} + 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(; @@ -390,7 +390,7 @@ reinitialization_algs(cbs::Vector{SymbolicContinuousCallback}) = 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(af::MutatingFunctionalAffect, s) = namespace_affect(af, s) +namespace_affects(af::ImperativeAffect, s) = namespace_affect(af, s) namespace_affects(::Nothing, s) = nothing function namespace_callback(cb::SymbolicContinuousCallback, s)::SymbolicContinuousCallback @@ -460,7 +460,7 @@ scalarize_affects(affects) = scalarize(affects) scalarize_affects(affects::Tuple) = FunctionalAffect(affects...) scalarize_affects(affects::NamedTuple) = FunctionalAffect(; affects...) scalarize_affects(affects::FunctionalAffect) = affects -scalarize_affects(affects::MutatingFunctionalAffect) = affects +scalarize_affects(affects::ImperativeAffect) = affects SymbolicDiscreteCallback(p::Pair) = SymbolicDiscreteCallback(p[1], p[2]) SymbolicDiscreteCallback(cb::SymbolicDiscreteCallback) = cb # passthrough @@ -468,7 +468,7 @@ 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 MutatingFunctionalAffect + if db.affects isa FunctionalAffect || db.affects isa ImperativeAffect # TODO println(io, " ", db.affects) else @@ -1011,7 +1011,7 @@ function check_assignable(sys, sym) end end -function compile_user_affect(affect::MutatingFunctionalAffect, cb, sys, dvs, ps; kwargs...) +function compile_user_affect(affect::ImperativeAffect, cb, sys, dvs, ps; kwargs...) #= Implementation sketch: generate observed function (oop), should save to a component array under obs_syms @@ -1113,7 +1113,7 @@ function compile_user_affect(affect::MutatingFunctionalAffect, cb, sys, dvs, ps; elseif applicable(user_affect, upd_component_array) user_affect(upd_component_array) else - @error "User affect function $user_affect needs to implement one of the supported MutatingFunctionalAffect callback forms; see the MutatingFunctionalAffect docstring for more details" + @error "User affect function $user_affect needs to implement one of the supported ImperativeAffect callback forms; see the ImperativeAffect docstring for more details" user_affect(upd_component_array, obs_component_array, integ, ctx) # this WILL error but it'll give a more sensible message end @@ -1127,7 +1127,7 @@ function compile_user_affect(affect::MutatingFunctionalAffect, cb, sys, dvs, ps; end end -function compile_affect(affect::Union{FunctionalAffect, MutatingFunctionalAffect}, cb, sys, dvs, ps; kwargs...) +function compile_affect(affect::Union{FunctionalAffect, ImperativeAffect}, cb, sys, dvs, ps; kwargs...) compile_user_affect(affect, cb, sys, dvs, ps; kwargs...) end diff --git a/src/systems/index_cache.jl b/src/systems/index_cache.jl index 55d819990b..bd295055fa 100644 --- a/src/systems/index_cache.jl +++ b/src/systems/index_cache.jl @@ -126,7 +126,7 @@ function IndexCache(sys::AbstractSystem) for affect in affs if affect isa Equation is_parameter(sys, affect.lhs) && push!(discs, affect.lhs) - elseif affect isa FunctionalAffect || affect isa MutatingFunctionalAffect + elseif affect isa FunctionalAffect || affect isa ImperativeAffect union!(discs, unwrap.(discretes(affect))) else error("Unhandled affect type $(typeof(affect))") diff --git a/test/symbolic_events.jl b/test/symbolic_events.jl index bc455ec06e..b301b65650 100644 --- a/test/symbolic_events.jl +++ b/test/symbolic_events.jl @@ -228,10 +228,10 @@ affect_neg = [x ~ 1] @test e[].affect == affect end -@testset "MutatingFunctionalAffect constructors" begin +@testset "ImperativeAffect constructors" begin fmfa(o, x, i, c) = nothing - m = ModelingToolkit.MutatingFunctionalAffect(fmfa) - @test m isa ModelingToolkit.MutatingFunctionalAffect + m = ModelingToolkit.ImperativeAffect(fmfa) + @test m isa ModelingToolkit.ImperativeAffect @test m.f == fmfa @test m.obs == [] @test m.obs_syms == [] @@ -239,8 +239,8 @@ end @test m.mod_syms == [] @test m.ctx === nothing - m = ModelingToolkit.MutatingFunctionalAffect(fmfa, (;)) - @test m isa ModelingToolkit.MutatingFunctionalAffect + m = ModelingToolkit.ImperativeAffect(fmfa, (;)) + @test m isa ModelingToolkit.ImperativeAffect @test m.f == fmfa @test m.obs == [] @test m.obs_syms == [] @@ -248,8 +248,8 @@ end @test m.mod_syms == [] @test m.ctx === nothing - m = ModelingToolkit.MutatingFunctionalAffect(fmfa, (; x)) - @test m isa ModelingToolkit.MutatingFunctionalAffect + m = ModelingToolkit.ImperativeAffect(fmfa, (; x)) + @test m isa ModelingToolkit.ImperativeAffect @test m.f == fmfa @test isequal(m.obs, []) @test m.obs_syms == [] @@ -257,8 +257,8 @@ end @test m.mod_syms == [:x] @test m.ctx === nothing - m = ModelingToolkit.MutatingFunctionalAffect(fmfa, (; y = x)) - @test m isa ModelingToolkit.MutatingFunctionalAffect + m = ModelingToolkit.ImperativeAffect(fmfa, (; y = x)) + @test m isa ModelingToolkit.ImperativeAffect @test m.f == fmfa @test isequal(m.obs, []) @test m.obs_syms == [] @@ -266,8 +266,8 @@ end @test m.mod_syms == [:y] @test m.ctx === nothing - m = ModelingToolkit.MutatingFunctionalAffect(fmfa; observed = (; y = x)) - @test m isa ModelingToolkit.MutatingFunctionalAffect + 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] @@ -275,8 +275,8 @@ end @test m.mod_syms == [] @test m.ctx === nothing - m = ModelingToolkit.MutatingFunctionalAffect(fmfa; modified = (; x)) - @test m isa ModelingToolkit.MutatingFunctionalAffect + m = ModelingToolkit.ImperativeAffect(fmfa; modified = (; x)) + @test m isa ModelingToolkit.ImperativeAffect @test m.f == fmfa @test isequal(m.obs, []) @test m.obs_syms == [] @@ -284,8 +284,8 @@ end @test m.mod_syms == [:x] @test m.ctx === nothing - m = ModelingToolkit.MutatingFunctionalAffect(fmfa; modified = (; y = x)) - @test m isa ModelingToolkit.MutatingFunctionalAffect + m = ModelingToolkit.ImperativeAffect(fmfa; modified = (; y = x)) + @test m isa ModelingToolkit.ImperativeAffect @test m.f == fmfa @test isequal(m.obs, []) @test m.obs_syms == [] @@ -293,8 +293,8 @@ end @test m.mod_syms == [:y] @test m.ctx === nothing - m = ModelingToolkit.MutatingFunctionalAffect(fmfa, (; x), (; x)) - @test m isa ModelingToolkit.MutatingFunctionalAffect + 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] @@ -302,8 +302,8 @@ end @test m.mod_syms == [:x] @test m.ctx === nothing - m = ModelingToolkit.MutatingFunctionalAffect(fmfa, (; y = x), (; y = x)) - @test m isa ModelingToolkit.MutatingFunctionalAffect + 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] @@ -311,9 +311,9 @@ end @test m.mod_syms == [:y] @test m.ctx === nothing - m = ModelingToolkit.MutatingFunctionalAffect( + m = ModelingToolkit.ImperativeAffect( fmfa; modified = (; y = x), observed = (; y = x)) - @test m isa ModelingToolkit.MutatingFunctionalAffect + @test m isa ModelingToolkit.ImperativeAffect @test m.f == fmfa @test isequal(m.obs, [x]) @test m.obs_syms == [:y] @@ -321,9 +321,9 @@ end @test m.mod_syms == [:y] @test m.ctx === nothing - m = ModelingToolkit.MutatingFunctionalAffect( + m = ModelingToolkit.ImperativeAffect( fmfa; modified = (; y = x), observed = (; y = x), ctx = 3) - @test m isa ModelingToolkit.MutatingFunctionalAffect + @test m isa ModelingToolkit.ImperativeAffect @test m.f == fmfa @test isequal(m.obs, [x]) @test m.obs_syms == [:y] @@ -331,8 +331,8 @@ end @test m.mod_syms == [:y] @test m.ctx === 3 - m = ModelingToolkit.MutatingFunctionalAffect(fmfa, (; x), (; x), 3) - @test m isa ModelingToolkit.MutatingFunctionalAffect + 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] @@ -1010,12 +1010,12 @@ end furnace_off = ModelingToolkit.SymbolicContinuousCallback( [temp ~ furnace_off_threshold], - ModelingToolkit.MutatingFunctionalAffect(modified = (; furnace_on)) do x, o, i, c + 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.MutatingFunctionalAffect(modified = (; furnace_on)) do x, o, i, c + ModelingToolkit.ImperativeAffect(modified = (; furnace_on)) do x, o, i, c @set! x.furnace_on = true end) @named sys = ODESystem( @@ -1027,12 +1027,12 @@ end furnace_off = ModelingToolkit.SymbolicContinuousCallback( [temp ~ furnace_off_threshold], - ModelingToolkit.MutatingFunctionalAffect(modified = (; furnace_on)) do x, o, i + ModelingToolkit.ImperativeAffect(modified = (; furnace_on)) do x, o, i @set! x.furnace_on = false end) furnace_enable = ModelingToolkit.SymbolicContinuousCallback( [temp ~ furnace_on_threshold], - ModelingToolkit.MutatingFunctionalAffect(modified = (; furnace_on)) do x, o, i + ModelingToolkit.ImperativeAffect(modified = (; furnace_on)) do x, o, i @set! x.furnace_on = true end) @named sys = ODESystem( @@ -1044,12 +1044,12 @@ end furnace_off = ModelingToolkit.SymbolicContinuousCallback( [temp ~ furnace_off_threshold], - ModelingToolkit.MutatingFunctionalAffect(modified = (; furnace_on)) do x, o + ModelingToolkit.ImperativeAffect(modified = (; furnace_on)) do x, o @set! x.furnace_on = false end) furnace_enable = ModelingToolkit.SymbolicContinuousCallback( [temp ~ furnace_on_threshold], - ModelingToolkit.MutatingFunctionalAffect(modified = (; furnace_on)) do x, o + ModelingToolkit.ImperativeAffect(modified = (; furnace_on)) do x, o @set! x.furnace_on = true end) @named sys = ODESystem( @@ -1061,12 +1061,12 @@ end furnace_off = ModelingToolkit.SymbolicContinuousCallback( [temp ~ furnace_off_threshold], - ModelingToolkit.MutatingFunctionalAffect(modified = (; furnace_on)) do x + ModelingToolkit.ImperativeAffect(modified = (; furnace_on)) do x @set! x.furnace_on = false end) furnace_enable = ModelingToolkit.SymbolicContinuousCallback( [temp ~ furnace_on_threshold], - ModelingToolkit.MutatingFunctionalAffect(modified = (; furnace_on)) do x + ModelingToolkit.ImperativeAffect(modified = (; furnace_on)) do x @set! x.furnace_on = true end) @named sys = ODESystem( @@ -1078,14 +1078,14 @@ end furnace_off = ModelingToolkit.SymbolicContinuousCallback( [temp ~ furnace_off_threshold], - ModelingToolkit.MutatingFunctionalAffect(modified = (; furnace_on)) do x + ModelingToolkit.ImperativeAffect(modified = (; furnace_on)) do x @set! x.furnace_on = false - end; initialize = ModelingToolkit.MutatingFunctionalAffect(modified = (; temp)) do x + end; initialize = ModelingToolkit.ImperativeAffect(modified = (; temp)) do x @set! x.temp = 0.2 end) furnace_enable = ModelingToolkit.SymbolicContinuousCallback( [temp ~ furnace_on_threshold], - ModelingToolkit.MutatingFunctionalAffect(modified = (; furnace_on)) do x, o, c, i + ModelingToolkit.ImperativeAffect(modified = (; furnace_on)) do x, o, c, i @set! x.furnace_on = true end) @named sys = ODESystem( @@ -1097,7 +1097,7 @@ end @test all(sol[temp][sol.t .!= 0.0] .<= 0.79) && all(sol[temp][sol.t .!= 0.0] .>= 0.2) end -@testset "MutatingFunctionalAffect errors and warnings" begin +@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 = [ @@ -1106,7 +1106,7 @@ end furnace_off = ModelingToolkit.SymbolicContinuousCallback( [temp ~ furnace_off_threshold], - ModelingToolkit.MutatingFunctionalAffect( + ModelingToolkit.ImperativeAffect( modified = (; furnace_on), observed = (; furnace_on)) do x, o, c, i @set! x.furnace_on = false end) @@ -1122,7 +1122,7 @@ end furnace_off = ModelingToolkit.SymbolicContinuousCallback( [temp ~ furnace_off_threshold], - ModelingToolkit.MutatingFunctionalAffect( + ModelingToolkit.ImperativeAffect( modified = (; furnace_on, tempsq), observed = (; furnace_on)) do x, o, c, i @set! x.furnace_on = false end) @@ -1135,7 +1135,7 @@ end @parameters not_actually_here furnace_off = ModelingToolkit.SymbolicContinuousCallback( [temp ~ furnace_off_threshold], - ModelingToolkit.MutatingFunctionalAffect(modified = (; furnace_on), + ModelingToolkit.ImperativeAffect(modified = (; furnace_on), observed = (; furnace_on, not_actually_here)) do x, o, c, i @set! x.furnace_on = false end) @@ -1148,7 +1148,7 @@ end furnace_off = ModelingToolkit.SymbolicContinuousCallback( [temp ~ furnace_off_threshold], - ModelingToolkit.MutatingFunctionalAffect(modified = (; furnace_on), + ModelingToolkit.ImperativeAffect(modified = (; furnace_on), observed = (; furnace_on)) do x, o, c, i return (;fictional2 = false) end) @@ -1181,14 +1181,14 @@ end end end qAevt = ModelingToolkit.SymbolicContinuousCallback([cos(100 * theta) ~ 0], - ModelingToolkit.MutatingFunctionalAffect((; qA, hA, hB, cnt), (; qB)) do x, o, i, c + ModelingToolkit.ImperativeAffect((; qA, hA, hB, cnt), (; qB)) do x, o, i, c @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.MutatingFunctionalAffect( + affect_neg = ModelingToolkit.ImperativeAffect( (; qA, hA, hB, cnt), (; qB)) do x, o, c, i @set! x.hA = x.qA @set! x.hB = o.qB @@ -1197,14 +1197,14 @@ end x end; rootfind = SciMLBase.RightRootFind) qBevt = ModelingToolkit.SymbolicContinuousCallback([cos(100 * theta - π / 2) ~ 0], - ModelingToolkit.MutatingFunctionalAffect((; qB, hA, hB, cnt), (; qA)) do x, o, i, c + ModelingToolkit.ImperativeAffect((; qB, hA, hB, cnt), (; qA)) do x, o, i, c @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.MutatingFunctionalAffect( + affect_neg = ModelingToolkit.ImperativeAffect( (; qB, hA, hB, cnt), (; qA)) do x, o, c, i @set! x.hA = o.qA @set! x.hB = x.qB From 54bb95af05f6212375ba3a9e2717147b45246ad9 Mon Sep 17 00:00:00 2001 From: Benjamin Chung Date: Thu, 17 Oct 2024 17:02:04 -0700 Subject: [PATCH 0180/1337] Fix tests --- src/systems/callbacks.jl | 19 +++++++++++-------- test/symbolic_events.jl | 14 ++++++++++++++ 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/src/systems/callbacks.jl b/src/systems/callbacks.jl index 73f2a7a6cf..96758ccf04 100644 --- a/src/systems/callbacks.jl +++ b/src/systems/callbacks.jl @@ -781,21 +781,24 @@ function generate_single_rootfinding_callback( 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 = SciMLBase.INITIALIZE_DEFAULT + initfn = user_initfun end + return ContinuousCallback( cond, affect_function.affect, affect_function.affect_neg, rootfind = cb.rootfind, - initialize = isnothing(affect_function.initialize) ? SciMLBase.INITIALIZE_DEFAULT : (c, u, t, i) -> affect_function.initialize(i), + initialize = initfn, finalize = isnothing(affect_function.finalize) ? SciMLBase.FINALIZE_DEFAULT : (c, u, t, i) -> affect_function.finalize(i), initializealg = reinitialization_alg(cb)) end @@ -878,8 +881,8 @@ function compile_affect_fn(cb, sys::AbstractODESystem, 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) - if isnothing(aff) + 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...) @@ -890,8 +893,8 @@ function compile_affect_fn(cb, sys::AbstractODESystem, dvs, ps, kwargs) else affect_neg = compile_optional_affect(eq_neg_aff) end - initialize = compile_optional_affect(initialize_affects(cb)) - finalize = compile_optional_affect(finalize_affects(cb)) + initialize = compile_optional_affect(initialize_affects(cb), NULL_AFFECT) + finalize = compile_optional_affect(finalize_affects(cb), NULL_AFFECT) (affect = affect, affect_neg = affect_neg, initialize = initialize, finalize = finalize) end @@ -1097,11 +1100,11 @@ function compile_user_affect(affect::ImperativeAffect, cb, sys, dvs, ps; kwargs. let user_affect = func(affect), ctx = context(affect) 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) + modvals = mod_og_val_fun(integ.u, integ.p, integ.t) upd_component_array = NamedTuple{mod_names}(modvals) # update the observed values - obs_component_array = NamedTuple{obs_sym_tuple}(obs_fun(integ.u, integ.p..., integ.t)) + obs_component_array = NamedTuple{obs_sym_tuple}(obs_fun(integ.u, integ.p, integ.t)) # let the user do their thing modvals = if applicable(user_affect, upd_component_array, obs_component_array, ctx, integ) diff --git a/test/symbolic_events.jl b/test/symbolic_events.jl index b301b65650..c2c26aae7f 100644 --- a/test/symbolic_events.jl +++ b/test/symbolic_events.jl @@ -1219,3 +1219,17 @@ end sol = solve(prob, Tsit5(); dtmax = 0.01) @test getp(sol, cnt)(sol) == 197 # 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 + + + +import RuntimeGeneratedFunctions +function (f::RuntimeGeneratedFunctions.RuntimeGeneratedFunction{argnames, cache_tag, context_tag, id})(args::Vararg{Any, N}) where {N, argnames, cache_tag, context_tag, id} + try + RuntimeGeneratedFunctions.generated_callfunc(f, args...) + catch e + @error "Caught error in RuntimeGeneratedFunction; source code follows" + func_expr = Expr(:->, Expr(:tuple, argnames...), RuntimeGeneratedFunctions._lookup_body(cache_tag, id)) + @show func_expr + rethrow(e) + end +end From 05e89abc0abfedfd4f30e696439b78253d81dc96 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Sat, 12 Oct 2024 13:32:36 +0530 Subject: [PATCH 0181/1337] feat: initial implementation of HomotopyContinuation interface --- Project.toml | 5 + ext/MTKHomotopyContinuationExt.jl | 123 +++++++++++++++++++++++ src/ModelingToolkit.jl | 3 + src/systems/nonlinear/nonlinearsystem.jl | 30 ++++++ 4 files changed, 161 insertions(+) create mode 100644 ext/MTKHomotopyContinuationExt.jl diff --git a/Project.toml b/Project.toml index 808d68af06..c12cae9f23 100644 --- a/Project.toml +++ b/Project.toml @@ -8,6 +8,7 @@ AbstractTrees = "1520ce14-60c1-5f80-bbc7-55ef81b5835c" ArrayInterface = "4fba245c-0d91-5ea0-9b3e-6abc04ee57a9" BlockArrays = "8e7c35d0-a365-5155-bbbb-fb81a777f24e" Combinatorics = "861a8166-3701-5b0c-9a16-15d98fcdc6aa" +CommonSolve = "38540f10-b2f7-11e9-35d8-d573e4eb0ff2" Compat = "34da2185-b29b-5c13-b0c7-acf172513d20" ConstructionBase = "187b0558-2788-49d3-abe0-74a17ed4e7c9" DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8" @@ -61,12 +62,14 @@ Unitful = "1986cc42-f94f-5a68-af5c-568840ba703d" BifurcationKit = "0f109fa4-8a5d-4b75-95aa-f515264e7665" ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4" DeepDiffs = "ab62b9b5-e342-54a8-a765-a90f495de1a6" +HomotopyContinuation = "f213a82b-91d6-5c5d-acf7-10f1c761b327" LabelledArrays = "2ee39098-c373-598a-b85f-a56591580800" [extensions] MTKBifurcationKitExt = "BifurcationKit" MTKChainRulesCoreExt = "ChainRulesCore" MTKDeepDiffsExt = "DeepDiffs" +MTKHomotopyContinuationExt = "HomotopyContinuation" MTKLabelledArraysExt = "LabelledArrays" [compat] @@ -76,6 +79,7 @@ BifurcationKit = "0.3" BlockArrays = "1.1" ChainRulesCore = "1" Combinatorics = "1" +CommonSolve = "0.2.4" Compat = "3.42, 4" ConstructionBase = "1" DataInterpolations = "6.4" @@ -97,6 +101,7 @@ ForwardDiff = "0.10.3" FunctionWrappers = "1.1" FunctionWrappersWrappers = "0.1" Graphs = "1.5.2" +HomotopyContinuation = "2.11" InteractiveUtils = "1" JuliaFormatter = "1.0.47" JumpProcesses = "9.13.1" diff --git a/ext/MTKHomotopyContinuationExt.jl b/ext/MTKHomotopyContinuationExt.jl new file mode 100644 index 0000000000..a36e5aef8d --- /dev/null +++ b/ext/MTKHomotopyContinuationExt.jl @@ -0,0 +1,123 @@ +module MTKHomotopyContinuationExt + +using ModelingToolkit +using ModelingToolkit.SciMLBase +using ModelingToolkit.Symbolics: unwrap +using ModelingToolkit.SymbolicIndexingInterface +using HomotopyContinuation +using ModelingToolkit: iscomplete, parameters, has_index_cache, get_index_cache, get_u0, + get_u0_p, check_eqs_u0, CommonSolve + +const MTK = ModelingToolkit + +function contains_variable(x, wrt) + any(isequal(x), wrt) && return true + iscall(x) || return false + return any(y -> contains_variable(y, wrt), arguments(x)) +end + +function is_polynomial(x, wrt) + x = unwrap(x) + symbolic_type(x) == NotSymbolic() && return true + iscall(x) || return true + contains_variable(x, wrt) || return true + any(isequal(x), wrt) && return true + + if operation(x) in (*, +, -) + return all(y -> is_polynomial(y, wrt), arguments(x)) + end + if operation(x) == (^) + b, p = arguments(x) + return is_polynomial(b, wrt) && !contains_variable(p, wrt) + end + return false +end + +function symbolics_to_hc(expr) + if iscall(expr) + if operation(expr) == getindex + args = arguments(expr) + return ModelKit.Variable(getname(args[1]), args[2:end]...) + else + return operation(expr)(symbolics_to_hc.(arguments(expr))...) + end + elseif symbolic_type(expr) == NotSymbolic() + return expr + else + return ModelKit.Variable(getname(expr)) + end +end + +struct MTKHomotopySystem{F, P, J, V} <: HomotopyContinuation.AbstractSystem + f::F + p::P + jac::J + vars::V + nexprs::Int +end + +Base.size(sys::MTKHomotopySystem) = (sys.nexprs, length(sys.vars)) +ModelKit.variables(sys::MTKHomotopySystem) = sys.vars + +function (sys::MTKHomotopySystem)(x, p = nothing) + sys.f(x, sys.p) +end + +function ModelKit.evaluate!(u, sys::MTKHomotopySystem, x, p = nothing) + sys.f(u, x, sys.p) +end + +function ModelKit.evaluate_and_jacobian!(u, U, sys::MTKHomotopySystem, x, p = nothing) + sys.f(u, x, sys.p) + sys.jac(U, x, sys.p) +end + +SymbolicIndexingInterface.parameter_values(s::MTKHomotopySystem) = s.p + +function MTK.HomotopyContinuationProblem( + sys::NonlinearSystem, u0map, parammap; compile = :all, eval_expression = false, eval_module = ModelingToolkit, kwargs...) + if !iscomplete(sys) + error("A completed `NonlinearSystem` is required. Call `complete` or `structural_simplify` on the system before creating a `HomotopyContinuationProblem`") + end + + dvs = unknowns(sys) + eqs = equations(sys) + + for eq in eqs + if !is_polynomial(eq.lhs, dvs) || !is_polynomial(eq.rhs, dvs) + error("Equation $eq is not a polynomial in the unknowns") + end + end + + nlfn, u0, p = MTK.process_SciMLProblem(NonlinearFunction{true}, sys, u0map, parammap; + jac = true, eval_expression, eval_module) + + hvars = symbolics_to_hc.(dvs) + mtkhsys = MTKHomotopySystem(nlfn.f, p, nlfn.jac, hvars, length(eqs)) + + obsfn = MTK.ObservedFunctionCache(sys; eval_expression, eval_module) + + return MTK.HomotopyContinuationProblem(u0, mtkhsys, sys, obsfn) +end + +function CommonSolve.solve(prob::MTK.HomotopyContinuationProblem; kwargs...) + sol = HomotopyContinuation.solve(prob.homotopy_continuation_system; kwargs...) + realsols = HomotopyContinuation.results(sol; only_real = true) + if isempty(realsols) + u = state_values(prob) + resid = prob.homotopy_continuation_system(u) + retcode = SciMLBase.ReturnCode.ConvergenceFailure + else + distance, idx = findmin(realsols) do result + norm(result.solution - state_values(prob)) + end + u = real.(realsols[idx].solution) + resid = prob.homotopy_continuation_system(u) + retcode = SciMLBase.ReturnCode.Success + end + + return SciMLBase.build_solution( + prob, :HomotopyContinuation, u, resid; retcode, original = sol) +end + +end diff --git a/src/ModelingToolkit.jl b/src/ModelingToolkit.jl index 2f57bb1765..7ea8cb247b 100644 --- a/src/ModelingToolkit.jl +++ b/src/ModelingToolkit.jl @@ -54,6 +54,7 @@ using Reexport using RecursiveArrayTools import Graphs: SimpleDiGraph, add_edge!, incidence_matrix import BlockArrays: BlockedArray, Block, blocksize, blocksizes +import CommonSolve using RuntimeGeneratedFunctions using RuntimeGeneratedFunctions: drop_expr @@ -281,4 +282,6 @@ export Clock, SolverStepClock, TimeDomain export MTKParameters, reorder_dimension_by_tunables!, reorder_dimension_by_tunables +export HomotopyContinuationProblem + end # module diff --git a/src/systems/nonlinear/nonlinearsystem.jl b/src/systems/nonlinear/nonlinearsystem.jl index d9ad826a2c..d048fc59c7 100644 --- a/src/systems/nonlinear/nonlinearsystem.jl +++ b/src/systems/nonlinear/nonlinearsystem.jl @@ -565,3 +565,33 @@ function Base.:(==)(sys1::NonlinearSystem, sys2::NonlinearSystem) _eq_unordered(get_ps(sys1), get_ps(sys2)) && all(s1 == s2 for (s1, s2) in zip(get_systems(sys1), get_systems(sys2))) end + +struct HomotopyContinuationProblem{uType, H, O} <: SciMLBase.AbstractNonlinearProblem{uType, true} + u0::uType + homotopy_continuation_system::H + sys::NonlinearSystem + obsfn::O +end + +function HomotopyContinuationProblem(args...; kwargs...) + error("Requires HomotopyContinuationExt") +end + +SymbolicIndexingInterface.symbolic_container(p::HomotopyContinuationProblem) = p.sys +SymbolicIndexingInterface.state_values(p::HomotopyContinuationProblem) = p.u0 +function SymbolicIndexingInterface.set_state!(p::HomotopyContinuationProblem, args...) + set_state!(p.u0, args...) +end +function SymbolicIndexingInterface.parameter_values(p::HomotopyContinuationProblem) + parameter_values(p.homotopy_continuation_system) +end +function SymbolicIndexingInterface.set_parameter!(p::HomotopyContinuationProblem, args...) + set_parameter!(parameter_values(p), args...) +end +function SymbolicIndexingInterface.observed(p::HomotopyContinuationProblem, sym) + if p.obsfn !== nothing + return p.obsfn(sym) + else + return SymbolicIndexingInterface.observed(p.sys, sym) + end +end From 6667457656881cf43db88944ffc16e0d8f0fc4a1 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Sat, 12 Oct 2024 20:08:08 +0530 Subject: [PATCH 0182/1337] test: add tests for HomotopyContinuation.jl interface --- test/extensions/Project.toml | 2 + test/extensions/homotopy_continuation.jl | 62 ++++++++++++++++++++++++ test/runtests.jl | 1 + 3 files changed, 65 insertions(+) create mode 100644 test/extensions/homotopy_continuation.jl diff --git a/test/extensions/Project.toml b/test/extensions/Project.toml index 81097d98d2..d11b88a6e9 100644 --- a/test/extensions/Project.toml +++ b/test/extensions/Project.toml @@ -3,10 +3,12 @@ BifurcationKit = "0f109fa4-8a5d-4b75-95aa-f515264e7665" ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4" ChainRulesTestUtils = "cdddcdb0-9152-4a09-a978-84456f9df70a" ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210" +HomotopyContinuation = "f213a82b-91d6-5c5d-acf7-10f1c761b327" LabelledArrays = "2ee39098-c373-598a-b85f-a56591580800" ModelingToolkit = "961ee093-0014-501f-94e3-6117800e7a78" OrdinaryDiffEq = "1dea7af3-3e70-54e6-95c3-0bf5283fa5ed" SciMLSensitivity = "1ed8b502-d754-442c-8d5d-10ac956f44a1" SciMLStructures = "53ae85a6-f571-4167-b2af-e1d143709226" SymbolicIndexingInterface = "2efcf032-c050-4f8e-a9bb-153293bab1f5" +Symbolics = "0c5d862f-8b57-4792-8d23-62f2024744c7" Zygote = "e88e6eb3-aa80-5325-afca-941959d7151f" diff --git a/test/extensions/homotopy_continuation.jl b/test/extensions/homotopy_continuation.jl new file mode 100644 index 0000000000..51114bfb3b --- /dev/null +++ b/test/extensions/homotopy_continuation.jl @@ -0,0 +1,62 @@ +using ModelingToolkit, NonlinearSolve, SymbolicIndexingInterface +using LinearAlgebra +using Test +import HomotopyContinuation + +@testset "No parameters" begin + @variables x y z + eqs = [0 ~ x^2 + y^2 + 2x * y + 0 ~ x^2 + 4x + 4 + 0 ~ y * z + 4x^2] + @mtkbuild sys = NonlinearSystem(eqs) + prob = HomotopyContinuationProblem(sys, [x => 1.0, y => 1.0, z => 1.0], []) + @test prob[x] == prob[y] == prob[z] == 1.0 + @test prob[x + y] == 2.0 + sol = solve(prob; threading = false) + @test SciMLBase.successful_retcode(sol) + @test norm(sol.resid)≈0.0 atol=1e-10 +end + +struct Wrapper + x::Matrix{Float64} +end + +@testset "Parameters" begin + wrapper(w::Wrapper) = det(w.x) + @register_symbolic wrapper(w::Wrapper) + + @variables x y z + @parameters p q::Int r::Wrapper + + eqs = [0 ~ x^2 + y^2 + p * x * y + 0 ~ x^2 + 4x + q + 0 ~ y * z + 4x^2 + wrapper(r)] + + @mtkbuild sys = NonlinearSystem(eqs) + prob = HomotopyContinuationProblem(sys, [x => 1.0, y => 1.0, z => 1.0], + [p => 2.0, q => 4, r => Wrapper([1.0 1.0; 0.0 0.0])]) + @test prob.ps[p] == 2.0 + @test prob.ps[q] == 4 + @test prob.ps[r].x == [1.0 1.0; 0.0 0.0] + @test prob.ps[p * q] == 8.0 + sol = solve(prob; threading = false) + @test SciMLBase.successful_retcode(sol) + @test norm(sol.resid)≈0.0 atol=1e-10 +end + +@testset "Array variables" begin + @variables x[1:3] + @parameters p[1:3] + _x = collect(x) + eqs = collect(0 .~ vec(sum(_x * _x'; dims = 2)) + collect(p)) + @mtkbuild sys = NonlinearSystem(eqs) + prob = HomotopyContinuationProblem(sys, [x => ones(3)], [p => 1:3]) + @test prob[x] == ones(3) + @test prob[p + x] == [2, 3, 4] + prob[x] = 2ones(3) + @test prob[x] == 2ones(3) + prob.ps[p] = [2, 3, 4] + @test prob.ps[p] == [2, 3, 4] + sol = @test_nowarn solve(prob; threading = false) + @test sol.retcode == ReturnCode.ConvergenceFailure +end diff --git a/test/runtests.jl b/test/runtests.jl index 1c48956458..c7edbfaaf5 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -110,6 +110,7 @@ end if GROUP == "All" || GROUP == "Extensions" activate_extensions_env() @safetestset "BifurcationKit Extension Test" include("extensions/bifurcationkit.jl") + @safetestset "HomotopyContinuation Extension Test" include("extensions/homotopy_continuation.jl") @safetestset "Auto Differentiation Test" include("extensions/ad.jl") @safetestset "LabelledArrays Test" include("labelledarrays.jl") end From 07aa8245a53ff50ef8eefd0ba29fa1d2b95bf28e Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 17 Oct 2024 12:59:45 +0530 Subject: [PATCH 0183/1337] docs: add documentation for HomotopyContinuation interface --- ext/MTKHomotopyContinuationExt.jl | 64 +++++++++++++++++++++--- src/systems/nonlinear/nonlinearsystem.jl | 28 +++++++++-- 2 files changed, 83 insertions(+), 9 deletions(-) diff --git a/ext/MTKHomotopyContinuationExt.jl b/ext/MTKHomotopyContinuationExt.jl index a36e5aef8d..51cb67e7b0 100644 --- a/ext/MTKHomotopyContinuationExt.jl +++ b/ext/MTKHomotopyContinuationExt.jl @@ -4,6 +4,7 @@ using ModelingToolkit using ModelingToolkit.SciMLBase using ModelingToolkit.Symbolics: unwrap using ModelingToolkit.SymbolicIndexingInterface +using ModelingToolkit.DocStringExtensions using HomotopyContinuation using ModelingToolkit: iscomplete, parameters, has_index_cache, get_index_cache, get_u0, get_u0_p, check_eqs_u0, CommonSolve @@ -11,11 +12,14 @@ using ModelingToolkit: iscomplete, parameters, has_index_cache, get_index_cache, const MTK = ModelingToolkit function contains_variable(x, wrt) - any(isequal(x), wrt) && return true - iscall(x) || return false - return any(y -> contains_variable(y, wrt), arguments(x)) + any(y -> occursin(y, x), wrt) end +""" +$(TYPEDSIGNATURES) + +Check if `x` is polynomial with respect to the variables in `wrt`. +""" function is_polynomial(x, wrt) x = unwrap(x) symbolic_type(x) == NotSymbolic() && return true @@ -33,6 +37,11 @@ function is_polynomial(x, wrt) return false end +""" +$(TYPEDSIGNATURES) + +Convert `expr` from a symbolics expression to one that uses `HomotopyContinuation.ModelKit`. +""" function symbolics_to_hc(expr) if iscall(expr) if operation(expr) == getindex @@ -48,11 +57,32 @@ function symbolics_to_hc(expr) end end +""" +$(TYPEDEF) + +A subtype of `HomotopyContinuation.AbstractSystem` used to solve `HomotopyContinuationProblem`s. +""" struct MTKHomotopySystem{F, P, J, V} <: HomotopyContinuation.AbstractSystem + """ + The generated function for the residual of the polynomial system. In-place. + """ f::F + """ + The parameter object. + """ p::P + """ + The generated function for the jacobian of the polynomial system. In-place. + """ jac::J + """ + The `HomotopyContinuation.ModelKit.Variable` representation of the unknowns of + the system. + """ vars::V + """ + The number of polynomials in the system. Must also be equal to `length(vars)`. + """ nexprs::Int end @@ -74,8 +104,17 @@ end SymbolicIndexingInterface.parameter_values(s::MTKHomotopySystem) = s.p +""" + $(TYPEDSIGNATURES) + +Create a `HomotopyContinuationProblem` from a `NonlinearSystem` with polynomial equations. +The problem will be solved by HomotopyContinuation.jl. The resultant `NonlinearSolution` +will contain the polynomial root closest to the point specified by `u0map` (if real roots +exist for the system). +""" function MTK.HomotopyContinuationProblem( - sys::NonlinearSystem, u0map, parammap; compile = :all, eval_expression = false, eval_module = ModelingToolkit, kwargs...) + sys::NonlinearSystem, u0map, parammap = nothing; eval_expression = false, + eval_module = ModelingToolkit, kwargs...) if !iscomplete(sys) error("A completed `NonlinearSystem` is required. Call `complete` or `structural_simplify` on the system before creating a `HomotopyContinuationProblem`") end @@ -100,8 +139,21 @@ function MTK.HomotopyContinuationProblem( return MTK.HomotopyContinuationProblem(u0, mtkhsys, sys, obsfn) end -function CommonSolve.solve(prob::MTK.HomotopyContinuationProblem; kwargs...) - sol = HomotopyContinuation.solve(prob.homotopy_continuation_system; kwargs...) +""" +$(TYPEDSIGNATURES) + +Solve a `HomotopyContinuationProblem`. Ignores the algorithm passed to it, and always +uses `HomotopyContinuation.jl`. All keyword arguments are forwarded to +`HomotopyContinuation.solve`. The original solution as returned by `HomotopyContinuation.jl` +will be available in the `.original` field of the returned `NonlinearSolution`. + +All keyword arguments have their default values in HomotopyContinuation.jl, except +`show_progress` which defaults to `false`. +""" +function CommonSolve.solve(prob::MTK.HomotopyContinuationProblem, + alg = nothing; show_progress = false, kwargs...) + sol = HomotopyContinuation.solve( + prob.homotopy_continuation_system; show_progress, kwargs...) realsols = HomotopyContinuation.results(sol; only_real = true) if isempty(realsols) u = state_values(prob) diff --git a/src/systems/nonlinear/nonlinearsystem.jl b/src/systems/nonlinear/nonlinearsystem.jl index d048fc59c7..01a250d46a 100644 --- a/src/systems/nonlinear/nonlinearsystem.jl +++ b/src/systems/nonlinear/nonlinearsystem.jl @@ -566,15 +566,37 @@ function Base.:(==)(sys1::NonlinearSystem, sys2::NonlinearSystem) all(s1 == s2 for (s1, s2) in zip(get_systems(sys1), get_systems(sys2))) end -struct HomotopyContinuationProblem{uType, H, O} <: SciMLBase.AbstractNonlinearProblem{uType, true} +""" +$(TYPEDEF) + +A type of Nonlinear problem which specializes on polynomial systems and uses +HomotopyContinuation.jl to solve the system. Requires importing HomotopyContinuation.jl to +create and solve. +""" +struct HomotopyContinuationProblem{uType, H, O} <: + SciMLBase.AbstractNonlinearProblem{uType, true} + """ + The initial values of states in the system. If there are multiple real roots of + the system, the one closest to this point is returned. + """ u0::uType + """ + A subtype of `HomotopyContinuation.AbstractSystem` to solve. Also contains the + parameter object. + """ homotopy_continuation_system::H + """ + The `NonlinearSystem` used to create this problem. Used for symbolic indexing. + """ sys::NonlinearSystem + """ + A function which generates and returns observed expressions for the given system. + """ obsfn::O end -function HomotopyContinuationProblem(args...; kwargs...) - error("Requires HomotopyContinuationExt") +function HomotopyContinuationProblem(::AbstractSystem, _u0, _p; kwargs...) + error("HomotopyContinuation.jl is required to create and solve `HomotopyContinuationProblem`s. Please run `Pkg.add(\"HomotopyContinuation\")` to continue.") end SymbolicIndexingInterface.symbolic_container(p::HomotopyContinuationProblem) = p.sys From f358fed206f233d06ccb8920d814a9d89885e21a Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Sat, 19 Oct 2024 15:59:17 +0530 Subject: [PATCH 0184/1337] test: fix linearize tests --- test/downstream/linearize.jl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/downstream/linearize.jl b/test/downstream/linearize.jl index 49b4a45629..98abdb0318 100644 --- a/test/downstream/linearize.jl +++ b/test/downstream/linearize.jl @@ -121,13 +121,13 @@ lsys = ModelingToolkit.reorder_unknowns(lsys0, unknowns(ssys), desired_order) lsyss, _ = ModelingToolkit.linearize_symbolic(pid, [reference.u, measurement.u], [ctr_output.u]) -@test substitute( +@test ModelingToolkit.fixpoint_sub( lsyss.A, ModelingToolkit.defaults_and_guesses(pid)) == lsys.A -@test substitute( +@test ModelingToolkit.fixpoint_sub( lsyss.B, ModelingToolkit.defaults_and_guesses(pid)) == lsys.B -@test substitute( +@test ModelingToolkit.fixpoint_sub( lsyss.C, ModelingToolkit.defaults_and_guesses(pid)) == lsys.C -@test substitute( +@test ModelingToolkit.fixpoint_sub( lsyss.D, ModelingToolkit.defaults_and_guesses(pid)) == lsys.D # Test with the reverse desired unknown order as well to verify that similarity transform and reoreder_unknowns really works From ecf01b3a3f1145da7b0d73469b8f5dc76eb65625 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Sat, 19 Oct 2024 16:57:06 +0530 Subject: [PATCH 0185/1337] feat: better detect and report non-polynomial systems --- ext/MTKHomotopyContinuationExt.jl | 23 ++++++++++++++++++--- test/extensions/homotopy_continuation.jl | 26 ++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/ext/MTKHomotopyContinuationExt.jl b/ext/MTKHomotopyContinuationExt.jl index 51cb67e7b0..81d9a840c9 100644 --- a/ext/MTKHomotopyContinuationExt.jl +++ b/ext/MTKHomotopyContinuationExt.jl @@ -2,7 +2,7 @@ module MTKHomotopyContinuationExt using ModelingToolkit using ModelingToolkit.SciMLBase -using ModelingToolkit.Symbolics: unwrap +using ModelingToolkit.Symbolics: unwrap, symtype using ModelingToolkit.SymbolicIndexingInterface using ModelingToolkit.DocStringExtensions using HomotopyContinuation @@ -32,8 +32,25 @@ function is_polynomial(x, wrt) end if operation(x) == (^) b, p = arguments(x) - return is_polynomial(b, wrt) && !contains_variable(p, wrt) + is_pow_integer = symtype(p) <: Integer + if !is_pow_integer + if symbolic_type(p) == NotSymbolic() + @warn "In $x: Exponent $p is not an integer" + else + @warn "In $x: Exponent $p is not an integer. Use `@parameters p::Integer` to declare integer parameters." + end + end + exponent_has_unknowns = contains_variable(p, wrt) + if exponent_has_unknowns + @warn "In $x: Exponent $p cannot contain unknowns of the system." + end + base_polynomial = is_polynomial(b, wrt) + if !base_polynomial + @warn "In $x: Base is not a polynomial" + end + return base_polynomial && !exponent_has_unknowns && is_pow_integer end + @warn "In $x: Unrecognized operation $(operation(x)). Allowed polynomial operations are `*, +, -, ^`" return false end @@ -124,7 +141,7 @@ function MTK.HomotopyContinuationProblem( for eq in eqs if !is_polynomial(eq.lhs, dvs) || !is_polynomial(eq.rhs, dvs) - error("Equation $eq is not a polynomial in the unknowns") + error("Equation $eq is not a polynomial in the unknowns. See warnings for further details.") end end diff --git a/test/extensions/homotopy_continuation.jl b/test/extensions/homotopy_continuation.jl index 51114bfb3b..ceabb6b6e3 100644 --- a/test/extensions/homotopy_continuation.jl +++ b/test/extensions/homotopy_continuation.jl @@ -60,3 +60,29 @@ end sol = @test_nowarn solve(prob; threading = false) @test sol.retcode == ReturnCode.ConvergenceFailure end + +@testset "Parametric exponent" begin + @variables x = 1.0 + @parameters n::Integer = 4 + @mtkbuild sys = NonlinearSystem([x^n + x^2 - 1 ~ 0]) + prob = HomotopyContinuationProblem(sys, []) + sol = solve(prob; threading = false) + @test SciMLBase.successful_retcode(sol) +end + +@testset "Polynomial check and warnings" begin + @variables x = 1.0 + @parameters n = 4 + @mtkbuild sys = NonlinearSystem([x^n + x^2 - 1 ~ 0]) + @test_warn ["Exponent", "not an integer", "@parameters"] @test_throws "not a polynomial" HomotopyContinuationProblem( + sys, []) + @mtkbuild sys = NonlinearSystem([x^1.5 + x^2 - 1 ~ 0]) + @test_warn ["Exponent", "not an integer"] @test_throws "not a polynomial" HomotopyContinuationProblem( + sys, []) + @mtkbuild sys = NonlinearSystem([x^x - x ~ 0]) + @test_warn ["Exponent", "unknowns"] @test_throws "not a polynomial" HomotopyContinuationProblem( + sys, []) + @mtkbuild sys = NonlinearSystem([((x^2) / (x + 3))^2 + x ~ 0]) + @test_warn ["Base", "not a polynomial", "Unrecognized operation", "/"] @test_throws "not a polynomial" HomotopyContinuationProblem( + sys, []) +end From 72ffd1e941a8ed7c94b0aadd1ca0305d8c29f8f8 Mon Sep 17 00:00:00 2001 From: Herman Sletmoen Date: Thu, 17 Oct 2024 14:12:42 +0200 Subject: [PATCH 0186/1337] Refactor show() --- src/systems/abstractsystem.jl | 115 +++++++++++++--------------------- 1 file changed, 44 insertions(+), 71 deletions(-) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index b6e2a79c3e..c9d0116434 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -1883,84 +1883,57 @@ function n_extra_equations(sys::AbstractSystem) nextras = n_outer_stream_variables + length(ceqs) end -function Base.show(io::IO, mime::MIME"text/plain", sys::AbstractSystem) +function Base.show(io::IO, mime::MIME"text/plain", sys::AbstractSystem; bold = true) + limit = get(io, :limit, false) # if output should be limited, + rows = first(displaysize(io)) ÷ 5 # then allocate ≈1/5 of display height to each list + + # Print name + printstyled(io, "Model $(nameof(sys))"; bold) + + # Print equations eqs = equations(sys) - vars = unknowns(sys) - nvars = length(vars) if eqs isa AbstractArray && eltype(eqs) <: Equation neqs = count(eq -> !(eq.lhs isa Connection), eqs) - Base.printstyled(io, "Model $(nameof(sys)) with $neqs "; bold = true) - nextras = n_extra_equations(sys) - if nextras > 0 - Base.printstyled(io, "("; bold = true) - Base.printstyled(io, neqs + nextras; bold = true, color = :magenta) - Base.printstyled(io, ") "; bold = true) - end - Base.printstyled(io, "equations\n"; bold = true) - else - Base.printstyled(io, "Model $(nameof(sys))\n"; bold = true) - end - # The reduced equations are usually very long. It's not that useful to print - # them. - #Base.print_matrix(io, eqs) - #println(io) - - rows = first(displaysize(io)) ÷ 5 - limit = get(io, :limit, false) - - Base.printstyled(io, "Unknowns ($nvars):"; bold = true) - nrows = min(nvars, limit ? rows : nvars) - limited = nrows < length(vars) - defs = has_defaults(sys) ? defaults(sys) : nothing - for i in 1:nrows - s = vars[i] - print(io, "\n ", s) - - if defs !== nothing - val = get(defs, s, nothing) - if val !== nothing - print(io, " [defaults to ") - show( - IOContext(io, :compact => true, :limit => true, - :displaysize => (1, displaysize(io)[2])), - val) - print(io, "]") - end - description = getdescription(s) - if description !== nothing && description != "" - print(io, ": ", description) - end - end - end - limited && print(io, "\n⋮") - println(io) - - vars = parameters(sys) - nvars = length(vars) - Base.printstyled(io, "Parameters ($nvars):"; bold = true) - nrows = min(nvars, limit ? rows : nvars) - limited = nrows < length(vars) - for i in 1:nrows - s = vars[i] - print(io, "\n ", s) - - if defs !== nothing - val = get(defs, s, nothing) - if val !== nothing - print(io, " [defaults to ") - show( - IOContext(io, :compact => true, :limit => true, - :displaysize => (1, displaysize(io)[2])), - val) - print(io, "]") + next = n_extra_equations(sys) + ntot = neqs + next + ntot > 0 && printstyled(io, "\nEquations ($ntot):"; bold) + neqs > 0 && print(io, "\n $neqs solvable … see equations(sys) for all") + next > 0 && print(io, "\n $next extra") + #Base.print_matrix(io, eqs) # usually too long and not useful to print all equations + end + + # Print variables + for varfunc in [unknowns, parameters] + vars = varfunc(sys) + nvars = length(vars) + nvars == 0 && continue # skip + header = titlecase(String(nameof(varfunc))) # e.g. "Unknowns" + printstyled(io, "\n$header ($nvars):"; bold) + nrows = min(nvars, limit ? rows : nvars) + defs = has_defaults(sys) ? defaults(sys) : nothing + for i in 1:nrows + s = vars[i] + print(io, "\n ", s) + if !isnothing(defs) + val = get(defs, s, nothing) + if !isnothing(val) + print(io, " [defaults to ") + show( + IOContext(io, :compact => true, :limit => true, + :displaysize => (1, displaysize(io)[2])), + val) + print(io, "]") + end + desc = getdescription(s) end - description = getdescription(s) - if description !== nothing && description != "" - print(io, ": ", description) + if !isnothing(desc) && desc != "" + print(io, ": ", desc) end end + limited = nrows < nvars + limited && print(io, "\n ⋮") # too many variables to print end - limited && print(io, "\n⋮") + return nothing end From c23cd6234da57e37d7235cc7e764017366279240 Mon Sep 17 00:00:00 2001 From: Herman Sletmoen Date: Thu, 17 Oct 2024 14:29:21 +0200 Subject: [PATCH 0187/1337] Include observed equations --- src/systems/abstractsystem.jl | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index c9d0116434..2712611c82 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -1894,10 +1894,12 @@ function Base.show(io::IO, mime::MIME"text/plain", sys::AbstractSystem; bold = t eqs = equations(sys) if eqs isa AbstractArray && eltype(eqs) <: Equation neqs = count(eq -> !(eq.lhs isa Connection), eqs) + nobs = has_observed(sys) ? length(observed(sys)) : 0 next = n_extra_equations(sys) - ntot = neqs + next + ntot = neqs + nobs + next ntot > 0 && printstyled(io, "\nEquations ($ntot):"; bold) - neqs > 0 && print(io, "\n $neqs solvable … see equations(sys) for all") + neqs > 0 && print(io, "\n $neqs solvable") + nobs > 0 && print(io, "\n $nobs observed") next > 0 && print(io, "\n $next extra") #Base.print_matrix(io, eqs) # usually too long and not useful to print all equations end From 8d7a65269c8e63d254b05be253fc6112ea4d6aa9 Mon Sep 17 00:00:00 2001 From: Herman Sletmoen Date: Thu, 17 Oct 2024 15:18:09 +0200 Subject: [PATCH 0188/1337] Show subsystem names --- src/systems/abstractsystem.jl | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index 2712611c82..117c37a70d 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -1890,6 +1890,14 @@ function Base.show(io::IO, mime::MIME"text/plain", sys::AbstractSystem; bold = t # Print name printstyled(io, "Model $(nameof(sys))"; bold) + # Print subsystems # TODO: limit + subs = nameof.(ModelingToolkit.get_systems(sys)) + nsubs = length(subs) + nsubs > 0 && printstyled(io, "\nSubsystems ($(nsubs)):"; bold) + for sub in subs + print(io, "\n ", sub) + end + # Print equations eqs = equations(sys) if eqs isa AbstractArray && eltype(eqs) <: Equation From ffc43d2a923d8654b17910bba0f55c94dc91171a Mon Sep 17 00:00:00 2001 From: Herman Sletmoen Date: Thu, 17 Oct 2024 16:18:57 +0200 Subject: [PATCH 0189/1337] Add description to all systems --- src/systems/abstractsystem.jl | 1 + src/systems/diffeqs/odesystem.jl | 11 ++++++++--- src/systems/diffeqs/sdesystem.jl | 15 ++++++++++----- src/systems/discrete_system/discrete_system.jl | 12 +++++++++--- src/systems/jumps/jumpsystem.jl | 9 ++++++--- src/systems/nonlinear/nonlinearsystem.jl | 12 +++++++++--- src/systems/optimization/constraints_system.jl | 11 ++++++++--- src/systems/optimization/optimizationsystem.jl | 9 ++++++--- src/systems/pde/pdesystem.jl | 7 ++++++- 9 files changed, 63 insertions(+), 24 deletions(-) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index 117c37a70d..2bc3335b81 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -986,6 +986,7 @@ for prop in [:eqs :ps :tspan :name + :description :var_to_name :ctrls :defaults diff --git a/src/systems/diffeqs/odesystem.jl b/src/systems/diffeqs/odesystem.jl index ec2c5f8157..b59f08878c 100644 --- a/src/systems/diffeqs/odesystem.jl +++ b/src/systems/diffeqs/odesystem.jl @@ -79,6 +79,10 @@ struct ODESystem <: AbstractODESystem """ name::Symbol """ + A description of the system. + """ + description::String + """ The internal systems. These are required to have unique names. """ systems::Vector{ODESystem} @@ -178,7 +182,7 @@ struct ODESystem <: AbstractODESystem parent::Any function ODESystem(tag, deqs, iv, dvs, ps, tspan, var_to_name, ctrls, observed, tgrad, - jac, ctrl_jac, Wfact, Wfact_t, name, systems, defaults, guesses, + jac, ctrl_jac, Wfact, Wfact_t, name, description, systems, defaults, guesses, torn_matching, initializesystem, initialization_eqs, schedule, connector_type, preface, cevents, devents, parameter_dependencies, @@ -199,7 +203,7 @@ struct ODESystem <: AbstractODESystem check_units(u, deqs) end new(tag, deqs, iv, dvs, ps, tspan, var_to_name, ctrls, observed, tgrad, jac, - ctrl_jac, Wfact, Wfact_t, name, systems, defaults, guesses, torn_matching, + ctrl_jac, Wfact, Wfact_t, name, description, systems, defaults, guesses, torn_matching, initializesystem, initialization_eqs, schedule, connector_type, preface, cevents, devents, parameter_dependencies, metadata, gui_metadata, is_dde, tearing_state, substitutions, complete, index_cache, @@ -213,6 +217,7 @@ function ODESystem(deqs::AbstractVector{<:Equation}, iv, dvs, ps; systems = ODESystem[], tspan = nothing, name = nothing, + description = "", default_u0 = Dict(), default_p = Dict(), defaults = _merge(Dict(default_u0), Dict(default_p)), @@ -290,7 +295,7 @@ function ODESystem(deqs::AbstractVector{<:Equation}, iv, dvs, ps; end ODESystem(Threads.atomic_add!(SYSTEM_COUNT, UInt(1)), deqs, iv′, dvs′, ps′, tspan, var_to_name, ctrl′, observed, tgrad, jac, - ctrl_jac, Wfact, Wfact_t, name, systems, defaults, guesses, nothing, initializesystem, + ctrl_jac, Wfact, Wfact_t, name, description, systems, defaults, guesses, nothing, initializesystem, initialization_eqs, schedule, connector_type, preface, cont_callbacks, disc_callbacks, parameter_dependencies, metadata, gui_metadata, is_dde, checks = checks) diff --git a/src/systems/diffeqs/sdesystem.jl b/src/systems/diffeqs/sdesystem.jl index 0d73aaf313..8343f35e17 100644 --- a/src/systems/diffeqs/sdesystem.jl +++ b/src/systems/diffeqs/sdesystem.jl @@ -80,6 +80,10 @@ struct SDESystem <: AbstractODESystem """ name::Symbol """ + A description of the system. + """ + description::String + """ The internal systems. These are required to have unique names. """ systems::Vector{SDESystem} @@ -142,7 +146,7 @@ struct SDESystem <: AbstractODESystem function SDESystem(tag, deqs, neqs, iv, dvs, ps, tspan, var_to_name, ctrls, observed, tgrad, jac, - ctrl_jac, Wfact, Wfact_t, name, systems, defaults, connector_type, + ctrl_jac, Wfact, Wfact_t, name, description, systems, defaults, connector_type, cevents, devents, parameter_dependencies, metadata = nothing, gui_metadata = nothing, complete = false, index_cache = nothing, parent = nothing, is_scalar_noise = false, is_dde = false, @@ -168,7 +172,7 @@ struct SDESystem <: AbstractODESystem end new(tag, deqs, neqs, iv, dvs, ps, tspan, var_to_name, ctrls, observed, tgrad, jac, ctrl_jac, - Wfact, Wfact_t, name, systems, defaults, connector_type, cevents, devents, + Wfact, Wfact_t, name, description, systems, defaults, connector_type, cevents, devents, parameter_dependencies, metadata, gui_metadata, complete, index_cache, parent, is_scalar_noise, is_dde, isscheduled) end @@ -183,6 +187,7 @@ function SDESystem(deqs::AbstractVector{<:Equation}, neqs::AbstractArray, iv, dv default_p = Dict(), defaults = _merge(Dict(default_u0), Dict(default_p)), name = nothing, + description = "", connector_type = nothing, checks = true, continuous_events = nothing, @@ -234,7 +239,7 @@ function SDESystem(deqs::AbstractVector{<:Equation}, neqs::AbstractArray, iv, dv end SDESystem(Threads.atomic_add!(SYSTEM_COUNT, UInt(1)), deqs, neqs, iv′, dvs′, ps′, tspan, var_to_name, ctrl′, observed, tgrad, jac, - ctrl_jac, Wfact, Wfact_t, name, systems, defaults, connector_type, + ctrl_jac, Wfact, Wfact_t, name, description, systems, defaults, connector_type, cont_callbacks, disc_callbacks, parameter_dependencies, metadata, gui_metadata, complete, index_cache, parent, is_scalar_noise, is_dde; checks = checks) end @@ -349,7 +354,7 @@ function stochastic_integral_transform(sys::SDESystem, correction_factor) end SDESystem(deqs, get_noiseeqs(sys), get_iv(sys), unknowns(sys), parameters(sys), - name = name, parameter_dependencies = parameter_dependencies(sys), checks = false) + name = name, description = get_description(sys), parameter_dependencies = parameter_dependencies(sys), checks = false) end """ @@ -457,7 +462,7 @@ function Girsanov_transform(sys::SDESystem, u; θ0 = 1.0) # return modified SDE System SDESystem(deqs, noiseeqs, get_iv(sys), unknown_vars, parameters(sys); defaults = Dict(θ => θ0), observed = [weight ~ θ / θ0], - name = name, parameter_dependencies = parameter_dependencies(sys), + name = name, description = get_description(sys), parameter_dependencies = parameter_dependencies(sys), checks = false) end diff --git a/src/systems/discrete_system/discrete_system.jl b/src/systems/discrete_system/discrete_system.jl index b09ecf5c4c..5e7ae46a08 100644 --- a/src/systems/discrete_system/discrete_system.jl +++ b/src/systems/discrete_system/discrete_system.jl @@ -42,6 +42,10 @@ struct DiscreteSystem <: AbstractTimeDependentSystem """ name::Symbol """ + A description of the system. + """ + description::String + """ The internal systems. These are required to have unique names. """ systems::Vector{DiscreteSystem} @@ -95,7 +99,7 @@ struct DiscreteSystem <: AbstractTimeDependentSystem function DiscreteSystem(tag, discreteEqs, iv, dvs, ps, tspan, var_to_name, observed, - name, + name, description, systems, defaults, preface, connector_type, parameter_dependencies = Equation[], metadata = nothing, gui_metadata = nothing, tearing_state = nothing, substitutions = nothing, @@ -111,7 +115,7 @@ struct DiscreteSystem <: AbstractTimeDependentSystem u = __get_unit_type(dvs, ps, iv) check_units(u, discreteEqs) end - new(tag, discreteEqs, iv, dvs, ps, tspan, var_to_name, observed, name, + new(tag, discreteEqs, iv, dvs, ps, tspan, var_to_name, observed, name, description, systems, defaults, preface, connector_type, parameter_dependencies, metadata, gui_metadata, @@ -128,6 +132,7 @@ function DiscreteSystem(eqs::AbstractVector{<:Equation}, iv, dvs, ps; systems = DiscreteSystem[], tspan = nothing, name = nothing, + description = "", default_u0 = Dict(), default_p = Dict(), defaults = _merge(Dict(default_u0), Dict(default_p)), @@ -164,7 +169,7 @@ function DiscreteSystem(eqs::AbstractVector{<:Equation}, iv, dvs, ps; throw(ArgumentError("System names must be unique.")) end DiscreteSystem(Threads.atomic_add!(SYSTEM_COUNT, UInt(1)), - eqs, iv′, dvs′, ps′, tspan, var_to_name, observed, name, systems, + eqs, iv′, dvs′, ps′, tspan, var_to_name, observed, name, description, systems, defaults, preface, connector_type, parameter_dependencies, metadata, gui_metadata, kwargs...) end @@ -221,6 +226,7 @@ function flatten(sys::DiscreteSystem, noeqs = false) observed = observed(sys), defaults = defaults(sys), name = nameof(sys), + description = get_description(sys), checks = false) end end diff --git a/src/systems/jumps/jumpsystem.jl b/src/systems/jumps/jumpsystem.jl index a714d4b364..98253569d2 100644 --- a/src/systems/jumps/jumpsystem.jl +++ b/src/systems/jumps/jumpsystem.jl @@ -74,6 +74,8 @@ struct JumpSystem{U <: ArrayPartition} <: AbstractTimeDependentSystem observed::Vector{Equation} """The name of the system.""" name::Symbol + """A description of the system.""" + description::String """The internal systems. These are required to have unique names.""" systems::Vector{JumpSystem} """ @@ -116,7 +118,7 @@ struct JumpSystem{U <: ArrayPartition} <: AbstractTimeDependentSystem index_cache::Union{Nothing, IndexCache} isscheduled::Bool - function JumpSystem{U}(tag, ap::U, iv, unknowns, ps, var_to_name, observed, name, + function JumpSystem{U}(tag, ap::U, iv, unknowns, ps, var_to_name, observed, name, description, systems, defaults, connector_type, devents, parameter_dependencies, metadata = nothing, gui_metadata = nothing, @@ -131,7 +133,7 @@ struct JumpSystem{U <: ArrayPartition} <: AbstractTimeDependentSystem u = __get_unit_type(unknowns, ps, iv) check_units(u, ap, iv) end - new{U}(tag, ap, iv, unknowns, ps, var_to_name, observed, name, systems, defaults, + new{U}(tag, ap, iv, unknowns, ps, var_to_name, observed, name, description, systems, defaults, connector_type, devents, parameter_dependencies, metadata, gui_metadata, complete, index_cache, isscheduled) end @@ -147,6 +149,7 @@ function JumpSystem(eqs, iv, unknowns, ps; default_p = Dict(), defaults = _merge(Dict(default_u0), Dict(default_p)), name = nothing, + description = "", connector_type = nothing, checks = true, continuous_events = nothing, @@ -193,7 +196,7 @@ function JumpSystem(eqs, iv, unknowns, ps; disc_callbacks = SymbolicDiscreteCallbacks(discrete_events) parameter_dependencies, ps = process_parameter_dependencies(parameter_dependencies, ps) JumpSystem{typeof(ap)}(Threads.atomic_add!(SYSTEM_COUNT, UInt(1)), - ap, value(iv), unknowns, ps, var_to_name, observed, name, systems, + ap, value(iv), unknowns, ps, var_to_name, observed, name, description, systems, defaults, connector_type, disc_callbacks, parameter_dependencies, metadata, gui_metadata, checks = checks) end diff --git a/src/systems/nonlinear/nonlinearsystem.jl b/src/systems/nonlinear/nonlinearsystem.jl index d9ad826a2c..07c73798f4 100644 --- a/src/systems/nonlinear/nonlinearsystem.jl +++ b/src/systems/nonlinear/nonlinearsystem.jl @@ -44,6 +44,10 @@ struct NonlinearSystem <: AbstractTimeIndependentSystem """ name::Symbol """ + A description of the system. + """ + description::String + """ The internal systems. These are required to have unique names. """ systems::Vector{NonlinearSystem} @@ -91,7 +95,7 @@ struct NonlinearSystem <: AbstractTimeIndependentSystem parent::Any isscheduled::Bool - function NonlinearSystem(tag, eqs, unknowns, ps, var_to_name, observed, jac, name, + function NonlinearSystem(tag, eqs, unknowns, ps, var_to_name, observed, jac, name, description, systems, defaults, connector_type, parameter_dependencies = Equation[], metadata = nothing, gui_metadata = nothing, @@ -102,7 +106,7 @@ struct NonlinearSystem <: AbstractTimeIndependentSystem u = __get_unit_type(unknowns, ps) check_units(u, eqs) end - new(tag, eqs, unknowns, ps, var_to_name, observed, jac, name, systems, defaults, + new(tag, eqs, unknowns, ps, var_to_name, observed, jac, name, description, systems, defaults, connector_type, parameter_dependencies, metadata, gui_metadata, tearing_state, substitutions, complete, index_cache, parent, isscheduled) end @@ -111,6 +115,7 @@ end function NonlinearSystem(eqs, unknowns, ps; observed = [], name = nothing, + description = "", default_u0 = Dict(), default_p = Dict(), defaults = _merge(Dict(default_u0), Dict(default_p)), @@ -157,7 +162,7 @@ function NonlinearSystem(eqs, unknowns, ps; parameter_dependencies, ps = process_parameter_dependencies( parameter_dependencies, ps) NonlinearSystem(Threads.atomic_add!(SYSTEM_COUNT, UInt(1)), - eqs, unknowns, ps, var_to_name, observed, jac, name, systems, defaults, + eqs, unknowns, ps, var_to_name, observed, jac, name, description, systems, defaults, connector_type, parameter_dependencies, metadata, gui_metadata, checks = checks) end @@ -554,6 +559,7 @@ function flatten(sys::NonlinearSystem, noeqs = false) observed = observed(sys), defaults = defaults(sys), name = nameof(sys), + description = get_description(sys), checks = false) end end diff --git a/src/systems/optimization/constraints_system.jl b/src/systems/optimization/constraints_system.jl index f9176df3a2..054244e005 100644 --- a/src/systems/optimization/constraints_system.jl +++ b/src/systems/optimization/constraints_system.jl @@ -45,6 +45,10 @@ struct ConstraintsSystem <: AbstractTimeIndependentSystem """ name::Symbol """ + A description of the system. + """ + description::String + """ The internal systems. These are required to have unique names. """ systems::Vector{ConstraintsSystem} @@ -79,7 +83,7 @@ struct ConstraintsSystem <: AbstractTimeIndependentSystem index_cache::Union{Nothing, IndexCache} function ConstraintsSystem(tag, constraints, unknowns, ps, var_to_name, observed, jac, - name, + name, description, systems, defaults, connector_type, metadata = nothing, tearing_state = nothing, substitutions = nothing, @@ -89,7 +93,7 @@ struct ConstraintsSystem <: AbstractTimeIndependentSystem u = __get_unit_type(unknowns, ps) check_units(u, constraints) end - new(tag, constraints, unknowns, ps, var_to_name, observed, jac, name, systems, + new(tag, constraints, unknowns, ps, var_to_name, observed, jac, name, description, systems, defaults, connector_type, metadata, tearing_state, substitutions, complete, index_cache) end @@ -100,6 +104,7 @@ equations(sys::ConstraintsSystem) = constraints(sys) # needed for Base.show function ConstraintsSystem(constraints, unknowns, ps; observed = [], name = nothing, + description = "", default_u0 = Dict(), default_p = Dict(), defaults = _merge(Dict(default_u0), Dict(default_p)), @@ -142,7 +147,7 @@ function ConstraintsSystem(constraints, unknowns, ps; isempty(observed) || collect_var_to_name!(var_to_name, (eq.lhs for eq in observed)) ConstraintsSystem(Threads.atomic_add!(SYSTEM_COUNT, UInt(1)), - cstr, unknowns, ps, var_to_name, observed, jac, name, systems, + cstr, unknowns, ps, var_to_name, observed, jac, name, description, systems, defaults, connector_type, metadata, checks = checks) end diff --git a/src/systems/optimization/optimizationsystem.jl b/src/systems/optimization/optimizationsystem.jl index 22488e8bad..1f543dc8d1 100644 --- a/src/systems/optimization/optimizationsystem.jl +++ b/src/systems/optimization/optimizationsystem.jl @@ -37,6 +37,8 @@ struct OptimizationSystem <: AbstractOptimizationSystem constraints::Vector{Union{Equation, Inequality}} """The name of the system.""" name::Symbol + """A description of the system.""" + description::String """The internal systems. These are required to have unique names.""" systems::Vector{OptimizationSystem} """ @@ -67,7 +69,7 @@ struct OptimizationSystem <: AbstractOptimizationSystem isscheduled::Bool function OptimizationSystem(tag, op, unknowns, ps, var_to_name, observed, - constraints, name, systems, defaults, metadata = nothing, + constraints, name, description, systems, defaults, metadata = nothing, gui_metadata = nothing, complete = false, index_cache = nothing, parent = nothing, isscheduled = false; checks::Union{Bool, Int} = true) @@ -78,7 +80,7 @@ struct OptimizationSystem <: AbstractOptimizationSystem check_units(u, constraints) end new(tag, op, unknowns, ps, var_to_name, observed, - constraints, name, systems, defaults, metadata, gui_metadata, complete, + constraints, name, description, systems, defaults, metadata, gui_metadata, complete, index_cache, parent, isscheduled) end end @@ -92,6 +94,7 @@ function OptimizationSystem(op, unknowns, ps; default_p = Dict(), defaults = _merge(Dict(default_u0), Dict(default_p)), name = nothing, + description = "", systems = OptimizationSystem[], checks = true, metadata = nothing, @@ -125,7 +128,7 @@ function OptimizationSystem(op, unknowns, ps; op′, unknowns′, ps′, var_to_name, observed, constraints, - name, systems, defaults, metadata, gui_metadata; + name, description, systems, defaults, metadata, gui_metadata; checks = checks) end diff --git a/src/systems/pde/pdesystem.jl b/src/systems/pde/pdesystem.jl index 7aa3f29191..eac540e401 100644 --- a/src/systems/pde/pdesystem.jl +++ b/src/systems/pde/pdesystem.jl @@ -78,6 +78,10 @@ struct PDESystem <: ModelingToolkit.AbstractMultivariateSystem """ name::Symbol """ + A description of the system. + """ + description::String + """ Metadata for the system, to be used by downstream packages. """ metadata::Any @@ -96,6 +100,7 @@ struct PDESystem <: ModelingToolkit.AbstractMultivariateSystem gui_metadata = nothing, eval_module = @__MODULE__, checks::Union{Bool, Int} = true, + description = "", name) if checks == true || (checks & CheckUnits) > 0 u = __get_unit_type(dvs, ivs, ps) @@ -127,7 +132,7 @@ struct PDESystem <: ModelingToolkit.AbstractMultivariateSystem end new(eqs, bcs, domain, ivs, dvs, ps, defaults, connector_type, systems, analytic, - analytic_func, name, metadata, gui_metadata) + analytic_func, name, description, metadata, gui_metadata) end end From 428bc8d0a760392b5e9c686322d4139541fe69ed Mon Sep 17 00:00:00 2001 From: Herman Sletmoen Date: Thu, 17 Oct 2024 16:23:02 +0200 Subject: [PATCH 0190/1337] Extend system descriptions --- src/systems/abstractsystem.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index 2bc3335b81..0dbe67b2a5 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -2972,12 +2972,13 @@ function extend(sys::AbstractSystem, basesys::AbstractSystem; name::Symbol = nam cevs = union(get_continuous_events(basesys), get_continuous_events(sys)) devs = union(get_discrete_events(basesys), get_discrete_events(sys)) defs = merge(get_defaults(basesys), get_defaults(sys)) # prefer `sys` + desc = join(filter(desc -> !isempty(desc), get_description.([sys, basesys])), " ") # concatenate non-empty descriptions with space meta = union_nothing(get_metadata(basesys), get_metadata(sys)) syss = union(get_systems(basesys), get_systems(sys)) args = length(ivs) == 0 ? (eqs, sts, ps) : (eqs, ivs[1], sts, ps) kwargs = (parameter_dependencies = dep_ps, observed = obs, continuous_events = cevs, discrete_events = devs, defaults = defs, systems = syss, metadata = meta, - name = name, gui_metadata = gui_metadata) + name = name, description = desc, gui_metadata = gui_metadata) # collect fields specific to some system types if basesys isa ODESystem From 59e23d4ea47b7172e9c5f2b4806c2a800b664e54 Mon Sep 17 00:00:00 2001 From: Herman Sletmoen Date: Thu, 17 Oct 2024 16:32:33 +0200 Subject: [PATCH 0191/1337] Show system and subsystem descriptions --- src/systems/abstractsystem.jl | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index 0dbe67b2a5..1d8b085afe 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -1888,15 +1888,26 @@ function Base.show(io::IO, mime::MIME"text/plain", sys::AbstractSystem; bold = t limit = get(io, :limit, false) # if output should be limited, rows = first(displaysize(io)) ÷ 5 # then allocate ≈1/5 of display height to each list - # Print name - printstyled(io, "Model $(nameof(sys))"; bold) + # Print name and description + desc = get_description(sys) + printstyled(io, "Model ", nameof(sys), ":"; bold) + !isempty(desc) && print(io, " ", desc) - # Print subsystems # TODO: limit - subs = nameof.(ModelingToolkit.get_systems(sys)) + # Print subsystems + subs = get_systems(sys) nsubs = length(subs) nsubs > 0 && printstyled(io, "\nSubsystems ($(nsubs)):"; bold) for sub in subs - print(io, "\n ", sub) + name = String(nameof(sub)) + print(io, "\n ", name) + desc = get_description(sub) + if !isempty(desc) + maxlen = displaysize(io)[2] - length(name) - 6 # remaining length of line + if limit && length(desc) > maxlen + desc = chop(desc, tail = length(desc) - maxlen) * "…" # too long + end + print(io, ": ", desc) + end end # Print equations From cc926f9c9665062c951230c6b1c83a32195999f6 Mon Sep 17 00:00:00 2001 From: Herman Sletmoen Date: Thu, 17 Oct 2024 18:31:31 +0200 Subject: [PATCH 0192/1337] Limit number of subsystems shown --- src/systems/abstractsystem.jl | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index 1d8b085afe..f98cc316f7 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -1896,8 +1896,10 @@ function Base.show(io::IO, mime::MIME"text/plain", sys::AbstractSystem; bold = t # Print subsystems subs = get_systems(sys) nsubs = length(subs) - nsubs > 0 && printstyled(io, "\nSubsystems ($(nsubs)):"; bold) - for sub in subs + nrows = min(nsubs, limit ? rows : nsubs) + nrows > 0 && printstyled(io, "\nSubsystems ($(nsubs)):"; bold) + for i in 1:nrows + sub = subs[i] name = String(nameof(sub)) print(io, "\n ", name) desc = get_description(sub) @@ -1909,6 +1911,8 @@ function Base.show(io::IO, mime::MIME"text/plain", sys::AbstractSystem; bold = t print(io, ": ", desc) end end + limited = nrows < nsubs + limited && print(io, "\n ⋮") # too many variables to print # Print equations eqs = equations(sys) From b02abba138af18f2d01bc6d28a8878429dca33d0 Mon Sep 17 00:00:00 2001 From: Herman Sletmoen Date: Thu, 17 Oct 2024 19:06:29 +0200 Subject: [PATCH 0193/1337] Pass description through flatten() --- src/systems/diffeqs/odesystem.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/src/systems/diffeqs/odesystem.jl b/src/systems/diffeqs/odesystem.jl index b59f08878c..155ee211db 100644 --- a/src/systems/diffeqs/odesystem.jl +++ b/src/systems/diffeqs/odesystem.jl @@ -398,6 +398,7 @@ function flatten(sys::ODESystem, noeqs = false) discrete_events = discrete_events(sys), defaults = defaults(sys), name = nameof(sys), + description = get_description(sys), initialization_eqs = initialization_equations(sys), is_dde = is_dde(sys), checks = false) From db9348c9e67d2168576a73fda956c5d4f3f19468 Mon Sep 17 00:00:00 2001 From: Herman Sletmoen Date: Thu, 17 Oct 2024 19:50:47 +0200 Subject: [PATCH 0194/1337] Hint to inspection functions when only parts of a system is shown --- src/systems/abstractsystem.jl | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index f98cc316f7..c79f805819 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -1884,7 +1884,7 @@ function n_extra_equations(sys::AbstractSystem) nextras = n_outer_stream_variables + length(ceqs) end -function Base.show(io::IO, mime::MIME"text/plain", sys::AbstractSystem; bold = true) +function Base.show(io::IO, mime::MIME"text/plain", sys::AbstractSystem; hint = true, bold = true) limit = get(io, :limit, false) # if output should be limited, rows = first(displaysize(io)) ÷ 5 # then allocate ≈1/5 of display height to each list @@ -1898,6 +1898,7 @@ function Base.show(io::IO, mime::MIME"text/plain", sys::AbstractSystem; bold = t nsubs = length(subs) nrows = min(nsubs, limit ? rows : nsubs) nrows > 0 && printstyled(io, "\nSubsystems ($(nsubs)):"; bold) + nrows > 0 && hint && print(io, " see ModelingToolkit.get_systems(sys)") for i in 1:nrows sub = subs[i] name = String(nameof(sub)) @@ -1912,7 +1913,7 @@ function Base.show(io::IO, mime::MIME"text/plain", sys::AbstractSystem; bold = t end end limited = nrows < nsubs - limited && print(io, "\n ⋮") # too many variables to print + limited && print(io, "\n ⋮") # too many to print # Print equations eqs = equations(sys) @@ -1922,6 +1923,7 @@ function Base.show(io::IO, mime::MIME"text/plain", sys::AbstractSystem; bold = t next = n_extra_equations(sys) ntot = neqs + nobs + next ntot > 0 && printstyled(io, "\nEquations ($ntot):"; bold) + ntot > 0 && hint && print(io, " see equations(sys)") neqs > 0 && print(io, "\n $neqs solvable") nobs > 0 && print(io, "\n $nobs observed") next > 0 && print(io, "\n $next extra") @@ -1935,6 +1937,7 @@ function Base.show(io::IO, mime::MIME"text/plain", sys::AbstractSystem; bold = t nvars == 0 && continue # skip header = titlecase(String(nameof(varfunc))) # e.g. "Unknowns" printstyled(io, "\n$header ($nvars):"; bold) + hint && print(io, " see $(nameof(varfunc))(sys)") nrows = min(nvars, limit ? rows : nvars) defs = has_defaults(sys) ? defaults(sys) : nothing for i in 1:nrows @@ -1957,7 +1960,7 @@ function Base.show(io::IO, mime::MIME"text/plain", sys::AbstractSystem; bold = t end end limited = nrows < nvars - limited && print(io, "\n ⋮") # too many variables to print + limited && printstyled(io, "\n ⋮") # too many variables to print end return nothing From d473a73b28ca7eb13b8be058995ddde8f31d2377 Mon Sep 17 00:00:00 2001 From: Herman Sletmoen Date: Fri, 18 Oct 2024 11:20:37 +0200 Subject: [PATCH 0195/1337] Export hierarchy(sys) to print subsystem hierarchy --- src/ModelingToolkit.jl | 2 +- src/systems/abstractsystem.jl | 18 ++++++++++++++---- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/ModelingToolkit.jl b/src/ModelingToolkit.jl index 2f57bb1765..7687122a61 100644 --- a/src/ModelingToolkit.jl +++ b/src/ModelingToolkit.jl @@ -243,7 +243,7 @@ export Equation, ConstrainedEquation export Term, Sym export SymScope, LocalScope, ParentScope, DelayParentScope, GlobalScope export independent_variable, equations, controls, observed, full_equations -export initialization_equations, guesses, defaults, parameter_dependencies +export initialization_equations, guesses, defaults, parameter_dependencies, hierarchy export structural_simplify, expand_connections, linearize, linearization_function export calculate_jacobian, generate_jacobian, generate_function, generate_custom_function diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index c79f805819..d8ee0337c8 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -1898,7 +1898,7 @@ function Base.show(io::IO, mime::MIME"text/plain", sys::AbstractSystem; hint = t nsubs = length(subs) nrows = min(nsubs, limit ? rows : nsubs) nrows > 0 && printstyled(io, "\nSubsystems ($(nsubs)):"; bold) - nrows > 0 && hint && print(io, " see ModelingToolkit.get_systems(sys)") + nrows > 0 && hint && print(io, " see hierarchy(sys)") for i in 1:nrows sub = subs[i] name = String(nameof(sub)) @@ -2902,12 +2902,22 @@ function Base.showerror(io::IO, e::HybridSystemNotSupportedException) print(io, "HybridSystemNotSupportedException: ", e.msg) end -function AbstractTrees.children(sys::ModelingToolkit.AbstractSystem) +function AbstractTrees.children(sys::AbstractSystem) ModelingToolkit.get_systems(sys) end -function AbstractTrees.printnode(io::IO, sys::ModelingToolkit.AbstractSystem) - print(io, nameof(sys)) +function AbstractTrees.printnode(io::IO, sys::AbstractSystem; describe = false, bold = false) + printstyled(io, nameof(sys); bold) + describe && !isempty(get_description(sys)) && print(io, ": ", get_description(sys)) end +""" + hierarchy(sys::AbstractSystem; describe = false, bold = describe, kwargs...) + +Print a tree of a system's hierarchy of subsystems. +""" +function hierarchy(sys::AbstractSystem; describe = false, bold = describe, kwargs...) + print_tree(sys; printnode_kw = (describe = describe, bold = bold), kwargs...) +end + function Base.IteratorEltype(::Type{<:TreeIterator{ModelingToolkit.AbstractSystem}}) Base.HasEltype() end From b3f3faf2b941bb9112b19c757bef8f173a1e77c9 Mon Sep 17 00:00:00 2001 From: Herman Sletmoen Date: Fri, 18 Oct 2024 17:02:24 +0200 Subject: [PATCH 0196/1337] Fix format --- src/systems/abstractsystem.jl | 3 ++- src/systems/diffeqs/odesystem.jl | 3 ++- src/systems/diffeqs/sdesystem.jl | 9 ++++++--- src/systems/jumps/jumpsystem.jl | 6 ++++-- src/systems/nonlinear/nonlinearsystem.jl | 6 ++++-- src/systems/optimization/constraints_system.jl | 3 ++- 6 files changed, 20 insertions(+), 10 deletions(-) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index d8ee0337c8..f1646861ee 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -2905,7 +2905,8 @@ end function AbstractTrees.children(sys::AbstractSystem) ModelingToolkit.get_systems(sys) end -function AbstractTrees.printnode(io::IO, sys::AbstractSystem; describe = false, bold = false) +function AbstractTrees.printnode( + io::IO, sys::AbstractSystem; describe = false, bold = false) printstyled(io, nameof(sys); bold) describe && !isempty(get_description(sys)) && print(io, ": ", get_description(sys)) end diff --git a/src/systems/diffeqs/odesystem.jl b/src/systems/diffeqs/odesystem.jl index 155ee211db..b901275121 100644 --- a/src/systems/diffeqs/odesystem.jl +++ b/src/systems/diffeqs/odesystem.jl @@ -295,7 +295,8 @@ function ODESystem(deqs::AbstractVector{<:Equation}, iv, dvs, ps; end ODESystem(Threads.atomic_add!(SYSTEM_COUNT, UInt(1)), deqs, iv′, dvs′, ps′, tspan, var_to_name, ctrl′, observed, tgrad, jac, - ctrl_jac, Wfact, Wfact_t, name, description, systems, defaults, guesses, nothing, initializesystem, + ctrl_jac, Wfact, Wfact_t, name, description, systems, + defaults, guesses, nothing, initializesystem, initialization_eqs, schedule, connector_type, preface, cont_callbacks, disc_callbacks, parameter_dependencies, metadata, gui_metadata, is_dde, checks = checks) diff --git a/src/systems/diffeqs/sdesystem.jl b/src/systems/diffeqs/sdesystem.jl index 8343f35e17..e8a72b81cb 100644 --- a/src/systems/diffeqs/sdesystem.jl +++ b/src/systems/diffeqs/sdesystem.jl @@ -172,7 +172,8 @@ struct SDESystem <: AbstractODESystem end new(tag, deqs, neqs, iv, dvs, ps, tspan, var_to_name, ctrls, observed, tgrad, jac, ctrl_jac, - Wfact, Wfact_t, name, description, systems, defaults, connector_type, cevents, devents, + Wfact, Wfact_t, name, description, systems, + defaults, connector_type, cevents, devents, parameter_dependencies, metadata, gui_metadata, complete, index_cache, parent, is_scalar_noise, is_dde, isscheduled) end @@ -354,7 +355,8 @@ function stochastic_integral_transform(sys::SDESystem, correction_factor) end SDESystem(deqs, get_noiseeqs(sys), get_iv(sys), unknowns(sys), parameters(sys), - name = name, description = get_description(sys), parameter_dependencies = parameter_dependencies(sys), checks = false) + name = name, description = get_description(sys), + parameter_dependencies = parameter_dependencies(sys), checks = false) end """ @@ -462,7 +464,8 @@ function Girsanov_transform(sys::SDESystem, u; θ0 = 1.0) # return modified SDE System SDESystem(deqs, noiseeqs, get_iv(sys), unknown_vars, parameters(sys); defaults = Dict(θ => θ0), observed = [weight ~ θ / θ0], - name = name, description = get_description(sys), parameter_dependencies = parameter_dependencies(sys), + name = name, description = get_description(sys), + parameter_dependencies = parameter_dependencies(sys), checks = false) end diff --git a/src/systems/jumps/jumpsystem.jl b/src/systems/jumps/jumpsystem.jl index 98253569d2..6409609a10 100644 --- a/src/systems/jumps/jumpsystem.jl +++ b/src/systems/jumps/jumpsystem.jl @@ -118,7 +118,8 @@ struct JumpSystem{U <: ArrayPartition} <: AbstractTimeDependentSystem index_cache::Union{Nothing, IndexCache} isscheduled::Bool - function JumpSystem{U}(tag, ap::U, iv, unknowns, ps, var_to_name, observed, name, description, + function JumpSystem{U}( + tag, ap::U, iv, unknowns, ps, var_to_name, observed, name, description, systems, defaults, connector_type, devents, parameter_dependencies, metadata = nothing, gui_metadata = nothing, @@ -133,7 +134,8 @@ struct JumpSystem{U <: ArrayPartition} <: AbstractTimeDependentSystem u = __get_unit_type(unknowns, ps, iv) check_units(u, ap, iv) end - new{U}(tag, ap, iv, unknowns, ps, var_to_name, observed, name, description, systems, defaults, + new{U}(tag, ap, iv, unknowns, ps, var_to_name, + observed, name, description, systems, defaults, connector_type, devents, parameter_dependencies, metadata, gui_metadata, complete, index_cache, isscheduled) end diff --git a/src/systems/nonlinear/nonlinearsystem.jl b/src/systems/nonlinear/nonlinearsystem.jl index 07c73798f4..f1b438f0bb 100644 --- a/src/systems/nonlinear/nonlinearsystem.jl +++ b/src/systems/nonlinear/nonlinearsystem.jl @@ -95,7 +95,8 @@ struct NonlinearSystem <: AbstractTimeIndependentSystem parent::Any isscheduled::Bool - function NonlinearSystem(tag, eqs, unknowns, ps, var_to_name, observed, jac, name, description, + function NonlinearSystem( + tag, eqs, unknowns, ps, var_to_name, observed, jac, name, description, systems, defaults, connector_type, parameter_dependencies = Equation[], metadata = nothing, gui_metadata = nothing, @@ -106,7 +107,8 @@ struct NonlinearSystem <: AbstractTimeIndependentSystem u = __get_unit_type(unknowns, ps) check_units(u, eqs) end - new(tag, eqs, unknowns, ps, var_to_name, observed, jac, name, description, systems, defaults, + new(tag, eqs, unknowns, ps, var_to_name, observed, + jac, name, description, systems, defaults, connector_type, parameter_dependencies, metadata, gui_metadata, tearing_state, substitutions, complete, index_cache, parent, isscheduled) end diff --git a/src/systems/optimization/constraints_system.jl b/src/systems/optimization/constraints_system.jl index 054244e005..a2756994ac 100644 --- a/src/systems/optimization/constraints_system.jl +++ b/src/systems/optimization/constraints_system.jl @@ -93,7 +93,8 @@ struct ConstraintsSystem <: AbstractTimeIndependentSystem u = __get_unit_type(unknowns, ps) check_units(u, constraints) end - new(tag, constraints, unknowns, ps, var_to_name, observed, jac, name, description, systems, + new(tag, constraints, unknowns, ps, var_to_name, + observed, jac, name, description, systems, defaults, connector_type, metadata, tearing_state, substitutions, complete, index_cache) end From b5b1a8a1665c9ccdb2f2d7e12cf49735d1c51e63 Mon Sep 17 00:00:00 2001 From: Herman Sletmoen Date: Sun, 20 Oct 2024 18:49:02 +0200 Subject: [PATCH 0197/1337] Dispatch on ODESystem to print initialization equation count --- src/systems/diffeqs/odesystem.jl | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/systems/diffeqs/odesystem.jl b/src/systems/diffeqs/odesystem.jl index b901275121..7055da3ac9 100644 --- a/src/systems/diffeqs/odesystem.jl +++ b/src/systems/diffeqs/odesystem.jl @@ -704,3 +704,15 @@ function add_accumulations(sys::ODESystem, vars::Vector{<:Pair}) @set! sys.unknowns = [get_unknowns(sys); avars] @set! sys.defaults = merge(get_defaults(sys), Dict(a => 0.0 for a in avars)) end + +function Base.show(io::IO, mime::MIME"text/plain", sys::ODESystem; hint = true, bold = true) + # Print general AbstractSystem information + invoke(Base.show, Tuple{typeof(io), typeof(mime), AbstractSystem}, io, mime, sys; hint, bold) + + # Print initialization equations (unique to ODESystems) + nini = length(initialization_equations(sys)) + nini > 0 && printstyled(io, "\nInitialization equations ($nini):"; bold) + nini > 0 && hint && print(io, " see initialization_equations(sys)") + + return nothing +end From 2bc4b161a83a16c347a264fc4a83a6bad3a6d00e Mon Sep 17 00:00:00 2001 From: Herman Sletmoen Date: Sun, 20 Oct 2024 19:31:37 +0200 Subject: [PATCH 0198/1337] Distinguish standard and connecting equations; rename and document n_extra_equations() --- src/systems/abstractsystem.jl | 21 +++++++++++++-------- test/components.jl | 2 +- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index f1646861ee..a84beb05da 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -1856,8 +1856,14 @@ function get_or_construct_tearing_state(sys) state end -# TODO: what about inputs? -function n_extra_equations(sys::AbstractSystem) +""" + n_expanded_connection_equations(sys::AbstractSystem) + +Returns the number of equations that the connections in `sys` expands to. +Equivalent to `length(equations(expand_connections(sys))) - length(filter(eq -> !(eq.lhs isa Connection), equations(sys)))`. +""" +function n_expanded_connection_equations(sys::AbstractSystem) + # TODO: what about inputs? isconnector(sys) && return length(get_unknowns(sys)) sys, (csets, _) = generate_connection_set(sys) ceqs, instream_csets = generate_connection_equations_and_stream_connections(csets) @@ -1919,14 +1925,13 @@ function Base.show(io::IO, mime::MIME"text/plain", sys::AbstractSystem; hint = t eqs = equations(sys) if eqs isa AbstractArray && eltype(eqs) <: Equation neqs = count(eq -> !(eq.lhs isa Connection), eqs) + next = n_expanded_connection_equations(sys) nobs = has_observed(sys) ? length(observed(sys)) : 0 - next = n_extra_equations(sys) - ntot = neqs + nobs + next + ntot = neqs + next + nobs ntot > 0 && printstyled(io, "\nEquations ($ntot):"; bold) - ntot > 0 && hint && print(io, " see equations(sys)") - neqs > 0 && print(io, "\n $neqs solvable") - nobs > 0 && print(io, "\n $nobs observed") - next > 0 && print(io, "\n $next extra") + neqs > 0 && print(io, "\n $neqs standard", hint ? ": see equations(sys)" : "") + next > 0 && print(io, "\n $next connecting", hint ? ": see equations(expand_connections(sys))" : "") + nobs > 0 && print(io, "\n $nobs observed", hint ? ": see observed(sys)" : "") #Base.print_matrix(io, eqs) # usually too long and not useful to print all equations end diff --git a/test/components.jl b/test/components.jl index 965578e0c5..ea63e3e758 100644 --- a/test/components.jl +++ b/test/components.jl @@ -41,7 +41,7 @@ end completed_rc_model = complete(rc_model) @test isequal(completed_rc_model.resistor.n.i, resistor.n.i) -@test ModelingToolkit.n_extra_equations(capacitor) == 2 +@test ModelingToolkit.n_expanded_connection_equations(capacitor) == 2 @test length(equations(structural_simplify(rc_model, allow_parameter = false))) == 2 sys = structural_simplify(rc_model) @test_throws ModelingToolkit.RepeatedStructuralSimplificationError structural_simplify(sys) From f7e956f45547e3cd500cd070ab6dce1c6d77f150 Mon Sep 17 00:00:00 2001 From: Herman Sletmoen Date: Sun, 20 Oct 2024 19:34:59 +0200 Subject: [PATCH 0199/1337] Add description to AbstractSystem documentation --- docs/src/basics/AbstractSystem.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/src/basics/AbstractSystem.md b/docs/src/basics/AbstractSystem.md index 2161613353..e68a7bb94e 100644 --- a/docs/src/basics/AbstractSystem.md +++ b/docs/src/basics/AbstractSystem.md @@ -63,6 +63,7 @@ Optionally, a system could have: - `get_defaults(sys)`: A `Dict` that maps variables into their default values for the current-level system. - `get_noiseeqs(sys)`: Noise equations of the current-level system. + - `get_description(sys)`: A string that describes what a system represents. - `get_metadata(sys)`: Any metadata about the system or its origin to be used by downstream packages. Note that if you know a system is an `AbstractTimeDependentSystem` you could use `get_iv` to get the From f57535113531300b6bc474646e7b6b57b1158020 Mon Sep 17 00:00:00 2001 From: Herman Sletmoen Date: Sun, 20 Oct 2024 19:52:24 +0200 Subject: [PATCH 0200/1337] Always check if AbstractSystems have a description --- src/systems/abstractsystem.jl | 9 +++++---- src/systems/diffeqs/odesystem.jl | 2 +- src/systems/diffeqs/sdesystem.jl | 4 ++-- src/systems/discrete_system/discrete_system.jl | 2 +- src/systems/nonlinear/nonlinearsystem.jl | 2 +- 5 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index a84beb05da..1a3c725166 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -498,6 +498,7 @@ end Substitutions(subs, deps) = Substitutions(subs, deps, nothing) Base.nameof(sys::AbstractSystem) = getfield(sys, :name) +description(sys::AbstractSystem) = has_description(sys) ? get_description(sys) : "" #Deprecated function independent_variable(sys::AbstractSystem) @@ -1895,7 +1896,7 @@ function Base.show(io::IO, mime::MIME"text/plain", sys::AbstractSystem; hint = t rows = first(displaysize(io)) ÷ 5 # then allocate ≈1/5 of display height to each list # Print name and description - desc = get_description(sys) + desc = description(sys) printstyled(io, "Model ", nameof(sys), ":"; bold) !isempty(desc) && print(io, " ", desc) @@ -1909,7 +1910,7 @@ function Base.show(io::IO, mime::MIME"text/plain", sys::AbstractSystem; hint = t sub = subs[i] name = String(nameof(sub)) print(io, "\n ", name) - desc = get_description(sub) + desc = description(sub) if !isempty(desc) maxlen = displaysize(io)[2] - length(name) - 6 # remaining length of line if limit && length(desc) > maxlen @@ -2913,7 +2914,7 @@ end function AbstractTrees.printnode( io::IO, sys::AbstractSystem; describe = false, bold = false) printstyled(io, nameof(sys); bold) - describe && !isempty(get_description(sys)) && print(io, ": ", get_description(sys)) + describe && !isempty(description(sys)) && print(io, ": ", description(sys)) end """ hierarchy(sys::AbstractSystem; describe = false, bold = describe, kwargs...) @@ -3006,7 +3007,7 @@ function extend(sys::AbstractSystem, basesys::AbstractSystem; name::Symbol = nam cevs = union(get_continuous_events(basesys), get_continuous_events(sys)) devs = union(get_discrete_events(basesys), get_discrete_events(sys)) defs = merge(get_defaults(basesys), get_defaults(sys)) # prefer `sys` - desc = join(filter(desc -> !isempty(desc), get_description.([sys, basesys])), " ") # concatenate non-empty descriptions with space + desc = join(filter(desc -> !isempty(desc), description.([sys, basesys])), " ") # concatenate non-empty descriptions with space meta = union_nothing(get_metadata(basesys), get_metadata(sys)) syss = union(get_systems(basesys), get_systems(sys)) args = length(ivs) == 0 ? (eqs, sts, ps) : (eqs, ivs[1], sts, ps) diff --git a/src/systems/diffeqs/odesystem.jl b/src/systems/diffeqs/odesystem.jl index 7055da3ac9..2341927ff1 100644 --- a/src/systems/diffeqs/odesystem.jl +++ b/src/systems/diffeqs/odesystem.jl @@ -399,7 +399,7 @@ function flatten(sys::ODESystem, noeqs = false) discrete_events = discrete_events(sys), defaults = defaults(sys), name = nameof(sys), - description = get_description(sys), + description = description(sys), initialization_eqs = initialization_equations(sys), is_dde = is_dde(sys), checks = false) diff --git a/src/systems/diffeqs/sdesystem.jl b/src/systems/diffeqs/sdesystem.jl index e8a72b81cb..9f3814774f 100644 --- a/src/systems/diffeqs/sdesystem.jl +++ b/src/systems/diffeqs/sdesystem.jl @@ -355,7 +355,7 @@ function stochastic_integral_transform(sys::SDESystem, correction_factor) end SDESystem(deqs, get_noiseeqs(sys), get_iv(sys), unknowns(sys), parameters(sys), - name = name, description = get_description(sys), + name = name, description = description(sys), parameter_dependencies = parameter_dependencies(sys), checks = false) end @@ -464,7 +464,7 @@ function Girsanov_transform(sys::SDESystem, u; θ0 = 1.0) # return modified SDE System SDESystem(deqs, noiseeqs, get_iv(sys), unknown_vars, parameters(sys); defaults = Dict(θ => θ0), observed = [weight ~ θ / θ0], - name = name, description = get_description(sys), + name = name, description = description(sys), parameter_dependencies = parameter_dependencies(sys), checks = false) end diff --git a/src/systems/discrete_system/discrete_system.jl b/src/systems/discrete_system/discrete_system.jl index 5e7ae46a08..3e220998cb 100644 --- a/src/systems/discrete_system/discrete_system.jl +++ b/src/systems/discrete_system/discrete_system.jl @@ -226,7 +226,7 @@ function flatten(sys::DiscreteSystem, noeqs = false) observed = observed(sys), defaults = defaults(sys), name = nameof(sys), - description = get_description(sys), + description = description(sys), checks = false) end end diff --git a/src/systems/nonlinear/nonlinearsystem.jl b/src/systems/nonlinear/nonlinearsystem.jl index f1b438f0bb..3b0e4b7e92 100644 --- a/src/systems/nonlinear/nonlinearsystem.jl +++ b/src/systems/nonlinear/nonlinearsystem.jl @@ -561,7 +561,7 @@ function flatten(sys::NonlinearSystem, noeqs = false) observed = observed(sys), defaults = defaults(sys), name = nameof(sys), - description = get_description(sys), + description = description(sys), checks = false) end end From 668bffae6b1744bf31cd44e9f75a88778ace515c Mon Sep 17 00:00:00 2001 From: Herman Sletmoen Date: Sun, 20 Oct 2024 20:44:09 +0200 Subject: [PATCH 0201/1337] Show separate Observed: category instead of mashing it in Equations: --- src/systems/abstractsystem.jl | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index 1a3c725166..ddaeb172f3 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -1927,12 +1927,10 @@ function Base.show(io::IO, mime::MIME"text/plain", sys::AbstractSystem; hint = t if eqs isa AbstractArray && eltype(eqs) <: Equation neqs = count(eq -> !(eq.lhs isa Connection), eqs) next = n_expanded_connection_equations(sys) - nobs = has_observed(sys) ? length(observed(sys)) : 0 - ntot = neqs + next + nobs + ntot = neqs + next ntot > 0 && printstyled(io, "\nEquations ($ntot):"; bold) neqs > 0 && print(io, "\n $neqs standard", hint ? ": see equations(sys)" : "") next > 0 && print(io, "\n $next connecting", hint ? ": see equations(expand_connections(sys))" : "") - nobs > 0 && print(io, "\n $nobs observed", hint ? ": see observed(sys)" : "") #Base.print_matrix(io, eqs) # usually too long and not useful to print all equations end @@ -1969,6 +1967,11 @@ function Base.show(io::IO, mime::MIME"text/plain", sys::AbstractSystem; hint = t limited && printstyled(io, "\n ⋮") # too many variables to print end + # Print observed + nobs = has_observed(sys) ? length(observed(sys)) : 0 + nobs > 0 && printstyled(io, "\nObserved ($nobs):"; bold) + nobs > 0 && hint && print(io, " see observed(sys)") + return nothing end From efad630bd65ed1ba74c0a640f1a5045a6a27af2a Mon Sep 17 00:00:00 2001 From: Herman Sletmoen Date: Sun, 20 Oct 2024 23:58:12 +0200 Subject: [PATCH 0202/1337] Format --- src/systems/abstractsystem.jl | 6 ++++-- src/systems/diffeqs/odesystem.jl | 3 ++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index ddaeb172f3..226c4ff55d 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -1891,7 +1891,8 @@ function n_expanded_connection_equations(sys::AbstractSystem) nextras = n_outer_stream_variables + length(ceqs) end -function Base.show(io::IO, mime::MIME"text/plain", sys::AbstractSystem; hint = true, bold = true) +function Base.show( + io::IO, mime::MIME"text/plain", sys::AbstractSystem; hint = true, bold = true) limit = get(io, :limit, false) # if output should be limited, rows = first(displaysize(io)) ÷ 5 # then allocate ≈1/5 of display height to each list @@ -1930,7 +1931,8 @@ function Base.show(io::IO, mime::MIME"text/plain", sys::AbstractSystem; hint = t ntot = neqs + next ntot > 0 && printstyled(io, "\nEquations ($ntot):"; bold) neqs > 0 && print(io, "\n $neqs standard", hint ? ": see equations(sys)" : "") - next > 0 && print(io, "\n $next connecting", hint ? ": see equations(expand_connections(sys))" : "") + next > 0 && print(io, "\n $next connecting", + hint ? ": see equations(expand_connections(sys))" : "") #Base.print_matrix(io, eqs) # usually too long and not useful to print all equations end diff --git a/src/systems/diffeqs/odesystem.jl b/src/systems/diffeqs/odesystem.jl index 2341927ff1..2176e53d55 100644 --- a/src/systems/diffeqs/odesystem.jl +++ b/src/systems/diffeqs/odesystem.jl @@ -707,7 +707,8 @@ end function Base.show(io::IO, mime::MIME"text/plain", sys::ODESystem; hint = true, bold = true) # Print general AbstractSystem information - invoke(Base.show, Tuple{typeof(io), typeof(mime), AbstractSystem}, io, mime, sys; hint, bold) + invoke(Base.show, Tuple{typeof(io), typeof(mime), AbstractSystem}, + io, mime, sys; hint, bold) # Print initialization equations (unique to ODESystems) nini = length(initialization_equations(sys)) From e5e8d83f30baeda27187999685d5453cc65c7706 Mon Sep 17 00:00:00 2001 From: Christopher Rackauckas Date: Mon, 21 Oct 2024 05:06:36 -0400 Subject: [PATCH 0203/1337] Update Project.toml --- test/extensions/Project.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/test/extensions/Project.toml b/test/extensions/Project.toml index 0e6ba6bc4a..81097d98d2 100644 --- a/test/extensions/Project.toml +++ b/test/extensions/Project.toml @@ -8,6 +8,5 @@ ModelingToolkit = "961ee093-0014-501f-94e3-6117800e7a78" OrdinaryDiffEq = "1dea7af3-3e70-54e6-95c3-0bf5283fa5ed" SciMLSensitivity = "1ed8b502-d754-442c-8d5d-10ac956f44a1" SciMLStructures = "53ae85a6-f571-4167-b2af-e1d143709226" -Setfield = "efcf1570-3423-57d1-acb7-fd33fddbac46" SymbolicIndexingInterface = "2efcf032-c050-4f8e-a9bb-153293bab1f5" Zygote = "e88e6eb3-aa80-5325-afca-941959d7151f" From 82735205d8fadab13baa2dd33c0dd42887109074 Mon Sep 17 00:00:00 2001 From: Christopher Rackauckas Date: Mon, 21 Oct 2024 05:07:08 -0400 Subject: [PATCH 0204/1337] Update bifurcationkit.jl --- test/extensions/bifurcationkit.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/extensions/bifurcationkit.jl b/test/extensions/bifurcationkit.jl index 724a3ebe64..698dd085c8 100644 --- a/test/extensions/bifurcationkit.jl +++ b/test/extensions/bifurcationkit.jl @@ -1,4 +1,4 @@ -using BifurcationKit, ModelingToolkit, Setfield, Test +using BifurcationKit, ModelingToolkit, Test using ModelingToolkit: t_nounits as t, D_nounits as D # Simple pitchfork diagram, compares solution to native BifurcationKit, checks they are identical. # Checks using `jac=false` option. @@ -36,7 +36,7 @@ let bprob_BK = BifurcationProblem(f_BK, [1.0, 1.0], [-1.0, 1.0], - (Setfield.@lens _[1]); + (@lens _[1]); record_from_solution = (x, p) -> x[1]) bif_dia_BK = bifurcationdiagram(bprob_BK, PALC(), From f74f184d95498281cd248ee6fcbb8fe6a3f502b4 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 21 Oct 2024 15:53:39 +0530 Subject: [PATCH 0205/1337] fix: retain nonnumeric parameter dependencies in initialization system --- src/systems/nonlinear/initializesystem.jl | 8 +++++++- test/initializationsystem.jl | 21 +++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/src/systems/nonlinear/initializesystem.jl b/src/systems/nonlinear/initializesystem.jl index 99e4e19d09..a79b44888e 100644 --- a/src/systems/nonlinear/initializesystem.jl +++ b/src/systems/nonlinear/initializesystem.jl @@ -138,8 +138,13 @@ function generate_initializesystem(sys::ODESystem; end # 5) parameter dependencies become equations, their LHS become unknowns + # non-numeric dependent parameters stay as parameter dependencies + new_parameter_deps = Equation[] for eq in parameter_dependencies(sys) - is_variable_floatingpoint(eq.lhs) || continue + if !is_variable_floatingpoint(eq.lhs) + push!(new_parameter_deps, eq) + continue + end varp = tovar(eq.lhs) paramsubs[eq.lhs] = varp push!(eqs_ics, eq) @@ -171,6 +176,7 @@ function generate_initializesystem(sys::ODESystem; pars; defaults = defs, checks = check_units, + parameter_dependencies = new_parameter_deps, name, metadata = meta, kwargs...) diff --git a/test/initializationsystem.jl b/test/initializationsystem.jl index e819980752..f6ec724b14 100644 --- a/test/initializationsystem.jl +++ b/test/initializationsystem.jl @@ -815,3 +815,24 @@ end prob2 = @test_nowarn remake(prob; u0 = [y => 0.5]) @test is_variable(prob.f.initializeprob, p) end + +struct Multiplier{T} + a::T + b::T +end + +function (m::Multiplier)(x, y) + m.a * x + m.b * y +end + +@register_symbolic Multiplier(x::Real, y::Real) + +@testset "Nonnumeric parameter dependencies are retained" begin + @variables x(t) y(t) + @parameters foo(::Real, ::Real) p + @mtkbuild sys = ODESystem([D(x) ~ t, 0 ~ foo(x, y)], t; + parameter_dependencies = [foo ~ Multiplier(p, 2p)], guesses = [y => -1.0]) + prob = ODEProblem(sys, [x => 1.0], (0.0, 1.0), [p => 1.0]) + integ = init(prob, Rosenbrock23()) + @test integ[y] ≈ -0.5 +end From 50075a6d250d878e19c6bbcc4ffad73165f48fea Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 21 Oct 2024 19:02:24 +0530 Subject: [PATCH 0206/1337] test: fix downstream linearization test --- test/downstream/linearize.jl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/downstream/linearize.jl b/test/downstream/linearize.jl index 49b4a45629..d12d1ffa81 100644 --- a/test/downstream/linearize.jl +++ b/test/downstream/linearize.jl @@ -95,10 +95,10 @@ lsys = ModelingToolkit.reorder_unknowns(lsys0, unknowns(ssys), desired_order) ## Symbolic linearization lsyss, _ = ModelingToolkit.linearize_symbolic(cl, [f.u], [p.x]) -@test substitute(lsyss.A, ModelingToolkit.defaults(cl)) == lsys.A -@test substitute(lsyss.B, ModelingToolkit.defaults(cl)) == lsys.B -@test substitute(lsyss.C, ModelingToolkit.defaults(cl)) == lsys.C -@test substitute(lsyss.D, ModelingToolkit.defaults(cl)) == lsys.D +@test ModelingToolkit.fixpoint_sub(lsyss.A, ModelingToolkit.defaults(cl)) == lsys.A +@test ModelingToolkit.fixpoint_sub(lsyss.B, ModelingToolkit.defaults(cl)) == lsys.B +@test ModelingToolkit.fixpoint_sub(lsyss.C, ModelingToolkit.defaults(cl)) == lsys.C +@test ModelingToolkit.fixpoint_sub(lsyss.D, ModelingToolkit.defaults(cl)) == lsys.D ## using ModelingToolkitStandardLibrary.Blocks: LimPID k = 400 From 170b09413a97bae6cea70d5c339f9f9aa6fced40 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 18 Oct 2024 20:40:00 +0530 Subject: [PATCH 0207/1337] fix: handle array guesses in `InitializationProblem` --- src/systems/diffeqs/abstractodesystem.jl | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index e0d4c72f2b..94650d43a8 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -1311,7 +1311,11 @@ function InitializationProblem{iip, specialize}(sys::AbstractODESystem, u0T = promote_type(u0T, typeof(fullmap[eq.lhs])) end if u0T != Union{} - u0map = Dict(k => symbolic_type(v) == NotSymbolic() ? u0T(v) : v + u0map = Dict(k => if symbolic_type(v) == NotSymbolic() && !is_array_of_symbolics(v) + v isa AbstractArray ? u0T.(v) : u0T(v) + else + v + end for (k, v) in u0map) end if neqs == nunknown From f531800d04665d96fd6e6c7fe05f7f8305853cee Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 21 Oct 2024 17:52:24 +0530 Subject: [PATCH 0208/1337] build: bump Symbolics compat --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 808d68af06..f57da969ff 100644 --- a/Project.toml +++ b/Project.toml @@ -126,7 +126,7 @@ SpecialFunctions = "0.7, 0.8, 0.9, 0.10, 1.0, 2" StaticArrays = "0.10, 0.11, 0.12, 1.0" SymbolicIndexingInterface = "0.3.31" SymbolicUtils = "3.7" -Symbolics = "6.14" +Symbolics = "6.15.2" URIs = "1" UnPack = "0.1, 1.0" Unitful = "1.1" From e563aaa2e7c6e1454e4ffe1afd98ade58e9d655c Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 6 Aug 2024 18:55:58 +0530 Subject: [PATCH 0209/1337] fix: better handle empty parameter dependencies --- src/systems/parameter_buffer.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/systems/parameter_buffer.jl b/src/systems/parameter_buffer.jl index 67b26ea50c..1bd90762bd 100644 --- a/src/systems/parameter_buffer.jl +++ b/src/systems/parameter_buffer.jl @@ -59,7 +59,7 @@ function MTKParameters( end p = Dict() missing_params = Set() - pdeps = has_parameter_dependencies(sys) ? parameter_dependencies(sys) : nothing + pdeps = has_parameter_dependencies(sys) ? parameter_dependencies(sys) : [] for sym in all_ps ttsym = default_toterm(sym) @@ -92,7 +92,7 @@ function MTKParameters( delete!(missing_params, ttsym) end - if pdeps !== nothing + if !isempty(pdeps) for eq in pdeps sym = eq.lhs expr = eq.rhs From f658073de0ab8a6933177a9d41016e17424ccd50 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 6 Aug 2024 18:57:09 +0530 Subject: [PATCH 0210/1337] feat: implement `size` and `IndexStyle` for `MTKParameters` --- src/systems/parameter_buffer.jl | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/systems/parameter_buffer.jl b/src/systems/parameter_buffer.jl index 1bd90762bd..8b5a303c29 100644 --- a/src/systems/parameter_buffer.jl +++ b/src/systems/parameter_buffer.jl @@ -678,6 +678,10 @@ end return len end +Base.size(ps::MTKParameters) = (length(ps),) + +Base.IndexStyle(::Type{T}) where {T <: MTKParameters} = IndexLinear() + Base.getindex(p::MTKParameters, pind::ParameterIndex) = parameter_values(p, pind) Base.setindex!(p::MTKParameters, val, pind::ParameterIndex) = set_parameter!(p, val, pind) From 2811b1d0d1226727134bd15199dd661a9c0ff64c Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 9 Aug 2024 14:02:54 +0530 Subject: [PATCH 0211/1337] refactor: make `repack` call `replace` --- src/systems/parameter_buffer.jl | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/src/systems/parameter_buffer.jl b/src/systems/parameter_buffer.jl index 8b5a303c29..72af9f594d 100644 --- a/src/systems/parameter_buffer.jl +++ b/src/systems/parameter_buffer.jl @@ -279,10 +279,7 @@ function SciMLStructures.canonicalize(::SciMLStructures.Tunable, p::MTKParameter arr = p.tunable repack = let p = p function (new_val) - if new_val !== p.tunable - copyto!(p.tunable, new_val) - end - return p + return SciMLStructures.replace(SciMLStructures.Tunable(), p, new_val) end end return arr, repack, true @@ -303,12 +300,9 @@ for (Portion, field, recurse) in [(SciMLStructures.Discrete, :discrete, 1) (Nonnumeric, :nonnumeric, 1)] @eval function SciMLStructures.canonicalize(::$Portion, p::MTKParameters) as_vector = buffer_to_arraypartition(p.$field) - repack = let as_vector = as_vector, p = p + repack = let p = p function (new_val) - if new_val !== as_vector - update_tuple_of_buffers(new_val, p.$field) - end - p + return SciMLStructures.replace(($Portion)(), p, new_val) end end return as_vector, repack, true From 94f1c3d354c95ed839adb11a61d06617d342d8a2 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 22 Oct 2024 16:38:35 +0530 Subject: [PATCH 0212/1337] test: fix repack test --- test/mtkparameters.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/mtkparameters.jl b/test/mtkparameters.jl index b4fc1a9677..603426aaf7 100644 --- a/test/mtkparameters.jl +++ b/test/mtkparameters.jl @@ -53,7 +53,7 @@ for (portion, values) in [(Tunable(), [1.0, 5.0, 6.0, 7.0]) SciMLStructures.replace!(portion, ps, ones(length(buffer))) # make sure it is in-place @test all(isone, canonicalize(portion, ps)[1]) - repack(zeros(length(buffer))) + global ps = repack(zeros(length(buffer))) @test all(iszero, canonicalize(portion, ps)[1]) end From cd2518e948febc3e1b699b6edf1a175c489b464d Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 22 Oct 2024 15:19:13 +0530 Subject: [PATCH 0213/1337] feat: use observed equations for guesses of observed variables --- src/systems/nonlinear/initializesystem.jl | 11 ++++++++++- test/initializationsystem.jl | 8 ++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/systems/nonlinear/initializesystem.jl b/src/systems/nonlinear/initializesystem.jl index a79b44888e..becace8ec5 100644 --- a/src/systems/nonlinear/initializesystem.jl +++ b/src/systems/nonlinear/initializesystem.jl @@ -165,7 +165,16 @@ function generate_initializesystem(sys::ODESystem; [p for p in parameters(sys) if !haskey(paramsubs, p)] ) - eqs_ics = Symbolics.substitute.([eqs_ics; observed(sys)], (paramsubs,)) + # 7) use observed equations for guesses of observed variables if not provided + obseqs = observed(sys) + for eq in obseqs + haskey(defs, eq.lhs) && continue + any(x -> isequal(default_toterm(x), eq.lhs), keys(defs)) && continue + + defs[eq.lhs] = eq.rhs + end + + eqs_ics = Symbolics.substitute.([eqs_ics; obseqs], (paramsubs,)) vars = [vars; collect(values(paramsubs))] for k in keys(defs) defs[k] = substitute(defs[k], paramsubs) diff --git a/test/initializationsystem.jl b/test/initializationsystem.jl index f6ec724b14..de48821cff 100644 --- a/test/initializationsystem.jl +++ b/test/initializationsystem.jl @@ -836,3 +836,11 @@ end integ = init(prob, Rosenbrock23()) @test integ[y] ≈ -0.5 end + +@testset "Use observed equations for guesses of observed variables" begin + @variables x(t) y(t) [state_priority = 100] + @mtkbuild sys = ODESystem( + [D(x) ~ x + t, y ~ 2x + 1], t; initialization_eqs = [x^3 + y^3 ~ 1]) + isys = ModelingToolkit.generate_initializesystem(sys) + @test isequal(defaults(isys)[y], 2x + 1) +end From 0c52a6e321dff668c671ff94f6261b42522f2282 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 22 Oct 2024 19:22:35 +0530 Subject: [PATCH 0214/1337] docs: add DataInterpolations to docs environment --- docs/Project.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/Project.toml b/docs/Project.toml index 078df7d696..0aefe3d701 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -1,6 +1,7 @@ [deps] BenchmarkTools = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf" BifurcationKit = "0f109fa4-8a5d-4b75-95aa-f515264e7665" +DataInterpolations = "82cc6244-b520-54b8-b5a6-8a565e85f1d0" DifferentialEquations = "0c46a032-eb83-5123-abaf-570d42b7fbaa" Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f" Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" @@ -23,6 +24,7 @@ Unitful = "1986cc42-f94f-5a68-af5c-568840ba703d" [compat] BenchmarkTools = "1.3" BifurcationKit = "0.3" +DataInterpolations = "6.5" DifferentialEquations = "7.6" Distributions = "0.25" Documenter = "1" From cfeaf23694a6b8b596cb42c1c957cbf5cd4b103f Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 22 Oct 2024 18:15:16 +0530 Subject: [PATCH 0215/1337] fix: propagate bounds information to variable metadata in `modelingtoolkitize` --- src/systems/optimization/modelingtoolkitize.jl | 9 +++++++++ test/modelingtoolkitize.jl | 10 ++++++++++ 2 files changed, 19 insertions(+) diff --git a/src/systems/optimization/modelingtoolkitize.jl b/src/systems/optimization/modelingtoolkitize.jl index 1ceea795c5..b66f113f6c 100644 --- a/src/systems/optimization/modelingtoolkitize.jl +++ b/src/systems/optimization/modelingtoolkitize.jl @@ -33,6 +33,15 @@ function modelingtoolkitize(prob::DiffEqBase.OptimizationProblem; end _vars = reshape(_vars, size(prob.u0)) vars = ArrayInterface.restructure(prob.u0, _vars) + if prob.ub !== nothing # lb is also !== nothing + vars = map(vars, prob.lb, prob.ub) do sym, lb, ub + if iszero(lb) && iszero(ub) || isinf(lb) && lb < 0 && isinf(ub) && ub > 0 + sym + else + Symbolics.setmetadata(sym, VariableBounds, (lb, ub)) + end + end + end params = if has_p if p_names === nothing && SciMLBase.has_sys(prob.f) p_names = Dict(parameter_index(prob.f.sys, sym) => sym diff --git a/test/modelingtoolkitize.jl b/test/modelingtoolkitize.jl index 32a9720f47..621bf530b7 100644 --- a/test/modelingtoolkitize.jl +++ b/test/modelingtoolkitize.jl @@ -67,6 +67,16 @@ sol = solve(prob, BFGS()) sol = solve(prob, Newton()) @test sol.objective < 1e-8 +prob = OptimizationProblem(ones(3); lb = [-Inf, 0.0, 1.0], ub = [Inf, 0.0, 2.0]) do u, p + sum(abs2, u) +end + +sys = complete(modelingtoolkitize(prob)) +@test !ModelingToolkit.hasbounds(unknowns(sys)[1]) +@test !ModelingToolkit.hasbounds(unknowns(sys)[2]) +@test ModelingToolkit.hasbounds(unknowns(sys)[3]) +@test ModelingToolkit.getbounds(unknowns(sys)[3]) == (1.0, 2.0) + ## SIR System Regression Test β = 0.01# infection rate From 2a3e0230a81706136443c9017e17686d7a9dc83a Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 22 Oct 2024 19:17:11 +0530 Subject: [PATCH 0216/1337] fix: mark bounded optimization unknowns as irreducible --- src/systems/optimization/optimizationsystem.jl | 14 +++++++++++++- src/variables.jl | 1 + test/optimizationsystem.jl | 14 +++++++++++++- 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/src/systems/optimization/optimizationsystem.jl b/src/systems/optimization/optimizationsystem.jl index 22488e8bad..271e0073ce 100644 --- a/src/systems/optimization/optimizationsystem.jl +++ b/src/systems/optimization/optimizationsystem.jl @@ -103,6 +103,17 @@ function OptimizationSystem(op, unknowns, ps; ps′ = value.(ps) op′ = value(scalarize(op)) + irreducible_subs = Dict() + for i in eachindex(unknowns′) + var = unknowns′[i] + if hasbounds(var) + irreducible_subs[var] = irrvar = setirreducible(var, true) + unknowns′[i] = irrvar + end + end + op′ = substitute(op′, irreducible_subs) + constraints = substitute.(constraints, (irreducible_subs,)) + if !(isempty(default_u0) && isempty(default_p)) Base.depwarn( "`default_u0` and `default_p` are deprecated. Use `defaults` instead.", @@ -113,7 +124,8 @@ function OptimizationSystem(op, unknowns, ps; throw(ArgumentError("System names must be unique.")) end defaults = todict(defaults) - defaults = Dict(value(k) => value(v) + defaults = Dict(substitute(value(k), irreducible_subs) => substitute( + value(v), irreducible_subs) for (k, v) in pairs(defaults) if value(v) !== nothing) var_to_name = Dict() diff --git a/src/variables.jl b/src/variables.jl index c0c875450c..fcfc7f9b1e 100644 --- a/src/variables.jl +++ b/src/variables.jl @@ -106,6 +106,7 @@ isoutput(x) = isvarkind(VariableOutput, x) # Before the solvability check, we already have handled IO variables, so # irreducibility is independent from IO. isirreducible(x) = isvarkind(VariableIrreducible, x) +setirreducible(x, v) = setmetadata(x, VariableIrreducible, v) state_priority(x) = convert(Float64, getmetadata(x, VariableStatePriority, 0.0))::Float64 function default_toterm(x) diff --git a/test/optimizationsystem.jl b/test/optimizationsystem.jl index 426d6d5de0..dfa11fca37 100644 --- a/test/optimizationsystem.jl +++ b/test/optimizationsystem.jl @@ -1,5 +1,5 @@ using ModelingToolkit, SparseArrays, Test, Optimization, OptimizationOptimJL, - OptimizationMOI, Ipopt, AmplNLWriter, Ipopt_jll + OptimizationMOI, Ipopt, AmplNLWriter, Ipopt_jll, SymbolicIndexingInterface using ModelingToolkit: get_metadata @testset "basic" begin @@ -347,3 +347,15 @@ end prob = @test_nowarn OptimizationProblem(sys, nothing) @test_nowarn solve(prob, NelderMead()) end + +@testset "Bounded unknowns are irreducible" begin + @variables x + @variables y [bounds = (-Inf, Inf)] + @variables z [bounds = (1.0, 2.0)] + obj = x^2 + y^2 + z^2 + cons = [y ~ 2x + z ~ 2y] + @mtkbuild sys = OptimizationSystem(obj, [x, y, z], []; constraints = cons) + @test is_variable(sys, z) + @test !is_variable(sys, y) +end From 065490971ad5bb91718f0cd2a61463060b962794 Mon Sep 17 00:00:00 2001 From: Benjamin Chung Date: Tue, 22 Oct 2024 18:36:18 -0700 Subject: [PATCH 0217/1337] Document ImperativeEffect and the SymbolicContinousCallback changes --- docs/Project.toml | 1 + docs/src/basics/Events.md | 198 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 199 insertions(+) diff --git a/docs/Project.toml b/docs/Project.toml index 078df7d696..15f1a6a7f0 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -14,6 +14,7 @@ OptimizationOptimJL = "36348300-93cb-4f02-beb5-3c3902f8871e" OrdinaryDiffEq = "1dea7af3-3e70-54e6-95c3-0bf5283fa5ed" Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80" SciMLStructures = "53ae85a6-f571-4167-b2af-e1d143709226" +Setfield = "efcf1570-3423-57d1-acb7-fd33fddbac46" StochasticDiffEq = "789caeaf-c7a9-5a7d-9973-96adeb23e2a0" SymbolicIndexingInterface = "2efcf032-c050-4f8e-a9bb-153293bab1f5" SymbolicUtils = "d1185830-fcd6-423d-90d6-eec64667417b" diff --git a/docs/src/basics/Events.md b/docs/src/basics/Events.md index f425fdce5b..9d3ba30780 100644 --- a/docs/src/basics/Events.md +++ b/docs/src/basics/Events.md @@ -378,3 +378,201 @@ sol.ps[c] # sol[c] will error, since `c` is not a timeseries value ``` It can be seen that the timeseries for `c` is not saved. + + +## [(Experimental) Imperative affects](@id imp_affects) +The `ImperativeAffect` can be used as an alternative to the aforementioned functional affect form. Note +that `ImperativeAffect` is still experimental; to emphasize this, we do not export it and it should be +included as `ModelingToolkit.ImperativeAffect`. It abstracts over how values are written back to the +system, simplifying the definitions and (in the future) allowing assignments back to observed values +by solving the nonlinear reinitialization problem afterwards. + +We will use two examples to describe `ImperativeAffect`: a simple heater and a quadrature encoder. +These examples will also demonstrate advanced usage of `ModelingToolkit.SymbolicContinousCallback`, +the low-level interface that the aforementioned tuple form converts into and allows control over the +exact SciMLCallbacks event that is generated for a continous event. + +### [Heater](@id heater_events) +Bang-bang control of a heater connected to a leaky plant requires hysteresis in order to prevent control oscillation. + +```@example events +@variables temp(t) +params = @parameters furnace_on_threshold=0.5 furnace_off_threshold=0.7 furnace_power=1.0 leakage=0.1 furnace_on(t)::Bool=false +eqs = [ + D(temp) ~ furnace_on * furnace_power - temp^2 * leakage +] +``` +Our plant is simple. We have a heater that's turned on and off by the clocked parameter `furnace_on` +which adds `furnace_power` forcing to the system when enabled. We then leak heat porportional to `leakage` +as a function of the square of the current temperature. + +We need a controller with hysteresis to conol the plant. We wish the furnace to turn on when the temperature +is below `furnace_on_threshold` and off when above `furnace_off_threshold`, while maintaining its current state +in between. To do this, we create two continous callbacks: +```@example events +using Setfield +furnace_disable = 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) +``` +We're using the explicit form of `SymbolicContinuousCallback` here, though +so far we aren't using anything that's not possible with the implicit interface. +You can also write +```julia +[temp ~ furnace_off_threshold] => ModelingToolkit.ImperativeAffect(modified = (; furnace_on)) do x, o, i, c + @set! x.furnace_on = false +end +``` +and it would work the same. + +The `ImperativeAffect` is the larger change in this example. `ImperativeAffect` has the constructor signature +```julia + ImperativeAffect(f::Function; modified::NamedTuple, observed::NamedTuple, ctx) +``` +that accepts the function to call, a named tuple of both the names of and symbolic values representing +values in the system to be modified, a named tuple of the values that are merely observed (that is, used from +the system but not modified), and a context that's passed to the affect function. + +In our example, each event merely changes whether the furnace is on or off. Accordingly, we pass a `modified` tuple +`(; furnace_on)` (creating a `NamedTuple` equivalent to `(furnace_on = furnace_on)`). `ImperativeAffect` will then +evaluate this before calling our function to fill out all of the numerical values, then apply them back to the system +once our affect function returns. Furthermore, it will check that it is possible to do this assignment. + +The function given to `ImperativeAffect` needs to have one of four signatures, checked in this order: +* `f(modified::NamedTuple, observed::NamedTuple, ctx, integrator)::NamedTuple` if the function needs the low-level integrator, +* `f(modified::NamedTuple, observed::NamedTuple, ctx)::NamedTuple` if the function needs the user-defined context, +* `f(modified::NamedTuple, observed::NamedTuple)::NamedTuple` if the function also reads observed values from the system, +* `f(modified::NamedTuple)::NamedTuple` if the function only writes values (unknowns or parameters) to the system. +The `do` block in the example implicitly constructs said function inline. For exposition, we use the full version (e.g. `x, o, i, c`) but this could be simplified to merely `x`. + +The function `f` will be called with `observed` and `modified` `NamedTuple`s that are derived from their respective `NamedTuple` definitions. +In our example, if `furnace_on` is `false`, then the value of the `x` that's passed in as `modified` will be `(furnace_on = false)`. +The modified values should be passed out in the same format: to set `furnace_on` to `true` we need to return a tuple `(furnace_on = true)`. +We use Setfield to do this in the example, recreating the result tuple before returning it. + +Accordingly, we can now interpret the `ImperativeAffect` definitions to mean that when `temp = furnace_off_threshold` we +will write `furnace_on = false` back to the system, and when `temp = furnace_on_threshold` we will write `furnace_on = true` back +to the system. + +```@example events +@named sys = ODESystem( + eqs, t, [temp], params; continuous_events = [furnace_disable, furnace_enable]) +ss = structural_simplify(sys) +prob = ODEProblem(ss, [temp => 0.0, furnace_on => true], (0.0, 10.0)) +sol = solve(prob, Tsit5()) +plot(sol) +hline!([sol.ps[furnace_off_threshold], sol.ps[furnace_on_threshold]], l = (:black, 1), primary = false) +``` + +Here we see exactly the desired hysteresis. The heater starts on until the temperature hits +`furnace_off_threshold`. The temperature then bleeds away until `furnace_on_threshold` at which +point the furnace turns on again until `furnace_off_threshold` and so on and so forth. The controller +is effectively regulating the temperature of the plant. + +### [Quadrature Encoder](@id quadrature) +For a more complex application we'll look at modeling a quadrature encoder attached to a shaft spinning at a constant speed. +Traditionally, a quadrature encoder is built out of a code wheel that interrupts the sensors at constant intervals and two sensors slightly out of phase with one another. +A state machine can take the pattern of pulses produced by the two sensors and determine the number of steps that the shaft has spun. The state machine takes the new value +from each sensor and the old values and decodes them into the direction that the wheel has spun in this step. + +```@example events + @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] +``` +Our continous-time system is extremely simple. We have two states, `theta` for the angle of the shaft +and `omega` for the rate at which it's spinning. We then have parameters for the state machine `qA, qB, hA, hB` +and a step count `cnt`. + +We'll then implement the decoder as a simple Julia function. +```@example events + 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 +``` +Based on the current and old state, this function will return 1 if the wheel spun in the positive direction, +-1 if in the negative, and 0 otherwise. + +The encoder state advances when the occlusion begins or ends. We model the +code wheel as simply detecting when `cos(100*theta)` is 0; if we're at a positive +edge of the 0 crossing, then we interpret that as occlusion (so the discrete `qA` goes to 1). Otherwise, if `cos` is +going negative, we interpret that as lack of occlusion (so the discrete goes to 0). The decoder function is +then invoked to update the count with this new information. + +We can implement this in one of two ways: using edge sign detection or right root finding. For exposition, we +will implement each sensor differently. + +For sensor A, we're using the edge detction method. By providing a different affect to `SymbolicContinuousCallback`'s +`affect_neg` argument, we can specify different behaviour for the negative crossing vs. the positive crossing of the root. +In our encoder, we interpret this as occlusion or nonocclusion of the sensor, update the internal state, and tick the decoder. +```@example events + qAevt = ModelingToolkit.SymbolicContinuousCallback([cos(100 * theta) ~ 0], + ModelingToolkit.ImperativeAffect((; qA, hA, hB, cnt), (; qB)) do x, o, i, c + @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) +``` + +The other way we can implement a sensor is by changing the root find. +Normally, we use left root finding; the affect will be invoked instantaneously before +the root is crossed. This makes it trickier to figure out what the new state is. +Instead, we can use right root finding: + +```@example events + qBevt = ModelingToolkit.SymbolicContinuousCallback([cos(100 * theta - π / 2) ~ 0], + ModelingToolkit.ImperativeAffect((; qB, hA, hB, cnt), (; qA, theta)) do x, o, i, c + @set! x.hA = o.qA + @set! x.hB = x.qB + @set! x.qB = clamp(sign(cos(100 * o.theta - π / 2)), 0.0, 1.0) + @set! x.cnt += decoder(x.hA, x.hB, o.qA, x.qB) + x + end; rootfind = SciMLBase.RightRootFind) +``` +Here, sensor B is located `π / 2` behind sensor A in angular space, so we're adjusting our +trigger function accordingly. We here ask for right root finding on the callback, so we know +that the value of said function will have the "new" sign rather than the old one. Thus, we can +determine the new state of the sensor from the sign of the indicator function evaluated at the +affect activation point, with -1 mapped to 0. + +We can now simulate the encoder. +```@example events + @named sys = ODESystem( + eqs, t, [theta, omega], params; continuous_events = [qAevt, qBevt]) + ss = structural_simplify(sys) + prob = ODEProblem(ss, [theta => 0.0], (0.0, pi)) + sol = solve(prob, Tsit5(); dtmax = 0.01) + sol.ps[cnt] +``` +`cos(100*theta)` will have 200 crossings in the half rotation we've gone through, so the encoder would notionally count 200 steps. +Our encoder counts 198 steps (it loses one step to initialization and one step due to the final state falling squarely on an edge). \ No newline at end of file From aecd59bfea213d5e0650057135afe7eb6262b988 Mon Sep 17 00:00:00 2001 From: Benjamin Chung Date: Tue, 22 Oct 2024 18:40:34 -0700 Subject: [PATCH 0218/1337] Formatter --- src/systems/callbacks.jl | 164 +++++++++++++++++++++++---------------- test/symbolic_events.jl | 20 ++--- 2 files changed, 107 insertions(+), 77 deletions(-) diff --git a/src/systems/callbacks.jl b/src/systems/callbacks.jl index 96758ccf04..e7198817b1 100644 --- a/src/systems/callbacks.jl +++ b/src/systems/callbacks.jl @@ -116,29 +116,33 @@ function ImperativeAffect(f::Function; modified::NamedTuple = NamedTuple{()}(()), ctx = nothing, skip_checks = false) - ImperativeAffect(f, + ImperativeAffect(f, collect(values(observed)), collect(keys(observed)), - collect(values(modified)), collect(keys(modified)), + collect(values(modified)), collect(keys(modified)), ctx, skip_checks) end function ImperativeAffect(f::Function, modified::NamedTuple; - observed::NamedTuple = NamedTuple{()}(()), ctx = nothing, skip_checks=false) - ImperativeAffect(f, observed = observed, modified = modified, ctx = ctx, skip_checks = skip_checks) + observed::NamedTuple = NamedTuple{()}(()), ctx = nothing, skip_checks = false) + ImperativeAffect( + f, observed = observed, modified = modified, ctx = ctx, skip_checks = skip_checks) end function ImperativeAffect( - f::Function, modified::NamedTuple, observed::NamedTuple; ctx = nothing, skip_checks=false) - ImperativeAffect(f, observed = observed, modified = modified, ctx = ctx, skip_checks = skip_checks) + f::Function, modified::NamedTuple, observed::NamedTuple; ctx = nothing, skip_checks = false) + ImperativeAffect( + f, observed = observed, modified = modified, ctx = ctx, skip_checks = skip_checks) end function ImperativeAffect( - f::Function, modified::NamedTuple, observed::NamedTuple, ctx; skip_checks=false) - ImperativeAffect(f, observed = observed, modified = modified, ctx = ctx, skip_checks = skip_checks) + f::Function, modified::NamedTuple, observed::NamedTuple, ctx; skip_checks = false) + ImperativeAffect( + f, observed = observed, modified = modified, ctx = ctx, skip_checks = skip_checks) end -function Base.show(io::IO, mfa::ImperativeAffect) - obs_vals = join(map((ob,nm) -> "$ob => $nm", mfa.obs, mfa.obs_syms), ", ") - mod_vals = join(map((md,nm) -> "$md => $nm", mfa.modified, mfa.mod_syms), ", ") +function Base.show(io::IO, mfa::ImperativeAffect) + obs_vals = join(map((ob, nm) -> "$ob => $nm", mfa.obs, mfa.obs_syms), ", ") + mod_vals = join(map((md, nm) -> "$md => $nm", mfa.modified, mfa.mod_syms), ", ") affect = mfa.f - print(io, "ImperativeAffect(observed: [$obs_vals], modified: [$mod_vals], affect:$affect)") + print(io, + "ImperativeAffect(observed: [$obs_vals], modified: [$mod_vals], affect:$affect)") end func(f::ImperativeAffect) = f.f context(a::ImperativeAffect) = a.ctx @@ -234,15 +238,16 @@ struct SymbolicContinuousCallback affect_neg::Union{Vector{Equation}, FunctionalAffect, ImperativeAffect, Nothing} rootfind::SciMLBase.RootfindOpt reinitializealg::SciMLBase.DAEInitializationAlgorithm - function SymbolicContinuousCallback(; - eqs::Vector{Equation}, - affect = NULL_AFFECT, - affect_neg = affect, - rootfind = SciMLBase.LeftRootFind, - initialize=NULL_AFFECT, - finalize=NULL_AFFECT, - reinitializealg=SciMLBase.CheckInit()) - new(eqs, initialize, finalize, make_affect(affect), make_affect(affect_neg), rootfind, reinitializealg) + function SymbolicContinuousCallback(; + eqs::Vector{Equation}, + affect = NULL_AFFECT, + affect_neg = affect, + rootfind = SciMLBase.LeftRootFind, + initialize = NULL_AFFECT, + finalize = NULL_AFFECT, + reinitializealg = SciMLBase.CheckInit()) + new(eqs, initialize, finalize, make_affect(affect), + make_affect(affect_neg), rootfind, reinitializealg) end # Default affect to nothing end make_affect(affect) = affect @@ -250,8 +255,8 @@ 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.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 Base.isempty(cb::SymbolicContinuousCallback) = isempty(cb.eqs) @@ -266,10 +271,9 @@ function Base.hash(cb::SymbolicContinuousCallback, s::UInt) hash(cb.rootfind, s) end - function Base.show(io::IO, cb::SymbolicContinuousCallback) indent = get(io, :indent, 0) - iio = IOContext(io, :indent => indent+1) + iio = IOContext(io, :indent => indent + 1) print(io, "SymbolicContinuousCallback(") print(iio, "Equations:") show(iio, equations(cb)) @@ -298,7 +302,7 @@ end function Base.show(io::IO, mime::MIME"text/plain", cb::SymbolicContinuousCallback) indent = get(io, :indent, 0) - iio = IOContext(io, :indent => indent+1) + iio = IOContext(io, :indent => indent + 1) println(io, "SymbolicContinuousCallback:") println(iio, "Equations:") show(iio, mime, equations(cb)) @@ -338,14 +342,18 @@ 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; - affect_neg = affect, rootfind = SciMLBase.LeftRootFind, initialize = NULL_AFFECT, finalize = NULL_AFFECT) + affect_neg = affect, rootfind = SciMLBase.LeftRootFind, + initialize = NULL_AFFECT, finalize = NULL_AFFECT) SymbolicContinuousCallback( - eqs = [eqs], affect = affect, affect_neg = affect_neg, rootfind = rootfind, initialize=initialize, finalize=finalize) + eqs = [eqs], affect = affect, affect_neg = affect_neg, rootfind = rootfind, + initialize = initialize, finalize = finalize) end function SymbolicContinuousCallback(eqs::Vector{Equation}, affect = NULL_AFFECT; - affect_neg = affect, rootfind = SciMLBase.LeftRootFind, initialize = NULL_AFFECT, finalize = NULL_AFFECT) + affect_neg = affect, rootfind = SciMLBase.LeftRootFind, + initialize = NULL_AFFECT, finalize = NULL_AFFECT) SymbolicContinuousCallback( - eqs = eqs, affect = affect, affect_neg = affect_neg, rootfind = rootfind, initialize=initialize, finalize=finalize) + eqs = eqs, affect = affect, affect_neg = affect_neg, rootfind = rootfind, + initialize = initialize, finalize = finalize) end SymbolicContinuousCallbacks(cb::SymbolicContinuousCallback) = [cb] @@ -385,8 +393,10 @@ function finalize_affects(cbs::Vector{SymbolicContinuousCallback}) end reinitialization_alg(cb::SymbolicContinuousCallback) = cb.reinitializealg -reinitialization_algs(cbs::Vector{SymbolicContinuousCallback}) = - mapreduce(reinitialization_alg, vcat, cbs, init = SciMLBase.DAEInitializationAlgorithm[]) +function reinitialization_algs(cbs::Vector{SymbolicContinuousCallback}) + mapreduce( + reinitialization_alg, vcat, cbs, init = SciMLBase.DAEInitializationAlgorithm[]) +end namespace_affects(af::Vector, s) = Equation[namespace_affect(a, s) for a in af] namespace_affects(af::FunctionalAffect, s) = namespace_affect(af, s) @@ -436,7 +446,8 @@ struct SymbolicDiscreteCallback affects::Any reinitializealg::SciMLBase.DAEInitializationAlgorithm - function SymbolicDiscreteCallback(condition, affects = NULL_AFFECT, reinitializealg=SciMLBase.CheckInit()) + function SymbolicDiscreteCallback( + condition, affects = NULL_AFFECT, reinitializealg = SciMLBase.CheckInit()) c = scalarize_condition(condition) a = scalarize_affects(affects) new(c, a, reinitializealg) @@ -498,8 +509,10 @@ function affects(cbs::Vector{SymbolicDiscreteCallback}) end reinitialization_alg(cb::SymbolicDiscreteCallback) = cb.reinitializealg -reinitialization_algs(cbs::Vector{SymbolicDiscreteCallback}) = - mapreduce(reinitialization_alg, vcat, cbs, init = SciMLBase.DAEInitializationAlgorithm[]) +function reinitialization_algs(cbs::Vector{SymbolicDiscreteCallback}) + mapreduce( + reinitialization_alg, vcat, cbs, init = SciMLBase.DAEInitializationAlgorithm[]) +end function namespace_callback(cb::SymbolicDiscreteCallback, s)::SymbolicDiscreteCallback af = affects(cb) @@ -781,7 +794,8 @@ function generate_single_rootfinding_callback( end end - user_initfun = isnothing(affect_function.initialize) ? SciMLBase.INITIALIZE_DEFAULT : (c, u, t, i) -> affect_function.initialize(i) + 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 @@ -795,17 +809,19 @@ function generate_single_rootfinding_callback( 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), + 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::AbstractODESystem, dvs = unknowns(sys), - ps = parameters(sys); rootfind = SciMLBase.RightRootFind, reinitialization = SciMLBase.CheckInit(), kwargs...) + ps = parameters(sys); rootfind = SciMLBase.RightRootFind, + reinitialization = SciMLBase.CheckInit(), kwargs...) eqs = map(cb -> flatten_equations(cb.eqs), cbs) num_eqs = length.(eqs) # fuse equations to create VectorContinuousCallback @@ -821,11 +837,12 @@ function generate_vector_rootfinding_callback( sys, rhss, dvs, ps; expression = Val{false}, kwargs...) affect_functions = @NamedTuple{ - affect::Function, - affect_neg::Union{Function, Nothing}, - initialize::Union{Function, Nothing}, + 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] + 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 @@ -861,17 +878,20 @@ function generate_vector_rootfinding_callback( if isnothing(func) continue else - func(integ) + func(integ) end end end end end end - initialize = handle_optional_setup_fn(map(fn -> fn.initialize, affect_functions), SciMLBase.INITIALIZE_DEFAULT) - finalize = handle_optional_setup_fn(map(fn -> fn.finalize, affect_functions), SciMLBase.FINALIZE_DEFAULT) + initialize = handle_optional_setup_fn( + map(fn -> fn.initialize, affect_functions), SciMLBase.INITIALIZE_DEFAULT) + 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) + cond, affect, affect_neg, length(eqs), rootfind = rootfind, initialize = initialize, + finalize = finalize, initializealg = reinitialization) end """ @@ -881,8 +901,8 @@ function compile_affect_fn(cb, sys::AbstractODESystem, 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 + 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...) @@ -918,13 +938,14 @@ function generate_rootfinding_callback(cbs, sys::AbstractODESystem, dvs = unknow # groupby would be very useful here, but alas cb_classes = Dict{ @NamedTuple{ - rootfind::SciMLBase.RootfindOpt, + rootfind::SciMLBase.RootfindOpt, reinitialization::SciMLBase.DAEInitializationAlgorithm}, Vector{SymbolicContinuousCallback}}() for cb in cbs push!( - get!(() -> SymbolicContinuousCallback[], cb_classes, ( - rootfind = cb.rootfind, - reinitialization = reinitialization_alg(cb))), + get!(() -> SymbolicContinuousCallback[], cb_classes, + ( + rootfind = cb.rootfind, + reinitialization = reinitialization_alg(cb))), cb) end @@ -932,7 +953,8 @@ function generate_rootfinding_callback(cbs, sys::AbstractODESystem, dvs = unknow 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...) + cbs_in_class, sys, dvs, ps; rootfind = equiv_class.rootfind, + reinitialization = equiv_class.reinitialization, kwargs...) end if length(compiled_callbacks) == 1 return compiled_callbacks[] @@ -984,29 +1006,34 @@ 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))); init=[]) + assignable_syms = reduce( + vcat, Symbolics.scalarize.(vcat(unknowns(sys), parameters(sys))); 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} +@generated function _generated_writeback(integ, setters::NamedTuple{NS1, <:Tuple}, + values::NamedTuple{NS2, <:Tuple}) where {NS1, NS2} setter_exprs = [] - for name in NS2 + 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) + 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)) + 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 @@ -1084,13 +1111,13 @@ function compile_user_affect(affect::ImperativeAffect, cb, sys, dvs, ps; kwargs. # okay so now to generate the stuff to assign it back into the system mod_pairs = mod_exprs .=> mod_syms - mod_names = (mod_syms..., ) + mod_names = (mod_syms...,) mod_og_val_fun = build_explicit_observed_function( sys, Symbolics.scalarize.(first.(mod_pairs)); array_type = :tuple) 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 @@ -1104,10 +1131,12 @@ function compile_user_affect(affect::ImperativeAffect, cb, sys, dvs, ps; kwargs. upd_component_array = NamedTuple{mod_names}(modvals) # update the observed values - obs_component_array = NamedTuple{obs_sym_tuple}(obs_fun(integ.u, integ.p, integ.t)) + obs_component_array = NamedTuple{obs_sym_tuple}(obs_fun( + integ.u, integ.p, integ.t)) # let the user do their thing - modvals = if applicable(user_affect, upd_component_array, obs_component_array, ctx, integ) + modvals = if applicable( + user_affect, upd_component_array, obs_component_array, ctx, integ) user_affect(upd_component_array, obs_component_array, ctx, integ) elseif applicable(user_affect, upd_component_array, obs_component_array, ctx) user_affect(upd_component_array, obs_component_array, ctx) @@ -1122,7 +1151,7 @@ function compile_user_affect(affect::ImperativeAffect, cb, sys, dvs, ps; kwargs. # write the new values back to the integrator _generated_writeback(integ, upd_funs, modvals) - + for idx in save_idxs SciMLBase.save_discretes!(integ, idx) end @@ -1130,7 +1159,8 @@ function compile_user_affect(affect::ImperativeAffect, cb, sys, dvs, ps; kwargs. end end -function compile_affect(affect::Union{FunctionalAffect, ImperativeAffect}, cb, sys, dvs, ps; kwargs...) +function compile_affect( + affect::Union{FunctionalAffect, ImperativeAffect}, cb, sys, dvs, ps; kwargs...) compile_user_affect(affect, cb, sys, dvs, ps; kwargs...) end diff --git a/test/symbolic_events.jl b/test/symbolic_events.jl index c2c26aae7f..f4f97fb2b9 100644 --- a/test/symbolic_events.jl +++ b/test/symbolic_events.jl @@ -1001,7 +1001,7 @@ end @test sol[b] == [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 +@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 = [ @@ -1080,7 +1080,7 @@ end [temp ~ furnace_off_threshold], ModelingToolkit.ImperativeAffect(modified = (; furnace_on)) do x @set! x.furnace_on = false - end; initialize = ModelingToolkit.ImperativeAffect(modified = (; temp)) do x + end; initialize = ModelingToolkit.ImperativeAffect(modified = (; temp)) do x @set! x.temp = 0.2 end) furnace_enable = ModelingToolkit.SymbolicContinuousCallback( @@ -1145,17 +1145,16 @@ end @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) + return (; fictional2 = false) end) @named sys = ODESystem( eqs, t, [temp, tempsq], params; continuous_events = [furnace_off]) ss = structural_simplify(sys) - prob=ODEProblem( + prob = ODEProblem( ss, [temp => 0.0, furnace_on => true], (0.0, 100.0)) @test_throws "Tried to write back to" solve(prob, Tsit5()) end @@ -1220,15 +1219,16 @@ end @test getp(sol, cnt)(sol) == 197 # 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 - - import RuntimeGeneratedFunctions -function (f::RuntimeGeneratedFunctions.RuntimeGeneratedFunction{argnames, cache_tag, context_tag, id})(args::Vararg{Any, N}) where {N, argnames, cache_tag, context_tag, id} +function (f::RuntimeGeneratedFunctions.RuntimeGeneratedFunction{ + argnames, cache_tag, context_tag, + id})(args::Vararg{Any, N}) where {N, argnames, cache_tag, context_tag, id} try RuntimeGeneratedFunctions.generated_callfunc(f, args...) - catch e + catch e @error "Caught error in RuntimeGeneratedFunction; source code follows" - func_expr = Expr(:->, Expr(:tuple, argnames...), RuntimeGeneratedFunctions._lookup_body(cache_tag, id)) + func_expr = Expr(:->, Expr(:tuple, argnames...), + RuntimeGeneratedFunctions._lookup_body(cache_tag, id)) @show func_expr rethrow(e) end From 89954e46437a7a6fd3536d699d2e3d1846020a48 Mon Sep 17 00:00:00 2001 From: Benjamin Chung Date: Tue, 22 Oct 2024 18:43:31 -0700 Subject: [PATCH 0219/1337] Formatter (2) --- docs/src/basics/Events.md | 161 +++++++++++++++++-------------- src/systems/diffeqs/odesystem.jl | 8 +- 2 files changed, 95 insertions(+), 74 deletions(-) diff --git a/docs/src/basics/Events.md b/docs/src/basics/Events.md index 9d3ba30780..4a59149308 100644 --- a/docs/src/basics/Events.md +++ b/docs/src/basics/Events.md @@ -379,36 +379,39 @@ sol.ps[c] # sol[c] will error, since `c` is not a timeseries value It can be seen that the timeseries for `c` is not saved. - ## [(Experimental) Imperative affects](@id imp_affects) + The `ImperativeAffect` can be used as an alternative to the aforementioned functional affect form. Note that `ImperativeAffect` is still experimental; to emphasize this, we do not export it and it should be -included as `ModelingToolkit.ImperativeAffect`. It abstracts over how values are written back to the +included as `ModelingToolkit.ImperativeAffect`. It abstracts over how values are written back to the system, simplifying the definitions and (in the future) allowing assignments back to observed values by solving the nonlinear reinitialization problem afterwards. -We will use two examples to describe `ImperativeAffect`: a simple heater and a quadrature encoder. +We will use two examples to describe `ImperativeAffect`: a simple heater and a quadrature encoder. These examples will also demonstrate advanced usage of `ModelingToolkit.SymbolicContinousCallback`, the low-level interface that the aforementioned tuple form converts into and allows control over the exact SciMLCallbacks event that is generated for a continous event. ### [Heater](@id heater_events) + Bang-bang control of a heater connected to a leaky plant requires hysteresis in order to prevent control oscillation. -```@example events +```@example events @variables temp(t) params = @parameters furnace_on_threshold=0.5 furnace_off_threshold=0.7 furnace_power=1.0 leakage=0.1 furnace_on(t)::Bool=false eqs = [ D(temp) ~ furnace_on * furnace_power - temp^2 * leakage ] ``` + Our plant is simple. We have a heater that's turned on and off by the clocked parameter `furnace_on` which adds `furnace_power` forcing to the system when enabled. We then leak heat porportional to `leakage` -as a function of the square of the current temperature. +as a function of the square of the current temperature. We need a controller with hysteresis to conol the plant. We wish the furnace to turn on when the temperature is below `furnace_on_threshold` and off when above `furnace_off_threshold`, while maintaining its current state in between. To do this, we create two continous callbacks: + ```@example events using Setfield furnace_disable = ModelingToolkit.SymbolicContinuousCallback( @@ -422,42 +425,49 @@ furnace_enable = ModelingToolkit.SymbolicContinuousCallback( @set! x.furnace_on = true end) ``` + We're using the explicit form of `SymbolicContinuousCallback` here, though so far we aren't using anything that's not possible with the implicit interface. -You can also write +You can also write + ```julia -[temp ~ furnace_off_threshold] => ModelingToolkit.ImperativeAffect(modified = (; furnace_on)) do x, o, i, c +[temp ~ furnace_off_threshold] => ModelingToolkit.ImperativeAffect(modified = (; + furnace_on)) do x, o, i, c @set! x.furnace_on = false end ``` + and it would work the same. The `ImperativeAffect` is the larger change in this example. `ImperativeAffect` has the constructor signature + ```julia - ImperativeAffect(f::Function; modified::NamedTuple, observed::NamedTuple, ctx) +ImperativeAffect(f::Function; modified::NamedTuple, observed::NamedTuple, ctx) ``` + that accepts the function to call, a named tuple of both the names of and symbolic values representing values in the system to be modified, a named tuple of the values that are merely observed (that is, used from the system but not modified), and a context that's passed to the affect function. In our example, each event merely changes whether the furnace is on or off. Accordingly, we pass a `modified` tuple -`(; furnace_on)` (creating a `NamedTuple` equivalent to `(furnace_on = furnace_on)`). `ImperativeAffect` will then +`(; furnace_on)` (creating a `NamedTuple` equivalent to `(furnace_on = furnace_on)`). `ImperativeAffect` will then evaluate this before calling our function to fill out all of the numerical values, then apply them back to the system once our affect function returns. Furthermore, it will check that it is possible to do this assignment. The function given to `ImperativeAffect` needs to have one of four signatures, checked in this order: -* `f(modified::NamedTuple, observed::NamedTuple, ctx, integrator)::NamedTuple` if the function needs the low-level integrator, -* `f(modified::NamedTuple, observed::NamedTuple, ctx)::NamedTuple` if the function needs the user-defined context, -* `f(modified::NamedTuple, observed::NamedTuple)::NamedTuple` if the function also reads observed values from the system, -* `f(modified::NamedTuple)::NamedTuple` if the function only writes values (unknowns or parameters) to the system. -The `do` block in the example implicitly constructs said function inline. For exposition, we use the full version (e.g. `x, o, i, c`) but this could be simplified to merely `x`. + + - `f(modified::NamedTuple, observed::NamedTuple, ctx, integrator)::NamedTuple` if the function needs the low-level integrator, + - `f(modified::NamedTuple, observed::NamedTuple, ctx)::NamedTuple` if the function needs the user-defined context, + - `f(modified::NamedTuple, observed::NamedTuple)::NamedTuple` if the function also reads observed values from the system, + - `f(modified::NamedTuple)::NamedTuple` if the function only writes values (unknowns or parameters) to the system. + The `do` block in the example implicitly constructs said function inline. For exposition, we use the full version (e.g. `x, o, i, c`) but this could be simplified to merely `x`. The function `f` will be called with `observed` and `modified` `NamedTuple`s that are derived from their respective `NamedTuple` definitions. -In our example, if `furnace_on` is `false`, then the value of the `x` that's passed in as `modified` will be `(furnace_on = false)`. +In our example, if `furnace_on` is `false`, then the value of the `x` that's passed in as `modified` will be `(furnace_on = false)`. The modified values should be passed out in the same format: to set `furnace_on` to `true` we need to return a tuple `(furnace_on = true)`. We use Setfield to do this in the example, recreating the result tuple before returning it. -Accordingly, we can now interpret the `ImperativeAffect` definitions to mean that when `temp = furnace_off_threshold` we +Accordingly, we can now interpret the `ImperativeAffect` definitions to mean that when `temp = furnace_off_threshold` we will write `furnace_on = false` back to the system, and when `temp = furnace_on_threshold` we will write `furnace_on = true` back to the system. @@ -468,7 +478,8 @@ ss = structural_simplify(sys) prob = ODEProblem(ss, [temp => 0.0, furnace_on => true], (0.0, 10.0)) sol = solve(prob, Tsit5()) plot(sol) -hline!([sol.ps[furnace_off_threshold], sol.ps[furnace_on_threshold]], l = (:black, 1), primary = false) +hline!([sol.ps[furnace_off_threshold], sol.ps[furnace_on_threshold]], + l = (:black, 1), primary = false) ``` Here we see exactly the desired hysteresis. The heater starts on until the temperature hits @@ -477,71 +488,76 @@ point the furnace turns on again until `furnace_off_threshold` and so on and so is effectively regulating the temperature of the plant. ### [Quadrature Encoder](@id quadrature) + For a more complex application we'll look at modeling a quadrature encoder attached to a shaft spinning at a constant speed. Traditionally, a quadrature encoder is built out of a code wheel that interrupts the sensors at constant intervals and two sensors slightly out of phase with one another. A state machine can take the pattern of pulses produced by the two sensors and determine the number of steps that the shaft has spun. The state machine takes the new value from each sensor and the old values and decodes them into the direction that the wheel has spun in this step. ```@example events - @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] +@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] ``` + Our continous-time system is extremely simple. We have two states, `theta` for the angle of the shaft -and `omega` for the rate at which it's spinning. We then have parameters for the state machine `qA, qB, hA, hB` +and `omega` for the rate at which it's spinning. We then have parameters for the state machine `qA, qB, hA, hB` and a step count `cnt`. We'll then implement the decoder as a simple Julia function. + ```@example events - 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 +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 ``` + Based on the current and old state, this function will return 1 if the wheel spun in the positive direction, -1 if in the negative, and 0 otherwise. -The encoder state advances when the occlusion begins or ends. We model the +The encoder state advances when the occlusion begins or ends. We model the code wheel as simply detecting when `cos(100*theta)` is 0; if we're at a positive edge of the 0 crossing, then we interpret that as occlusion (so the discrete `qA` goes to 1). Otherwise, if `cos` is going negative, we interpret that as lack of occlusion (so the discrete goes to 0). The decoder function is then invoked to update the count with this new information. We can implement this in one of two ways: using edge sign detection or right root finding. For exposition, we -will implement each sensor differently. +will implement each sensor differently. For sensor A, we're using the edge detction method. By providing a different affect to `SymbolicContinuousCallback`'s `affect_neg` argument, we can specify different behaviour for the negative crossing vs. the positive crossing of the root. In our encoder, we interpret this as occlusion or nonocclusion of the sensor, update the internal state, and tick the decoder. + ```@example events - qAevt = ModelingToolkit.SymbolicContinuousCallback([cos(100 * theta) ~ 0], - ModelingToolkit.ImperativeAffect((; qA, hA, hB, cnt), (; qB)) do x, o, i, c - @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) +qAevt = ModelingToolkit.SymbolicContinuousCallback([cos(100 * theta) ~ 0], + ModelingToolkit.ImperativeAffect((; qA, hA, hB, cnt), (; qB)) do x, o, i, c + @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) ``` The other way we can implement a sensor is by changing the root find. @@ -550,29 +566,32 @@ the root is crossed. This makes it trickier to figure out what the new state is. Instead, we can use right root finding: ```@example events - qBevt = ModelingToolkit.SymbolicContinuousCallback([cos(100 * theta - π / 2) ~ 0], - ModelingToolkit.ImperativeAffect((; qB, hA, hB, cnt), (; qA, theta)) do x, o, i, c - @set! x.hA = o.qA - @set! x.hB = x.qB - @set! x.qB = clamp(sign(cos(100 * o.theta - π / 2)), 0.0, 1.0) - @set! x.cnt += decoder(x.hA, x.hB, o.qA, x.qB) - x - end; rootfind = SciMLBase.RightRootFind) +qBevt = ModelingToolkit.SymbolicContinuousCallback([cos(100 * theta - π / 2) ~ 0], + ModelingToolkit.ImperativeAffect((; qB, hA, hB, cnt), (; qA, theta)) do x, o, i, c + @set! x.hA = o.qA + @set! x.hB = x.qB + @set! x.qB = clamp(sign(cos(100 * o.theta - π / 2)), 0.0, 1.0) + @set! x.cnt += decoder(x.hA, x.hB, o.qA, x.qB) + x + end; rootfind = SciMLBase.RightRootFind) ``` -Here, sensor B is located `π / 2` behind sensor A in angular space, so we're adjusting our + +Here, sensor B is located `π / 2` behind sensor A in angular space, so we're adjusting our trigger function accordingly. We here ask for right root finding on the callback, so we know -that the value of said function will have the "new" sign rather than the old one. Thus, we can +that the value of said function will have the "new" sign rather than the old one. Thus, we can determine the new state of the sensor from the sign of the indicator function evaluated at the affect activation point, with -1 mapped to 0. We can now simulate the encoder. + ```@example events - @named sys = ODESystem( - eqs, t, [theta, omega], params; continuous_events = [qAevt, qBevt]) - ss = structural_simplify(sys) - prob = ODEProblem(ss, [theta => 0.0], (0.0, pi)) - sol = solve(prob, Tsit5(); dtmax = 0.01) - sol.ps[cnt] +@named sys = ODESystem( + eqs, t, [theta, omega], params; continuous_events = [qAevt, qBevt]) +ss = structural_simplify(sys) +prob = ODEProblem(ss, [theta => 0.0], (0.0, pi)) +sol = solve(prob, Tsit5(); dtmax = 0.01) +sol.ps[cnt] ``` + `cos(100*theta)` will have 200 crossings in the half rotation we've gone through, so the encoder would notionally count 200 steps. -Our encoder counts 198 steps (it loses one step to initialization and one step due to the final state falling squarely on an edge). \ No newline at end of file +Our encoder counts 198 steps (it loses one step to initialization and one step due to the final state falling squarely on an edge). diff --git a/src/systems/diffeqs/odesystem.jl b/src/systems/diffeqs/odesystem.jl index e99911eec6..1cc8273b4d 100644 --- a/src/systems/diffeqs/odesystem.jl +++ b/src/systems/diffeqs/odesystem.jl @@ -444,7 +444,7 @@ function build_explicit_observed_function(sys, ts; param_only = false, op = Operator, throw = true, - array_type=:array) + array_type = :array) if (isscalar = symbolic_type(ts) !== NotSymbolic()) ts = [ts] end @@ -589,10 +589,12 @@ function build_explicit_observed_function(sys, ts; oop_mtkp_wrapper = mtkparams_wrapper end - output_expr = isscalar ? ts[1] : (array_type == :array ? MakeArray(ts, output_type) : MakeTuple(ts)) + output_expr = isscalar ? ts[1] : + (array_type == :array ? MakeArray(ts, output_type) : MakeTuple(ts)) # Need to keep old method of building the function since it uses `output_type`, # which can't be provided to `build_function` - oop_fn = Func(args, [], pre(Let(obsexprs, output_expr, false))) |> array_wrapper[1] |> oop_mtkp_wrapper |> toexpr + oop_fn = Func(args, [], pre(Let(obsexprs, output_expr, false))) |> array_wrapper[1] |> + oop_mtkp_wrapper |> toexpr oop_fn = expression ? oop_fn : eval_or_rgf(oop_fn; eval_expression, eval_module) if !isscalar From 711fb8c654be38ce7377b85f3025ee7934cd5de8 Mon Sep 17 00:00:00 2001 From: Benjamin Chung Date: Tue, 22 Oct 2024 18:44:45 -0700 Subject: [PATCH 0220/1337] Spelling --- docs/src/basics/Events.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/src/basics/Events.md b/docs/src/basics/Events.md index 4a59149308..8088e0f52a 100644 --- a/docs/src/basics/Events.md +++ b/docs/src/basics/Events.md @@ -388,9 +388,9 @@ system, simplifying the definitions and (in the future) allowing assignments bac by solving the nonlinear reinitialization problem afterwards. We will use two examples to describe `ImperativeAffect`: a simple heater and a quadrature encoder. -These examples will also demonstrate advanced usage of `ModelingToolkit.SymbolicContinousCallback`, +These examples will also demonstrate advanced usage of `ModelingToolkit.SymbolicContinuousCallback`, the low-level interface that the aforementioned tuple form converts into and allows control over the -exact SciMLCallbacks event that is generated for a continous event. +exact SciMLCallbacks event that is generated for a continuous event. ### [Heater](@id heater_events) @@ -405,7 +405,7 @@ eqs = [ ``` Our plant is simple. We have a heater that's turned on and off by the clocked parameter `furnace_on` -which adds `furnace_power` forcing to the system when enabled. We then leak heat porportional to `leakage` +which adds `furnace_power` forcing to the system when enabled. We then leak heat proportional to `leakage` as a function of the square of the current temperature. We need a controller with hysteresis to conol the plant. We wish the furnace to turn on when the temperature @@ -537,7 +537,7 @@ then invoked to update the count with this new information. We can implement this in one of two ways: using edge sign detection or right root finding. For exposition, we will implement each sensor differently. -For sensor A, we're using the edge detction method. By providing a different affect to `SymbolicContinuousCallback`'s +For sensor A, we're using the edge detection method. By providing a different affect to `SymbolicContinuousCallback`'s `affect_neg` argument, we can specify different behaviour for the negative crossing vs. the positive crossing of the root. In our encoder, we interpret this as occlusion or nonocclusion of the sensor, update the internal state, and tick the decoder. From a8ea3698e71afe91dd9040a6fb80bab22c7d6c95 Mon Sep 17 00:00:00 2001 From: Benjamin Chung Date: Tue, 22 Oct 2024 18:46:54 -0700 Subject: [PATCH 0221/1337] Spelling (2) --- docs/src/basics/Events.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/src/basics/Events.md b/docs/src/basics/Events.md index 8088e0f52a..e583af8d9e 100644 --- a/docs/src/basics/Events.md +++ b/docs/src/basics/Events.md @@ -410,7 +410,7 @@ as a function of the square of the current temperature. We need a controller with hysteresis to conol the plant. We wish the furnace to turn on when the temperature is below `furnace_on_threshold` and off when above `furnace_off_threshold`, while maintaining its current state -in between. To do this, we create two continous callbacks: +in between. To do this, we create two continuous callbacks: ```@example events using Setfield @@ -501,7 +501,7 @@ eqs = [D(theta) ~ omega omega ~ 1.0] ``` -Our continous-time system is extremely simple. We have two states, `theta` for the angle of the shaft +Our continuous-time system is extremely simple. We have two states, `theta` for the angle of the shaft and `omega` for the rate at which it's spinning. We then have parameters for the state machine `qA, qB, hA, hB` and a step count `cnt`. From 6a304e7d7f8960d7f2cc63d1be93d8f41f6db824 Mon Sep 17 00:00:00 2001 From: Benjamin Chung Date: Tue, 22 Oct 2024 18:48:15 -0700 Subject: [PATCH 0222/1337] Remove debug RGF shim --- test/symbolic_events.jl | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/test/symbolic_events.jl b/test/symbolic_events.jl index f4f97fb2b9..96a57ec40e 100644 --- a/test/symbolic_events.jl +++ b/test/symbolic_events.jl @@ -1217,19 +1217,4 @@ end prob = ODEProblem(ss, [theta => 0.0], (0.0, pi)) sol = solve(prob, Tsit5(); dtmax = 0.01) @test getp(sol, cnt)(sol) == 197 # 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 - -import RuntimeGeneratedFunctions -function (f::RuntimeGeneratedFunctions.RuntimeGeneratedFunction{ - argnames, cache_tag, context_tag, - id})(args::Vararg{Any, N}) where {N, argnames, cache_tag, context_tag, id} - try - RuntimeGeneratedFunctions.generated_callfunc(f, args...) - catch e - @error "Caught error in RuntimeGeneratedFunction; source code follows" - func_expr = Expr(:->, Expr(:tuple, argnames...), - RuntimeGeneratedFunctions._lookup_body(cache_tag, id)) - @show func_expr - rethrow(e) - end -end +end \ No newline at end of file From 35ec1c59b31ab3d0c6a421482d7e48459a00111c Mon Sep 17 00:00:00 2001 From: Benjamin Chung Date: Tue, 22 Oct 2024 18:50:32 -0700 Subject: [PATCH 0223/1337] Formatter (3) --- test/symbolic_events.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/symbolic_events.jl b/test/symbolic_events.jl index 96a57ec40e..c021f99eea 100644 --- a/test/symbolic_events.jl +++ b/test/symbolic_events.jl @@ -1217,4 +1217,4 @@ end prob = ODEProblem(ss, [theta => 0.0], (0.0, pi)) sol = solve(prob, Tsit5(); dtmax = 0.01) @test getp(sol, cnt)(sol) == 197 # 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 \ No newline at end of file +end From 4c3d6fcc1eaedbec33cfd3f979a958110ff4eace Mon Sep 17 00:00:00 2001 From: Christopher Rackauckas Date: Wed, 23 Oct 2024 06:21:05 +0000 Subject: [PATCH 0224/1337] Update docs/src/basics/Events.md Co-authored-by: Fredrik Bagge Carlson --- docs/src/basics/Events.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/basics/Events.md b/docs/src/basics/Events.md index e583af8d9e..41fa96d619 100644 --- a/docs/src/basics/Events.md +++ b/docs/src/basics/Events.md @@ -394,7 +394,7 @@ exact SciMLCallbacks event that is generated for a continuous event. ### [Heater](@id heater_events) -Bang-bang control of a heater connected to a leaky plant requires hysteresis in order to prevent control oscillation. +Bang-bang control of a heater connected to a leaky plant requires hysteresis in order to prevent rapid control oscillation. ```@example events @variables temp(t) From cdddeb9f62ffcd12357dc96648167fe8514993be Mon Sep 17 00:00:00 2001 From: Christopher Rackauckas Date: Wed, 23 Oct 2024 06:21:34 +0000 Subject: [PATCH 0225/1337] Update docs/src/basics/Events.md Co-authored-by: Fredrik Bagge Carlson --- docs/src/basics/Events.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/basics/Events.md b/docs/src/basics/Events.md index 41fa96d619..add870b7f3 100644 --- a/docs/src/basics/Events.md +++ b/docs/src/basics/Events.md @@ -408,7 +408,7 @@ Our plant is simple. We have a heater that's turned on and off by the clocked pa which adds `furnace_power` forcing to the system when enabled. We then leak heat proportional to `leakage` as a function of the square of the current temperature. -We need a controller with hysteresis to conol the plant. We wish the furnace to turn on when the temperature +We need a controller with hysteresis to control the plant. We wish the furnace to turn on when the temperature is below `furnace_on_threshold` and off when above `furnace_off_threshold`, while maintaining its current state in between. To do this, we create two continuous callbacks: From 2ac2d230664916fdd0151cb6257adbf9dd7b2ccb Mon Sep 17 00:00:00 2001 From: Christopher Rackauckas Date: Wed, 23 Oct 2024 06:21:54 +0000 Subject: [PATCH 0226/1337] Update docs/src/basics/Events.md Co-authored-by: Fredrik Bagge Carlson --- docs/src/basics/Events.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/basics/Events.md b/docs/src/basics/Events.md index add870b7f3..0d9149e23c 100644 --- a/docs/src/basics/Events.md +++ b/docs/src/basics/Events.md @@ -561,7 +561,7 @@ qAevt = ModelingToolkit.SymbolicContinuousCallback([cos(100 * theta) ~ 0], ``` The other way we can implement a sensor is by changing the root find. -Normally, we use left root finding; the affect will be invoked instantaneously before +Normally, we use left root finding; the affect will be invoked instantaneously _before_ the root is crossed. This makes it trickier to figure out what the new state is. Instead, we can use right root finding: From d11d98d7f6788f771cb4a580df742073950ef35c Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 23 Oct 2024 16:13:57 +0530 Subject: [PATCH 0227/1337] fix: fix `linearization_function` not handling dependent parameter mapping --- src/systems/abstractsystem.jl | 1 + test/downstream/linearize.jl | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index b6e2a79c3e..ada8a2a056 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -2458,6 +2458,7 @@ function linearization_function(sys::AbstractSystem, inputs, newps = deepcopy(sys_ps) for (k, v) in p if is_parameter(sys, k) + v = fixpoint_sub(v, p) setp(sys, k)(newps, v) end end diff --git a/test/downstream/linearize.jl b/test/downstream/linearize.jl index d12d1ffa81..dde56d9bb1 100644 --- a/test/downstream/linearize.jl +++ b/test/downstream/linearize.jl @@ -307,6 +307,12 @@ matrices = linfun([1.0], Dict(p => 3.0), 1.0) # this would be 1 if the parameter value isn't respected @test matrices.f_u[] == 3.0 +@testset "linearization_function handles dependent values" begin + @parameters q + matrices = @test_nowarn linfun([1.0], Dict(p => 3q, q => 1.0), 1.0) + @test matrices.f_u[] == 3.0 +end + @testset "Issue #2941" begin @variables x(t) y(t) @parameters p From bbda4123e450eb2c9a9c4e4cf17b48a479d0c920 Mon Sep 17 00:00:00 2001 From: Benjamin Chung Date: Tue, 10 Sep 2024 12:45:14 -0700 Subject: [PATCH 0228/1337] Add support for the initializealg argument in SciMLBase callbacks --- src/systems/callbacks.jl | 54 ++++++++++++----- test/symbolic_events.jl | 121 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 160 insertions(+), 15 deletions(-) diff --git a/src/systems/callbacks.jl b/src/systems/callbacks.jl index 86cab57634..617475d536 100644 --- a/src/systems/callbacks.jl +++ b/src/systems/callbacks.jl @@ -106,15 +106,25 @@ Affects (i.e. `affect` and `affect_neg`) can be specified as either: + `read_parameters` is a vector of the parameters that are *used* by `f!`. Their indices are passed to `f` in `p` similarly to the indices of `unknowns` passed in `u`. + `modified_parameters` is a vector of the parameters that are *modified* by `f!`. Note that a parameter will not appear in `p` if it only appears in `modified_parameters`; it must appear in both `parameters` and `modified_parameters` if it is used in the affect definition. + `ctx` is a user-defined context object passed to `f!` when invoked. This value is aliased for each problem. + +Callbacks that impact a DAE are applied, then the DAE is reinitialized using `reinitializealg` (which defaults to `SciMLBase.CheckInit`). +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. """ struct SymbolicContinuousCallback eqs::Vector{Equation} affect::Union{Vector{Equation}, FunctionalAffect} affect_neg::Union{Vector{Equation}, FunctionalAffect, Nothing} rootfind::SciMLBase.RootfindOpt - function SymbolicContinuousCallback(; eqs::Vector{Equation}, affect = NULL_AFFECT, - affect_neg = affect, rootfind = SciMLBase.LeftRootFind) - new(eqs, make_affect(affect), make_affect(affect_neg), rootfind) + reinitializealg::SciMLBase.DAEInitializationAlgorithm + function SymbolicContinuousCallback(; + eqs::Vector{Equation}, + affect = NULL_AFFECT, + affect_neg = affect, + rootfind = SciMLBase.LeftRootFind, + reinitializealg=SciMLBase.CheckInit()) + new(eqs, make_affect(affect), make_affect(affect_neg), rootfind, reinitializealg) end # Default affect to nothing end make_affect(affect) = affect @@ -183,6 +193,10 @@ function affect_negs(cbs::Vector{SymbolicContinuousCallback}) mapreduce(affect_negs, vcat, cbs, init = Equation[]) end +reinitialization_alg(cb::SymbolicContinuousCallback) = cb.reinitializealg +reinitialization_algs(cbs::Vector{SymbolicContinuousCallback}) = + mapreduce(reinitialization_alg, vcat, cbs, init = SciMLBase.DAEInitializationAlgorithm[]) + 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 @@ -225,11 +239,12 @@ struct SymbolicDiscreteCallback # TODO: Iterative condition::Any affects::Any + reinitializealg::SciMLBase.DAEInitializationAlgorithm - function SymbolicDiscreteCallback(condition, affects = NULL_AFFECT) + function SymbolicDiscreteCallback(condition, affects = NULL_AFFECT, reinitializealg=SciMLBase.CheckInit()) c = scalarize_condition(condition) a = scalarize_affects(affects) - new(c, a) + new(c, a, reinitializealg) end # Default affect to nothing end @@ -286,6 +301,10 @@ function affects(cbs::Vector{SymbolicDiscreteCallback}) reduce(vcat, affects(cb) for cb in cbs; init = []) end +reinitialization_alg(cb::SymbolicDiscreteCallback) = cb.reinitializealg +reinitialization_algs(cbs::Vector{SymbolicDiscreteCallback}) = + mapreduce(reinitialization_alg, vcat, cbs, init = SciMLBase.DAEInitializationAlgorithm[]) + function namespace_callback(cb::SymbolicDiscreteCallback, s)::SymbolicDiscreteCallback af = affects(cb) af = af isa AbstractVector ? namespace_affect.(af, Ref(s)) : namespace_affect(af, s) @@ -579,13 +598,14 @@ function generate_single_rootfinding_callback( initfn = SciMLBase.INITIALIZE_DEFAULT end return ContinuousCallback( - cond, affect_function.affect, affect_function.affect_neg, - rootfind = cb.rootfind, initialize = initfn) + cond, affect_function.affect, affect_function.affect_neg, rootfind = cb.rootfind, + initialize = initfn, + initializealg = reinitialization_alg(cb)) end function generate_vector_rootfinding_callback( cbs, sys::AbstractODESystem, dvs = unknowns(sys), - ps = parameters(sys); rootfind = SciMLBase.RightRootFind, kwargs...) + ps = parameters(sys); rootfind = SciMLBase.RightRootFind, reinitialization = SciMLBase.CheckInit(), kwargs...) eqs = map(cb -> flatten_equations(cb.eqs), cbs) num_eqs = length.(eqs) # fuse equations to create VectorContinuousCallback @@ -650,7 +670,7 @@ function generate_vector_rootfinding_callback( initfn = SciMLBase.INITIALIZE_DEFAULT end return VectorContinuousCallback( - cond, affect, affect_neg, length(eqs), rootfind = rootfind, initialize = initfn) + cond, affect, affect_neg, length(eqs), rootfind = rootfind, initialize = initfn, initializealg = reinitialization) end """ @@ -690,10 +710,14 @@ function generate_rootfinding_callback(cbs, sys::AbstractODESystem, dvs = unknow # group the cbs by what rootfind op they use # groupby would be very useful here, but alas cb_classes = Dict{ - @NamedTuple{rootfind::SciMLBase.RootfindOpt}, Vector{SymbolicContinuousCallback}}() + @NamedTuple{ + rootfind::SciMLBase.RootfindOpt, + reinitialization::SciMLBase.DAEInitializationAlgorithm}, Vector{SymbolicContinuousCallback}}() for cb in cbs push!( - get!(() -> SymbolicContinuousCallback[], cb_classes, (rootfind = cb.rootfind,)), + get!(() -> SymbolicContinuousCallback[], cb_classes, ( + rootfind = cb.rootfind, + reinitialization = reinitialization_alg(cb))), cb) end @@ -701,7 +725,7 @@ function generate_rootfinding_callback(cbs, sys::AbstractODESystem, dvs = unknow 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, kwargs...) + cbs_in_class, sys, dvs, ps; rootfind = equiv_class.rootfind, reinitialization=equiv_class.reinitialization, kwargs...) end if length(compiled_callbacks) == 1 return compiled_callbacks[] @@ -772,10 +796,10 @@ function generate_timed_callback(cb, sys, dvs, ps; postprocess_affect_expr! = no end if cond isa AbstractVector # Preset Time - return PresetTimeCallback(cond, as; initialize = initfn) + return PresetTimeCallback(cond, as; initialize = initfn, initializealg=reinitialization_alg(cb)) else # Periodic - return PeriodicCallback(as, cond; initialize = initfn) + return PeriodicCallback(as, cond; initialize = initfn, initializealg=reinitialization_alg(cb)) end end @@ -800,7 +824,7 @@ function generate_discrete_callback(cb, sys, dvs, ps; postprocess_affect_expr! = else initfn = SciMLBase.INITIALIZE_DEFAULT end - return DiscreteCallback(c, as; initialize = initfn) + return DiscreteCallback(c, as; initialize = initfn, initializealg=reinitialization_alg(cb)) end end diff --git a/test/symbolic_events.jl b/test/symbolic_events.jl index e1d12814ef..7084ab5b96 100644 --- a/test/symbolic_events.jl +++ b/test/symbolic_events.jl @@ -867,6 +867,108 @@ end @test sign.(cos.(3 * (required_crossings_c2 .+ 1e-6))) == sign.(last.(cr2)) end +@testset "Discrete variable timeseries" begin + @variables x(t) + @parameters a(t) b(t) c(t) + cb1 = [x ~ 1.0] => [a ~ -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] + @test sol[b] == [5.0, 5.0] + @test sol[c] == [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0] +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) @@ -887,3 +989,22 @@ 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 "Bump" begin + @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 "initialization not satisifed" solve(prob, Rodas5()) + + cb = [x ~ 0.0] => [y ~ 1] + @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] => [x ~ 1, y ~ 1] + @mtkbuild pend = ODESystem(eqs, t;continuous_events = [cb]) + prob = ODEProblem(pend, [x => 1], (0.0, 3.0), guesses = [y => x]) + @test all(≈(0.0; atol=1e-9), solve(prob, Rodas5())[[x,y]][end]) +end \ No newline at end of file From afbf1884399cd2921e3a7551131125a5b6b20362 Mon Sep 17 00:00:00 2001 From: Benjamin Chung Date: Wed, 23 Oct 2024 03:49:31 -0700 Subject: [PATCH 0229/1337] Run formatter --- src/systems/callbacks.jl | 56 ++++++++++++++++++++++++---------------- test/symbolic_events.jl | 51 ++++++++++++++++++------------------ 2 files changed, 60 insertions(+), 47 deletions(-) diff --git a/src/systems/callbacks.jl b/src/systems/callbacks.jl index 617475d536..bca98c5c83 100644 --- a/src/systems/callbacks.jl +++ b/src/systems/callbacks.jl @@ -118,12 +118,12 @@ struct SymbolicContinuousCallback affect_neg::Union{Vector{Equation}, FunctionalAffect, Nothing} rootfind::SciMLBase.RootfindOpt reinitializealg::SciMLBase.DAEInitializationAlgorithm - function SymbolicContinuousCallback(; - eqs::Vector{Equation}, - affect = NULL_AFFECT, - affect_neg = affect, - rootfind = SciMLBase.LeftRootFind, - reinitializealg=SciMLBase.CheckInit()) + function SymbolicContinuousCallback(; + eqs::Vector{Equation}, + affect = NULL_AFFECT, + affect_neg = affect, + rootfind = SciMLBase.LeftRootFind, + reinitializealg = SciMLBase.CheckInit()) new(eqs, make_affect(affect), make_affect(affect_neg), rootfind, reinitializealg) end # Default affect to nothing end @@ -194,8 +194,10 @@ function affect_negs(cbs::Vector{SymbolicContinuousCallback}) end reinitialization_alg(cb::SymbolicContinuousCallback) = cb.reinitializealg -reinitialization_algs(cbs::Vector{SymbolicContinuousCallback}) = - mapreduce(reinitialization_alg, vcat, cbs, init = SciMLBase.DAEInitializationAlgorithm[]) +function reinitialization_algs(cbs::Vector{SymbolicContinuousCallback}) + mapreduce( + reinitialization_alg, vcat, cbs, init = SciMLBase.DAEInitializationAlgorithm[]) +end namespace_affects(af::Vector, s) = Equation[namespace_affect(a, s) for a in af] namespace_affects(af::FunctionalAffect, s) = namespace_affect(af, s) @@ -241,7 +243,8 @@ struct SymbolicDiscreteCallback affects::Any reinitializealg::SciMLBase.DAEInitializationAlgorithm - function SymbolicDiscreteCallback(condition, affects = NULL_AFFECT, reinitializealg=SciMLBase.CheckInit()) + function SymbolicDiscreteCallback( + condition, affects = NULL_AFFECT, reinitializealg = SciMLBase.CheckInit()) c = scalarize_condition(condition) a = scalarize_affects(affects) new(c, a, reinitializealg) @@ -302,8 +305,10 @@ function affects(cbs::Vector{SymbolicDiscreteCallback}) end reinitialization_alg(cb::SymbolicDiscreteCallback) = cb.reinitializealg -reinitialization_algs(cbs::Vector{SymbolicDiscreteCallback}) = - mapreduce(reinitialization_alg, vcat, cbs, init = SciMLBase.DAEInitializationAlgorithm[]) +function reinitialization_algs(cbs::Vector{SymbolicDiscreteCallback}) + mapreduce( + reinitialization_alg, vcat, cbs, init = SciMLBase.DAEInitializationAlgorithm[]) +end function namespace_callback(cb::SymbolicDiscreteCallback, s)::SymbolicDiscreteCallback af = affects(cb) @@ -598,14 +603,15 @@ function generate_single_rootfinding_callback( initfn = SciMLBase.INITIALIZE_DEFAULT end return ContinuousCallback( - cond, affect_function.affect, affect_function.affect_neg, rootfind = cb.rootfind, + cond, affect_function.affect, affect_function.affect_neg, rootfind = cb.rootfind, initialize = initfn, initializealg = reinitialization_alg(cb)) end function generate_vector_rootfinding_callback( cbs, sys::AbstractODESystem, dvs = unknowns(sys), - ps = parameters(sys); rootfind = SciMLBase.RightRootFind, reinitialization = SciMLBase.CheckInit(), kwargs...) + ps = parameters(sys); rootfind = SciMLBase.RightRootFind, + reinitialization = SciMLBase.CheckInit(), kwargs...) eqs = map(cb -> flatten_equations(cb.eqs), cbs) num_eqs = length.(eqs) # fuse equations to create VectorContinuousCallback @@ -670,7 +676,8 @@ function generate_vector_rootfinding_callback( initfn = SciMLBase.INITIALIZE_DEFAULT end return VectorContinuousCallback( - cond, affect, affect_neg, length(eqs), rootfind = rootfind, initialize = initfn, initializealg = reinitialization) + cond, affect, affect_neg, length(eqs), rootfind = rootfind, + initialize = initfn, initializealg = reinitialization) end """ @@ -711,13 +718,14 @@ function generate_rootfinding_callback(cbs, sys::AbstractODESystem, dvs = unknow # groupby would be very useful here, but alas cb_classes = Dict{ @NamedTuple{ - rootfind::SciMLBase.RootfindOpt, + rootfind::SciMLBase.RootfindOpt, reinitialization::SciMLBase.DAEInitializationAlgorithm}, Vector{SymbolicContinuousCallback}}() for cb in cbs push!( - get!(() -> SymbolicContinuousCallback[], cb_classes, ( - rootfind = cb.rootfind, - reinitialization = reinitialization_alg(cb))), + get!(() -> SymbolicContinuousCallback[], cb_classes, + ( + rootfind = cb.rootfind, + reinitialization = reinitialization_alg(cb))), cb) end @@ -725,7 +733,8 @@ function generate_rootfinding_callback(cbs, sys::AbstractODESystem, dvs = unknow 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...) + cbs_in_class, sys, dvs, ps; rootfind = equiv_class.rootfind, + reinitialization = equiv_class.reinitialization, kwargs...) end if length(compiled_callbacks) == 1 return compiled_callbacks[] @@ -796,10 +805,12 @@ function generate_timed_callback(cb, sys, dvs, ps; postprocess_affect_expr! = no end if cond isa AbstractVector # Preset Time - return PresetTimeCallback(cond, as; initialize = initfn, initializealg=reinitialization_alg(cb)) + return PresetTimeCallback( + cond, as; initialize = initfn, initializealg = reinitialization_alg(cb)) else # Periodic - return PeriodicCallback(as, cond; initialize = initfn, initializealg=reinitialization_alg(cb)) + return PeriodicCallback( + as, cond; initialize = initfn, initializealg = reinitialization_alg(cb)) end end @@ -824,7 +835,8 @@ function generate_discrete_callback(cb, sys, dvs, ps; postprocess_affect_expr! = else initfn = SciMLBase.INITIALIZE_DEFAULT end - return DiscreteCallback(c, as; initialize = initfn, initializealg=reinitialization_alg(cb)) + return DiscreteCallback( + c, as; initialize = initfn, initializealg = reinitialization_alg(cb)) end end diff --git a/test/symbolic_events.jl b/test/symbolic_events.jl index 7084ab5b96..998786fa67 100644 --- a/test/symbolic_events.jl +++ b/test/symbolic_events.jl @@ -890,13 +890,14 @@ 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] + 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() @@ -908,7 +909,7 @@ end port.p ~ p_set end end - + @mtkmodel BinaryValve begin @constants begin p_ref::Float64 = 1.0, [description = "Reference pressure drop in bar"] @@ -918,7 +919,7 @@ end port_in = LiquidPort() port_out = LiquidPort() end - @parameters begin + @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"] @@ -935,16 +936,16 @@ end Δ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 + 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) + 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 @@ -953,22 +954,22 @@ end 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 ] + [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) @@ -991,20 +992,20 @@ end end @testset "Bump" begin - @variables x(t) [irreducible=true] y(t) [irreducible=true] + @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]) + @mtkbuild pend = ODESystem(eqs, t; continuous_events = [cb]) prob = ODEProblem(pend, [x => 1], (0.0, 3.0), guesses = [y => x]) @test_throws "initialization not satisifed" solve(prob, Rodas5()) - + cb = [x ~ 0.0] => [y ~ 1] - @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] => [x ~ 1, y ~ 1] - @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 all(≈(0.0; atol=1e-9), solve(prob, Rodas5())[[x,y]][end]) -end \ No newline at end of file + @test all(≈(0.0; atol = 1e-9), solve(prob, Rodas5())[[x, y]][end]) +end From 2c49ef2def541d45755751faa89ff8c4fa44bbba Mon Sep 17 00:00:00 2001 From: Benjamin Chung Date: Wed, 23 Oct 2024 03:53:08 -0700 Subject: [PATCH 0230/1337] Sidestep typo --- test/symbolic_events.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/symbolic_events.jl b/test/symbolic_events.jl index 998786fa67..670eea76fe 100644 --- a/test/symbolic_events.jl +++ b/test/symbolic_events.jl @@ -997,7 +997,7 @@ end 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 "initialization not satisifed" solve(prob, Rodas5()) + @test_throws "CheckInit specified but initialization" solve(prob, Rodas5()) cb = [x ~ 0.0] => [y ~ 1] @mtkbuild pend = ODESystem(eqs, t; continuous_events = [cb]) From f89450029514546579f8cb1a27c5be60a4c100af Mon Sep 17 00:00:00 2001 From: Benjamin Chung Date: Wed, 23 Oct 2024 04:16:29 -0700 Subject: [PATCH 0231/1337] Fix test dup --- test/symbolic_events.jl | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/test/symbolic_events.jl b/test/symbolic_events.jl index 670eea76fe..26593f980c 100644 --- a/test/symbolic_events.jl +++ b/test/symbolic_events.jl @@ -867,27 +867,6 @@ end @test sign.(cos.(3 * (required_crossings_c2 .+ 1e-6))) == sign.(last.(cr2)) end -@testset "Discrete variable timeseries" begin - @variables x(t) - @parameters a(t) b(t) c(t) - cb1 = [x ~ 1.0] => [a ~ -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] - @test sol[b] == [5.0, 5.0] - @test sol[c] == [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0] -end - @testset "Discrete event reinitialization (#3142)" begin @connector LiquidPort begin p(t)::Float64, [description = "Set pressure in bar", From 47f84fe14cfd17f681d3ec5ae29221672815eb3a Mon Sep 17 00:00:00 2001 From: Benjamin Chung Date: Wed, 23 Oct 2024 04:17:51 -0700 Subject: [PATCH 0232/1337] Fix language bug --- 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 bca98c5c83..c427ae02a1 100644 --- a/src/systems/callbacks.jl +++ b/src/systems/callbacks.jl @@ -107,7 +107,7 @@ Affects (i.e. `affect` and `affect_neg`) can be specified as either: + `modified_parameters` is a vector of the parameters that are *modified* by `f!`. Note that a parameter will not appear in `p` if it only appears in `modified_parameters`; it must appear in both `parameters` and `modified_parameters` if it is used in the affect definition. + `ctx` is a user-defined context object passed to `f!` when invoked. This value is aliased for each problem. -Callbacks that impact a DAE are applied, then the DAE is reinitialized using `reinitializealg` (which defaults to `SciMLBase.CheckInit`). +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. From 9bf734dcbc03123b72fb179a47370d9aa484869f Mon Sep 17 00:00:00 2001 From: Benjamin Chung Date: Wed, 23 Oct 2024 15:28:56 -0700 Subject: [PATCH 0233/1337] Simplify callback interface, fix references --- docs/src/basics/Events.md | 22 +++++++-------- src/systems/callbacks.jl | 31 ++++++-------------- test/symbolic_events.jl | 59 +++------------------------------------ 3 files changed, 23 insertions(+), 89 deletions(-) diff --git a/docs/src/basics/Events.md b/docs/src/basics/Events.md index 0d9149e23c..de1c1000ce 100644 --- a/docs/src/basics/Events.md +++ b/docs/src/basics/Events.md @@ -416,12 +416,12 @@ in between. To do this, we create two continuous callbacks: using Setfield furnace_disable = ModelingToolkit.SymbolicContinuousCallback( [temp ~ furnace_off_threshold], - ModelingToolkit.ImperativeAffect(modified = (; furnace_on)) do x, o, i, c + ModelingToolkit.ImperativeAffect(modified = (; furnace_on)) do x, o, c, i @set! x.furnace_on = false end) furnace_enable = ModelingToolkit.SymbolicContinuousCallback( [temp ~ furnace_on_threshold], - ModelingToolkit.ImperativeAffect(modified = (; furnace_on)) do x, o, i, c + ModelingToolkit.ImperativeAffect(modified = (; furnace_on)) do x, o, c, i @set! x.furnace_on = true end) ``` @@ -454,18 +454,16 @@ In our example, each event merely changes whether the furnace is on or off. Acco evaluate this before calling our function to fill out all of the numerical values, then apply them back to the system once our affect function returns. Furthermore, it will check that it is possible to do this assignment. -The function given to `ImperativeAffect` needs to have one of four signatures, checked in this order: - - - `f(modified::NamedTuple, observed::NamedTuple, ctx, integrator)::NamedTuple` if the function needs the low-level integrator, - - `f(modified::NamedTuple, observed::NamedTuple, ctx)::NamedTuple` if the function needs the user-defined context, - - `f(modified::NamedTuple, observed::NamedTuple)::NamedTuple` if the function also reads observed values from the system, - - `f(modified::NamedTuple)::NamedTuple` if the function only writes values (unknowns or parameters) to the system. - The `do` block in the example implicitly constructs said function inline. For exposition, we use the full version (e.g. `x, o, i, c`) but this could be simplified to merely `x`. +The function given to `ImperativeAffect` needs to have the signature: +```julia + f(modified::NamedTuple, observed::NamedTuple, ctx, integrator)::NamedTuple +``` The function `f` will be called with `observed` and `modified` `NamedTuple`s that are derived from their respective `NamedTuple` definitions. In our example, if `furnace_on` is `false`, then the value of the `x` that's passed in as `modified` will be `(furnace_on = false)`. The modified values should be passed out in the same format: to set `furnace_on` to `true` we need to return a tuple `(furnace_on = true)`. -We use Setfield to do this in the example, recreating the result tuple before returning it. +The examples does this with Setfield, recreating the result tuple before returning it; the returned tuple may optionally be missing values as +well, in which case those values will not be written back to the problem. Accordingly, we can now interpret the `ImperativeAffect` definitions to mean that when `temp = furnace_off_threshold` we will write `furnace_on = false` back to the system, and when `temp = furnace_on_threshold` we will write `furnace_on = true` back @@ -543,7 +541,7 @@ In our encoder, we interpret this as occlusion or nonocclusion of the sensor, up ```@example events qAevt = ModelingToolkit.SymbolicContinuousCallback([cos(100 * theta) ~ 0], - ModelingToolkit.ImperativeAffect((; qA, hA, hB, cnt), (; qB)) do x, o, i, c + 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 @@ -567,7 +565,7 @@ Instead, we can use right root finding: ```@example events qBevt = ModelingToolkit.SymbolicContinuousCallback([cos(100 * theta - π / 2) ~ 0], - ModelingToolkit.ImperativeAffect((; qB, hA, hB, cnt), (; qA, theta)) do x, o, i, c + ModelingToolkit.ImperativeAffect((; qB, hA, hB, cnt), (; qA, theta)) do x, o, c, i @set! x.hA = o.qA @set! x.hB = x.qB @set! x.qB = clamp(sign(cos(100 * o.theta - π / 2)), 0.0, 1.0) diff --git a/src/systems/callbacks.jl b/src/systems/callbacks.jl index e7198817b1..618a1f3a83 100644 --- a/src/systems/callbacks.jl +++ b/src/systems/callbacks.jl @@ -76,12 +76,11 @@ end `ImperativeAffect` is a helper for writing affect functions that will compute observed values and ensure that modified values are correctly written back into the system. The affect function `f` needs to have -one of four signatures: -* `f(modified::NamedTuple)::NamedTuple` if the function only writes values (unknowns or parameters) to the system, -* `f(modified::NamedTuple, observed::NamedTuple)::NamedTuple` if the function also reads observed values from the system, -* `f(modified::NamedTuple, observed::NamedTuple, ctx)::NamedTuple` if the function needs the user-defined context, -* `f(modified::NamedTuple, observed::NamedTuple, ctx, integrator)::NamedTuple` if the function needs the low-level integrator. -These will be checked in reverse order (that is, the four-argument version first, than the 3, etc). +the signature + +``` + f(modified::NamedTuple, observed::NamedTuple, ctx, integrator)::NamedTuple +``` The function `f` will be called with `observed` and `modified` `NamedTuple`s that are derived from their respective `NamedTuple` definitions. Each declaration`NamedTuple` should map an expression to a symbol; for example if we pass `observed=(; x = a + b)` this will alias the result of executing `a+b` in the system as `x` @@ -1046,7 +1045,7 @@ function compile_user_affect(affect::ImperativeAffect, cb, sys, dvs, ps; kwargs. Implementation sketch: generate observed function (oop), should save to a component array under obs_syms do the same stuff as the normal FA for pars_syms - call the affect method - test if it's OOP or IP using applicable + call the affect method unpack and apply the resulting values =# function check_dups(syms, exprs) # = (syms_dedup, exprs_dedup) @@ -1135,22 +1134,10 @@ function compile_user_affect(affect::ImperativeAffect, cb, sys, dvs, ps; kwargs. integ.u, integ.p, integ.t)) # let the user do their thing - modvals = if applicable( - user_affect, upd_component_array, obs_component_array, ctx, integ) - user_affect(upd_component_array, obs_component_array, ctx, integ) - elseif applicable(user_affect, upd_component_array, obs_component_array, ctx) - user_affect(upd_component_array, obs_component_array, ctx) - elseif applicable(user_affect, upd_component_array, obs_component_array) - user_affect(upd_component_array, obs_component_array) - elseif applicable(user_affect, upd_component_array) - user_affect(upd_component_array) - else - @error "User affect function $user_affect needs to implement one of the supported ImperativeAffect callback forms; see the ImperativeAffect docstring for more details" - user_affect(upd_component_array, obs_component_array, integ, ctx) # this WILL error but it'll give a more sensible message - end - + upd_vals = user_affect(upd_component_array, obs_component_array, ctx, integ) + # write the new values back to the integrator - _generated_writeback(integ, upd_funs, modvals) + _generated_writeback(integ, upd_funs, upd_vals) for idx in save_idxs SciMLBase.save_discretes!(integ, idx) diff --git a/test/symbolic_events.jl b/test/symbolic_events.jl index c021f99eea..3c3e9168e7 100644 --- a/test/symbolic_events.jl +++ b/test/symbolic_events.jl @@ -1027,60 +1027,9 @@ end furnace_off = ModelingToolkit.SymbolicContinuousCallback( [temp ~ furnace_off_threshold], - ModelingToolkit.ImperativeAffect(modified = (; furnace_on)) do x, o, i - @set! x.furnace_on = false - end) - furnace_enable = ModelingToolkit.SymbolicContinuousCallback( - [temp ~ furnace_on_threshold], - ModelingToolkit.ImperativeAffect(modified = (; furnace_on)) do x, o, 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) - - furnace_off = ModelingToolkit.SymbolicContinuousCallback( - [temp ~ furnace_off_threshold], - ModelingToolkit.ImperativeAffect(modified = (; furnace_on)) do x, o - @set! x.furnace_on = false - end) - furnace_enable = ModelingToolkit.SymbolicContinuousCallback( - [temp ~ furnace_on_threshold], - ModelingToolkit.ImperativeAffect(modified = (; furnace_on)) do x, o - @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 - @set! x.furnace_on = false - end) - furnace_enable = ModelingToolkit.SymbolicContinuousCallback( - [temp ~ furnace_on_threshold], - ModelingToolkit.ImperativeAffect(modified = (; furnace_on)) do x - @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 + ModelingToolkit.ImperativeAffect(modified = (; furnace_on)) do x, o, c, i @set! x.furnace_on = false - end; initialize = ModelingToolkit.ImperativeAffect(modified = (; temp)) do x + end; initialize = ModelingToolkit.ImperativeAffect(modified = (; temp)) do x, o, c, i @set! x.temp = 0.2 end) furnace_enable = ModelingToolkit.SymbolicContinuousCallback( @@ -1180,7 +1129,7 @@ end end end qAevt = ModelingToolkit.SymbolicContinuousCallback([cos(100 * theta) ~ 0], - ModelingToolkit.ImperativeAffect((; qA, hA, hB, cnt), (; qB)) do x, o, i, c + 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 @@ -1196,7 +1145,7 @@ end x end; rootfind = SciMLBase.RightRootFind) qBevt = ModelingToolkit.SymbolicContinuousCallback([cos(100 * theta - π / 2) ~ 0], - ModelingToolkit.ImperativeAffect((; qB, hA, hB, cnt), (; qA)) do x, o, i, c + 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 From 1a3f7d4e4916c12d26a4659334a859a9c3d5ca8b Mon Sep 17 00:00:00 2001 From: Benjamin Chung Date: Wed, 23 Oct 2024 15:33:20 -0700 Subject: [PATCH 0234/1337] Make array_type an actual type, though only in a limited sense --- src/systems/callbacks.jl | 4 ++-- src/systems/diffeqs/odesystem.jl | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/systems/callbacks.jl b/src/systems/callbacks.jl index 618a1f3a83..c34681a20c 100644 --- a/src/systems/callbacks.jl +++ b/src/systems/callbacks.jl @@ -1105,7 +1105,7 @@ function compile_user_affect(affect::ImperativeAffect, cb, sys, dvs, ps; kwargs. end obs_fun = build_explicit_observed_function( sys, Symbolics.scalarize.(obs_exprs); - array_type = :tuple) + array_type = Tuple) obs_sym_tuple = (obs_syms...,) # okay so now to generate the stuff to assign it back into the system @@ -1113,7 +1113,7 @@ function compile_user_affect(affect::ImperativeAffect, cb, sys, dvs, ps; kwargs. mod_names = (mod_syms...,) mod_og_val_fun = build_explicit_observed_function( sys, Symbolics.scalarize.(first.(mod_pairs)); - array_type = :tuple) + array_type = Tuple) upd_funs = NamedTuple{mod_names}((setu.((sys,), first.(mod_pairs))...,)) diff --git a/src/systems/diffeqs/odesystem.jl b/src/systems/diffeqs/odesystem.jl index 1cc8273b4d..374cbab5f8 100644 --- a/src/systems/diffeqs/odesystem.jl +++ b/src/systems/diffeqs/odesystem.jl @@ -429,7 +429,7 @@ Options not otherwise specified are: * `op = Operator` sets the recursion terminator for the walk done by `vars` to identify the variables that appear in `ts`. See the documentation for `vars` for more detail. * `throw = true` if true, throw an error when generating a function for `ts` that reference variables that do not exist * `drop_expr` is deprecated. -* `array_type`; only used if the output is an array (that is, `!isscalar(ts)`). If `:array`, then it will generate an array, if `:tuple` then it will generate a tuple. +* `array_type`; only used if the output is an array (that is, `!isscalar(ts)`). If it is `Vector`, then it will generate an array, if `Tuple` then it will generate a tuple. """ function build_explicit_observed_function(sys, ts; inputs = nothing, @@ -444,7 +444,7 @@ function build_explicit_observed_function(sys, ts; param_only = false, op = Operator, throw = true, - array_type = :array) + array_type = Vector) if (isscalar = symbolic_type(ts) !== NotSymbolic()) ts = [ts] end @@ -590,7 +590,7 @@ function build_explicit_observed_function(sys, ts; end output_expr = isscalar ? ts[1] : - (array_type == :array ? MakeArray(ts, output_type) : MakeTuple(ts)) + (array_type <: Vector ? MakeArray(ts, output_type) : MakeTuple(ts)) # Need to keep old method of building the function since it uses `output_type`, # which can't be provided to `build_function` oop_fn = Func(args, [], pre(Let(obsexprs, output_expr, false))) |> array_wrapper[1] |> From 6c7e4b84ae40e0bf1cbe1c61f6e4c41202ee449c Mon Sep 17 00:00:00 2001 From: Benjamin Chung Date: Wed, 23 Oct 2024 15:34:59 -0700 Subject: [PATCH 0235/1337] Clean up language indocs --- docs/src/basics/Events.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/src/basics/Events.md b/docs/src/basics/Events.md index de1c1000ce..902be98fd4 100644 --- a/docs/src/basics/Events.md +++ b/docs/src/basics/Events.md @@ -404,7 +404,7 @@ eqs = [ ] ``` -Our plant is simple. We have a heater that's turned on and off by the clocked parameter `furnace_on` +Our plant is simple. We have a heater that's turned on and off by the time-indexed parameter `furnace_on` which adds `furnace_power` forcing to the system when enabled. We then leak heat proportional to `leakage` as a function of the square of the current temperature. @@ -499,9 +499,9 @@ eqs = [D(theta) ~ omega omega ~ 1.0] ``` -Our continuous-time system is extremely simple. We have two states, `theta` for the angle of the shaft +Our continuous-time system is extremely simple. We have two unknown variables `theta` for the angle of the shaft and `omega` for the rate at which it's spinning. We then have parameters for the state machine `qA, qB, hA, hB` -and a step count `cnt`. +(corresponding to the current quadrature of the A/B sensors and the historical ones) and a step count `cnt`. We'll then implement the decoder as a simple Julia function. From 06ecd2ee541d94bcf92c39543583f9bc0646286d Mon Sep 17 00:00:00 2001 From: Benjamin Chung Date: Wed, 23 Oct 2024 21:24:46 -0700 Subject: [PATCH 0236/1337] Format --- docs/src/basics/Events.md | 7 ++++--- src/systems/callbacks.jl | 2 +- test/symbolic_events.jl | 3 ++- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/docs/src/basics/Events.md b/docs/src/basics/Events.md index 902be98fd4..4c9803ef57 100644 --- a/docs/src/basics/Events.md +++ b/docs/src/basics/Events.md @@ -456,13 +456,14 @@ once our affect function returns. Furthermore, it will check that it is possible The function given to `ImperativeAffect` needs to have the signature: -```julia - f(modified::NamedTuple, observed::NamedTuple, ctx, integrator)::NamedTuple +```julia +f(modified::NamedTuple, observed::NamedTuple, ctx, integrator)::NamedTuple ``` + The function `f` will be called with `observed` and `modified` `NamedTuple`s that are derived from their respective `NamedTuple` definitions. In our example, if `furnace_on` is `false`, then the value of the `x` that's passed in as `modified` will be `(furnace_on = false)`. The modified values should be passed out in the same format: to set `furnace_on` to `true` we need to return a tuple `(furnace_on = true)`. -The examples does this with Setfield, recreating the result tuple before returning it; the returned tuple may optionally be missing values as +The examples does this with Setfield, recreating the result tuple before returning it; the returned tuple may optionally be missing values as well, in which case those values will not be written back to the problem. Accordingly, we can now interpret the `ImperativeAffect` definitions to mean that when `temp = furnace_off_threshold` we diff --git a/src/systems/callbacks.jl b/src/systems/callbacks.jl index c34681a20c..1061139a8a 100644 --- a/src/systems/callbacks.jl +++ b/src/systems/callbacks.jl @@ -1135,7 +1135,7 @@ function compile_user_affect(affect::ImperativeAffect, cb, sys, dvs, ps; kwargs. # let the user do their thing upd_vals = user_affect(upd_component_array, obs_component_array, ctx, integ) - + # write the new values back to the integrator _generated_writeback(integ, upd_funs, upd_vals) diff --git a/test/symbolic_events.jl b/test/symbolic_events.jl index 3c3e9168e7..77961cb01f 100644 --- a/test/symbolic_events.jl +++ b/test/symbolic_events.jl @@ -1029,7 +1029,8 @@ end [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 + end; initialize = ModelingToolkit.ImperativeAffect(modified = (; + temp)) do x, o, c, i @set! x.temp = 0.2 end) furnace_enable = ModelingToolkit.SymbolicContinuousCallback( From 8f2a9e4ba76f55939b71b93ca293740cc523e2cc Mon Sep 17 00:00:00 2001 From: Christopher Rackauckas Date: Thu, 24 Oct 2024 05:06:30 +0000 Subject: [PATCH 0237/1337] Update Project.toml --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index f2564304a8..6650abee74 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "ModelingToolkit" uuid = "961ee093-0014-501f-94e3-6117800e7a78" authors = ["Yingbo Ma ", "Chris Rackauckas and contributors"] -version = "9.46.1" +version = "9.47.0" [deps] AbstractTrees = "1520ce14-60c1-5f80-bbc7-55ef81b5835c" From 1e3618d99cf2daca3e985e0b7e7a822b81332752 Mon Sep 17 00:00:00 2001 From: Christopher Rackauckas Date: Thu, 24 Oct 2024 05:20:03 +0000 Subject: [PATCH 0238/1337] Update bifurcationkit.jl --- test/extensions/bifurcationkit.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/extensions/bifurcationkit.jl b/test/extensions/bifurcationkit.jl index 698dd085c8..c2dc5685aa 100644 --- a/test/extensions/bifurcationkit.jl +++ b/test/extensions/bifurcationkit.jl @@ -36,7 +36,7 @@ let bprob_BK = BifurcationProblem(f_BK, [1.0, 1.0], [-1.0, 1.0], - (@lens _[1]); + (BifurcationKit.@optic _[1]); record_from_solution = (x, p) -> x[1]) bif_dia_BK = bifurcationdiagram(bprob_BK, PALC(), From a03f061f049b8cab15843b91acc6f2530f1a9c7d Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 16 Oct 2024 18:52:33 +0530 Subject: [PATCH 0239/1337] fix: fix variable discovery in arrays of `Num` passed to callable params --- src/utils.jl | 10 ++++++++-- test/odesystem.jl | 8 ++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/utils.jl b/src/utils.jl index d2e8a3ea38..830ec98e44 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -389,7 +389,13 @@ function vars!(vars, O; op = Differential) f = getcalledparameter(O) push!(vars, f) for arg in arguments(O) - vars!(vars, arg; op) + if symbolic_type(arg) == NotSymbolic() && arg isa AbstractArray + for el in arg + vars!(vars, unwrap(el); op) + end + else + vars!(vars, arg; op) + end end return vars end @@ -397,7 +403,7 @@ function vars!(vars, O; op = Differential) end if symbolic_type(O) == NotSymbolic() && O isa AbstractArray for arg in O - vars!(vars, arg; op) + vars!(vars, unwrap(arg); op) end return vars end diff --git a/test/odesystem.jl b/test/odesystem.jl index 9446d105e0..cbc24300cd 100644 --- a/test/odesystem.jl +++ b/test/odesystem.jl @@ -1447,3 +1447,11 @@ end @parameters p @test_nowarn ODESystem(Equation[], t; parameter_dependencies = [p ~ 1.0], name = :a) end + +@testset "Variable discovery in arrays of `Num` inside callable symbolic" begin + @variables x(t) y(t) + @parameters foo(::AbstractVector) + sys = @test_nowarn ODESystem(D(x) ~ foo([x, 2y]), t; name = :sys) + @test length(unknowns(sys)) == 2 + @test any(isequal(y), unknowns(sys)) +end From 5c2edf4ff43796be3284a42843fc73d75967bc58 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 16 Oct 2024 18:53:17 +0530 Subject: [PATCH 0240/1337] fix: improve hack supporting unscalarized usage of array observed variables --- .../symbolics_tearing.jl | 98 +++++++++++++------ test/structural_transformation/utils.jl | 14 +++ 2 files changed, 83 insertions(+), 29 deletions(-) diff --git a/src/structural_transformation/symbolics_tearing.jl b/src/structural_transformation/symbolics_tearing.jl index 84cee928cd..889b75c611 100644 --- a/src/structural_transformation/symbolics_tearing.jl +++ b/src/structural_transformation/symbolics_tearing.jl @@ -574,48 +574,78 @@ function tearing_reassemble(state::TearingState, var_eq_matching, # TODO: compute the dependency correctly so that we don't have to do this obs = [fast_substitute(observed(sys), obs_sub); subeqs] - # HACK: Substitute non-scalarized symbolic arrays of observed variables - # E.g. if `p[1] ~ (...)` and `p[2] ~ (...)` then substitute `p => [p[1], p[2]]` in all equations - # ideally, we want to support equations such as `p ~ [p[1], p[2]]` which will then be handled - # by the topological sorting and dependency identification pieces - obs_arr_subs = Dict() + unknowns = Any[v + for (i, v) in enumerate(fullvars) + if diff_to_var[i] === nothing && ispresent(i)] + if !isempty(extra_vars) + for v in extra_vars + push!(unknowns, old_fullvars[v]) + end + end + @set! sys.unknowns = unknowns + # HACK: Add equations for array observed variables. If `p[i] ~ (...)` + # are equations, add an equation `p ~ [p[1], p[2], ...]` + # allow topsort to reorder them + # only add the new equation if all `p[i]` are present and the unscalarized + # form is used in any equation (observed or not) + # we first count the number of times the scalarized form of each observed + # variable occurs in observed equations (and unknowns if it's split). + + # map of array observed variable (unscalarized) to number of its + # scalarized terms that appear in observed equations + arr_obs_occurrences = Dict() + # to check if array variables occur in unscalarized form anywhere + all_vars = Set() for eq in obs + vars!(all_vars, eq.rhs) lhs = eq.lhs iscall(lhs) || continue operation(lhs) === getindex || continue - Symbolics.shape(lhs) !== Symbolics.Unknown() || continue + Symbolics.shape(lhs) != Symbolics.Unknown() || continue arg1 = arguments(lhs)[1] - haskey(obs_arr_subs, arg1) && continue - obs_arr_subs[arg1] = [arg1[i] for i in eachindex(arg1)] # e.g. p => [p[1], p[2]] - index_first = eachindex(arg1)[1] - + cnt = get(arr_obs_occurrences, arg1, 0) + arr_obs_occurrences[arg1] = cnt + 1 + continue + end + # count variables in unknowns if they are scalarized forms of variables + # also present as observed. e.g. if `x[1]` is an unknown and `x[2] ~ (..)` + # is an observed equation. + for sym in unknowns + iscall(sym) || continue + operation(sym) === getindex || continue + Symbolics.shape(sym) != Symbolics.Unknown() || continue + arg1 = arguments(sym)[1] + cnt = get(arr_obs_occurrences, arg1, 0) + cnt == 0 && continue + arr_obs_occurrences[arg1] = cnt + 1 + end + for eq in neweqs + vars!(all_vars, eq.rhs) + end + obs_arr_eqs = Equation[] + for (arrvar, cnt) in arr_obs_occurrences + cnt == length(arrvar) || continue + arrvar in all_vars || continue + # firstindex returns 1 for multidimensional array symbolics + firstind = first(eachindex(arrvar)) + scal = [arrvar[i] for i in eachindex(arrvar)] # respect non-1-indexed arrays # TODO: get rid of this hack together with the above hack, then remove OffsetArrays dependency - obs_arr_subs[arg1] = Origin(index_first)(obs_arr_subs[arg1]) - end - for i in eachindex(neweqs) - neweqs[i] = fast_substitute(neweqs[i], obs_arr_subs; operator = Symbolics.Operator) - end - for i in eachindex(obs) - obs[i] = fast_substitute(obs[i], obs_arr_subs; operator = Symbolics.Operator) - end - for i in eachindex(subeqs) - subeqs[i] = fast_substitute(subeqs[i], obs_arr_subs; operator = Symbolics.Operator) + # `change_origin` is required because `Origin(firstind)(scal)` makes codegen + # try to `create_array(OffsetArray{...}, ...)` which errors. + # `term(Origin(firstind), scal)` doesn't retain the `symtype` and `size` + # of `scal`. + push!(obs_arr_eqs, arrvar ~ change_origin(Origin(firstind), scal)) end + append!(obs, obs_arr_eqs) + append!(subeqs, obs_arr_eqs) + # need to re-sort subeqs + subeqs = ModelingToolkit.topsort_equations(subeqs, [eq.lhs for eq in subeqs]) @set! sys.eqs = neweqs @set! sys.observed = obs - unknowns = Any[v - for (i, v) in enumerate(fullvars) - if diff_to_var[i] === nothing && ispresent(i)] - if !isempty(extra_vars) - for v in extra_vars - push!(unknowns, old_fullvars[v]) - end - end - @set! sys.unknowns = unknowns @set! sys.substitutions = Substitutions(subeqs, deps) # Only makes sense for time-dependent @@ -629,6 +659,16 @@ function tearing_reassemble(state::TearingState, var_eq_matching, return invalidate_cache!(sys) end +function change_origin(origin, arr) + return origin(arr) +end + +@register_array_symbolic change_origin(origin::Origin, arr::AbstractArray) begin + size = size(arr) + eltype = eltype(arr) + ndims = ndims(arr) +end + function tearing(state::TearingState; kwargs...) state.structure.solvable_graph === nothing && find_solvables!(state; kwargs...) complete!(state.structure) diff --git a/test/structural_transformation/utils.jl b/test/structural_transformation/utils.jl index 8644d96945..c7146bab65 100644 --- a/test/structural_transformation/utils.jl +++ b/test/structural_transformation/utils.jl @@ -40,3 +40,17 @@ end @test ModelingToolkit.𝑠neighbors(g, 1) == [2] @test ModelingToolkit.𝑑neighbors(g, 2) == [1] end + +@testset "array observed used unscalarized in another observed" begin + @variables x(t) y(t)[1:2] z(t)[1:2] + @parameters foo(::AbstractVector)[1:2] + _tmp_fn(x) = 2x + @mtkbuild sys = ODESystem( + [D(x) ~ z[1] + z[2] + foo(z)[1], y[1] ~ 2t, y[2] ~ 3t, z ~ foo(y)], t) + @test length(equations(sys)) == 1 + @test length(observed(sys)) == 6 + @test any(eq -> isequal(eq.lhs, y), observed(sys)) + @test any(eq -> isequal(eq.lhs, z), observed(sys)) + prob = ODEProblem(sys, [x => 1.0], (0.0, 1.0), [foo => _tmp_fn]) + @test_nowarn prob.f(prob.u0, prob.p, 0.0) +end From 6c3576f0f18918fb5643fdedc4d9842eede18bc2 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 18 Oct 2024 20:33:59 +0530 Subject: [PATCH 0241/1337] feat: add simple CSE for array scalarization case --- .../symbolics_tearing.jl | 54 ++++++++++++++++++- test/structural_transformation/utils.jl | 18 ++++++- 2 files changed, 69 insertions(+), 3 deletions(-) diff --git a/src/structural_transformation/symbolics_tearing.jl b/src/structural_transformation/symbolics_tearing.jl index 889b75c611..d7bec888b5 100644 --- a/src/structural_transformation/symbolics_tearing.jl +++ b/src/structural_transformation/symbolics_tearing.jl @@ -584,6 +584,16 @@ function tearing_reassemble(state::TearingState, var_eq_matching, end @set! sys.unknowns = unknowns + # HACK: Since we don't support array equations, any equation of the sort + # `x[1:n] ~ f(...)[1:n]` gets turned into `x[1] ~ f(...)[1], x[2] ~ f(...)[2]`. Repeatedly + # calling `f` gets _very_ expensive. this hack performs a limited form of CSE specifically + # for this case to avoid the unnecessary cost. + # This and the below hack are implemented simultaneously + + # mapping of rhs to temporary CSE variable + # `f(...) => tmpvar` in above example + rhs_to_tempvar = Dict() + # HACK: Add equations for array observed variables. If `p[i] ~ (...)` # are equations, add an equation `p ~ [p[1], p[2], ...]` # allow topsort to reorder them @@ -597,9 +607,42 @@ function tearing_reassemble(state::TearingState, var_eq_matching, arr_obs_occurrences = Dict() # to check if array variables occur in unscalarized form anywhere all_vars = Set() - for eq in obs - vars!(all_vars, eq.rhs) + for (i, eq) in enumerate(obs) lhs = eq.lhs + rhs = eq.rhs + vars!(all_vars, rhs) + + # HACK 1 + if (!ModelingToolkit.isvariable(rhs) || ModelingToolkit.iscalledparameter(rhs)) && + iscall(rhs) && operation(rhs) === getindex && + Symbolics.shape(rhs) != Symbolics.Unknown() + rhs_arr = arguments(rhs)[1] + if !haskey(rhs_to_tempvar, rhs_arr) + tempvar = gensym(Symbol(lhs)) + N = length(rhs_arr) + tempvar = unwrap(Symbolics.variable( + tempvar; T = Symbolics.symtype(rhs_arr))) + tempvar = setmetadata( + tempvar, Symbolics.ArrayShapeCtx, Symbolics.shape(rhs_arr)) + tempeq = tempvar ~ rhs_arr + rhs_to_tempvar[rhs_arr] = tempvar + push!(obs, tempeq) + push!(subeqs, tempeq) + end + + # getindex_wrapper is used because `observed2graph` treats `x` and `x[i]` as different, + # so it doesn't find a dependency between this equation and `tempvar ~ rhs_arr` + # which fails the topological sort + neweq = lhs ~ getindex_wrapper( + rhs_to_tempvar[rhs_arr], Tuple(arguments(rhs)[2:end])) + obs[i] = neweq + subeqi = findfirst(isequal(eq), subeqs) + if subeqi !== nothing + subeqs[subeqi] = neweq + end + end + # end HACK 1 + iscall(lhs) || continue operation(lhs) === getindex || continue Symbolics.shape(lhs) != Symbolics.Unknown() || continue @@ -640,6 +683,7 @@ function tearing_reassemble(state::TearingState, var_eq_matching, end append!(obs, obs_arr_eqs) append!(subeqs, obs_arr_eqs) + # need to re-sort subeqs subeqs = ModelingToolkit.topsort_equations(subeqs, [eq.lhs for eq in subeqs]) @@ -659,6 +703,12 @@ function tearing_reassemble(state::TearingState, var_eq_matching, return invalidate_cache!(sys) end +# PART OF HACK 1 +getindex_wrapper(x, i) = x[i...] + +@register_symbolic getindex_wrapper(x::AbstractArray, i::Tuple{Vararg{Int}}) + +# PART OF HACK 2 function change_origin(origin, arr) return origin(arr) end diff --git a/test/structural_transformation/utils.jl b/test/structural_transformation/utils.jl index c7146bab65..dded8333f2 100644 --- a/test/structural_transformation/utils.jl +++ b/test/structural_transformation/utils.jl @@ -48,9 +48,25 @@ end @mtkbuild sys = ODESystem( [D(x) ~ z[1] + z[2] + foo(z)[1], y[1] ~ 2t, y[2] ~ 3t, z ~ foo(y)], t) @test length(equations(sys)) == 1 - @test length(observed(sys)) == 6 + @test length(observed(sys)) == 7 @test any(eq -> isequal(eq.lhs, y), observed(sys)) @test any(eq -> isequal(eq.lhs, z), observed(sys)) prob = ODEProblem(sys, [x => 1.0], (0.0, 1.0), [foo => _tmp_fn]) @test_nowarn prob.f(prob.u0, prob.p, 0.0) end + +@testset "scalarized array observed calling same function multiple times" begin + @variables x(t) y(t)[1:2] + @parameters foo(::Real)[1:2] + val = Ref(0) + function _tmp_fn2(x) + val[] += 1 + return [x, 2x] + end + @mtkbuild sys = ODESystem([D(x) ~ y[1] + y[2], y ~ foo(x)], t) + @test length(equations(sys)) == 1 + @test length(observed(sys)) == 3 + prob = ODEProblem(sys, [x => 1.0], (0.0, 1.0), [foo => _tmp_fn2]) + @test_nowarn prob.f(prob.u0, prob.p, 0.0) + @test val[] == 1 +end From c023e7ebba82d5d83b7f6448ad04a67f37fccefa Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 23 Oct 2024 20:17:18 +0530 Subject: [PATCH 0242/1337] fix: undo the hack in `generate_initializesystem` --- src/systems/nonlinear/initializesystem.jl | 45 ++++++++++++++++++++--- test/structural_transformation/utils.jl | 16 ++++++++ 2 files changed, 56 insertions(+), 5 deletions(-) diff --git a/src/systems/nonlinear/initializesystem.jl b/src/systems/nonlinear/initializesystem.jl index becace8ec5..0bdb770c54 100644 --- a/src/systems/nonlinear/initializesystem.jl +++ b/src/systems/nonlinear/initializesystem.jl @@ -12,7 +12,9 @@ function generate_initializesystem(sys::ODESystem; algebraic_only = false, check_units = true, check_defguess = false, name = nameof(sys), kwargs...) - vars = unique([unknowns(sys); getfield.((observed(sys)), :lhs)]) + trueobs = unhack_observed(observed(sys)) + @show trueobs + vars = unique([unknowns(sys); getfield.(trueobs, :lhs)]) vars_set = Set(vars) # for efficient in-lookup eqs = equations(sys) @@ -24,7 +26,7 @@ function generate_initializesystem(sys::ODESystem; D = Differential(get_iv(sys)) diffmap = merge( Dict(eq.lhs => eq.rhs for eq in eqs_diff), - Dict(D(eq.lhs) => D(eq.rhs) for eq in observed(sys)) + Dict(D(eq.lhs) => D(eq.rhs) for eq in trueobs) ) # 1) process dummy derivatives and u0map into initialization system @@ -166,15 +168,14 @@ function generate_initializesystem(sys::ODESystem; ) # 7) use observed equations for guesses of observed variables if not provided - obseqs = observed(sys) - for eq in obseqs + for eq in trueobs haskey(defs, eq.lhs) && continue any(x -> isequal(default_toterm(x), eq.lhs), keys(defs)) && continue defs[eq.lhs] = eq.rhs end - eqs_ics = Symbolics.substitute.([eqs_ics; obseqs], (paramsubs,)) + eqs_ics = Symbolics.substitute.([eqs_ics; trueobs], (paramsubs,)) vars = [vars; collect(values(paramsubs))] for k in keys(defs) defs[k] = substitute(defs[k], paramsubs) @@ -324,3 +325,37 @@ function SciMLBase.remake_initializeprob(sys::ODESystem, odefn, u0, t0, p) return nothing, nothing, nothing, nothing end end + +""" +Counteracts the CSE/array variable hacks in `symbolics_tearing.jl` so it works with +initialization. +""" +function unhack_observed(eqs::Vector{Equation}) + subs = Dict() + tempvars = Set() + rm_idxs = Int[] + for (i, eq) in enumerate(eqs) + iscall(eq.rhs) || continue + if operation(eq.rhs) == StructuralTransformations.change_origin + push!(rm_idxs, i) + continue + end + if operation(eq.rhs) == StructuralTransformations.getindex_wrapper + var, idxs = arguments(eq.rhs) + subs[eq.rhs] = var[idxs...] + push!(tempvars, var) + end + end + + for (i, eq) in enumerate(eqs) + if eq.lhs in tempvars + subs[eq.lhs] = eq.rhs + push!(rm_idxs, i) + end + end + + eqs = eqs[setdiff(eachindex(eqs), rm_idxs)] + return map(eqs) do eq + fixpoint_sub(eq.lhs, subs) ~ fixpoint_sub(eq.rhs, subs) + end +end diff --git a/test/structural_transformation/utils.jl b/test/structural_transformation/utils.jl index dded8333f2..ea3552ff0d 100644 --- a/test/structural_transformation/utils.jl +++ b/test/structural_transformation/utils.jl @@ -53,6 +53,14 @@ end @test any(eq -> isequal(eq.lhs, z), observed(sys)) prob = ODEProblem(sys, [x => 1.0], (0.0, 1.0), [foo => _tmp_fn]) @test_nowarn prob.f(prob.u0, prob.p, 0.0) + + isys = ModelingToolkit.generate_initializesystem(sys) + @test length(unknowns(isys)) == 5 + @test length(equations(isys)) == 4 + @test !any(equations(isys)) do eq + iscall(eq.rhs) && operation(eq.rhs) in [StructuralTransformations.getindex_wrapper, + StructuralTransformations.change_origin] + end end @testset "scalarized array observed calling same function multiple times" begin @@ -69,4 +77,12 @@ end prob = ODEProblem(sys, [x => 1.0], (0.0, 1.0), [foo => _tmp_fn2]) @test_nowarn prob.f(prob.u0, prob.p, 0.0) @test val[] == 1 + + isys = ModelingToolkit.generate_initializesystem(sys) + @test length(unknowns(isys)) == 3 + @test length(equations(isys)) == 2 + @test !any(equations(isys)) do eq + iscall(eq.rhs) && operation(eq.rhs) in [StructuralTransformations.getindex_wrapper, + StructuralTransformations.change_origin] + end end From 79a7fc933b858e51412abc486ce1018fb8484638 Mon Sep 17 00:00:00 2001 From: Christopher Rackauckas Date: Thu, 24 Oct 2024 04:51:51 +0000 Subject: [PATCH 0243/1337] Update src/systems/nonlinear/initializesystem.jl --- src/systems/nonlinear/initializesystem.jl | 1 - 1 file changed, 1 deletion(-) diff --git a/src/systems/nonlinear/initializesystem.jl b/src/systems/nonlinear/initializesystem.jl index 0bdb770c54..6c7457e49b 100644 --- a/src/systems/nonlinear/initializesystem.jl +++ b/src/systems/nonlinear/initializesystem.jl @@ -13,7 +13,6 @@ function generate_initializesystem(sys::ODESystem; check_units = true, check_defguess = false, name = nameof(sys), kwargs...) trueobs = unhack_observed(observed(sys)) - @show trueobs vars = unique([unknowns(sys); getfield.(trueobs, :lhs)]) vars_set = Set(vars) # for efficient in-lookup From d2fe0eb1a482ec7dc28b39bc21c7367bc1b60f8e Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 24 Oct 2024 11:43:38 +0530 Subject: [PATCH 0244/1337] feat: allow CSE and array hacks to be disabled --- .../symbolics_tearing.jl | 79 +++++++++++-------- test/structural_transformation/utils.jl | 44 +++++++++++ 2 files changed, 90 insertions(+), 33 deletions(-) diff --git a/src/structural_transformation/symbolics_tearing.jl b/src/structural_transformation/symbolics_tearing.jl index d7bec888b5..cabf0415ea 100644 --- a/src/structural_transformation/symbolics_tearing.jl +++ b/src/structural_transformation/symbolics_tearing.jl @@ -230,7 +230,7 @@ end =# function tearing_reassemble(state::TearingState, var_eq_matching, - full_var_eq_matching = nothing; simplify = false, mm = nothing) + full_var_eq_matching = nothing; simplify = false, mm = nothing, cse_hack = true, array_hack = true) @unpack fullvars, sys, structure = state @unpack solvable_graph, var_to_diff, eq_to_diff, graph = structure extra_vars = Int[] @@ -584,24 +584,48 @@ function tearing_reassemble(state::TearingState, var_eq_matching, end @set! sys.unknowns = unknowns - # HACK: Since we don't support array equations, any equation of the sort - # `x[1:n] ~ f(...)[1:n]` gets turned into `x[1] ~ f(...)[1], x[2] ~ f(...)[2]`. Repeatedly - # calling `f` gets _very_ expensive. this hack performs a limited form of CSE specifically - # for this case to avoid the unnecessary cost. - # This and the below hack are implemented simultaneously + obs, subeqs = cse_and_array_hacks( + obs, subeqs, unknowns, neweqs; cse = cse_hack, array = array_hack) + @set! sys.eqs = neweqs + @set! sys.observed = obs + + @set! sys.substitutions = Substitutions(subeqs, deps) + + # Only makes sense for time-dependent + # TODO: generalize to SDE + if sys isa ODESystem + @set! sys.schedule = Schedule(var_eq_matching, dummy_sub) + end + sys = schedule(sys) + @set! state.sys = sys + @set! sys.tearing_state = state + return invalidate_cache!(sys) +end + +""" +# HACK 1 + +Since we don't support array equations, any equation of the sort `x[1:n] ~ f(...)[1:n]` +gets turned into `x[1] ~ f(...)[1], x[2] ~ f(...)[2]`. Repeatedly calling `f` gets +_very_ expensive. this hack performs a limited form of CSE specifically for this case to +avoid the unnecessary cost. This and the below hack are implemented simultaneously + +# HACK 2 + +Add equations for array observed variables. If `p[i] ~ (...)` are equations, add an +equation `p ~ [p[1], p[2], ...]` allow topsort to reorder them only add the new equation +if all `p[i]` are present and the unscalarized form is used in any equation (observed or +not) we first count the number of times the scalarized form of each observed variable +occurs in observed equations (and unknowns if it's split). +""" +function cse_and_array_hacks(obs, subeqs, unknowns, neweqs; cse = true, array = true) + # HACK 1 # mapping of rhs to temporary CSE variable # `f(...) => tmpvar` in above example rhs_to_tempvar = Dict() - # HACK: Add equations for array observed variables. If `p[i] ~ (...)` - # are equations, add an equation `p ~ [p[1], p[2], ...]` - # allow topsort to reorder them - # only add the new equation if all `p[i]` are present and the unscalarized - # form is used in any equation (observed or not) - # we first count the number of times the scalarized form of each observed - # variable occurs in observed equations (and unknowns if it's split). - + # HACK 2 # map of array observed variable (unscalarized) to number of its # scalarized terms that appear in observed equations arr_obs_occurrences = Dict() @@ -613,7 +637,8 @@ function tearing_reassemble(state::TearingState, var_eq_matching, vars!(all_vars, rhs) # HACK 1 - if (!ModelingToolkit.isvariable(rhs) || ModelingToolkit.iscalledparameter(rhs)) && + if cse && + (!ModelingToolkit.isvariable(rhs) || ModelingToolkit.iscalledparameter(rhs)) && iscall(rhs) && operation(rhs) === getindex && Symbolics.shape(rhs) != Symbolics.Unknown() rhs_arr = arguments(rhs)[1] @@ -643,6 +668,7 @@ function tearing_reassemble(state::TearingState, var_eq_matching, end # end HACK 1 + array || continue iscall(lhs) || continue operation(lhs) === getindex || continue Symbolics.shape(lhs) != Symbolics.Unknown() || continue @@ -687,20 +713,7 @@ function tearing_reassemble(state::TearingState, var_eq_matching, # need to re-sort subeqs subeqs = ModelingToolkit.topsort_equations(subeqs, [eq.lhs for eq in subeqs]) - @set! sys.eqs = neweqs - @set! sys.observed = obs - - @set! sys.substitutions = Substitutions(subeqs, deps) - - # Only makes sense for time-dependent - # TODO: generalize to SDE - if sys isa ODESystem - @set! sys.schedule = Schedule(var_eq_matching, dummy_sub) - end - sys = schedule(sys) - @set! state.sys = sys - @set! sys.tearing_state = state - return invalidate_cache!(sys) + return obs, subeqs end # PART OF HACK 1 @@ -733,10 +746,10 @@ new residual equations after tearing. End users are encouraged to call [`structu instead, which calls this function internally. """ function tearing(sys::AbstractSystem, state = TearingState(sys); mm = nothing, - simplify = false, kwargs...) + simplify = false, cse_hack = true, array_hack = true, kwargs...) var_eq_matching, full_var_eq_matching = tearing(state) invalidate_cache!(tearing_reassemble( - state, var_eq_matching, full_var_eq_matching; mm, simplify)) + state, var_eq_matching, full_var_eq_matching; mm, simplify, cse_hack, array_hack)) end """ @@ -758,7 +771,7 @@ Perform index reduction and use the dummy derivative technique to ensure that the system is balanced. """ function dummy_derivative(sys, state = TearingState(sys); simplify = false, - mm = nothing, kwargs...) + mm = nothing, cse_hack = true, array_hack = true, kwargs...) jac = let state = state (eqs, vars) -> begin symeqs = EquationsView(state)[eqs] @@ -782,5 +795,5 @@ function dummy_derivative(sys, state = TearingState(sys); simplify = false, end var_eq_matching = dummy_derivative_graph!(state, jac; state_priority, kwargs...) - tearing_reassemble(state, var_eq_matching; simplify, mm) + tearing_reassemble(state, var_eq_matching; simplify, mm, cse_hack, array_hack) end diff --git a/test/structural_transformation/utils.jl b/test/structural_transformation/utils.jl index ea3552ff0d..04600a7a6b 100644 --- a/test/structural_transformation/utils.jl +++ b/test/structural_transformation/utils.jl @@ -86,3 +86,47 @@ end StructuralTransformations.change_origin] end end + +@testset "array and cse hacks can be disabled" begin + @testset "fully_determined = true" begin + @variables x(t) y(t)[1:2] z(t)[1:2] + @parameters foo(::AbstractVector)[1:2] + _tmp_fn(x) = 2x + @named sys = ODESystem( + [D(x) ~ z[1] + z[2] + foo(z)[1], y[1] ~ 2t, y[2] ~ 3t, z ~ foo(y)], t) + + sys1 = structural_simplify(sys; cse_hack = false) + @test length(observed(sys1)) == 6 + @test !any(observed(sys1)) do eq + iscall(eq.rhs) && + operation(eq.rhs) == StructuralTransformations.getindex_wrapper + end + + sys2 = structural_simplify(sys; array_hack = false) + @test length(observed(sys2)) == 5 + @test !any(observed(sys2)) do eq + iscall(eq.rhs) && operation(eq.rhs) == StructuralTransformations.change_origin + end + end + + @testset "fully_determined = false" begin + @variables x(t) y(t)[1:2] z(t)[1:2] w(t) + @parameters foo(::AbstractVector)[1:2] + _tmp_fn(x) = 2x + @named sys = ODESystem( + [D(x) ~ z[1] + z[2] + foo(z)[1] + w, y[1] ~ 2t, y[2] ~ 3t, z ~ foo(y)], t) + + sys1 = structural_simplify(sys; cse_hack = false, fully_determined = false) + @test length(observed(sys1)) == 6 + @test !any(observed(sys1)) do eq + iscall(eq.rhs) && + operation(eq.rhs) == StructuralTransformations.getindex_wrapper + end + + sys2 = structural_simplify(sys; array_hack = false, fully_determined = false) + @test length(observed(sys2)) == 5 + @test !any(observed(sys2)) do eq + iscall(eq.rhs) && operation(eq.rhs) == StructuralTransformations.change_origin + end + end +end From 877cf03449738dc68f631c664a37e2b67bc49dca Mon Sep 17 00:00:00 2001 From: Christopher Rackauckas Date: Thu, 24 Oct 2024 07:34:08 +0000 Subject: [PATCH 0245/1337] Update test/extensions/bifurcationkit.jl --- test/extensions/bifurcationkit.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/extensions/bifurcationkit.jl b/test/extensions/bifurcationkit.jl index c2dc5685aa..b8c95dca99 100644 --- a/test/extensions/bifurcationkit.jl +++ b/test/extensions/bifurcationkit.jl @@ -37,7 +37,7 @@ let [1.0, 1.0], [-1.0, 1.0], (BifurcationKit.@optic _[1]); - record_from_solution = (x, p) -> x[1]) + record_from_solution = (x, p; k...) -> x[1]) bif_dia_BK = bifurcationdiagram(bprob_BK, PALC(), 2, From 31d8273c60496f5f1b28ecf5b91455d911f4226e Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 24 Oct 2024 13:19:29 +0530 Subject: [PATCH 0246/1337] fix: fix in-place observed function generation --- src/systems/diffeqs/odesystem.jl | 4 ++-- test/odesystem.jl | 12 ++++++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/systems/diffeqs/odesystem.jl b/src/systems/diffeqs/odesystem.jl index ec2c5f8157..6788c64c8e 100644 --- a/src/systems/diffeqs/odesystem.jl +++ b/src/systems/diffeqs/odesystem.jl @@ -576,8 +576,8 @@ function build_explicit_observed_function(sys, ts; iip_fn = build_function(ts, args...; postprocess_fbody = pre, - wrap_code = array_wrapper .∘ wrap_assignments(isscalar, obsexprs) .∘ - mtkparams_wrapper, + wrap_code = mtkparams_wrapper .∘ array_wrapper .∘ + wrap_assignments(isscalar, obsexprs), expression = Val{true})[2] if !expression iip_fn = eval_or_rgf(iip_fn; eval_expression, eval_module) diff --git a/test/odesystem.jl b/test/odesystem.jl index 9446d105e0..27ceafd210 100644 --- a/test/odesystem.jl +++ b/test/odesystem.jl @@ -1447,3 +1447,15 @@ end @parameters p @test_nowarn ODESystem(Equation[], t; parameter_dependencies = [p ~ 1.0], name = :a) end + +@testset "Inplace observed" begin + @variables x(t) + @parameters p[1:2] q + @mtkbuild sys = ODESystem(D(x) ~ sum(p) * x + q * t, t) + prob = ODEProblem(sys, [x => 1.0], (0.0, 1.0), [p => ones(2), q => 2]) + obsfn = ModelingToolkit.build_explicit_observed_function( + sys, [p..., q], return_inplace = true)[2] + buf = zeros(3) + obsfn(buf, prob.u0, prob.p, 0.0) + @test buf ≈ [1.0, 1.0, 2.0] +end From 21d1ce7e3707c890633d363bba21301d3a69bdbd Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 24 Oct 2024 15:05:28 +0530 Subject: [PATCH 0247/1337] fix: construct `initializeprob` if initial value is symbolic --- src/systems/problem_utils.jl | 7 ++++++- test/initializationsystem.jl | 16 ++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/systems/problem_utils.jl b/src/systems/problem_utils.jl index e530e62eed..cc2e959ccd 100644 --- a/src/systems/problem_utils.jl +++ b/src/systems/problem_utils.jl @@ -433,9 +433,14 @@ function process_SciMLProblem( solvablepars = [p for p in parameters(sys) if is_parameter_solvable(p, pmap, defs, guesses)] + has_dependent_unknowns = any(unknowns(sys)) do sym + val = get(op, sym, nothing) + val === nothing && return false + return symbolic_type(val) != NotSymbolic() || is_array_of_symbolics(val) + end if build_initializeprob && (((implicit_dae || has_observed_u0s || !isempty(missing_unknowns) || - !isempty(solvablepars)) && + !isempty(solvablepars) || has_dependent_unknowns) && get_tearing_state(sys) !== nothing) || !isempty(initialization_equations(sys))) && t !== nothing initializeprob = ModelingToolkit.InitializationProblem( diff --git a/test/initializationsystem.jl b/test/initializationsystem.jl index de48821cff..3eddba2b78 100644 --- a/test/initializationsystem.jl +++ b/test/initializationsystem.jl @@ -844,3 +844,19 @@ end isys = ModelingToolkit.generate_initializesystem(sys) @test isequal(defaults(isys)[y], 2x + 1) end + +@testset "Create initializeprob when unknown has dependent value" begin + @variables x(t) y(t) + @mtkbuild sys = ODESystem([D(x) ~ x, D(y) ~ t * y], t; defaults = [x => 2y]) + prob = ODEProblem(sys, [y => 1.0], (0.0, 1.0)) + @test prob.f.initializeprob !== nothing + integ = init(prob) + @test integ[x] ≈ 2.0 + + @variables x(t)[1:2] y(t) + @mtkbuild sys = ODESystem([D(x) ~ x, D(y) ~ t], t; defaults = [x => [y, 3.0]]) + prob = ODEProblem(sys, [y => 1.0], (0.0, 1.0)) + @test prob.f.initializeprob !== nothing + integ = init(prob) + @test integ[x] ≈ [1.0, 3.0] +end From 6081a502fdd102d311ba296a94c4963ea1a61c65 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 24 Oct 2024 15:27:58 +0530 Subject: [PATCH 0248/1337] fix: recursively unwrap arrays of symbolics in `process_SciMLProblem` --- src/systems/problem_utils.jl | 21 +++++++++++++++++++-- test/initial_values.jl | 9 +++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/src/systems/problem_utils.jl b/src/systems/problem_utils.jl index cc2e959ccd..ce46f2762a 100644 --- a/src/systems/problem_utils.jl +++ b/src/systems/problem_utils.jl @@ -171,7 +171,24 @@ function to_varmap(vals, varlist::Vector) check_eqs_u0(varlist, varlist, vals) vals = vec(varlist) .=> vec(vals) end - return anydict(unwrap(k) => unwrap(v) for (k, v) in anydict(vals)) + return recursive_unwrap(anydict(vals)) +end + +""" + $(TYPEDSIGNATURES) + +Recursively call `Symbolics.unwrap` on `x`. Useful when `x` is an array of (potentially) +symbolic values, all of which need to be unwrapped. Specializes when `x isa AbstractDict` +to unwrap keys and values, returning an `AnyDict`. +""" +function recursive_unwrap(x::AbstractArray) + symbolic_type(x) == ArraySymbolic() ? unwrap(x) : recursive_unwrap.(x) +end + +recursive_unwrap(x) = unwrap(x) + +function recursive_unwrap(x::AbstractDict) + return anydict(unwrap(k) => recursive_unwrap(v) for (k, v) in x) end """ @@ -410,7 +427,7 @@ function process_SciMLProblem( u0map = to_varmap(u0map, dvs) _pmap = pmap pmap = to_varmap(pmap, ps) - defs = add_toterms(defaults(sys)) + defs = add_toterms(recursive_unwrap(defaults(sys))) cmap, cs = get_cmap(sys) kwargs = NamedTuple(kwargs) diff --git a/test/initial_values.jl b/test/initial_values.jl index 2ff1b0c2a3..12fb7633e9 100644 --- a/test/initial_values.jl +++ b/test/initial_values.jl @@ -119,3 +119,12 @@ end prob = ODEProblem(sys, [], (1.0, 2.0), []) @test prob[x] == 1.0 @test prob.ps[p] == 2.0 + +@testset "Array of symbolics is unwrapped" begin + @variables x(t)[1:2] y(t) + @mtkbuild sys = ODESystem([D(x) ~ x, D(y) ~ t], t; defaults = [x => [y, 3.0]]) + prob = ODEProblem(sys, [y => 1.0], (0.0, 1.0)) + @test eltype(prob.u0) <: Float64 + prob = ODEProblem(sys, [x => [y, 4.0], y => 2.0], (0.0, 1.0)) + @test eltype(prob.u0) <: Float64 +end From 2184598d1d88f0a3f1b1f760d4040d5651f2e5b3 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 24 Oct 2024 15:42:51 +0530 Subject: [PATCH 0249/1337] fix: handle all parameter values from defaults in `split = false` systems --- src/systems/problem_utils.jl | 2 +- test/initial_values.jl | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/systems/problem_utils.jl b/src/systems/problem_utils.jl index ce46f2762a..daf1c164d8 100644 --- a/src/systems/problem_utils.jl +++ b/src/systems/problem_utils.jl @@ -279,7 +279,7 @@ function better_varmap_to_vars(varmap::AbstractDict, vars::Vector; end vals = map(x -> varmap[x], vars) - if container_type <: Union{AbstractDict, Tuple, Nothing} + if container_type <: Union{AbstractDict, Tuple, Nothing, SciMLBase.NullParameters} container_type = Array end diff --git a/test/initial_values.jl b/test/initial_values.jl index 12fb7633e9..9911bab7f4 100644 --- a/test/initial_values.jl +++ b/test/initial_values.jl @@ -128,3 +128,11 @@ prob = ODEProblem(sys, [], (1.0, 2.0), []) prob = ODEProblem(sys, [x => [y, 4.0], y => 2.0], (0.0, 1.0)) @test eltype(prob.u0) <: Float64 end + +@testset "split=false systems with all parameter defaults" begin + @variables x(t) = 1.0 + @parameters p=1.0 q=2.0 r=3.0 + @mtkbuild sys=ODESystem(D(x) ~ p * x + q * t + r, t) split=false + prob = @test_nowarn ODEProblem(sys, [], (0.0, 1.0)) + @test prob.p isa Vector{Float64} +end From 4b1fa7a30dc5ff9b6f11042ac8eadd038fc26e9a Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 24 Oct 2024 15:48:15 +0530 Subject: [PATCH 0250/1337] docs: bump docs compat for `BifurcationKit` --- docs/Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Project.toml b/docs/Project.toml index 0aefe3d701..16fc8b4b07 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -23,7 +23,7 @@ Unitful = "1986cc42-f94f-5a68-af5c-568840ba703d" [compat] BenchmarkTools = "1.3" -BifurcationKit = "0.3" +BifurcationKit = "0.4" DataInterpolations = "6.5" DifferentialEquations = "7.6" Distributions = "0.25" From 10253638d71ad9101f113b0dcaf1828e43615541 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 24 Oct 2024 20:16:14 +0530 Subject: [PATCH 0251/1337] feat: extend CSE hack to non-observed equations --- .../symbolics_tearing.jl | 45 ++++++++++++++++--- src/systems/nonlinear/initializesystem.jl | 24 +++++++--- test/structural_transformation/utils.jl | 22 +++++++++ 3 files changed, 79 insertions(+), 12 deletions(-) diff --git a/src/structural_transformation/symbolics_tearing.jl b/src/structural_transformation/symbolics_tearing.jl index cabf0415ea..a854acb9b1 100644 --- a/src/structural_transformation/symbolics_tearing.jl +++ b/src/structural_transformation/symbolics_tearing.jl @@ -584,7 +584,7 @@ function tearing_reassemble(state::TearingState, var_eq_matching, end @set! sys.unknowns = unknowns - obs, subeqs = cse_and_array_hacks( + obs, subeqs, deps = cse_and_array_hacks( obs, subeqs, unknowns, neweqs; cse = cse_hack, array = array_hack) @set! sys.eqs = neweqs @@ -637,10 +637,7 @@ function cse_and_array_hacks(obs, subeqs, unknowns, neweqs; cse = true, array = vars!(all_vars, rhs) # HACK 1 - if cse && - (!ModelingToolkit.isvariable(rhs) || ModelingToolkit.iscalledparameter(rhs)) && - iscall(rhs) && operation(rhs) === getindex && - Symbolics.shape(rhs) != Symbolics.Unknown() + if cse && is_getindexed_array(rhs) rhs_arr = arguments(rhs)[1] if !haskey(rhs_to_tempvar, rhs_arr) tempvar = gensym(Symbol(lhs)) @@ -677,6 +674,33 @@ function cse_and_array_hacks(obs, subeqs, unknowns, neweqs; cse = true, array = arr_obs_occurrences[arg1] = cnt + 1 continue end + + # Also do CSE for `equations(sys)` + if cse + for (i, eq) in enumerate(neweqs) + (; lhs, rhs) = eq + is_getindexed_array(rhs) || continue + rhs_arr = arguments(rhs)[1] + if !haskey(rhs_to_tempvar, rhs_arr) + tempvar = gensym(Symbol(lhs)) + N = length(rhs_arr) + tempvar = unwrap(Symbolics.variable( + tempvar; T = Symbolics.symtype(rhs_arr))) + tempvar = setmetadata( + tempvar, Symbolics.ArrayShapeCtx, Symbolics.shape(rhs_arr)) + tempeq = tempvar ~ rhs_arr + rhs_to_tempvar[rhs_arr] = tempvar + push!(obs, tempeq) + push!(subeqs, tempeq) + end + # don't need getindex_wrapper, but do it anyway to know that this + # hack took place + neweq = lhs ~ getindex_wrapper( + rhs_to_tempvar[rhs_arr], Tuple(arguments(rhs)[2:end])) + neweqs[i] = neweq + end + end + # count variables in unknowns if they are scalarized forms of variables # also present as observed. e.g. if `x[1]` is an unknown and `x[2] ~ (..)` # is an observed equation. @@ -713,7 +737,16 @@ function cse_and_array_hacks(obs, subeqs, unknowns, neweqs; cse = true, array = # need to re-sort subeqs subeqs = ModelingToolkit.topsort_equations(subeqs, [eq.lhs for eq in subeqs]) - return obs, subeqs + deps = Vector{Int}[i == 1 ? Int[] : collect(1:(i - 1)) + for i in 1:length(subeqs)] + + return obs, subeqs, deps +end + +function is_getindexed_array(rhs) + (!ModelingToolkit.isvariable(rhs) || ModelingToolkit.iscalledparameter(rhs)) && + iscall(rhs) && operation(rhs) === getindex && + Symbolics.shape(rhs) != Symbolics.Unknown() end # PART OF HACK 1 diff --git a/src/systems/nonlinear/initializesystem.jl b/src/systems/nonlinear/initializesystem.jl index 6c7457e49b..eefe393acc 100644 --- a/src/systems/nonlinear/initializesystem.jl +++ b/src/systems/nonlinear/initializesystem.jl @@ -12,11 +12,10 @@ function generate_initializesystem(sys::ODESystem; algebraic_only = false, check_units = true, check_defguess = false, name = nameof(sys), kwargs...) - trueobs = unhack_observed(observed(sys)) + trueobs, eqs = unhack_observed(observed(sys), equations(sys)) vars = unique([unknowns(sys); getfield.(trueobs, :lhs)]) vars_set = Set(vars) # for efficient in-lookup - eqs = equations(sys) idxs_diff = isdiffeq.(eqs) idxs_alge = .!idxs_diff @@ -329,11 +328,11 @@ end Counteracts the CSE/array variable hacks in `symbolics_tearing.jl` so it works with initialization. """ -function unhack_observed(eqs::Vector{Equation}) +function unhack_observed(obseqs::Vector{Equation}, eqs::Vector{Equation}) subs = Dict() tempvars = Set() rm_idxs = Int[] - for (i, eq) in enumerate(eqs) + for (i, eq) in enumerate(obseqs) iscall(eq.rhs) || continue if operation(eq.rhs) == StructuralTransformations.change_origin push!(rm_idxs, i) @@ -347,14 +346,27 @@ function unhack_observed(eqs::Vector{Equation}) end for (i, eq) in enumerate(eqs) + iscall(eq.rhs) || continue + if operation(eq.rhs) == StructuralTransformations.getindex_wrapper + var, idxs = arguments(eq.rhs) + subs[eq.rhs] = var[idxs...] + push!(tempvars, var) + end + end + + for (i, eq) in enumerate(obseqs) if eq.lhs in tempvars subs[eq.lhs] = eq.rhs push!(rm_idxs, i) end end - eqs = eqs[setdiff(eachindex(eqs), rm_idxs)] - return map(eqs) do eq + obseqs = obseqs[setdiff(eachindex(obseqs), rm_idxs)] + obseqs = map(obseqs) do eq + fixpoint_sub(eq.lhs, subs) ~ fixpoint_sub(eq.rhs, subs) + end + eqs = map(eqs) do eq fixpoint_sub(eq.lhs, subs) ~ fixpoint_sub(eq.rhs, subs) end + return obseqs, eqs end diff --git a/test/structural_transformation/utils.jl b/test/structural_transformation/utils.jl index 04600a7a6b..2704559f72 100644 --- a/test/structural_transformation/utils.jl +++ b/test/structural_transformation/utils.jl @@ -85,6 +85,28 @@ end iscall(eq.rhs) && operation(eq.rhs) in [StructuralTransformations.getindex_wrapper, StructuralTransformations.change_origin] end + + @testset "CSE hack in equations(sys)" begin + val[] = 0 + @variables z(t)[1:2] + @mtkbuild sys = ODESystem( + [D(y) ~ foo(x), D(x) ~ sum(y), zeros(2) ~ foo(prod(z))], t) + @test length(equations(sys)) == 5 + @test length(observed(sys)) == 2 + prob = ODEProblem( + sys, [y => ones(2), z => 2ones(2), x => 3.0], (0.0, 1.0), [foo => _tmp_fn2]) + @test_nowarn prob.f(prob.u0, prob.p, 0.0) + @test val[] == 2 + + isys = ModelingToolkit.generate_initializesystem(sys) + @test length(unknowns(isys)) == 5 + @test length(equations(isys)) == 2 + @test !any(equations(isys)) do eq + iscall(eq.rhs) && + operation(eq.rhs) in [StructuralTransformations.getindex_wrapper, + StructuralTransformations.change_origin] + end + end end @testset "array and cse hacks can be disabled" begin From 93c1e8fadf792f663a052adca749c082df61f25e Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 25 Oct 2024 13:37:59 +0530 Subject: [PATCH 0252/1337] fix: fix `u0_constructor` for `DDEProblem`/`SDDEProblem` --- src/systems/diffeqs/abstractodesystem.jl | 8 ++++++++ test/dde.jl | 10 +++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index 94650d43a8..8d9e0b5381 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -879,6 +879,7 @@ function DiffEqBase.DDEProblem{iip}(sys::AbstractODESystem, u0map = [], check_length = true, eval_expression = false, eval_module = @__MODULE__, + u0_constructor = identity, kwargs...) where {iip} if !iscomplete(sys) error("A completed system is required. Call `complete` or `structural_simplify` on the system before creating a `DDEProblem`") @@ -892,6 +893,9 @@ function DiffEqBase.DDEProblem{iip}(sys::AbstractODESystem, u0map = [], h(p, t) = h_oop(p, t) h(p::MTKParameters, t) = h_oop(p..., t) u0 = h(p, tspan[1]) + if u0 !== nothing + u0 = u0_constructor(u0) + end cbs = process_events(sys; callback, eval_expression, eval_module, kwargs...) kwargs = filter_kwargs(kwargs) @@ -914,6 +918,7 @@ function DiffEqBase.SDDEProblem{iip}(sys::AbstractODESystem, u0map = [], sparsenoise = nothing, eval_expression = false, eval_module = @__MODULE__, + u0_constructor = identity, kwargs...) where {iip} if !iscomplete(sys) error("A completed system is required. Call `complete` or `structural_simplify` on the system before creating a `SDDEProblem`") @@ -929,6 +934,9 @@ function DiffEqBase.SDDEProblem{iip}(sys::AbstractODESystem, u0map = [], h(p::MTKParameters, t) = h_oop(p..., t) h(out, p::MTKParameters, t) = h_iip(out, p..., t) u0 = h(p, tspan[1]) + if u0 !== nothing + u0 = u0_constructor(u0) + end cbs = process_events(sys; callback, eval_expression, eval_module, kwargs...) kwargs = filter_kwargs(kwargs) diff --git a/test/dde.jl b/test/dde.jl index f5e72ee1bb..ec076fba53 100644 --- a/test/dde.jl +++ b/test/dde.jl @@ -1,4 +1,4 @@ -using ModelingToolkit, DelayDiffEq, Test +using ModelingToolkit, DelayDiffEq, StaticArrays, Test using SymbolicIndexingInterface: is_markovian using ModelingToolkit: t_nounits as t, D_nounits as D @@ -89,6 +89,10 @@ eqs = [D(x(t)) ~ a * x(t) + b * x(t - τ) + c + (α * x(t) + γ) * η] prob_mtk = SDDEProblem(sys, [x(t) => 1.0 + t], tspan; constant_lags = (τ,)); @test_nowarn sol_mtk = solve(prob_mtk, RKMil()) +prob_sa = SDDEProblem( + sys, [x(t) => 1.0 + t], tspan; constant_lags = (τ,), u0_constructor = SVector{1}) +@test prob_sa.u0 isa SVector{4, Float64} + @parameters x(..) a function oscillator(; name, k = 1.0, τ = 0.01) @@ -126,6 +130,10 @@ obsfn = ModelingToolkit.build_explicit_observed_function( @test_nowarn sol[[sys.osc1.delx, sys.osc2.delx]] @test sol[sys.osc1.delx] ≈ sol(sol.t .- 0.01; idxs = sys.osc1.x).u +prob_sa = DDEProblem(sys, [], (0.0, 10.0); constant_lags = [sys.osc1.τ, sys.osc2.τ], + u0_constructor = SVector{4}) +@test prob_sa.u0 isa SVector{4, Float64} + @testset "DDE observed with array variables" begin @component function valve(; name) @parameters begin From 8c1793109dc6a7e4e423ddb08abb8a51f9806ca2 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 25 Oct 2024 15:56:31 +0530 Subject: [PATCH 0253/1337] test: fix `u0_constructor` test for `SDDEProblem` --- test/dde.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/dde.jl b/test/dde.jl index ec076fba53..fb099ab8c6 100644 --- a/test/dde.jl +++ b/test/dde.jl @@ -91,7 +91,7 @@ prob_mtk = SDDEProblem(sys, [x(t) => 1.0 + t], tspan; constant_lags = (τ,)); prob_sa = SDDEProblem( sys, [x(t) => 1.0 + t], tspan; constant_lags = (τ,), u0_constructor = SVector{1}) -@test prob_sa.u0 isa SVector{4, Float64} +@test prob_sa.u0 isa SVector{1, Float64} @parameters x(..) a From 7f7f65cc3e9dde8fec99037f4df27221088ed818 Mon Sep 17 00:00:00 2001 From: Benjamin Chung Date: Fri, 25 Oct 2024 10:17:26 -0700 Subject: [PATCH 0254/1337] Clear up some of the documentation language --- docs/src/basics/Events.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/docs/src/basics/Events.md b/docs/src/basics/Events.md index 4c9803ef57..90b1b4036d 100644 --- a/docs/src/basics/Events.md +++ b/docs/src/basics/Events.md @@ -383,14 +383,13 @@ It can be seen that the timeseries for `c` is not saved. The `ImperativeAffect` can be used as an alternative to the aforementioned functional affect form. Note that `ImperativeAffect` is still experimental; to emphasize this, we do not export it and it should be -included as `ModelingToolkit.ImperativeAffect`. It abstracts over how values are written back to the -system, simplifying the definitions and (in the future) allowing assignments back to observed values -by solving the nonlinear reinitialization problem afterwards. +included as `ModelingToolkit.ImperativeAffect`. `ImperativeAffect` aims to simplify the manipulation of +system state. We will use two examples to describe `ImperativeAffect`: a simple heater and a quadrature encoder. These examples will also demonstrate advanced usage of `ModelingToolkit.SymbolicContinuousCallback`, -the low-level interface that the aforementioned tuple form converts into and allows control over the -exact SciMLCallbacks event that is generated for a continuous event. +the low-level interface of the tuple form converts into that allows control over the SciMLBase-level +event that is generated for a continuous event. ### [Heater](@id heater_events) From e750219fcb2d746b5b3bf2c52bcf22c19c48955c Mon Sep 17 00:00:00 2001 From: Benjamin Chung Date: Fri, 25 Oct 2024 10:49:42 -0700 Subject: [PATCH 0255/1337] Format --- docs/src/basics/Events.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/src/basics/Events.md b/docs/src/basics/Events.md index 90b1b4036d..23e1e6d7d1 100644 --- a/docs/src/basics/Events.md +++ b/docs/src/basics/Events.md @@ -383,12 +383,12 @@ It can be seen that the timeseries for `c` is not saved. The `ImperativeAffect` can be used as an alternative to the aforementioned functional affect form. Note that `ImperativeAffect` is still experimental; to emphasize this, we do not export it and it should be -included as `ModelingToolkit.ImperativeAffect`. `ImperativeAffect` aims to simplify the manipulation of +included as `ModelingToolkit.ImperativeAffect`. `ImperativeAffect` aims to simplify the manipulation of system state. We will use two examples to describe `ImperativeAffect`: a simple heater and a quadrature encoder. These examples will also demonstrate advanced usage of `ModelingToolkit.SymbolicContinuousCallback`, -the low-level interface of the tuple form converts into that allows control over the SciMLBase-level +the low-level interface of the tuple form converts into that allows control over the SciMLBase-level event that is generated for a continuous event. ### [Heater](@id heater_events) From 0af572f64648b3885f5491fa6a66050586e2a002 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Sat, 26 Oct 2024 13:56:14 +0530 Subject: [PATCH 0256/1337] build: bump minor version --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index fbf612261d..9dfd9ed0d3 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "ModelingToolkit" uuid = "961ee093-0014-501f-94e3-6117800e7a78" authors = ["Yingbo Ma ", "Chris Rackauckas and contributors"] -version = "9.47.0" +version = "9.48.0" [deps] AbstractTrees = "1520ce14-60c1-5f80-bbc7-55ef81b5835c" From d95a3f7252b8d250b053540bee61a2c3eb5a948e Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 25 Oct 2024 21:48:36 +0530 Subject: [PATCH 0257/1337] feat: support rational functions in `HomotopyContinuationProblem` --- ext/MTKHomotopyContinuationExt.jl | 100 ++++++++++++++++++++--- src/systems/nonlinear/nonlinearsystem.jl | 8 +- test/extensions/homotopy_continuation.jl | 44 +++++++++- 3 files changed, 136 insertions(+), 16 deletions(-) diff --git a/ext/MTKHomotopyContinuationExt.jl b/ext/MTKHomotopyContinuationExt.jl index 81d9a840c9..e4a9243ff9 100644 --- a/ext/MTKHomotopyContinuationExt.jl +++ b/ext/MTKHomotopyContinuationExt.jl @@ -2,7 +2,7 @@ module MTKHomotopyContinuationExt using ModelingToolkit using ModelingToolkit.SciMLBase -using ModelingToolkit.Symbolics: unwrap, symtype +using ModelingToolkit.Symbolics: unwrap, symtype, BasicSymbolic, simplify_fractions using ModelingToolkit.SymbolicIndexingInterface using ModelingToolkit.DocStringExtensions using HomotopyContinuation @@ -27,7 +27,7 @@ function is_polynomial(x, wrt) contains_variable(x, wrt) || return true any(isequal(x), wrt) && return true - if operation(x) in (*, +, -) + if operation(x) in (*, +, -, /) return all(y -> is_polynomial(y, wrt), arguments(x)) end if operation(x) == (^) @@ -57,6 +57,57 @@ end """ $(TYPEDSIGNATURES) +Given a `x`, a polynomial in variables in `wrt` which may contain rational functions, +express `x` as a single rational function with polynomial `num` and denominator `den`. +Return `(num, den)`. +""" +function handle_rational_polynomials(x, wrt) + x = unwrap(x) + symbolic_type(x) == NotSymbolic() && return x, 1 + iscall(x) || return x, 1 + contains_variable(x, wrt) || return x, 1 + any(isequal(x), wrt) && return x, 1 + + # simplify_fractions cancels out some common factors + # and expands (a / b)^c to a^c / b^c, so we only need + # to handle these cases + x = simplify_fractions(x) + op = operation(x) + args = arguments(x) + + if op == / + # numerator and denominator are trivial + num, den = args + # but also search for rational functions in numerator + n, d = handle_rational_polynomials(num, wrt) + num, den = n, den * d + elseif op == + + num = 0 + den = 1 + + # we don't need to do common denominator + # because we don't care about cases where denominator + # is zero. The expression is zero when all the numerators + # are zero. + for arg in args + n, d = handle_rational_polynomials(arg, wrt) + num += n + den *= d + end + else + return x, 1 + end + # if the denominator isn't a polynomial in `wrt`, better to not include it + # to reduce the size of the gcd polynomial + if !contains_variable(den, wrt) + return num / den, 1 + end + return num, den +end + +""" +$(TYPEDSIGNATURES) + Convert `expr` from a symbolics expression to one that uses `HomotopyContinuation.ModelKit`. """ function symbolics_to_hc(expr) @@ -139,51 +190,74 @@ function MTK.HomotopyContinuationProblem( dvs = unknowns(sys) eqs = equations(sys) - for eq in eqs + denoms = [] + eqs2 = map(eqs) do eq if !is_polynomial(eq.lhs, dvs) || !is_polynomial(eq.rhs, dvs) error("Equation $eq is not a polynomial in the unknowns. See warnings for further details.") end + num, den = handle_rational_polynomials(eq.rhs - eq.lhs, dvs) + push!(denoms, den) + return 0 ~ num end - nlfn, u0, p = MTK.process_SciMLProblem(NonlinearFunction{true}, sys, u0map, parammap; + sys2 = MTK.@set sys.eqs = eqs2 + + nlfn, u0, p = MTK.process_SciMLProblem(NonlinearFunction{true}, sys2, u0map, parammap; jac = true, eval_expression, eval_module) + denominator = MTK.build_explicit_observed_function(sys, denoms) + hvars = symbolics_to_hc.(dvs) mtkhsys = MTKHomotopySystem(nlfn.f, p, nlfn.jac, hvars, length(eqs)) obsfn = MTK.ObservedFunctionCache(sys; eval_expression, eval_module) - return MTK.HomotopyContinuationProblem(u0, mtkhsys, sys, obsfn) + return MTK.HomotopyContinuationProblem(u0, mtkhsys, denominator, sys, obsfn) end """ $(TYPEDSIGNATURES) Solve a `HomotopyContinuationProblem`. Ignores the algorithm passed to it, and always -uses `HomotopyContinuation.jl`. All keyword arguments are forwarded to -`HomotopyContinuation.solve`. The original solution as returned by `HomotopyContinuation.jl` -will be available in the `.original` field of the returned `NonlinearSolution`. +uses `HomotopyContinuation.jl`. All keyword arguments except the ones listed below are +forwarded to `HomotopyContinuation.solve`. The original solution as returned by +`HomotopyContinuation.jl` will be available in the `.original` field of the returned +`NonlinearSolution`. All keyword arguments have their default values in HomotopyContinuation.jl, except `show_progress` which defaults to `false`. + +Extra keyword arguments: +- `denominator_abstol`: In case `prob` is solving a rational function, roots which cause + the denominator to be below `denominator_abstol` will be discarded. """ function CommonSolve.solve(prob::MTK.HomotopyContinuationProblem, - alg = nothing; show_progress = false, kwargs...) + alg = nothing; show_progress = false, denominator_abstol = 1e-8, kwargs...) sol = HomotopyContinuation.solve( prob.homotopy_continuation_system; show_progress, kwargs...) realsols = HomotopyContinuation.results(sol; only_real = true) if isempty(realsols) u = state_values(prob) - resid = prob.homotopy_continuation_system(u) retcode = SciMLBase.ReturnCode.ConvergenceFailure else + T = eltype(state_values(prob)) distance, idx = findmin(realsols) do result + if any(<=(denominator_abstol), + prob.denominator(real.(result.solution), parameter_values(prob))) + return T(Inf) + end norm(result.solution - state_values(prob)) end - u = real.(realsols[idx].solution) - resid = prob.homotopy_continuation_system(u) - retcode = SciMLBase.ReturnCode.Success + # all roots cause denominator to be zero + if isinf(distance) + u = state_values(prob) + retcode = SciMLBase.ReturnCode.Infeasible + else + u = real.(realsols[idx].solution) + retcode = SciMLBase.ReturnCode.Success + end end + resid = prob.homotopy_continuation_system(u) return SciMLBase.build_solution( prob, :HomotopyContinuation, u, resid; retcode, original = sol) diff --git a/src/systems/nonlinear/nonlinearsystem.jl b/src/systems/nonlinear/nonlinearsystem.jl index 01a250d46a..bdad40d52f 100644 --- a/src/systems/nonlinear/nonlinearsystem.jl +++ b/src/systems/nonlinear/nonlinearsystem.jl @@ -573,7 +573,7 @@ A type of Nonlinear problem which specializes on polynomial systems and uses HomotopyContinuation.jl to solve the system. Requires importing HomotopyContinuation.jl to create and solve. """ -struct HomotopyContinuationProblem{uType, H, O} <: +struct HomotopyContinuationProblem{uType, H, D, O} <: SciMLBase.AbstractNonlinearProblem{uType, true} """ The initial values of states in the system. If there are multiple real roots of @@ -586,6 +586,12 @@ struct HomotopyContinuationProblem{uType, H, O} <: """ homotopy_continuation_system::H """ + A function with signature `(u, p) -> resid`. In case of rational functions, this + is used to rule out roots of the system which would cause the denominator to be + zero. + """ + denominator::D + """ The `NonlinearSystem` used to create this problem. Used for symbolic indexing. """ sys::NonlinearSystem diff --git a/test/extensions/homotopy_continuation.jl b/test/extensions/homotopy_continuation.jl index ceabb6b6e3..6d7279899f 100644 --- a/test/extensions/homotopy_continuation.jl +++ b/test/extensions/homotopy_continuation.jl @@ -82,7 +82,47 @@ end @mtkbuild sys = NonlinearSystem([x^x - x ~ 0]) @test_warn ["Exponent", "unknowns"] @test_throws "not a polynomial" HomotopyContinuationProblem( sys, []) - @mtkbuild sys = NonlinearSystem([((x^2) / (x + 3))^2 + x ~ 0]) - @test_warn ["Base", "not a polynomial", "Unrecognized operation", "/"] @test_throws "not a polynomial" HomotopyContinuationProblem( + @mtkbuild sys = NonlinearSystem([((x^2) / sin(x))^2 + x ~ 0]) + @test_warn ["Unrecognized", "sin"] @test_throws "not a polynomial" HomotopyContinuationProblem( sys, []) end + +@testset "Rational functions" begin + @variables x=2.0 y=2.0 + @parameters n = 4 + @mtkbuild sys = NonlinearSystem([ + 0 ~ (x^2 - n * x + n) * (x - 1) / (x - 2) / (x - 3) + ]) + prob = HomotopyContinuationProblem(sys, []) + sol = solve(prob; threading = false) + @test sol[x] ≈ 1.0 + p = parameter_values(prob) + for invalid in [2.0, 3.0] + @test prob.denominator([invalid], p)[1] <= 1e-8 + end + + @named sys = NonlinearSystem( + [ + 0 ~ (x - 2) / (x - 4) + ((x - 3) / (y - 7)) / ((x^2 - 4x + y) / (x - 2.5)), + 0 ~ ((y - 3) / (y - 4)) * (n / (y - 5)) + ((x - 1.5) / (x - 5.5))^2 + ], + [x, y], + [n]) + sys = complete(sys) + prob = HomotopyContinuationProblem(sys, []) + sol = solve(prob; threading = false) + disallowed_x = [4, 5.5] + disallowed_y = [7, 5, 4] + @test all(!isapprox(sol[x]; atol = 1e-8), disallowed_x) + @test all(!isapprox(sol[y]; atol = 1e-8), disallowed_y) + @test sol[x^2 - 4x + y] >= 1e-8 + + p = parameter_values(prob) + for val in disallowed_x + @test any(<=(1e-8), prob.denominator([val, 2.0], p)) + end + for val in disallowed_y + @test any(<=(1e-8), prob.denominator([2.0, val], p)) + end + @test prob.denominator([2.0, 4.0], p)[1] <= 1e-8 +end From 7aae63db19b3bf6168d20881875a36fc65ec8bd9 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Sat, 26 Oct 2024 13:58:35 +0530 Subject: [PATCH 0258/1337] build: bump SciMLBase compat --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 9dfd9ed0d3..958772cb05 100644 --- a/Project.toml +++ b/Project.toml @@ -121,7 +121,7 @@ REPL = "1" RecursiveArrayTools = "3.26" Reexport = "0.2, 1" RuntimeGeneratedFunctions = "0.5.9" -SciMLBase = "2.56.1" +SciMLBase = "2.57.1" SciMLStructures = "1.0" Serialization = "1" Setfield = "0.7, 0.8, 1" From 5a94e137d10673b8cec2a35c664892745fe66370 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 25 Oct 2024 15:08:21 +0530 Subject: [PATCH 0259/1337] fix: fix `getproperty`, `hasproperty` for simplified hierarchical systems --- src/systems/abstractsystem.jl | 41 +++++++++++------- src/systems/systems.jl | 2 +- test/components.jl | 36 +++++++++++++++ test/model_parsing.jl | 20 ++++----- test/runtests.jl | 82 +++++++++++++++++------------------ 5 files changed, 113 insertions(+), 68 deletions(-) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index ada8a2a056..f114bc3fa7 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -921,7 +921,7 @@ the global structure of the system. One property to note is that if a system is complete, the system will no longer namespace its subsystems or variables, i.e. `isequal(complete(sys).v.i, v.i)`. """ -function complete(sys::AbstractSystem; split = true) +function complete(sys::AbstractSystem; split = true, flatten = true) if !(sys isa JumpSystem) newunknowns = OrderedSet() newparams = OrderedSet() @@ -931,9 +931,21 @@ function complete(sys::AbstractSystem; split = true) # `GlobalScope`d unknowns will be picked up and added there @set! sys.ps = unique!(vcat(get_ps(sys), collect(newparams))) end + if flatten + if sys isa Union{OptimizationSystem, ConstraintsSystem, JumpSystem} + newsys = sys + else + newsys = expand_connections(sys) + end + newsys = ModelingToolkit.flatten(newsys) + if has_parent(newsys) && get_parent(sys) === nothing + @set! newsys.parent = complete(sys; split = false, flatten = false) + end + sys = newsys + end if split && has_index_cache(sys) @set! sys.index_cache = IndexCache(sys) - all_ps = parameters(sys) + all_ps = get_ps(sys) if !isempty(all_ps) # reorder parameters by portions ps_split = reorder_parameters(sys, all_ps) @@ -1102,6 +1114,9 @@ function Base.propertynames(sys::AbstractSystem; private = false) if private return fieldnames(typeof(sys)) else + if has_parent(sys) && (parent = get_parent(sys); parent !== nothing) + sys = parent + end names = Symbol[] for s in get_systems(sys) push!(names, getname(s)) @@ -1144,20 +1159,6 @@ function getvar(sys::AbstractSystem, name::Symbol; namespace = !iscomplete(sys)) avs = get_var_to_name(sys) v = get(avs, name, nothing) v === nothing || return namespace ? renamespace(sys, v) : v - else - sts = get_unknowns(sys) - i = findfirst(x -> getname(x) == name, sts) - if i !== nothing - return namespace ? renamespace(sys, sts[i]) : sts[i] - end - - if has_ps(sys) - ps = get_ps(sys) - i = findfirst(x -> getname(x) == name, ps) - if i !== nothing - return namespace ? renamespace(sys, ps[i]) : ps[i] - end - end end sts = get_unknowns(sys) @@ -1166,6 +1167,14 @@ function getvar(sys::AbstractSystem, name::Symbol; namespace = !iscomplete(sys)) return namespace ? renamespace(sys, sts[i]) : sts[i] end + if has_ps(sys) + ps = get_ps(sys) + i = findfirst(x -> getname(x) == name, ps) + if i !== nothing + return namespace ? renamespace(sys, ps[i]) : ps[i] + end + end + if has_observed(sys) obs = get_observed(sys) i = findfirst(x -> getname(x.lhs) == name, obs) diff --git a/src/systems/systems.jl b/src/systems/systems.jl index 78cc78989f..848980605f 100644 --- a/src/systems/systems.jl +++ b/src/systems/systems.jl @@ -47,7 +47,7 @@ function structural_simplify( """) end if newsys isa ODESystem || has_parent(newsys) - @set! newsys.parent = complete(sys; split) + @set! newsys.parent = complete(sys; split, flatten = false) end newsys = complete(newsys; split) if has_defaults(newsys) && (defs = get_defaults(newsys)) !== nothing diff --git a/test/components.jl b/test/components.jl index 965578e0c5..3daf1b9100 100644 --- a/test/components.jl +++ b/test/components.jl @@ -314,3 +314,39 @@ sol = solve(prob, Tsit5()) end @test string(Base.doc(Pin2)) == "Hey there, Pin2!\n" end + +@testset "Issue#3016 Hierarchical indexing" begin + @mtkmodel Inner begin + @parameters begin + p + end + end + @mtkmodel Outer begin + @components begin + inner = Inner() + end + @variables begin + x(t) + end + @equations begin + x ~ inner.p + end + end + + @named outer = Outer() + simp = structural_simplify(outer) + + @test sort(propertynames(outer)) == [:inner, :t, :x] + @test propertynames(simp) == propertynames(outer) + @test sort(propertynames(outer.inner)) == [:p, :t] + @test propertynames(simp.inner) == propertynames(outer.inner) + + for sym in (:t, :x) + @test_nowarn getproperty(simp, sym) + @test_nowarn getproperty(outer, sym) + end + @test_nowarn simp.inner.p + @test_nowarn outer.inner.p + @test_throws ArgumentError simp.inner₊p + @test_throws ArgumentError outer.inner₊p +end diff --git a/test/model_parsing.jl b/test/model_parsing.jl index 5126bf5e66..2dcb8e3f11 100644 --- a/test/model_parsing.jl +++ b/test/model_parsing.jl @@ -562,11 +562,11 @@ end end @named if_in_sys = InsideTheBlock() - if_in_sys = complete(if_in_sys) + if_in_sys = complete(if_in_sys; flatten = false) @named elseif_in_sys = InsideTheBlock(flag = 2) - elseif_in_sys = complete(elseif_in_sys) + elseif_in_sys = complete(elseif_in_sys; flatten = false) @named else_in_sys = InsideTheBlock(flag = 3) - else_in_sys = complete(else_in_sys) + else_in_sys = complete(else_in_sys; flatten = false) @test sort(getname.(parameters(if_in_sys))) == [:eq, :if_parameter] @test sort(getname.(parameters(elseif_in_sys))) == [:elseif_parameter, :eq] @@ -653,13 +653,13 @@ end end @named if_out_sys = OutsideTheBlock(condition = 1) - if_out_sys = complete(if_out_sys) + if_out_sys = complete(if_out_sys; flatten = false) @named elseif_out_sys = OutsideTheBlock(condition = 2) - elseif_out_sys = complete(elseif_out_sys) + elseif_out_sys = complete(elseif_out_sys; flatten = false) @named else_out_sys = OutsideTheBlock(condition = 10) - else_out_sys = complete(else_out_sys) + else_out_sys = complete(else_out_sys; flatten = false) @named ternary_out_sys = OutsideTheBlock(condition = 4) - else_out_sys = complete(else_out_sys) + else_out_sys = complete(else_out_sys; flatten = false) @test getname.(parameters(if_out_sys)) == [:if_parameter, :default_parameter] @test getname.(parameters(elseif_out_sys)) == [:elseif_parameter, :default_parameter] @@ -708,10 +708,10 @@ end end @named ternary_true = TernaryBranchingOutsideTheBlock() - ternary_true = complete(ternary_true) + ternary_true = complete(ternary_true; flatten = false) @named ternary_false = TernaryBranchingOutsideTheBlock(condition = false) - ternary_false = complete(ternary_false) + ternary_false = complete(ternary_false; flatten = false) @test getname.(parameters(ternary_true)) == [:ternary_parameter_true] @test getname.(parameters(ternary_false)) == [:ternary_parameter_false] @@ -758,7 +758,7 @@ end end @named component = Component() - component = complete(component) + component = complete(component; flatten = false) @test nameof.(ModelingToolkit.get_systems(component)) == [ :comprehension_1, diff --git a/test/runtests.jl b/test/runtests.jl index c7edbfaaf5..5db3308540 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -19,48 +19,48 @@ end @time begin if GROUP == "All" || GROUP == "InterfaceI" @testset "InterfaceI" begin - @safetestset "Linear Algebra Test" include("linalg.jl") - @safetestset "AbstractSystem Test" include("abstractsystem.jl") - @safetestset "Variable Scope Tests" include("variable_scope.jl") - @safetestset "Symbolic Parameters Test" include("symbolic_parameters.jl") - @safetestset "Parsing Test" include("variable_parsing.jl") - @safetestset "Simplify Test" include("simplify.jl") - @safetestset "Direct Usage Test" include("direct.jl") - @safetestset "System Linearity Test" include("linearity.jl") - @safetestset "Input Output Test" include("input_output_handling.jl") - @safetestset "Clock Test" include("clock.jl") - @safetestset "ODESystem Test" include("odesystem.jl") - @safetestset "Dynamic Quantities Test" include("dq_units.jl") - @safetestset "Unitful Quantities Test" include("units.jl") - @safetestset "Mass Matrix Test" include("mass_matrix.jl") - @safetestset "InitializationSystem Test" include("initializationsystem.jl") - @safetestset "Guess Propagation" include("guess_propagation.jl") - @safetestset "Hierarchical Initialization Equations" include("hierarchical_initialization_eqs.jl") - @safetestset "Reduction Test" include("reduction.jl") - @safetestset "Split Parameters Test" include("split_parameters.jl") - @safetestset "StaticArrays Test" include("static_arrays.jl") - @safetestset "Components Test" include("components.jl") - @safetestset "Model Parsing Test" include("model_parsing.jl") - @safetestset "Error Handling" include("error_handling.jl") - @safetestset "StructuralTransformations" include("structural_transformation/runtests.jl") - @safetestset "State Selection Test" include("state_selection.jl") - @safetestset "Symbolic Event Test" include("symbolic_events.jl") - @safetestset "Stream Connect Test" include("stream_connectors.jl") - @safetestset "Domain Connect Test" include("domain_connectors.jl") - @safetestset "Lowering Integration Test" include("lowering_solving.jl") - @safetestset "Dependency Graph Test" include("dep_graphs.jl") - @safetestset "Function Registration Test" include("function_registration.jl") - @safetestset "Precompiled Modules Test" include("precompile_test.jl") - @safetestset "DAE Jacobians Test" include("dae_jacobian.jl") - @safetestset "Jacobian Sparsity" include("jacobiansparsity.jl") + # @safetestset "Linear Algebra Test" include("linalg.jl") + # @safetestset "AbstractSystem Test" include("abstractsystem.jl") + # @safetestset "Variable Scope Tests" include("variable_scope.jl") + # @safetestset "Symbolic Parameters Test" include("symbolic_parameters.jl") + # @safetestset "Parsing Test" include("variable_parsing.jl") + # @safetestset "Simplify Test" include("simplify.jl") + # @safetestset "Direct Usage Test" include("direct.jl") + # @safetestset "System Linearity Test" include("linearity.jl") + # @safetestset "Input Output Test" include("input_output_handling.jl") + # @safetestset "Clock Test" include("clock.jl") + # @safetestset "ODESystem Test" include("odesystem.jl") + # @safetestset "Dynamic Quantities Test" include("dq_units.jl") + # @safetestset "Unitful Quantities Test" include("units.jl") + # @safetestset "Mass Matrix Test" include("mass_matrix.jl") + # @safetestset "InitializationSystem Test" include("initializationsystem.jl") + # @safetestset "Guess Propagation" include("guess_propagation.jl") + # @safetestset "Hierarchical Initialization Equations" include("hierarchical_initialization_eqs.jl") + # @safetestset "Reduction Test" include("reduction.jl") + # @safetestset "Split Parameters Test" include("split_parameters.jl") + # @safetestset "StaticArrays Test" include("static_arrays.jl") + # @safetestset "Components Test" include("components.jl") + # @safetestset "Model Parsing Test" include("model_parsing.jl") + # @safetestset "Error Handling" include("error_handling.jl") + # @safetestset "StructuralTransformations" include("structural_transformation/runtests.jl") + # @safetestset "State Selection Test" include("state_selection.jl") + # @safetestset "Symbolic Event Test" include("symbolic_events.jl") + # @safetestset "Stream Connect Test" include("stream_connectors.jl") + # @safetestset "Domain Connect Test" include("domain_connectors.jl") + # @safetestset "Lowering Integration Test" include("lowering_solving.jl") + # @safetestset "Dependency Graph Test" include("dep_graphs.jl") + # @safetestset "Function Registration Test" include("function_registration.jl") + # @safetestset "Precompiled Modules Test" include("precompile_test.jl") + # @safetestset "DAE Jacobians Test" include("dae_jacobian.jl") + # @safetestset "Jacobian Sparsity" include("jacobiansparsity.jl") @safetestset "Modelingtoolkitize Test" include("modelingtoolkitize.jl") - @safetestset "FuncAffect Test" include("funcaffect.jl") - @safetestset "Constants Test" include("constants.jl") - @safetestset "Parameter Dependency Test" include("parameter_dependencies.jl") - @safetestset "Generate Custom Function Test" include("generate_custom_function.jl") - @safetestset "Initial Values Test" include("initial_values.jl") - @safetestset "Equation Type Accessors Test" include("equation_type_accessors.jl") - @safetestset "Equations with complex values" include("complex.jl") + # @safetestset "FuncAffect Test" include("funcaffect.jl") + # @safetestset "Constants Test" include("constants.jl") + # @safetestset "Parameter Dependency Test" include("parameter_dependencies.jl") + # @safetestset "Generate Custom Function Test" include("generate_custom_function.jl") + # @safetestset "Initial Values Test" include("initial_values.jl") + # @safetestset "Equation Type Accessors Test" include("equation_type_accessors.jl") + # @safetestset "Equations with complex values" include("complex.jl") end end From f9ff0a20000feb454dc4ea6408254413d6d43b0f Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Sat, 26 Oct 2024 23:31:23 +0530 Subject: [PATCH 0260/1337] fix: add `flatten` for `OptimizationSystem` --- src/systems/optimization/optimizationsystem.jl | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/systems/optimization/optimizationsystem.jl b/src/systems/optimization/optimizationsystem.jl index 271e0073ce..2e796867ae 100644 --- a/src/systems/optimization/optimizationsystem.jl +++ b/src/systems/optimization/optimizationsystem.jl @@ -141,6 +141,22 @@ function OptimizationSystem(op, unknowns, ps; checks = checks) end +function flatten(sys::OptimizationSystem) + systems = get_systems(sys) + isempty(systems) && return sys + + return OptimizationSystem( + objective(sys), + unknowns(sys), + parameters(sys); + observed = observed(sys), + constraints = constraints(sys), + defaults = defaults(sys), + name = nameof(sys), + checks = false + ) +end + function calculate_gradient(sys::OptimizationSystem) expand_derivatives.(gradient(objective(sys), unknowns(sys))) end From d8e47b18880df6f76a42055882290e43d0e0c7ec Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 28 Oct 2024 11:26:20 +0530 Subject: [PATCH 0261/1337] fix: fix flattening in `complete` --- src/systems/abstractsystem.jl | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index f114bc3fa7..216a83f85f 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -932,10 +932,11 @@ function complete(sys::AbstractSystem; split = true, flatten = true) @set! sys.ps = unique!(vcat(get_ps(sys), collect(newparams))) end if flatten - if sys isa Union{OptimizationSystem, ConstraintsSystem, JumpSystem} - newsys = sys - else + if (eqs = equations(sys)) isa Vector && + any(eq -> eq isa Equation && isconnection(eq.lhs), eqs) newsys = expand_connections(sys) + else + newsys = sys end newsys = ModelingToolkit.flatten(newsys) if has_parent(newsys) && get_parent(sys) === nothing From 21efe808cc75970f8ec5b5feca1046644d69aace Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 28 Oct 2024 11:26:25 +0530 Subject: [PATCH 0262/1337] test: uncomment runtests --- test/runtests.jl | 82 ++++++++++++++++++++++++------------------------ 1 file changed, 41 insertions(+), 41 deletions(-) diff --git a/test/runtests.jl b/test/runtests.jl index 5db3308540..c7edbfaaf5 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -19,48 +19,48 @@ end @time begin if GROUP == "All" || GROUP == "InterfaceI" @testset "InterfaceI" begin - # @safetestset "Linear Algebra Test" include("linalg.jl") - # @safetestset "AbstractSystem Test" include("abstractsystem.jl") - # @safetestset "Variable Scope Tests" include("variable_scope.jl") - # @safetestset "Symbolic Parameters Test" include("symbolic_parameters.jl") - # @safetestset "Parsing Test" include("variable_parsing.jl") - # @safetestset "Simplify Test" include("simplify.jl") - # @safetestset "Direct Usage Test" include("direct.jl") - # @safetestset "System Linearity Test" include("linearity.jl") - # @safetestset "Input Output Test" include("input_output_handling.jl") - # @safetestset "Clock Test" include("clock.jl") - # @safetestset "ODESystem Test" include("odesystem.jl") - # @safetestset "Dynamic Quantities Test" include("dq_units.jl") - # @safetestset "Unitful Quantities Test" include("units.jl") - # @safetestset "Mass Matrix Test" include("mass_matrix.jl") - # @safetestset "InitializationSystem Test" include("initializationsystem.jl") - # @safetestset "Guess Propagation" include("guess_propagation.jl") - # @safetestset "Hierarchical Initialization Equations" include("hierarchical_initialization_eqs.jl") - # @safetestset "Reduction Test" include("reduction.jl") - # @safetestset "Split Parameters Test" include("split_parameters.jl") - # @safetestset "StaticArrays Test" include("static_arrays.jl") - # @safetestset "Components Test" include("components.jl") - # @safetestset "Model Parsing Test" include("model_parsing.jl") - # @safetestset "Error Handling" include("error_handling.jl") - # @safetestset "StructuralTransformations" include("structural_transformation/runtests.jl") - # @safetestset "State Selection Test" include("state_selection.jl") - # @safetestset "Symbolic Event Test" include("symbolic_events.jl") - # @safetestset "Stream Connect Test" include("stream_connectors.jl") - # @safetestset "Domain Connect Test" include("domain_connectors.jl") - # @safetestset "Lowering Integration Test" include("lowering_solving.jl") - # @safetestset "Dependency Graph Test" include("dep_graphs.jl") - # @safetestset "Function Registration Test" include("function_registration.jl") - # @safetestset "Precompiled Modules Test" include("precompile_test.jl") - # @safetestset "DAE Jacobians Test" include("dae_jacobian.jl") - # @safetestset "Jacobian Sparsity" include("jacobiansparsity.jl") + @safetestset "Linear Algebra Test" include("linalg.jl") + @safetestset "AbstractSystem Test" include("abstractsystem.jl") + @safetestset "Variable Scope Tests" include("variable_scope.jl") + @safetestset "Symbolic Parameters Test" include("symbolic_parameters.jl") + @safetestset "Parsing Test" include("variable_parsing.jl") + @safetestset "Simplify Test" include("simplify.jl") + @safetestset "Direct Usage Test" include("direct.jl") + @safetestset "System Linearity Test" include("linearity.jl") + @safetestset "Input Output Test" include("input_output_handling.jl") + @safetestset "Clock Test" include("clock.jl") + @safetestset "ODESystem Test" include("odesystem.jl") + @safetestset "Dynamic Quantities Test" include("dq_units.jl") + @safetestset "Unitful Quantities Test" include("units.jl") + @safetestset "Mass Matrix Test" include("mass_matrix.jl") + @safetestset "InitializationSystem Test" include("initializationsystem.jl") + @safetestset "Guess Propagation" include("guess_propagation.jl") + @safetestset "Hierarchical Initialization Equations" include("hierarchical_initialization_eqs.jl") + @safetestset "Reduction Test" include("reduction.jl") + @safetestset "Split Parameters Test" include("split_parameters.jl") + @safetestset "StaticArrays Test" include("static_arrays.jl") + @safetestset "Components Test" include("components.jl") + @safetestset "Model Parsing Test" include("model_parsing.jl") + @safetestset "Error Handling" include("error_handling.jl") + @safetestset "StructuralTransformations" include("structural_transformation/runtests.jl") + @safetestset "State Selection Test" include("state_selection.jl") + @safetestset "Symbolic Event Test" include("symbolic_events.jl") + @safetestset "Stream Connect Test" include("stream_connectors.jl") + @safetestset "Domain Connect Test" include("domain_connectors.jl") + @safetestset "Lowering Integration Test" include("lowering_solving.jl") + @safetestset "Dependency Graph Test" include("dep_graphs.jl") + @safetestset "Function Registration Test" include("function_registration.jl") + @safetestset "Precompiled Modules Test" include("precompile_test.jl") + @safetestset "DAE Jacobians Test" include("dae_jacobian.jl") + @safetestset "Jacobian Sparsity" include("jacobiansparsity.jl") @safetestset "Modelingtoolkitize Test" include("modelingtoolkitize.jl") - # @safetestset "FuncAffect Test" include("funcaffect.jl") - # @safetestset "Constants Test" include("constants.jl") - # @safetestset "Parameter Dependency Test" include("parameter_dependencies.jl") - # @safetestset "Generate Custom Function Test" include("generate_custom_function.jl") - # @safetestset "Initial Values Test" include("initial_values.jl") - # @safetestset "Equation Type Accessors Test" include("equation_type_accessors.jl") - # @safetestset "Equations with complex values" include("complex.jl") + @safetestset "FuncAffect Test" include("funcaffect.jl") + @safetestset "Constants Test" include("constants.jl") + @safetestset "Parameter Dependency Test" include("parameter_dependencies.jl") + @safetestset "Generate Custom Function Test" include("generate_custom_function.jl") + @safetestset "Initial Values Test" include("initial_values.jl") + @safetestset "Equation Type Accessors Test" include("equation_type_accessors.jl") + @safetestset "Equations with complex values" include("complex.jl") end end From 3055c6345878877396a5a8d6a407b4e42c332716 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 28 Oct 2024 11:38:09 +0530 Subject: [PATCH 0263/1337] fix: only evaluate required keys of varmap in `process_SciMLProblem` --- src/systems/problem_utils.jl | 17 ++++++++++------- test/initial_values.jl | 13 +++++++++++++ 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/src/systems/problem_utils.jl b/src/systems/problem_utils.jl index daf1c164d8..bcec03040f 100644 --- a/src/systems/problem_utils.jl +++ b/src/systems/problem_utils.jl @@ -301,12 +301,14 @@ end """ $(TYPEDSIGNATURES) -Performs symbolic substitution on the values in `varmap`, using `varmap` itself as the -set of substitution rules. -""" -function evaluate_varmap!(varmap::AbstractDict) - for (k, v) in varmap - varmap[k] = fixpoint_sub(v, varmap) +Performs symbolic substitution on the values in `varmap` for the keys in `vars`, using +`varmap` itself as the set of substitution rules. If an entry is `vars` is not a key +in `varmap`, it is ignored. +""" +function evaluate_varmap!(varmap::AbstractDict, vars) + for k in vars + haskey(varmap, k) || continue + varmap[k] = fixpoint_sub(varmap[k], varmap) end end @@ -499,7 +501,7 @@ function process_SciMLProblem( add_observed!(sys, op) add_parameter_dependencies!(sys, op) - evaluate_varmap!(op) + evaluate_varmap!(op, dvs) u0 = better_varmap_to_vars( op, dvs; tofloat = true, use_union = false, @@ -511,6 +513,7 @@ function process_SciMLProblem( check_eqs_u0(eqs, dvs, u0; check_length, kwargs...) + evaluate_varmap!(op, ps) if is_split(sys) p = MTKParameters(sys, op) else diff --git a/test/initial_values.jl b/test/initial_values.jl index 9911bab7f4..fc386242a6 100644 --- a/test/initial_values.jl +++ b/test/initial_values.jl @@ -136,3 +136,16 @@ end prob = @test_nowarn ODEProblem(sys, [], (0.0, 1.0)) @test prob.p isa Vector{Float64} end + +@testset "Issue#3153" begin + @variables x(t) y(t) + @parameters c1 c2 c3 + eqs = [D(x) ~ y, + y ~ ifelse(t < c1, 0.0, (-c1 + t)^(c3))] + sps = [x, y] + ps = [c1, c2, c3] + @mtkbuild osys = ODESystem(eqs, t, sps, ps) + u0map = [x => 1.0] + pmap = [c1 => 5.0, c2 => 1.0, c3 => 1.2] + oprob = ODEProblem(osys, u0map, (0.0, 10.0), pmap) +end From b2decf70152faf90b381cd564043a30f7633946f Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 28 Oct 2024 11:38:15 +0530 Subject: [PATCH 0264/1337] test: uncomment runtests --- test/runtests.jl | 82 ++++++++++++++++++++++++------------------------ 1 file changed, 41 insertions(+), 41 deletions(-) diff --git a/test/runtests.jl b/test/runtests.jl index 5db3308540..c7edbfaaf5 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -19,48 +19,48 @@ end @time begin if GROUP == "All" || GROUP == "InterfaceI" @testset "InterfaceI" begin - # @safetestset "Linear Algebra Test" include("linalg.jl") - # @safetestset "AbstractSystem Test" include("abstractsystem.jl") - # @safetestset "Variable Scope Tests" include("variable_scope.jl") - # @safetestset "Symbolic Parameters Test" include("symbolic_parameters.jl") - # @safetestset "Parsing Test" include("variable_parsing.jl") - # @safetestset "Simplify Test" include("simplify.jl") - # @safetestset "Direct Usage Test" include("direct.jl") - # @safetestset "System Linearity Test" include("linearity.jl") - # @safetestset "Input Output Test" include("input_output_handling.jl") - # @safetestset "Clock Test" include("clock.jl") - # @safetestset "ODESystem Test" include("odesystem.jl") - # @safetestset "Dynamic Quantities Test" include("dq_units.jl") - # @safetestset "Unitful Quantities Test" include("units.jl") - # @safetestset "Mass Matrix Test" include("mass_matrix.jl") - # @safetestset "InitializationSystem Test" include("initializationsystem.jl") - # @safetestset "Guess Propagation" include("guess_propagation.jl") - # @safetestset "Hierarchical Initialization Equations" include("hierarchical_initialization_eqs.jl") - # @safetestset "Reduction Test" include("reduction.jl") - # @safetestset "Split Parameters Test" include("split_parameters.jl") - # @safetestset "StaticArrays Test" include("static_arrays.jl") - # @safetestset "Components Test" include("components.jl") - # @safetestset "Model Parsing Test" include("model_parsing.jl") - # @safetestset "Error Handling" include("error_handling.jl") - # @safetestset "StructuralTransformations" include("structural_transformation/runtests.jl") - # @safetestset "State Selection Test" include("state_selection.jl") - # @safetestset "Symbolic Event Test" include("symbolic_events.jl") - # @safetestset "Stream Connect Test" include("stream_connectors.jl") - # @safetestset "Domain Connect Test" include("domain_connectors.jl") - # @safetestset "Lowering Integration Test" include("lowering_solving.jl") - # @safetestset "Dependency Graph Test" include("dep_graphs.jl") - # @safetestset "Function Registration Test" include("function_registration.jl") - # @safetestset "Precompiled Modules Test" include("precompile_test.jl") - # @safetestset "DAE Jacobians Test" include("dae_jacobian.jl") - # @safetestset "Jacobian Sparsity" include("jacobiansparsity.jl") + @safetestset "Linear Algebra Test" include("linalg.jl") + @safetestset "AbstractSystem Test" include("abstractsystem.jl") + @safetestset "Variable Scope Tests" include("variable_scope.jl") + @safetestset "Symbolic Parameters Test" include("symbolic_parameters.jl") + @safetestset "Parsing Test" include("variable_parsing.jl") + @safetestset "Simplify Test" include("simplify.jl") + @safetestset "Direct Usage Test" include("direct.jl") + @safetestset "System Linearity Test" include("linearity.jl") + @safetestset "Input Output Test" include("input_output_handling.jl") + @safetestset "Clock Test" include("clock.jl") + @safetestset "ODESystem Test" include("odesystem.jl") + @safetestset "Dynamic Quantities Test" include("dq_units.jl") + @safetestset "Unitful Quantities Test" include("units.jl") + @safetestset "Mass Matrix Test" include("mass_matrix.jl") + @safetestset "InitializationSystem Test" include("initializationsystem.jl") + @safetestset "Guess Propagation" include("guess_propagation.jl") + @safetestset "Hierarchical Initialization Equations" include("hierarchical_initialization_eqs.jl") + @safetestset "Reduction Test" include("reduction.jl") + @safetestset "Split Parameters Test" include("split_parameters.jl") + @safetestset "StaticArrays Test" include("static_arrays.jl") + @safetestset "Components Test" include("components.jl") + @safetestset "Model Parsing Test" include("model_parsing.jl") + @safetestset "Error Handling" include("error_handling.jl") + @safetestset "StructuralTransformations" include("structural_transformation/runtests.jl") + @safetestset "State Selection Test" include("state_selection.jl") + @safetestset "Symbolic Event Test" include("symbolic_events.jl") + @safetestset "Stream Connect Test" include("stream_connectors.jl") + @safetestset "Domain Connect Test" include("domain_connectors.jl") + @safetestset "Lowering Integration Test" include("lowering_solving.jl") + @safetestset "Dependency Graph Test" include("dep_graphs.jl") + @safetestset "Function Registration Test" include("function_registration.jl") + @safetestset "Precompiled Modules Test" include("precompile_test.jl") + @safetestset "DAE Jacobians Test" include("dae_jacobian.jl") + @safetestset "Jacobian Sparsity" include("jacobiansparsity.jl") @safetestset "Modelingtoolkitize Test" include("modelingtoolkitize.jl") - # @safetestset "FuncAffect Test" include("funcaffect.jl") - # @safetestset "Constants Test" include("constants.jl") - # @safetestset "Parameter Dependency Test" include("parameter_dependencies.jl") - # @safetestset "Generate Custom Function Test" include("generate_custom_function.jl") - # @safetestset "Initial Values Test" include("initial_values.jl") - # @safetestset "Equation Type Accessors Test" include("equation_type_accessors.jl") - # @safetestset "Equations with complex values" include("complex.jl") + @safetestset "FuncAffect Test" include("funcaffect.jl") + @safetestset "Constants Test" include("constants.jl") + @safetestset "Parameter Dependency Test" include("parameter_dependencies.jl") + @safetestset "Generate Custom Function Test" include("generate_custom_function.jl") + @safetestset "Initial Values Test" include("initial_values.jl") + @safetestset "Equation Type Accessors Test" include("equation_type_accessors.jl") + @safetestset "Equations with complex values" include("complex.jl") end end From 7aba2fc7b5196d713ce46c053e49efb71b403bc3 Mon Sep 17 00:00:00 2001 From: Christopher Rackauckas Date: Mon, 28 Oct 2024 06:13:19 -0100 Subject: [PATCH 0265/1337] Update src/systems/problem_utils.jl --- src/systems/problem_utils.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/systems/problem_utils.jl b/src/systems/problem_utils.jl index bcec03040f..3e50cc41cc 100644 --- a/src/systems/problem_utils.jl +++ b/src/systems/problem_utils.jl @@ -302,7 +302,7 @@ end $(TYPEDSIGNATURES) Performs symbolic substitution on the values in `varmap` for the keys in `vars`, using -`varmap` itself as the set of substitution rules. If an entry is `vars` is not a key +`varmap` itself as the set of substitution rules. If an entry in `vars` is not a key in `varmap`, it is ignored. """ function evaluate_varmap!(varmap::AbstractDict, vars) From 108d055dbb8b7f894f4095d08a271b5612e282e0 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 28 Oct 2024 13:09:28 +0530 Subject: [PATCH 0266/1337] feat: add support for `IntervalNonlinearProblem` --- src/ModelingToolkit.jl | 2 + src/systems/nonlinear/nonlinearsystem.jl | 121 +++++++++++++++++++++-- test/nonlinearsystem.jl | 22 +++++ 3 files changed, 139 insertions(+), 6 deletions(-) diff --git a/src/ModelingToolkit.jl b/src/ModelingToolkit.jl index 46177e902e..bdb9168fa0 100644 --- a/src/ModelingToolkit.jl +++ b/src/ModelingToolkit.jl @@ -226,6 +226,8 @@ export JumpSystem export ODEProblem, SDEProblem export NonlinearFunction, NonlinearFunctionExpr export NonlinearProblem, NonlinearProblemExpr +export IntervalNonlinearFunction, IntervalNonlinearFunctionExpr +export IntervalNonlinearProblem, IntervalNonlinearProblemExpr export OptimizationProblem, OptimizationProblemExpr, constraints export SteadyStateProblem, SteadyStateProblemExpr export JumpProblem diff --git a/src/systems/nonlinear/nonlinearsystem.jl b/src/systems/nonlinear/nonlinearsystem.jl index e7adb549a3..44e77d5a0f 100644 --- a/src/systems/nonlinear/nonlinearsystem.jl +++ b/src/systems/nonlinear/nonlinearsystem.jl @@ -258,13 +258,18 @@ end function generate_function( sys::NonlinearSystem, dvs = unknowns(sys), ps = parameters(sys); - wrap_code = identity, kwargs...) + wrap_code = identity, scalar = false, kwargs...) rhss = [deq.rhs for deq in equations(sys)] + dvs′ = value.(dvs) + if scalar + rhss = only(rhss) + dvs′ = only(dvs) + end pre, sol_states = get_substitutions_and_solved_unknowns(sys) wrap_code = wrap_code .∘ wrap_array_vars(sys, rhss; dvs, ps) .∘ - wrap_parameter_dependencies(sys, false) + wrap_parameter_dependencies(sys, scalar) p = reorder_parameters(sys, value.(ps)) - return build_function(rhss, value.(dvs), p...; postprocess_fbody = pre, + return build_function(rhss, dvs′, p...; postprocess_fbody = pre, states = sol_states, wrap_code, kwargs...) end @@ -288,7 +293,7 @@ SciMLBase.NonlinearFunction{iip}(sys::NonlinearSystem, dvs = unknowns(sys), kwargs...) where {iip} ``` -Create an `NonlinearFunction` from the [`NonlinearSystem`](@ref). The arguments +Create a `NonlinearFunction` from the [`NonlinearSystem`](@ref). The arguments `dvs` and `ps` are used to set the order of the dependent variable and parameter vectors, respectively. """ @@ -351,6 +356,34 @@ function SciMLBase.NonlinearFunction{iip}(sys::NonlinearSystem, dvs = unknowns(s observed = observedfun) end +""" +$(TYPEDSIGNATURES) + +Create an `IntervalNonlinearFunction` from the [`NonlinearSystem`](@ref). The arguments +`dvs` and `ps` are used to set the order of the dependent variable and parameter vectors, +respectively. +""" +function SciMLBase.IntervalNonlinearFunction( + sys::NonlinearSystem, dvs = unknowns(sys), ps = parameters(sys), u0 = nothing; + p = nothing, eval_expression = false, eval_module = @__MODULE__, kwargs...) + if !iscomplete(sys) + error("A completed `NonlinearSystem` is required. Call `complete` or `structural_simplify` on the system before creating a `IntervalNonlinearFunction`") + end + if !isone(length(dvs)) || !isone(length(equations(sys))) + error("`IntervalNonlinearFunction` only supports systems with a single equation and a single unknown.") + end + + f_gen = generate_function( + sys, dvs, ps; expression = Val{true}, scalar = true, kwargs...) + f_oop = eval_or_rgf(f_gen; eval_expression, eval_module) + f(u, p) = f_oop(u, p) + f(u, p::MTKParameters) = f_oop(u, p...) + + observedfun = ObservedFunctionCache(sys; eval_expression, eval_module) + + IntervalNonlinearFunction{false}(f; observed = observedfun, sys = sys) +end + """ ```julia SciMLBase.NonlinearFunctionExpr{iip}(sys::NonlinearSystem, dvs = unknowns(sys), @@ -361,14 +394,14 @@ SciMLBase.NonlinearFunctionExpr{iip}(sys::NonlinearSystem, dvs = unknowns(sys), kwargs...) where {iip} ``` -Create a Julia expression for an `ODEFunction` from the [`ODESystem`](@ref). +Create a Julia expression for a `NonlinearFunction` from the [`NonlinearSystem`](@ref). The arguments `dvs` and `ps` are used to set the order of the dependent variable and parameter vectors, respectively. """ struct NonlinearFunctionExpr{iip} end function NonlinearFunctionExpr{iip}(sys::NonlinearSystem, dvs = unknowns(sys), - ps = parameters(sys), u0 = nothing, p = nothing; + ps = parameters(sys), u0 = nothing; p = nothing, version = nothing, tgrad = false, jac = false, linenumbers = false, @@ -412,6 +445,34 @@ function NonlinearFunctionExpr{iip}(sys::NonlinearSystem, dvs = unknowns(sys), !linenumbers ? Base.remove_linenums!(ex) : ex end +""" +$(TYPEDSIGNATURES) + +Create a Julia expression for an `IntervalNonlinearFunction` from the +[`NonlinearSystem`](@ref). The arguments `dvs` and `ps` are used to set the order of the +dependent variable and parameter vectors, respectively. +""" +function IntervalNonlinearFunctionExpr( + sys::NonlinearSystem, dvs = unknowns(sys), ps = parameters(sys), + u0 = nothing; p = nothing, linenumbers = false, kwargs...) + if !iscomplete(sys) + error("A completed `NonlinearSystem` is required. Call `complete` or `structural_simplify` on the system before creating a `IntervalNonlinearFunctionExpr`") + end + if !isone(length(dvs)) || !isone(length(equations(sys))) + error("`IntervalNonlinearFunctionExpr` only supports systems with a single equation and a single unknown.") + end + + f = generate_function(sys, dvs, ps; expression = Val{true}, scalar = true, kwargs...) + + IntervalNonlinearFunction{false}(f; sys = sys) + + ex = quote + f = $f + NonlinearFunction{false}(f) + end + !linenumbers ? Base.remove_linenums!(ex) : ex +end + """ ```julia DiffEqBase.NonlinearProblem{iip}(sys::NonlinearSystem, u0map, @@ -470,6 +531,26 @@ function DiffEqBase.NonlinearLeastSquaresProblem{iip}(sys::NonlinearSystem, u0ma NonlinearLeastSquaresProblem{iip}(f, u0, p; filter_kwargs(kwargs)...) end +""" +$(TYPEDSIGNATURES) + +Generate an `IntervalNonlinearProblem` from a `NonlinearSystem` and allow for automatically +symbolically calculating numerical enhancements. +""" +function DiffEqBase.IntervalNonlinearProblem(sys::NonlinearSystem, uspan::NTuple{2}, + parammap = SciMLBase.NullParameters(); kwargs...) + if !iscomplete(sys) + error("A completed `NonlinearSystem` is required. Call `complete` or `structural_simplify` on the system before creating a `IntervalNonlinearProblem`") + end + if !isone(length(unknowns(sys))) || !isone(length(equations(sys))) + error("`IntervalNonlinearProblem` only supports with a single equation and a single unknown.") + end + f, u0, p = process_SciMLProblem( + IntervalNonlinearFunction, sys, unknowns(sys) .=> uspan[1], parammap; kwargs...) + + return IntervalNonlinearProblem(f, uspan, p; filter_kwargs(kwargs)...) +end + """ ```julia DiffEqBase.NonlinearProblemExpr{iip}(sys::NonlinearSystem, u0map, @@ -550,6 +631,34 @@ function NonlinearLeastSquaresProblemExpr{iip}(sys::NonlinearSystem, u0map, !linenumbers ? Base.remove_linenums!(ex) : ex end +""" +$(TYPEDSIGNATURES) + +Generates a Julia expression for an IntervalNonlinearProblem from a +NonlinearSystem and allows for automatically symbolically calculating +numerical enhancements. +""" +function IntervalNonlinearProblemExpr(sys::NonlinearSystem, uspan::NTuple{2}, + parammap = SciMLBase.NullParameters(); kwargs...) + if !iscomplete(sys) + error("A completed `NonlinearSystem` is required. Call `complete` or `structural_simplify` on the system before creating a `IntervalNonlinearProblemExpr`") + end + if !isone(length(unknowns(sys))) || !isone(length(equations(sys))) + error("`IntervalNonlinearProblemExpr` only supports with a single equation and a single unknown.") + end + f, u0, p = process_SciMLProblem( + IntervalNonlinearFunctionExpr, sys, unknowns(sys) .=> uspan[1], parammap; kwargs...) + linenumbers = get(kwargs, :linenumbers, true) + + ex = quote + f = $f + uspan = $uspan + p = $p + IntervalNonlinearProblem(f, uspan, p; $(filter_kwargs(kwargs)...)) + end + !linenumbers ? Base.remove_linenums!(ex) : ex +end + function flatten(sys::NonlinearSystem, noeqs = false) systems = get_systems(sys) if isempty(systems) diff --git a/test/nonlinearsystem.jl b/test/nonlinearsystem.jl index c50f32e463..f766ca0131 100644 --- a/test/nonlinearsystem.jl +++ b/test/nonlinearsystem.jl @@ -358,3 +358,25 @@ end @test_nowarn solve(prob) end end + +@testset "IntervalNonlinearProblem" begin + @variables x + @parameters p + @named nlsys = NonlinearSystem([0 ~ x * x - p]) + + for sys in [complete(nlsys), complete(nlsys; split = false)] + prob = IntervalNonlinearProblem(sys, (0.0, 2.0), [p => 1.0]) + sol = @test_nowarn solve(prob, ITP()) + @test SciMLBase.successful_retcode(sol) + @test_nowarn IntervalNonlinearProblemExpr(sys, (0.0, 2.0), [p => 1.0]) + end + + @variables y + @mtkbuild sys = NonlinearSystem([0 ~ x * x - p * x + p, 0 ~ x * y + p]) + @test_throws ["single equation", "unknown"] IntervalNonlinearProblem(sys, (0.0, 1.0)) + @test_throws ["single equation", "unknown"] IntervalNonlinearFunction(sys, (0.0, 1.0)) + @test_throws ["single equation", "unknown"] IntervalNonlinearProblemExpr( + sys, (0.0, 1.0)) + @test_throws ["single equation", "unknown"] IntervalNonlinearFunctionExpr( + sys, (0.0, 1.0)) +end From f7bf33ac394c5379cc229592e0974777f53badff Mon Sep 17 00:00:00 2001 From: Christopher Tessum Date: Sun, 13 Oct 2024 14:10:41 -0500 Subject: [PATCH 0267/1337] Don't check units during initialization ...because initialization problems don't correctly handle units and units have already been checked during system initialization. --- src/systems/problem_utils.jl | 4 ++-- test/initializationsystem.jl | 17 +++++++++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/src/systems/problem_utils.jl b/src/systems/problem_utils.jl index 3e50cc41cc..695e489db0 100644 --- a/src/systems/problem_utils.jl +++ b/src/systems/problem_utils.jl @@ -396,7 +396,7 @@ Keyword arguments: - `eval_module`: If `eval_expression == true`, the module to `eval` into. Otherwise, the module in which to generate the `RuntimeGeneratedFunction`. - `fully_determined`: Override whether the initialization system is fully determined. -- `check_units`: Enable or disable unit checks. +- `check_units`: Enable or disable unit checks when constructing the initialization problem. - `tofloat`, `use_union`: Passed to [`better_varmap_to_vars`](@ref) for building `u0` (and possibly `p`). - `u0_constructor`: A function to apply to the `u0` value returned from `better_varmap_to_vars` @@ -414,7 +414,7 @@ function process_SciMLProblem( implicit_dae = false, t = nothing, guesses = AnyDict(), warn_initialize_determined = true, initialization_eqs = [], eval_expression = false, eval_module = @__MODULE__, fully_determined = false, - check_units = true, tofloat = true, use_union = false, + check_units = false, tofloat = true, use_union = false, u0_constructor = identity, du0map = nothing, check_length = true, symbolic_u0 = false, kwargs...) dvs = unknowns(sys) ps = parameters(sys) diff --git a/test/initializationsystem.jl b/test/initializationsystem.jl index 3eddba2b78..c9783c9659 100644 --- a/test/initializationsystem.jl +++ b/test/initializationsystem.jl @@ -3,6 +3,7 @@ using ForwardDiff using SymbolicIndexingInterface, SciMLStructures using SciMLStructures: Tunable using ModelingToolkit: t_nounits as t, D_nounits as D +using DynamicQuantities @parameters g @variables x(t) y(t) [state_priority = 10] λ(t) @@ -860,3 +861,19 @@ end integ = init(prob) @test integ[x] ≈ [1.0, 3.0] end + +@testset "units" begin + t = ModelingToolkit.t + D = ModelingToolkit.D + @parameters g [unit = u"m/s^2"] L=1 [unit = u"m^2"] + @variables x(t) [unit = u"m"] y(t) [unit = u"m" state_priority = 10] λ(t) [unit = u"s^-2"] + eqs = [D(D(x)) ~ λ * x + D(D(y)) ~ λ * y - g + x^2 + y^2 ~ L] + @mtkbuild pend = ODESystem(eqs, t) + + prob = ODEProblem(pend, [x => 1, y => 0], (0.0, 1.5), [g => 1], + guesses = ModelingToolkit.missing_variable_defaults(pend)) + sol = solve(prob, Rodas5P()) + @test SciMLBase.successful_retcode(sol) +end From 0f5cf1a9970b266a4f8c4ff963cd79e0b9a883c3 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 28 Oct 2024 14:49:00 +0530 Subject: [PATCH 0268/1337] refactor: rename `check_units` to `check_initialization_units` --- src/systems/problem_utils.jl | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/systems/problem_utils.jl b/src/systems/problem_utils.jl index 695e489db0..f7f13b9e19 100644 --- a/src/systems/problem_utils.jl +++ b/src/systems/problem_utils.jl @@ -396,7 +396,8 @@ Keyword arguments: - `eval_module`: If `eval_expression == true`, the module to `eval` into. Otherwise, the module in which to generate the `RuntimeGeneratedFunction`. - `fully_determined`: Override whether the initialization system is fully determined. -- `check_units`: Enable or disable unit checks when constructing the initialization problem. +- `check_initialization_units`: Enable or disable unit checks when constructing the + initialization problem. - `tofloat`, `use_union`: Passed to [`better_varmap_to_vars`](@ref) for building `u0` (and possibly `p`). - `u0_constructor`: A function to apply to the `u0` value returned from `better_varmap_to_vars` @@ -414,7 +415,7 @@ function process_SciMLProblem( implicit_dae = false, t = nothing, guesses = AnyDict(), warn_initialize_determined = true, initialization_eqs = [], eval_expression = false, eval_module = @__MODULE__, fully_determined = false, - check_units = false, tofloat = true, use_union = false, + check_initialization_units = false, tofloat = true, use_union = false, u0_constructor = identity, du0map = nothing, check_length = true, symbolic_u0 = false, kwargs...) dvs = unknowns(sys) ps = parameters(sys) @@ -464,7 +465,7 @@ function process_SciMLProblem( !isempty(initialization_equations(sys))) && t !== nothing initializeprob = ModelingToolkit.InitializationProblem( sys, t, u0map, pmap; guesses, warn_initialize_determined, - initialization_eqs, eval_expression, eval_module, fully_determined, check_units) + initialization_eqs, eval_expression, eval_module, fully_determined, check_units = check_initialization_units) initializeprobmap = getu(initializeprob, unknowns(sys)) punknowns = [p From 616e5d1f3eae00bedb12071b979af99e685bdccc Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 28 Oct 2024 15:03:25 +0530 Subject: [PATCH 0269/1337] refactor: format --- src/systems/problem_utils.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/systems/problem_utils.jl b/src/systems/problem_utils.jl index f7f13b9e19..977f637d47 100644 --- a/src/systems/problem_utils.jl +++ b/src/systems/problem_utils.jl @@ -465,7 +465,8 @@ function process_SciMLProblem( !isempty(initialization_equations(sys))) && t !== nothing initializeprob = ModelingToolkit.InitializationProblem( sys, t, u0map, pmap; guesses, warn_initialize_determined, - initialization_eqs, eval_expression, eval_module, fully_determined, check_units = check_initialization_units) + initialization_eqs, eval_expression, eval_module, fully_determined, + check_units = check_initialization_units) initializeprobmap = getu(initializeprob, unknowns(sys)) punknowns = [p From 26074bd51b6b35744fa4850b4673dfcc4e8cbb24 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 28 Oct 2024 15:54:42 +0530 Subject: [PATCH 0270/1337] fix: fix denominator checking in `HomotopyContinuationExt` --- ext/MTKHomotopyContinuationExt.jl | 15 +++++++++++++-- test/extensions/homotopy_continuation.jl | 14 ++++++++++---- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/ext/MTKHomotopyContinuationExt.jl b/ext/MTKHomotopyContinuationExt.jl index e4a9243ff9..ebd8afbfc1 100644 --- a/ext/MTKHomotopyContinuationExt.jl +++ b/ext/MTKHomotopyContinuationExt.jl @@ -196,7 +196,18 @@ function MTK.HomotopyContinuationProblem( error("Equation $eq is not a polynomial in the unknowns. See warnings for further details.") end num, den = handle_rational_polynomials(eq.rhs - eq.lhs, dvs) - push!(denoms, den) + + # make factors different elements, otherwise the nonzero factors artificially + # inflate the error of the zero factor. + if iscall(den) && operation(den) == * + for arg in arguments(den) + # ignore constant factors + symbolic_type(arg) == NotSymbolic() && continue + push!(denoms, abs(arg)) + end + elseif symbolic_type(den) != NotSymbolic() + push!(denoms, abs(den)) + end return 0 ~ num end @@ -232,7 +243,7 @@ Extra keyword arguments: the denominator to be below `denominator_abstol` will be discarded. """ function CommonSolve.solve(prob::MTK.HomotopyContinuationProblem, - alg = nothing; show_progress = false, denominator_abstol = 1e-8, kwargs...) + alg = nothing; show_progress = false, denominator_abstol = 1e-7, kwargs...) sol = HomotopyContinuation.solve( prob.homotopy_continuation_system; show_progress, kwargs...) realsols = HomotopyContinuation.results(sol; only_real = true) diff --git a/test/extensions/homotopy_continuation.jl b/test/extensions/homotopy_continuation.jl index 6d7279899f..9bd64fa7fb 100644 --- a/test/extensions/homotopy_continuation.jl +++ b/test/extensions/homotopy_continuation.jl @@ -98,7 +98,9 @@ end @test sol[x] ≈ 1.0 p = parameter_values(prob) for invalid in [2.0, 3.0] - @test prob.denominator([invalid], p)[1] <= 1e-8 + for err in [-9e-8, 0, 9e-8] + @test any(<=(1e-7), prob.denominator([invalid + err, 2.0], p)) + end end @named sys = NonlinearSystem( @@ -115,14 +117,18 @@ end disallowed_y = [7, 5, 4] @test all(!isapprox(sol[x]; atol = 1e-8), disallowed_x) @test all(!isapprox(sol[y]; atol = 1e-8), disallowed_y) - @test sol[x^2 - 4x + y] >= 1e-8 + @test abs(sol[x^2 - 4x + y]) >= 1e-8 p = parameter_values(prob) for val in disallowed_x - @test any(<=(1e-8), prob.denominator([val, 2.0], p)) + for err in [-9e-8, 0, 9e-8] + @test any(<=(1e-7), prob.denominator([val + err, 2.0], p)) + end end for val in disallowed_y - @test any(<=(1e-8), prob.denominator([2.0, val], p)) + for err in [-9e-8, 0, 9e-8] + @test any(<=(1e-7), prob.denominator([2.0, val + err], p)) + end end @test prob.denominator([2.0, 4.0], p)[1] <= 1e-8 end From add7059c8a52df7401aee9af3f7e6d8833b59db4 Mon Sep 17 00:00:00 2001 From: vyudu Date: Mon, 28 Oct 2024 17:14:30 -0400 Subject: [PATCH 0271/1337] make Observable definition comply with new record_from_solution definition --- ext/MTKBifurcationKitExt.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/MTKBifurcationKitExt.jl b/ext/MTKBifurcationKitExt.jl index a0bd3bfc02..e5b57efc5a 100644 --- a/ext/MTKBifurcationKitExt.jl +++ b/ext/MTKBifurcationKitExt.jl @@ -59,7 +59,7 @@ struct ObservableRecordFromSolution{S, T} end end # Functor function that computes the value. -function (orfs::ObservableRecordFromSolution)(x, p) +function (orfs::ObservableRecordFromSolution)(x, p; k...) # Updates the state values (in subs_vals). for state_idx in 1:(orfs.state_end_idxs) orfs.subs_vals[state_idx] = orfs.subs_vals[state_idx][1] => x[state_idx] From eb681c10403031e3a1ee28c96f9d4b2defc3e389 Mon Sep 17 00:00:00 2001 From: vyudu Date: Mon, 28 Oct 2024 17:26:25 -0400 Subject: [PATCH 0272/1337] add test --- test/extensions/bifurcationkit.jl | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/extensions/bifurcationkit.jl b/test/extensions/bifurcationkit.jl index b8c95dca99..629edf46a6 100644 --- a/test/extensions/bifurcationkit.jl +++ b/test/extensions/bifurcationkit.jl @@ -162,4 +162,12 @@ let bf = bifurcationdiagram(bp, PALC(), 2, opts_br) @test bf.γ.specialpoint[1].param≈0.1 atol=1e-4 rtol=1e-4 + + # Test with plot variable as observable + pvar = ModelingToolkit.get_var_to_name(fol)[:RHS] + bp = BifurcationProblem(fol, u0, par, bif_par; plot_var = pvar) + opts_br = ContinuationPar(p_min = -1.0, + p_max = 1.0) + bf = bifurcationdiagram(bp, PALC(), 2, opts_br) + @test bf.γ.specialpoint[1].param≈0.1 atol=1e-4 rtol=1e-4 end From d3c8933b7d005fffad8ebb37513f14a0dbba1af4 Mon Sep 17 00:00:00 2001 From: Christopher Rackauckas Date: Tue, 29 Oct 2024 08:52:53 -0100 Subject: [PATCH 0273/1337] Update Documentation.yml to build docs with LTS Should fix https://github.com/SciML/ModelingToolkit.jl/issues/3163 --- .github/workflows/Documentation.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/Documentation.yml b/.github/workflows/Documentation.yml index ca24f79d0b..dceee5f1d2 100644 --- a/.github/workflows/Documentation.yml +++ b/.github/workflows/Documentation.yml @@ -20,7 +20,7 @@ jobs: - uses: actions/checkout@v4 - uses: julia-actions/setup-julia@latest with: - version: '1' + version: 'lts' - run: sudo apt-get update && sudo apt-get install -y xorg-dev mesa-utils xvfb libgl1 freeglut3-dev libxrandr-dev libxinerama-dev libxcursor-dev libxi-dev libxext-dev - name: Install dependencies run: DISPLAY=:0 xvfb-run -s '-screen 0 1024x768x24' julia --project=docs/ -e 'using Pkg; Pkg.develop(PackageSpec(path=pwd())); Pkg.instantiate()' From 91eabc77fde9cc47d73bb1f482f1e73bd663a5b5 Mon Sep 17 00:00:00 2001 From: Christopher Rackauckas Date: Tue, 29 Oct 2024 08:53:48 -0100 Subject: [PATCH 0274/1337] Update Project.toml --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 958772cb05..656fef6173 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "ModelingToolkit" uuid = "961ee093-0014-501f-94e3-6117800e7a78" authors = ["Yingbo Ma ", "Chris Rackauckas and contributors"] -version = "9.48.0" +version = "9.49.0" [deps] AbstractTrees = "1520ce14-60c1-5f80-bbc7-55ef81b5835c" From d48dc57ff2a15372ce4e6cd939e62ef333a3b746 Mon Sep 17 00:00:00 2001 From: vyudu Date: Mon, 28 Oct 2024 17:14:30 -0400 Subject: [PATCH 0275/1337] make Observable definition comply with new record_from_solution definition --- ext/MTKBifurcationKitExt.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/MTKBifurcationKitExt.jl b/ext/MTKBifurcationKitExt.jl index a0bd3bfc02..e5b57efc5a 100644 --- a/ext/MTKBifurcationKitExt.jl +++ b/ext/MTKBifurcationKitExt.jl @@ -59,7 +59,7 @@ struct ObservableRecordFromSolution{S, T} end end # Functor function that computes the value. -function (orfs::ObservableRecordFromSolution)(x, p) +function (orfs::ObservableRecordFromSolution)(x, p; k...) # Updates the state values (in subs_vals). for state_idx in 1:(orfs.state_end_idxs) orfs.subs_vals[state_idx] = orfs.subs_vals[state_idx][1] => x[state_idx] From e27da938be5ff7f69578eac71bd8c61e621e0f32 Mon Sep 17 00:00:00 2001 From: vyudu Date: Mon, 28 Oct 2024 17:26:25 -0400 Subject: [PATCH 0276/1337] add test --- test/extensions/bifurcationkit.jl | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/extensions/bifurcationkit.jl b/test/extensions/bifurcationkit.jl index b8c95dca99..629edf46a6 100644 --- a/test/extensions/bifurcationkit.jl +++ b/test/extensions/bifurcationkit.jl @@ -162,4 +162,12 @@ let bf = bifurcationdiagram(bp, PALC(), 2, opts_br) @test bf.γ.specialpoint[1].param≈0.1 atol=1e-4 rtol=1e-4 + + # Test with plot variable as observable + pvar = ModelingToolkit.get_var_to_name(fol)[:RHS] + bp = BifurcationProblem(fol, u0, par, bif_par; plot_var = pvar) + opts_br = ContinuationPar(p_min = -1.0, + p_max = 1.0) + bf = bifurcationdiagram(bp, PALC(), 2, opts_br) + @test bf.γ.specialpoint[1].param≈0.1 atol=1e-4 rtol=1e-4 end From 4f89f08b7474469434ea38e3a1bcc5d7f5643d3e Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 30 Oct 2024 12:05:24 +0530 Subject: [PATCH 0277/1337] fix: fix `complete` not properly expanding systems --- src/systems/abstractsystem.jl | 4 ++-- test/odesystem.jl | 28 ++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index de389ab3b8..c53c900d27 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -933,8 +933,8 @@ function complete(sys::AbstractSystem; split = true, flatten = true) @set! sys.ps = unique!(vcat(get_ps(sys), collect(newparams))) end if flatten - if (eqs = equations(sys)) isa Vector && - any(eq -> eq isa Equation && isconnection(eq.lhs), eqs) + eqs = equations(sys) + if eqs isa AbstractArray && eltype(eqs) <: Equation newsys = expand_connections(sys) else newsys = sys diff --git a/test/odesystem.jl b/test/odesystem.jl index 9286fb3c01..dfc755da98 100644 --- a/test/odesystem.jl +++ b/test/odesystem.jl @@ -1467,3 +1467,31 @@ end obsfn(buf, prob.u0, prob.p, 0.0) @test buf ≈ [1.0, 1.0, 2.0] end + +@testset "`complete` expands connections" begin + using ModelingToolkitStandardLibrary.Electrical + @mtkmodel RC begin + @parameters begin + R = 1.0 + C = 1.0 + V = 1.0 + end + @components begin + resistor = Resistor(R = R) + capacitor = Capacitor(C = C, v = 0.0) + source = Voltage() + constant = Constant(k = V) + ground = Ground() + end + @equations begin + connect(constant.output, source.V) + connect(source.p, resistor.p) + connect(resistor.n, capacitor.p) + connect(capacitor.n, source.n, ground.g) + end + end + @named sys = RC() + total_eqs = length(equations(expand_connections(sys))) + sys2 = complete(sys) + @test length(equations(sys2)) == total_eqs +end From 2d1554a778d659a645237a90342c37b37a223539 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 30 Oct 2024 12:50:37 +0530 Subject: [PATCH 0278/1337] fix: support non-tunable parameters with DDEs --- src/systems/diffeqs/abstractodesystem.jl | 11 +++++++---- test/dde.jl | 17 +++++++++++++++++ 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index 8d9e0b5381..eeac9c7626 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -189,7 +189,9 @@ function generate_function(sys::AbstractODESystem, dvs = unknowns(sys), wrap_code = identity, kwargs...) if isdde - eqs = delay_to_function(sys) + issplit = has_index_cache(sys) && get_index_cache(sys) !== nothing + eqs = delay_to_function( + sys; history_arg = issplit ? MTKPARAMETERS_ARG : DEFAULT_PARAMS_ARG) else eqs = [eq for eq in equations(sys)] end @@ -211,7 +213,10 @@ function generate_function(sys::AbstractODESystem, dvs = unknowns(sys), t = get_iv(sys) if isdde - build_function(rhss, u, DDE_HISTORY_FUN, p..., t; kwargs...) + build_function(rhss, u, DDE_HISTORY_FUN, p..., t; kwargs..., + wrap_code = wrap_code .∘ wrap_mtkparameters(sys, false, 3) .∘ + wrap_array_vars(sys, rhss; dvs, ps) .∘ + wrap_parameter_dependencies(sys, false)) else pre, sol_states = get_substitutions_and_solved_unknowns(sys) @@ -570,9 +575,7 @@ function DiffEqBase.DDEFunction{iip}(sys::AbstractODESystem, dvs = unknowns(sys) kwargs...) f_oop, f_iip = eval_or_rgf.(f_gen; eval_expression, eval_module) f(u, h, p, t) = f_oop(u, h, p, t) - f(u, h, p::MTKParameters, t) = f_oop(u, h, p..., t) f(du, u, h, p, t) = f_iip(du, u, h, p, t) - f(du, u, h, p::MTKParameters, t) = f_iip(du, u, h, p..., t) DDEFunction{iip}(f, sys = sys) end diff --git a/test/dde.jl b/test/dde.jl index fb099ab8c6..c6e64fc1a5 100644 --- a/test/dde.jl +++ b/test/dde.jl @@ -170,3 +170,20 @@ prob_sa = DDEProblem(sys, [], (0.0, 10.0); constant_lags = [sys.osc1.τ, sys.osc sol(sol.t .- prob.ps[ssys.valve.τ]; idxs = ssys.valve.opening).u .+ sum.(sol[ssys.vvecs.x]) end + +@testset "Issue#3165 DDEs with non-tunables" begin + @variables x(..) = 1.0 + @parameters w=1.0 [tunable = false] τ=0.5 + eqs = [D(x(t)) ~ -w * x(t - τ)] + + @named sys = System(eqs, t) + sys = structural_simplify(sys) + + prob = DDEProblem(sys, + [], + (0.0, 10.0), + constant_lags = [τ]) + + alg = MethodOfSteps(Vern7()) + @test_nowarn solve(prob, alg) +end From d1289974104a59b7e3661d1aef3aca52be652b22 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 30 Oct 2024 15:37:09 +0530 Subject: [PATCH 0279/1337] fix: support non-tunable parameters with SDDEs --- src/systems/diffeqs/abstractodesystem.jl | 11 ++++------- src/systems/diffeqs/sdesystem.jl | 6 +++++- test/dde.jl | 11 +++++++++++ 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index eeac9c7626..518b48e929 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -215,7 +215,7 @@ function generate_function(sys::AbstractODESystem, dvs = unknowns(sys), if isdde build_function(rhss, u, DDE_HISTORY_FUN, p..., t; kwargs..., wrap_code = wrap_code .∘ wrap_mtkparameters(sys, false, 3) .∘ - wrap_array_vars(sys, rhss; dvs, ps) .∘ + wrap_array_vars(sys, rhss; dvs, ps, history = true) .∘ wrap_parameter_dependencies(sys, false)) else pre, sol_states = get_substitutions_and_solved_unknowns(sys) @@ -598,17 +598,14 @@ function DiffEqBase.SDDEFunction{iip}(sys::AbstractODESystem, dvs = unknowns(sys expression_module = eval_module, checkbounds = checkbounds, kwargs...) f_oop, f_iip = eval_or_rgf.(f_gen; eval_expression, eval_module) + f(u, h, p, t) = f_oop(u, h, p, t) + f(du, u, h, p, t) = f_iip(du, u, h, p, t) + g_gen = generate_diffusion_function(sys, dvs, ps; expression = Val{true}, isdde = true, kwargs...) g_oop, g_iip = eval_or_rgf.(g_gen; eval_expression, eval_module) - f(u, h, p, t) = f_oop(u, h, p, t) - f(u, h, p::MTKParameters, t) = f_oop(u, h, p..., t) - f(du, u, h, p, t) = f_iip(du, u, h, p, t) - f(du, u, h, p::MTKParameters, t) = f_iip(du, u, h, p..., t) g(u, h, p, t) = g_oop(u, h, p, t) - g(u, h, p::MTKParameters, t) = g_oop(u, h, p..., t) g(du, u, h, p, t) = g_iip(du, u, h, p, t) - g(du, u, h, p::MTKParameters, t) = g_iip(du, u, h, p..., t) SDDEFunction{iip}(f, g, sys = sys) end diff --git a/src/systems/diffeqs/sdesystem.jl b/src/systems/diffeqs/sdesystem.jl index 9f3814774f..011ba6e216 100644 --- a/src/systems/diffeqs/sdesystem.jl +++ b/src/systems/diffeqs/sdesystem.jl @@ -302,7 +302,11 @@ function generate_diffusion_function(sys::SDESystem, dvs = unknowns(sys), (map(x -> time_varying_as_func(value(x), sys), ps),) end if isdde - return build_function(eqs, u, DDE_HISTORY_FUN, p..., get_iv(sys); kwargs...) + return build_function(eqs, u, DDE_HISTORY_FUN, p..., get_iv(sys); kwargs..., + wrap_code = get(kwargs, :wrap_code, identity) .∘ + wrap_mtkparameters(sys, false, 3) .∘ + wrap_array_vars(sys, eqs; dvs, ps, history = true) .∘ + wrap_parameter_dependencies(sys, false)) else return build_function(eqs, u, p..., get_iv(sys); kwargs...) end diff --git a/test/dde.jl b/test/dde.jl index c6e64fc1a5..aa4536e604 100644 --- a/test/dde.jl +++ b/test/dde.jl @@ -186,4 +186,15 @@ end alg = MethodOfSteps(Vern7()) @test_nowarn solve(prob, alg) + + @brownian r + eqs = [D(x(t)) ~ -w * x(t - τ) + r] + @named sys = System(eqs, t) + sys = structural_simplify(sys) + prob = SDDEProblem(sys, + [], + (0.0, 10.0), + constant_lags = [τ]) + + @test_nowarn solve(prob, RKMil()) end From dddd69884244be31e9f8d9d3a83b9b126eba16ce Mon Sep 17 00:00:00 2001 From: Venkateshprasad <32921645+ven-k@users.noreply.github.com> Date: Wed, 30 Oct 2024 18:27:34 +0000 Subject: [PATCH 0280/1337] fix: unpack components of the base sys in implicit extend --- src/systems/model_parsing.jl | 22 +++++++++++----------- test/model_parsing.jl | 26 ++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 11 deletions(-) diff --git a/src/systems/model_parsing.jl b/src/systems/model_parsing.jl index ab70646824..e8955b5b84 100644 --- a/src/systems/model_parsing.jl +++ b/src/systems/model_parsing.jl @@ -772,18 +772,17 @@ function Base.names(model::Model) map(first, get(model.structure, :components, EMPTY_VoVoSYMBOL)))) end -function _parse_extend!(ext, a, b, dict, expr, kwargs, vars, additional_args) +function _parse_extend!(ext, a, b, dict, expr, kwargs, vars, implicit_arglist) extend_args!(a, b, dict, expr, kwargs) - # `additional_args` doubles as a flag to check the mode of `@extend`. It is + # `implicit_arglist` doubles as a flag to check the mode of `@extend`. It is # `nothing` for explicit destructuring. # The following block modifies the arguments of both base and higher systems # for the implicit extend statements. - if additional_args !== nothing + if implicit_arglist !== nothing b.args = [b.args[1]] - allvars = [additional_args.args..., vars.args...] push!(b.args, Expr(:parameters)) - for var in allvars + for var in implicit_arglist.args push!(b.args[end].args, var) if !haskey(dict[:kwargs], var) push!(dict[:kwargs], var => Dict(:value => NO_VALUE)) @@ -814,8 +813,8 @@ function parse_extend!(exprs, ext, dict, mod, body, kwargs) end a, b = b.args # This doubles as a flag to identify the mode of `@extend` - additional_args = nothing - _parse_extend!(ext, a, b, dict, expr, kwargs, vars, additional_args) + implicit_arglist = nothing + _parse_extend!(ext, a, b, dict, expr, kwargs, vars, implicit_arglist) else error("When explicitly destructing in `@extend` please use the syntax: `@extend a, b = oneport = OnePort()`.") end @@ -825,11 +824,12 @@ function parse_extend!(exprs, ext, dict, mod, body, kwargs) b = body if (model = getproperty(mod, b.args[1])) isa Model vars = Expr(:tuple) - append!(vars.args, _arguments(model)) - additional_args = Expr(:tuple) - append!(additional_args.args, + append!(vars.args, names(model)) + implicit_arglist = Expr(:tuple) + append!(implicit_arglist.args, _arguments(model)) + append!(implicit_arglist.args, keys(get(model.structure, :structural_parameters, EMPTY_DICT))) - _parse_extend!(ext, a, b, dict, expr, kwargs, vars, additional_args) + _parse_extend!(ext, a, b, dict, expr, kwargs, vars, implicit_arglist) else error("Cannot infer the exact `Model` that `@extend $(body)` refers." * " Please specify the names that it brings into scope by:" * diff --git a/test/model_parsing.jl b/test/model_parsing.jl index 2dcb8e3f11..9cdd8712d4 100644 --- a/test/model_parsing.jl +++ b/test/model_parsing.jl @@ -932,3 +932,29 @@ end @test getdefault(main_sys.p2) == 12 @test getdefault(main_sys.v1) == 13 end + +@mtkmodel InnerModel begin + @parameters begin + p + end +end + +@mtkmodel MidModel begin + @components begin + inmodel = InnerModel() + end +end + +@mtkmodel OuterModel begin + @extend MidModel() + @equations begin + inmodel.p ~ 0 + end +end + +# The base system is fetched from the module while extending implicitly. This +# way of defining fails when defined inside the `@testset`. So, it is moved out. +@testset "Test unpacking of components in implicit extend" begin + @named out = OuterModel() + @test OuterModel.structure[:extend][1] == [:inmodel] +end From 2c2480937ee6cdc8e49e94695c11d6889a25f029 Mon Sep 17 00:00:00 2001 From: Sam Isaacson Date: Sun, 3 Nov 2024 14:29:20 -0500 Subject: [PATCH 0281/1337] cleanup JumpSystem constructor to match ODESystem better --- src/systems/jumps/jumpsystem.jl | 81 +++++++++++++++++++++++---------- 1 file changed, 57 insertions(+), 24 deletions(-) diff --git a/src/systems/jumps/jumpsystem.jl b/src/systems/jumps/jumpsystem.jl index 6409609a10..6a74c6018b 100644 --- a/src/systems/jumps/jumpsystem.jl +++ b/src/systems/jumps/jumpsystem.jl @@ -96,6 +96,11 @@ struct JumpSystem{U <: ArrayPartition} <: AbstractTimeDependentSystem """ discrete_events::Vector{SymbolicDiscreteCallback} """ + 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} + """ Topologically sorted parameter dependency equations, where all symbols are parameters and the LHS is a single parameter. """ @@ -160,13 +165,31 @@ function JumpSystem(eqs, iv, unknowns, ps; metadata = nothing, gui_metadata = nothing, kwargs...) + + # variable processing, similar to ODESystem name === nothing && throw(ArgumentError("The `name` keyword must be provided. Please consider using the `@named` macro")) - eqs = scalarize.(eqs) - sysnames = nameof.(systems) - if length(unique(sysnames)) != length(sysnames) - throw(ArgumentError("System names must be unique.")) + iv′ = value(iv) + us′ = value.(unknowns) + ps′ = value.(ps) + parameter_dependencies, ps = process_parameter_dependencies(parameter_dependencies, ps′) + if !(isempty(default_u0) && isempty(default_p)) + Base.depwarn( + "`default_u0` and `default_p` are deprecated. Use `defaults` instead.", + :JumpSystem, force = true) end + defaults = todict(defaults) + var_to_name = Dict() + process_variables!(var_to_name, defaults, us′) + process_variables!(var_to_name, defaults, ps′) + process_variables!(var_to_name, defaults, [eq.lhs for eq in parameter_dependencies]) + process_variables!(var_to_name, defaults, [eq.rhs for eq in parameter_dependencies]) + defaults = Dict{Any, Any}(value(k) => value(v) for (k, v) in pairs(defaults) + if value(v) !== nothing) + isempty(observed) || collect_var_to_name!(var_to_name, (eq.lhs for eq in observed)) + + # equation processing + eqs = scalarize.(eqs) ap = ArrayPartition(MassActionJump[], ConstantRateJump[], VariableRateJump[]) for eq in eqs if eq isa MassActionJump @@ -179,30 +202,42 @@ function JumpSystem(eqs, iv, unknowns, ps; error("JumpSystem equations must contain MassActionJumps, ConstantRateJumps, or VariableRateJumps.") end end - if !(isempty(default_u0) && isempty(default_p)) - Base.depwarn( - "`default_u0` and `default_p` are deprecated. Use `defaults` instead.", - :JumpSystem, force = true) + + sysnames = nameof.(systems) + if length(unique(sysnames)) != length(sysnames) + throw(ArgumentError("System names must be unique.")) end - defaults = todict(defaults) - defaults = Dict(value(k) => value(v) - for (k, v) in pairs(defaults) if value(v) !== nothing) - unknowns, ps = value.(unknowns), value.(ps) - var_to_name = Dict() - process_variables!(var_to_name, defaults, unknowns) - process_variables!(var_to_name, defaults, ps) - isempty(observed) || collect_var_to_name!(var_to_name, (eq.lhs for eq in observed)) (continuous_events === nothing) || error("JumpSystems currently only support discrete events.") disc_callbacks = SymbolicDiscreteCallbacks(discrete_events) - parameter_dependencies, ps = process_parameter_dependencies(parameter_dependencies, ps) + JumpSystem{typeof(ap)}(Threads.atomic_add!(SYSTEM_COUNT, UInt(1)), - ap, value(iv), unknowns, ps, var_to_name, observed, name, description, systems, + ap, iv′, us′, ps′, var_to_name, observed, name, description, systems, defaults, connector_type, disc_callbacks, parameter_dependencies, metadata, gui_metadata, checks = checks) end +##### MTK dispatches for JumpSystems ##### +function collect_vars!(unknowns, parameters, j::MassActionJump, iv; depth = 0, + op = Differential) + for field in (j.scaled_rates, j.reactant_stoch, j.net_stoch) + collect_vars!(unknowns, parameters, field, iv; depth, op) + end + return nothing +end + +function collect_vars!(unknowns, parameters, j::Union{ConstantRateJump,VariableRateJump}, + iv; depth = 0, op = Differential) + collect_vars!(unknowns, parameters, j.condition, iv; depth, op) + for eq in j.affect + (eq isa Equation) && collect_vars!(unknowns, parameters, eq, iv; depth, op) + end + return nothing +end + +########################################## + has_massactionjumps(js::JumpSystem) = !isempty(equations(js).x[1]) has_constantratejumps(js::JumpSystem) = !isempty(equations(js).x[2]) has_variableratejumps(js::JumpSystem) = !isempty(equations(js).x[3]) @@ -240,9 +275,8 @@ function assemble_vrj( 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!, outputidxs); + eval_expression, eval_module) VariableRateJump(rate, affect) end @@ -269,9 +303,8 @@ function assemble_crj( 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!, outputidxs); + eval_expression, eval_module) ConstantRateJump(rate, affect) end From dfcbe0ff21cca35c3d9347d42651ad20d3900cb7 Mon Sep 17 00:00:00 2001 From: CompatHelper Julia Date: Mon, 4 Nov 2024 00:24:44 +0000 Subject: [PATCH 0282/1337] CompatHelper: bump compat for SimpleNonlinearSolve to 2, (keep existing compat) --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 656fef6173..1c821c46db 100644 --- a/Project.toml +++ b/Project.toml @@ -125,7 +125,7 @@ SciMLBase = "2.57.1" SciMLStructures = "1.0" Serialization = "1" Setfield = "0.7, 0.8, 1" -SimpleNonlinearSolve = "0.1.0, 1" +SimpleNonlinearSolve = "0.1.0, 1, 2" SparseArrays = "1" SpecialFunctions = "0.7, 0.8, 0.9, 0.10, 1.0, 2" StaticArrays = "0.10, 0.11, 0.12, 1.0" From 6468f207589667e365f428065b8bdb85fc430978 Mon Sep 17 00:00:00 2001 From: CompatHelper Julia Date: Mon, 4 Nov 2024 00:24:48 +0000 Subject: [PATCH 0283/1337] CompatHelper: bump compat for NonlinearSolve to 4, (keep existing compat) --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 656fef6173..ef8fce84c2 100644 --- a/Project.toml +++ b/Project.toml @@ -111,7 +111,7 @@ Libdl = "1" LinearAlgebra = "1" MLStyle = "0.4.17" NaNMath = "0.3, 1" -NonlinearSolve = "3.14" +NonlinearSolve = "3.14, 4" OffsetArrays = "1" OrderedCollections = "1" OrdinaryDiffEq = "6.82.0" From 4af92778f8fc72701887e0bf379783a55553a5c7 Mon Sep 17 00:00:00 2001 From: CompatHelper Julia Date: Mon, 4 Nov 2024 00:25:21 +0000 Subject: [PATCH 0284/1337] CompatHelper: bump compat for NonlinearSolve to 4 for package docs, (keep existing compat) --- docs/Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Project.toml b/docs/Project.toml index 16fc8b4b07..edc956c3d2 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -30,7 +30,7 @@ Distributions = "0.25" Documenter = "1" DynamicQuantities = "^0.11.2, 0.12, 1" ModelingToolkit = "8.33, 9" -NonlinearSolve = "3" +NonlinearSolve = "3, 4" Optim = "1.7" Optimization = "3.9" OptimizationOptimJL = "0.1" From 9e04b036ed2250d29134647684b9bdff45cc625e Mon Sep 17 00:00:00 2001 From: Herman Sletmoen Date: Mon, 4 Nov 2024 14:02:20 +0100 Subject: [PATCH 0285/1337] Extend description and name in the same way --- src/systems/abstractsystem.jl | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index c53c900d27..10401d91c5 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -2995,13 +2995,14 @@ end """ $(TYPEDSIGNATURES) -Extend the `basesys` with `sys`, the resulting system would inherit `sys`'s name -by default. +Extend `basesys` with `sys`. +By default, the resulting system inherits `sys`'s name and description. See also [`compose`](@ref). """ -function extend(sys::AbstractSystem, basesys::AbstractSystem; name::Symbol = nameof(sys), - gui_metadata = get_gui_metadata(sys)) +function extend(sys::AbstractSystem, basesys::AbstractSystem; + name::Symbol = nameof(sys), description = description(sys), + gui_metadata = get_gui_metadata(sys)) T = SciMLBase.parameterless_type(basesys) ivs = independent_variables(basesys) if !(sys isa T) @@ -3023,13 +3024,12 @@ function extend(sys::AbstractSystem, basesys::AbstractSystem; name::Symbol = nam cevs = union(get_continuous_events(basesys), get_continuous_events(sys)) devs = union(get_discrete_events(basesys), get_discrete_events(sys)) defs = merge(get_defaults(basesys), get_defaults(sys)) # prefer `sys` - desc = join(filter(desc -> !isempty(desc), description.([sys, basesys])), " ") # concatenate non-empty descriptions with space meta = union_nothing(get_metadata(basesys), get_metadata(sys)) syss = union(get_systems(basesys), get_systems(sys)) args = length(ivs) == 0 ? (eqs, sts, ps) : (eqs, ivs[1], sts, ps) kwargs = (parameter_dependencies = dep_ps, observed = obs, continuous_events = cevs, discrete_events = devs, defaults = defs, systems = syss, metadata = meta, - name = name, description = desc, gui_metadata = gui_metadata) + name = name, description = description, gui_metadata = gui_metadata) # collect fields specific to some system types if basesys isa ODESystem @@ -3041,8 +3041,8 @@ function extend(sys::AbstractSystem, basesys::AbstractSystem; name::Symbol = nam return T(args...; kwargs...) end -function Base.:(&)(sys::AbstractSystem, basesys::AbstractSystem; name::Symbol = nameof(sys)) - extend(sys, basesys; name = name) +function Base.:(&)(sys::AbstractSystem, basesys::AbstractSystem; kwargs...) + extend(sys, basesys; kwargs...) end """ From 413d258c5e90f146657b588e898a8d342f430b9e Mon Sep 17 00:00:00 2001 From: Sam Isaacson Date: Mon, 4 Nov 2024 10:01:44 -0500 Subject: [PATCH 0286/1337] get tests workings --- src/systems/jumps/jumpsystem.jl | 32 ++++++++++++++--------------- test/jumpsystem.jl | 36 +++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 16 deletions(-) diff --git a/src/systems/jumps/jumpsystem.jl b/src/systems/jumps/jumpsystem.jl index 6a74c6018b..d59a2df5ce 100644 --- a/src/systems/jumps/jumpsystem.jl +++ b/src/systems/jumps/jumpsystem.jl @@ -96,11 +96,6 @@ struct JumpSystem{U <: ArrayPartition} <: AbstractTimeDependentSystem """ discrete_events::Vector{SymbolicDiscreteCallback} """ - 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} - """ Topologically sorted parameter dependency equations, where all symbols are parameters and the LHS is a single parameter. """ @@ -172,13 +167,14 @@ function JumpSystem(eqs, iv, unknowns, ps; iv′ = value(iv) us′ = value.(unknowns) ps′ = value.(ps) - parameter_dependencies, ps = process_parameter_dependencies(parameter_dependencies, ps′) + parameter_dependencies, ps′ = process_parameter_dependencies( + parameter_dependencies, ps′) if !(isempty(default_u0) && isempty(default_p)) Base.depwarn( "`default_u0` and `default_p` are deprecated. Use `defaults` instead.", :JumpSystem, force = true) end - defaults = todict(defaults) + defaults = Dict{Any,Any}(todict(defaults)) var_to_name = Dict() process_variables!(var_to_name, defaults, us′) process_variables!(var_to_name, defaults, ps′) @@ -188,7 +184,14 @@ function JumpSystem(eqs, iv, unknowns, ps; if value(v) !== nothing) isempty(observed) || collect_var_to_name!(var_to_name, (eq.lhs for eq in observed)) + sysnames = nameof.(systems) + if length(unique(sysnames)) != length(sysnames) + throw(ArgumentError("System names must be unique.")) + end + # equation processing + # this and the treatment of continuous events are the only part + # unique to JumpSystems eqs = scalarize.(eqs) ap = ArrayPartition(MassActionJump[], ConstantRateJump[], VariableRateJump[]) for eq in eqs @@ -203,11 +206,6 @@ function JumpSystem(eqs, iv, unknowns, ps; end end - sysnames = nameof.(systems) - if length(unique(sysnames)) != length(sysnames) - throw(ArgumentError("System names must be unique.")) - end - (continuous_events === nothing) || error("JumpSystems currently only support discrete events.") disc_callbacks = SymbolicDiscreteCallbacks(discrete_events) @@ -220,17 +218,19 @@ end ##### MTK dispatches for JumpSystems ##### function collect_vars!(unknowns, parameters, j::MassActionJump, iv; depth = 0, - op = Differential) + op = Differential) for field in (j.scaled_rates, j.reactant_stoch, j.net_stoch) - collect_vars!(unknowns, parameters, field, iv; depth, op) + for el in field + collect_vars!(unknowns, parameters, el, iv; depth, op) + end end return nothing end function collect_vars!(unknowns, parameters, j::Union{ConstantRateJump,VariableRateJump}, iv; depth = 0, op = Differential) - collect_vars!(unknowns, parameters, j.condition, iv; depth, op) - for eq in j.affect + collect_vars!(unknowns, parameters, j.rate, iv; depth, op) + for eq in j.affect! (eq isa Equation) && collect_vars!(unknowns, parameters, eq, iv; depth, op) end return nothing diff --git a/test/jumpsystem.jl b/test/jumpsystem.jl index 24b9abbc8a..16181d1938 100644 --- a/test/jumpsystem.jl +++ b/test/jumpsystem.jl @@ -340,3 +340,39 @@ let @test all(abs.(cmean .- cmean2) .<= 0.05 .* cmean) end + + +# collect_vars! tests for jumps +let + @variables x1(t) x2(t) x3(t) x4(t) x5(t) + @parameters p1 p2 p3 p4 p5 + j1 = ConstantRateJump(p1, [x1 ~ x1 + 1]) + j2 = MassActionJump(p2, [x2 => 1], [x3 => -1]) + j3 = VariableRateJump(p3, [x3 ~ x3 + 1, x4 ~ x4 + 1]) + j4 = MassActionJump(p4*p5, [x1 => 1, x5 => 1], [x1 => -1, x5 => -1, x2 => 1]) + us = Set() + ps = Set() + iv = t + + MT.collect_vars!(us, ps, j1, iv) + @test issetequal(us, [x1]) + @test issetequal(ps, [p1]) + + empty!(us) + empty!(ps) + MT.collect_vars!(us, ps, j2, iv) + @test issetequal(us, [x2, x3]) + @test issetequal(ps, [p2]) + + empty!(us) + empty!(ps) + MT.collect_vars!(us, ps, j3, iv) + @test issetequal(us, [x3, x4]) + @test issetequal(ps, [p3]) + + empty!(us) + empty!(ps) + MT.collect_vars!(us, ps, j4, iv) + @test issetequal(us, [x1, x5, x2]) + @test issetequal(ps, [p4, p5]) +end From 4daf8846d9b9555b3db071bde36c708d4ec2ce30 Mon Sep 17 00:00:00 2001 From: Sam Isaacson Date: Mon, 4 Nov 2024 10:10:08 -0500 Subject: [PATCH 0287/1337] tweak collect_vars --- src/systems/jumps/jumpsystem.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/systems/jumps/jumpsystem.jl b/src/systems/jumps/jumpsystem.jl index d59a2df5ce..f444a2480e 100644 --- a/src/systems/jumps/jumpsystem.jl +++ b/src/systems/jumps/jumpsystem.jl @@ -219,7 +219,8 @@ end ##### MTK dispatches for JumpSystems ##### function collect_vars!(unknowns, parameters, j::MassActionJump, iv; depth = 0, op = Differential) - for field in (j.scaled_rates, j.reactant_stoch, j.net_stoch) + collect_vars!(unknowns, parameters, j.scaled_rates, iv; depth, op) + for field in (j.reactant_stoch, j.net_stoch) for el in field collect_vars!(unknowns, parameters, el, iv; depth, op) end From 75f4ec6121b1c99eab4fef12a084b1df6c3067a0 Mon Sep 17 00:00:00 2001 From: Sam Isaacson Date: Mon, 4 Nov 2024 11:38:26 -0500 Subject: [PATCH 0288/1337] update collecting vars with scoping --- src/systems/abstractsystem.jl | 17 ++++++------ src/systems/jumps/jumpsystem.jl | 2 ++ test/jumpsystem.jl | 46 +++++++++++++++++++++++++++++++++ 3 files changed, 56 insertions(+), 9 deletions(-) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index c53c900d27..829310927f 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -923,15 +923,14 @@ One property to note is that if a system is complete, the system will no longer namespace its subsystems or variables, i.e. `isequal(complete(sys).v.i, v.i)`. """ function complete(sys::AbstractSystem; split = true, flatten = true) - if !(sys isa JumpSystem) - newunknowns = OrderedSet() - newparams = OrderedSet() - iv = has_iv(sys) ? get_iv(sys) : nothing - collect_scoped_vars!(newunknowns, newparams, sys, iv; depth = -1) - # don't update unknowns to not disturb `structural_simplify` order - # `GlobalScope`d unknowns will be picked up and added there - @set! sys.ps = unique!(vcat(get_ps(sys), collect(newparams))) - end + newunknowns = OrderedSet() + newparams = OrderedSet() + iv = has_iv(sys) ? get_iv(sys) : nothing + collect_scoped_vars!(newunknowns, newparams, sys, iv; depth = -1) + # don't update unknowns to not disturb `structural_simplify` order + # `GlobalScope`d unknowns will be picked up and added there + @set! sys.ps = unique!(vcat(get_ps(sys), collect(newparams))) + if flatten eqs = equations(sys) if eqs isa AbstractArray && eltype(eqs) <: Equation diff --git a/src/systems/jumps/jumpsystem.jl b/src/systems/jumps/jumpsystem.jl index f444a2480e..2fb92db62f 100644 --- a/src/systems/jumps/jumpsystem.jl +++ b/src/systems/jumps/jumpsystem.jl @@ -217,6 +217,7 @@ function JumpSystem(eqs, iv, unknowns, ps; end ##### MTK dispatches for JumpSystems ##### +eqtype_supports_collect_vars(j::MassActionJump) = true function collect_vars!(unknowns, parameters, j::MassActionJump, iv; depth = 0, op = Differential) collect_vars!(unknowns, parameters, j.scaled_rates, iv; depth, op) @@ -228,6 +229,7 @@ function collect_vars!(unknowns, parameters, j::MassActionJump, iv; depth = 0, return nothing end +eqtype_supports_collect_vars(j::Union{ConstantRateJump,VariableRateJump}) = true function collect_vars!(unknowns, parameters, j::Union{ConstantRateJump,VariableRateJump}, iv; depth = 0, op = Differential) collect_vars!(unknowns, parameters, j.rate, iv; depth, op) diff --git a/test/jumpsystem.jl b/test/jumpsystem.jl index 16181d1938..693cd39ddd 100644 --- a/test/jumpsystem.jl +++ b/test/jumpsystem.jl @@ -376,3 +376,49 @@ let @test issetequal(us, [x1, x5, x2]) @test issetequal(ps, [p4, p5]) end + +# scoping tests +let + @variables x1(t) x2(t) x3(t) x4(t) x5(t) + x2 = ParentScope(x2) + x3 = ParentScope(ParentScope(x3)) + x4 = DelayParentScope(x4, 2) + x5 = GlobalScope(x5) + @parameters p1 p2 p3 p4 p5 + p2 = ParentScope(p2) + p3 = ParentScope(ParentScope(p3)) + p4 = DelayParentScope(p4, 2) + p5 = GlobalScope(p5) + + j1 = ConstantRateJump(p1, [x1 ~ x1 + 1]) + j2 = MassActionJump(p2, [x2 => 1], [x3 => -1]) + j3 = VariableRateJump(p3, [x3 ~ x3 + 1, x4 ~ 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]) + + us = Set(); ps = Set() + iv = t + MT.collect_scoped_vars!(us, ps, js, iv) + @test issetequal(us, [x2]) + @test issetequal(ps, [p2]) + + empty!.((us,ps)) + MT.collect_scoped_vars!(us, ps, js, iv; depth = 0) + @test issetequal(us, [x1]) + @test issetequal(ps, [p1]) + + empty!.((us,ps)) + MT.collect_scoped_vars!(us, ps, js, iv; depth = 1) + @test issetequal(us, [x2]) + @test issetequal(ps, [p2]) + + empty!.((us,ps)) + MT.collect_scoped_vars!(us, ps, js, iv; depth = 2) + @test issetequal(us, [x3, x4]) + @test issetequal(ps, [p3, p4]) + + empty!.((us,ps)) + MT.collect_scoped_vars!(us, ps, js, iv; depth = -1) + @test issetequal(us, [x5]) + @test issetequal(ps, [p5]) +end From e6ebcb9b5ca21e796670d731ff6f4913ce21a1ac Mon Sep 17 00:00:00 2001 From: Sam Isaacson Date: Mon, 4 Nov 2024 13:09:34 -0500 Subject: [PATCH 0289/1337] format --- src/systems/abstractsystem.jl | 2 +- src/systems/jumps/jumpsystem.jl | 19 ++++++++++--------- test/jumpsystem.jl | 20 ++++++++++---------- 3 files changed, 21 insertions(+), 20 deletions(-) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index 829310927f..e565bb27e1 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -930,7 +930,7 @@ function complete(sys::AbstractSystem; split = true, flatten = true) # don't update unknowns to not disturb `structural_simplify` order # `GlobalScope`d unknowns will be picked up and added there @set! sys.ps = unique!(vcat(get_ps(sys), collect(newparams))) - + if flatten eqs = equations(sys) if eqs isa AbstractArray && eltype(eqs) <: Equation diff --git a/src/systems/jumps/jumpsystem.jl b/src/systems/jumps/jumpsystem.jl index 2fb92db62f..9fa073b4b0 100644 --- a/src/systems/jumps/jumpsystem.jl +++ b/src/systems/jumps/jumpsystem.jl @@ -174,14 +174,15 @@ function JumpSystem(eqs, iv, unknowns, ps; "`default_u0` and `default_p` are deprecated. Use `defaults` instead.", :JumpSystem, force = true) end - defaults = Dict{Any,Any}(todict(defaults)) + defaults = Dict{Any, Any}(todict(defaults)) var_to_name = Dict() process_variables!(var_to_name, defaults, us′) process_variables!(var_to_name, defaults, ps′) process_variables!(var_to_name, defaults, [eq.lhs for eq in parameter_dependencies]) process_variables!(var_to_name, defaults, [eq.rhs for eq in parameter_dependencies]) - defaults = Dict{Any, Any}(value(k) => value(v) for (k, v) in pairs(defaults) - if value(v) !== nothing) + #! format: off + defaults = Dict{Any, Any}(value(k) => value(v) for (k, v) in pairs(defaults) if value(v) !== nothing) + #! format: on isempty(observed) || collect_var_to_name!(var_to_name, (eq.lhs for eq in observed)) sysnames = nameof.(systems) @@ -218,8 +219,8 @@ end ##### MTK dispatches for JumpSystems ##### eqtype_supports_collect_vars(j::MassActionJump) = true -function collect_vars!(unknowns, parameters, j::MassActionJump, iv; depth = 0, - op = Differential) +function collect_vars!(unknowns, parameters, j::MassActionJump, iv; depth = 0, + op = Differential) collect_vars!(unknowns, parameters, j.scaled_rates, iv; depth, op) for field in (j.reactant_stoch, j.net_stoch) for el in field @@ -229,8 +230,8 @@ function collect_vars!(unknowns, parameters, j::MassActionJump, iv; depth = 0, return nothing end -eqtype_supports_collect_vars(j::Union{ConstantRateJump,VariableRateJump}) = true -function collect_vars!(unknowns, parameters, j::Union{ConstantRateJump,VariableRateJump}, +eqtype_supports_collect_vars(j::Union{ConstantRateJump, VariableRateJump}) = true +function collect_vars!(unknowns, parameters, j::Union{ConstantRateJump, VariableRateJump}, iv; depth = 0, op = Differential) collect_vars!(unknowns, parameters, j.rate, iv; depth, op) for eq in j.affect! @@ -278,7 +279,7 @@ function assemble_vrj( 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); + affect = eval_or_rgf(generate_affect_function(js, vrj.affect!, outputidxs); eval_expression, eval_module) VariableRateJump(rate, affect) end @@ -306,7 +307,7 @@ function assemble_crj( 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); + affect = eval_or_rgf(generate_affect_function(js, crj.affect!, outputidxs); eval_expression, eval_module) ConstantRateJump(rate, affect) end diff --git a/test/jumpsystem.jl b/test/jumpsystem.jl index 693cd39ddd..30cb636226 100644 --- a/test/jumpsystem.jl +++ b/test/jumpsystem.jl @@ -341,15 +341,14 @@ let @test all(abs.(cmean .- cmean2) .<= 0.05 .* cmean) end - # collect_vars! tests for jumps -let +let @variables x1(t) x2(t) x3(t) x4(t) x5(t) @parameters p1 p2 p3 p4 p5 j1 = ConstantRateJump(p1, [x1 ~ x1 + 1]) j2 = MassActionJump(p2, [x2 => 1], [x3 => -1]) j3 = VariableRateJump(p3, [x3 ~ x3 + 1, x4 ~ x4 + 1]) - j4 = MassActionJump(p4*p5, [x1 => 1, x5 => 1], [x1 => -1, x5 => -1, x2 => 1]) + j4 = MassActionJump(p4 * p5, [x1 => 1, x5 => 1], [x1 => -1, x5 => -1, x2 => 1]) us = Set() ps = Set() iv = t @@ -389,35 +388,36 @@ let p3 = ParentScope(ParentScope(p3)) p4 = DelayParentScope(p4, 2) p5 = GlobalScope(p5) - + j1 = ConstantRateJump(p1, [x1 ~ x1 + 1]) j2 = MassActionJump(p2, [x2 => 1], [x3 => -1]) j3 = VariableRateJump(p3, [x3 ~ x3 + 1, x4 ~ x4 + 1]) - j4 = MassActionJump(p4*p5, [x1 => 1, x5 => 1], [x1 => -1, x5 => -1, x2 => 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]) - us = Set(); ps = Set() + us = Set() + ps = Set() iv = t MT.collect_scoped_vars!(us, ps, js, iv) @test issetequal(us, [x2]) @test issetequal(ps, [p2]) - empty!.((us,ps)) + empty!.((us, ps)) MT.collect_scoped_vars!(us, ps, js, iv; depth = 0) @test issetequal(us, [x1]) @test issetequal(ps, [p1]) - empty!.((us,ps)) + empty!.((us, ps)) MT.collect_scoped_vars!(us, ps, js, iv; depth = 1) @test issetequal(us, [x2]) @test issetequal(ps, [p2]) - empty!.((us,ps)) + empty!.((us, ps)) MT.collect_scoped_vars!(us, ps, js, iv; depth = 2) @test issetequal(us, [x3, x4]) @test issetequal(ps, [p3, p4]) - empty!.((us,ps)) + empty!.((us, ps)) MT.collect_scoped_vars!(us, ps, js, iv; depth = -1) @test issetequal(us, [x5]) @test issetequal(ps, [p5]) From 29ffd992535ddcd5427d69905b6905719c747442 Mon Sep 17 00:00:00 2001 From: Sam Isaacson Date: Mon, 4 Nov 2024 14:17:22 -0500 Subject: [PATCH 0290/1337] add equations --- src/systems/jumps/jumpsystem.jl | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/systems/jumps/jumpsystem.jl b/src/systems/jumps/jumpsystem.jl index 9fa073b4b0..8b2e8e5ded 100644 --- a/src/systems/jumps/jumpsystem.jl +++ b/src/systems/jumps/jumpsystem.jl @@ -194,7 +194,8 @@ function JumpSystem(eqs, iv, unknowns, ps; # this and the treatment of continuous events are the only part # unique to JumpSystems eqs = scalarize.(eqs) - ap = ArrayPartition(MassActionJump[], ConstantRateJump[], VariableRateJump[]) + ap = ArrayPartition( + MassActionJump[], ConstantRateJump[], VariableRateJump[], Equation[]) for eq in eqs if eq isa MassActionJump push!(ap.x[1], eq) @@ -202,8 +203,10 @@ function JumpSystem(eqs, iv, unknowns, ps; push!(ap.x[2], eq) elseif eq isa VariableRateJump push!(ap.x[3], eq) + elseif eq isa Equation + push!(ap.x[4], eq) else - error("JumpSystem equations must contain MassActionJumps, ConstantRateJumps, or VariableRateJumps.") + error("JumpSystem equations must contain MassActionJumps, ConstantRateJumps, VariableRateJumps, or Equations.") end end @@ -245,6 +248,7 @@ end has_massactionjumps(js::JumpSystem) = !isempty(equations(js).x[1]) has_constantratejumps(js::JumpSystem) = !isempty(equations(js).x[2]) has_variableratejumps(js::JumpSystem) = !isempty(equations(js).x[3]) +has_equations(js::JumpSystem) = !isempty(equations(js).x[4]) function generate_rate_function(js::JumpSystem, rate) consts = collect_constants(rate) @@ -390,6 +394,11 @@ function DiffEqBase.DiscreteProblem(sys::JumpSystem, u0map, tspan::Union{Tuple, if !iscomplete(sys) error("A completed `JumpSystem` is required. Call `complete` or `structural_simplify` on the system before creating a `DiscreteProblem`") end + + if has_equations(sys) + error("The passed in JumpSystem contains `Equations`, please use a problem type that supports equations such as such as ODEProblem.") + end + _, u0, p = process_SciMLProblem(EmptySciMLFunction, sys, u0map, parammap; t = tspan === nothing ? nothing : tspan[1], use_union, tofloat = false, check_length = false) f = DiffEqBase.DISCRETE_INPLACE_DEFAULT From 6a720421d8ebc628b8b9e97f868916954a66b36d Mon Sep 17 00:00:00 2001 From: Sam Isaacson Date: Mon, 4 Nov 2024 15:06:43 -0500 Subject: [PATCH 0291/1337] hybrid jump-ODEs --- src/systems/jumps/jumpsystem.jl | 32 ++++++++++++++++++++++---------- test/jumpsystem.jl | 22 ++++++++++++++++++++++ 2 files changed, 44 insertions(+), 10 deletions(-) diff --git a/src/systems/jumps/jumpsystem.jl b/src/systems/jumps/jumpsystem.jl index 8b2e8e5ded..7950d2cc98 100644 --- a/src/systems/jumps/jumpsystem.jl +++ b/src/systems/jumps/jumpsystem.jl @@ -482,19 +482,31 @@ function DiffEqBase.ODEProblem(sys::JumpSystem, u0map, tspan::Union{Tuple, Nothi use_union = false, eval_expression = false, eval_module = @__MODULE__, + check_length = false, kwargs...) if !iscomplete(sys) error("A completed `JumpSystem` is required. Call `complete` or `structural_simplify` on the system before creating a `DiscreteProblem`") end - _, u0, p = process_SciMLProblem(EmptySciMLFunction, sys, u0map, parammap; - t = tspan === nothing ? nothing : tspan[1], use_union, tofloat = false, check_length = false) - - observedfun = ObservedFunctionCache(sys; eval_expression, eval_module) - - f = (du, u, p, t) -> (du .= 0; nothing) - df = ODEFunction(f; sys, observed = observedfun) - ODEProblem(df, u0, tspan, p; kwargs...) + # forward everything to be an ODESystem but the jumps + if has_equations(sys) + osys = ODESystem(equations(sys).x[4], get_iv(sys), unknowns(sys), parameters(sys); + observed = observed(sys), name = nameof(sys), description = description(sys), + systems = get_systems(sys), defaults = defaults(sys), + discrete_events = discrete_events(sys), + parameter_dependencies = parameter_dependencies(sys), + metadata = get_metadata(sys), gui_metadata = get_gui_metadata(sys)) + osys = complete(osys) + return ODEProblem(osys, u0map, tspan, parammap; check_length, kwargs...) + else + _, u0, p = process_SciMLProblem(EmptySciMLFunction, sys, u0map, parammap; + t = tspan === nothing ? nothing : tspan[1], use_union, tofloat = false, + check_length = false) + f = (du, u, p, t) -> (du .= 0; nothing) + observedfun = ObservedFunctionCache(sys; eval_expression, eval_module) + df = ODEFunction(f; sys, observed = observedfun) + return ODEProblem(df, u0, tspan, p; check_length, kwargs...) + end end """ @@ -530,8 +542,8 @@ function JumpProcesses.JumpProblem(js::JumpSystem, prob, for j in eqs.x[2]] vrjs = VariableRateJump[assemble_vrj(js, j, unknowntoid; eval_expression, eval_module) for j in eqs.x[3]] - ((prob isa DiscreteProblem) && !isempty(vrjs)) && - error("Use continuous problems such as an ODEProblem or a SDEProblem with VariableRateJumps") + ((prob isa DiscreteProblem) && (!isempty(vrjs) || has_equations(js))) && + error("Use continuous problems such as an ODEProblem or a SDEProblem with VariableRateJumps and/or coupled differential equations.") jset = JumpSet(Tuple(vrjs), Tuple(crjs), nothing, majs) # dep graphs are only for constant rate jumps diff --git a/test/jumpsystem.jl b/test/jumpsystem.jl index 30cb636226..76d11f6205 100644 --- a/test/jumpsystem.jl +++ b/test/jumpsystem.jl @@ -422,3 +422,25 @@ let @test issetequal(us, [x5]) @test issetequal(ps, [p5]) end + +# PDMP test +let + @variables X(t) Y(t) + @parameters k1 k2 + rate1 = k1 * X + affect1! = [X ~ X - 1] + rate2 = k1 + affect2! = [Y ~ Y + 1] + eqs = [D(X) ~ k2, D(Y) ~ -k2/100*Y] + vrj1 = VariableRateJump(rate1, affect1!) + vrj2 = VariableRateJump(rate2, affect2!) + @named jsys = JumpSystem([vrj1, vrj2, eqs[1], eqs[2]], t, [X, Y], [k1, k2]) + jsys = complete(jsys) + u0 = [X => 0.0, Y => 0.0] + p = [k1 => 1.0, k2 => 20.0] + tspan = (0.0, 20.0) + oprob = ODEProblem(jsys, u0, tspan, p) + jprob = JumpProblem(jsys, oprob; rng) + sol = solve(jprob, Tsit5()) + +end \ No newline at end of file From 922800bf24c38bc3d63fc6c66547e2734084e288 Mon Sep 17 00:00:00 2001 From: Sam Isaacson Date: Mon, 4 Nov 2024 15:42:56 -0500 Subject: [PATCH 0292/1337] add continuous events --- src/systems/jumps/jumpsystem.jl | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/src/systems/jumps/jumpsystem.jl b/src/systems/jumps/jumpsystem.jl index 7950d2cc98..f046b7ed78 100644 --- a/src/systems/jumps/jumpsystem.jl +++ b/src/systems/jumps/jumpsystem.jl @@ -88,6 +88,11 @@ 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 @@ -120,8 +125,7 @@ struct JumpSystem{U <: ArrayPartition} <: AbstractTimeDependentSystem function JumpSystem{U}( tag, ap::U, iv, unknowns, ps, var_to_name, observed, name, description, - systems, - defaults, connector_type, devents, parameter_dependencies, + systems, defaults, connector_type, cevents, devents, parameter_dependencies, metadata = nothing, gui_metadata = nothing, complete = false, index_cache = nothing, isscheduled = false; checks::Union{Bool, Int} = true) where {U <: ArrayPartition} @@ -136,8 +140,8 @@ struct JumpSystem{U <: ArrayPartition} <: AbstractTimeDependentSystem end new{U}(tag, ap, iv, unknowns, ps, var_to_name, observed, name, description, systems, defaults, - connector_type, devents, parameter_dependencies, metadata, gui_metadata, - complete, index_cache, isscheduled) + connector_type, cevents, devents, parameter_dependencies, metadata, + gui_metadata, complete, index_cache, isscheduled) end end function JumpSystem(tag, ap, iv, states, ps, var_to_name, args...; kwargs...) @@ -210,13 +214,12 @@ function JumpSystem(eqs, iv, unknowns, ps; end end - (continuous_events === nothing) || - error("JumpSystems currently only support discrete events.") + cont_callbacks = SymbolicContinuousCallbacks(continuous_events) disc_callbacks = SymbolicDiscreteCallbacks(discrete_events) JumpSystem{typeof(ap)}(Threads.atomic_add!(SYSTEM_COUNT, UInt(1)), ap, iv′, us′, ps′, var_to_name, observed, name, description, systems, - defaults, connector_type, disc_callbacks, parameter_dependencies, + defaults, connector_type, cont_callbacks, disc_callbacks, parameter_dependencies, metadata, gui_metadata, checks = checks) end @@ -399,6 +402,10 @@ function DiffEqBase.DiscreteProblem(sys::JumpSystem, u0map, tspan::Union{Tuple, error("The passed in JumpSystem contains `Equations`, please use a problem type that supports equations such as such as ODEProblem.") end + if !isempty(continuous_events(sys)) + error("The passed in JumpSystem contains `ContinuousEvents`, please use a problem type that supports continuous events such as such as ODEProblem.") + end + _, u0, p = process_SciMLProblem(EmptySciMLFunction, sys, u0map, parammap; t = tspan === nothing ? nothing : tspan[1], use_union, tofloat = false, check_length = false) f = DiffEqBase.DISCRETE_INPLACE_DEFAULT @@ -488,12 +495,12 @@ function DiffEqBase.ODEProblem(sys::JumpSystem, u0map, tspan::Union{Tuple, Nothi error("A completed `JumpSystem` is required. Call `complete` or `structural_simplify` on the system before creating a `DiscreteProblem`") end - # forward everything to be an ODESystem but the jumps + # forward everything to be an ODESystem but the jumps and discrete events if has_equations(sys) osys = ODESystem(equations(sys).x[4], get_iv(sys), unknowns(sys), parameters(sys); observed = observed(sys), name = nameof(sys), description = description(sys), systems = get_systems(sys), defaults = defaults(sys), - discrete_events = discrete_events(sys), + continuous_events = continuous_events(sys), parameter_dependencies = parameter_dependencies(sys), metadata = get_metadata(sys), gui_metadata = get_gui_metadata(sys)) osys = complete(osys) @@ -542,8 +549,11 @@ function JumpProcesses.JumpProblem(js::JumpSystem, prob, for j in eqs.x[2]] vrjs = VariableRateJump[assemble_vrj(js, j, unknowntoid; eval_expression, eval_module) for j in eqs.x[3]] - ((prob isa DiscreteProblem) && (!isempty(vrjs) || has_equations(js))) && - error("Use continuous problems such as an ODEProblem or a SDEProblem with VariableRateJumps and/or coupled differential equations.") + if prob isa DiscreteProblem + if (!isempty(vrjs) || has_equations(js) || !isempty(continous_events(js))) + error("Use continuous problems such as an ODEProblem or a SDEProblem with VariableRateJumps, coupled differential equations, or continuous events.") + end + end jset = JumpSet(Tuple(vrjs), Tuple(crjs), nothing, majs) # dep graphs are only for constant rate jumps From a6c5c6cf9366686ad078084c2ce92d8a8e58cde2 Mon Sep 17 00:00:00 2001 From: Sam Isaacson Date: Mon, 4 Nov 2024 15:45:58 -0500 Subject: [PATCH 0293/1337] typos --- src/systems/jumps/jumpsystem.jl | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/systems/jumps/jumpsystem.jl b/src/systems/jumps/jumpsystem.jl index f046b7ed78..4cf82e4bec 100644 --- a/src/systems/jumps/jumpsystem.jl +++ b/src/systems/jumps/jumpsystem.jl @@ -398,12 +398,8 @@ function DiffEqBase.DiscreteProblem(sys::JumpSystem, u0map, tspan::Union{Tuple, error("A completed `JumpSystem` is required. Call `complete` or `structural_simplify` on the system before creating a `DiscreteProblem`") end - if has_equations(sys) - error("The passed in JumpSystem contains `Equations`, please use a problem type that supports equations such as such as ODEProblem.") - end - - if !isempty(continuous_events(sys)) - error("The passed in JumpSystem contains `ContinuousEvents`, please use a problem type that supports continuous events such as such as ODEProblem.") + if has_equations(sys) || (!isempty(continuous_events(sys))) + error("The passed in JumpSystem contains `Equation`s or continuous events, please use a problem type that supports these features, such as ODEProblem.") end _, u0, p = process_SciMLProblem(EmptySciMLFunction, sys, u0map, parammap; From 4f1cd262144f152d67fc9ebbaefeee9d59c7751d Mon Sep 17 00:00:00 2001 From: Sam Isaacson Date: Mon, 4 Nov 2024 17:04:07 -0500 Subject: [PATCH 0294/1337] spelling --- 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 4cf82e4bec..94cf2b67e8 100644 --- a/src/systems/jumps/jumpsystem.jl +++ b/src/systems/jumps/jumpsystem.jl @@ -546,7 +546,7 @@ function JumpProcesses.JumpProblem(js::JumpSystem, prob, vrjs = VariableRateJump[assemble_vrj(js, j, unknowntoid; eval_expression, eval_module) for j in eqs.x[3]] if prob isa DiscreteProblem - if (!isempty(vrjs) || has_equations(js) || !isempty(continous_events(js))) + if (!isempty(vrjs) || has_equations(js) || !isempty(continuous_events(js))) error("Use continuous problems such as an ODEProblem or a SDEProblem with VariableRateJumps, coupled differential equations, or continuous events.") end end From eda30b9c5e165798bf64f6170cce148ebce9526e Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 5 Nov 2024 14:20:47 +0530 Subject: [PATCH 0295/1337] fix: `complete` with `split = false` removes the index cache --- src/systems/abstractsystem.jl | 2 ++ test/odesystem.jl | 9 +++++++++ 2 files changed, 11 insertions(+) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index 10401d91c5..9a2b2369bd 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -985,6 +985,8 @@ function complete(sys::AbstractSystem; split = true, flatten = true) end @set! sys.ps = ordered_ps end + elseif has_index_cache(sys) + @set! sys.index_cache = nothing end if isdefined(sys, :initializesystem) && get_initializesystem(sys) !== nothing @set! sys.initializesystem = complete(get_initializesystem(sys); split) diff --git a/test/odesystem.jl b/test/odesystem.jl index dfc755da98..d7a2658f63 100644 --- a/test/odesystem.jl +++ b/test/odesystem.jl @@ -1495,3 +1495,12 @@ end sys2 = complete(sys) @test length(equations(sys2)) == total_eqs end + +@testset "`complete` with `split = false` removes the index cache" begin + @variables x(t) + @parameters p + @mtkbuild sys = ODESystem(D(x) ~ p * t, t) + @test ModelingToolkit.get_index_cache(sys) !== nothing + sys2 = complete(sys; split = false) + @test ModelingToolkit.get_index_cache(sys2) === nothing +end From eaf5edc646b41d41f30878cb0b368756ba0c017c Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 5 Nov 2024 14:20:51 +0530 Subject: [PATCH 0296/1337] refactor: format --- src/systems/abstractsystem.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index 9a2b2369bd..80bc3d44d4 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -3003,8 +3003,8 @@ By default, the resulting system inherits `sys`'s name and description. See also [`compose`](@ref). """ function extend(sys::AbstractSystem, basesys::AbstractSystem; - name::Symbol = nameof(sys), description = description(sys), - gui_metadata = get_gui_metadata(sys)) + name::Symbol = nameof(sys), description = description(sys), + gui_metadata = get_gui_metadata(sys)) T = SciMLBase.parameterless_type(basesys) ivs = independent_variables(basesys) if !(sys isa T) From 022af77d450f68fd611885afb77cb8de5820f065 Mon Sep 17 00:00:00 2001 From: Sam Isaacson Date: Tue, 5 Nov 2024 14:42:41 -0500 Subject: [PATCH 0297/1337] Add continuous event support --- src/systems/callbacks.jl | 16 ++++++++-------- src/systems/jumps/jumpsystem.jl | 6 ++---- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/src/systems/callbacks.jl b/src/systems/callbacks.jl index c427ae02a1..9ca69bf151 100644 --- a/src/systems/callbacks.jl +++ b/src/systems/callbacks.jl @@ -1,6 +1,6 @@ #################################### system operations ##################################### get_continuous_events(sys::AbstractSystem) = SymbolicContinuousCallback[] -get_continuous_events(sys::AbstractODESystem) = getfield(sys, :continuous_events) +get_continuous_events(sys::AbstractTimeDependentSystem) = getfield(sys, :continuous_events) has_continuous_events(sys::AbstractSystem) = isdefined(sys, :continuous_events) has_discrete_events(sys::AbstractSystem) = isdefined(sys, :discrete_events) @@ -560,7 +560,7 @@ function compile_affect(eqs::Vector{Equation}, cb, sys, dvs, ps; outputidxs = no end end -function generate_rootfinding_callback(sys::AbstractODESystem, dvs = unknowns(sys), +function generate_rootfinding_callback(sys::AbstractTimeDependentSystem, dvs = unknowns(sys), ps = parameters(sys); kwargs...) cbs = continuous_events(sys) isempty(cbs) && return nothing @@ -571,7 +571,7 @@ Generate a single rootfinding callback; this happens if there is only one equati generate_rootfinding_callback and thus we can produce a ContinuousCallback instead of a VectorContinuousCallback. """ function generate_single_rootfinding_callback( - eq, cb, sys::AbstractODESystem, dvs = unknowns(sys), + eq, cb, sys::AbstractTimeDependentSystem, dvs = unknowns(sys), ps = parameters(sys); kwargs...) if !isequal(eq.lhs, 0) eq = 0 ~ eq.lhs - eq.rhs @@ -609,7 +609,7 @@ function generate_single_rootfinding_callback( end function generate_vector_rootfinding_callback( - cbs, sys::AbstractODESystem, dvs = unknowns(sys), + cbs, sys::AbstractTimeDependentSystem, dvs = unknowns(sys), ps = parameters(sys); rootfind = SciMLBase.RightRootFind, reinitialization = SciMLBase.CheckInit(), kwargs...) eqs = map(cb -> flatten_equations(cb.eqs), cbs) @@ -683,7 +683,7 @@ end """ Compile a single continuous callback affect function(s). """ -function compile_affect_fn(cb, sys::AbstractODESystem, dvs, ps, kwargs) +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...) @@ -698,7 +698,7 @@ function compile_affect_fn(cb, sys::AbstractODESystem, dvs, ps, kwargs) (affect = affect, affect_neg = affect_neg) end -function generate_rootfinding_callback(cbs, sys::AbstractODESystem, dvs = unknowns(sys), +function generate_rootfinding_callback(cbs, sys::AbstractTimeDependentSystem, dvs = unknowns(sys), ps = parameters(sys); kwargs...) eqs = map(cb -> flatten_equations(cb.eqs), cbs) num_eqs = length.(eqs) @@ -859,12 +859,12 @@ merge_cb(x, ::Nothing) = x merge_cb(x, y) = CallbackSet(x, y) function process_events(sys; callback = nothing, kwargs...) - if has_continuous_events(sys) + if has_continuous_events(sys) && !isempty(continuous_events(sys)) contin_cb = generate_rootfinding_callback(sys; kwargs...) else contin_cb = nothing end - if has_discrete_events(sys) + if has_discrete_events(sys) && !isempty(discrete_events(sys)) discrete_cb = generate_discrete_callbacks(sys; kwargs...) else discrete_cb = nothing diff --git a/src/systems/jumps/jumpsystem.jl b/src/systems/jumps/jumpsystem.jl index 94cf2b67e8..e497d86141 100644 --- a/src/systems/jumps/jumpsystem.jl +++ b/src/systems/jumps/jumpsystem.jl @@ -485,7 +485,6 @@ function DiffEqBase.ODEProblem(sys::JumpSystem, u0map, tspan::Union{Tuple, Nothi use_union = false, eval_expression = false, eval_module = @__MODULE__, - check_length = false, kwargs...) if !iscomplete(sys) error("A completed `JumpSystem` is required. Call `complete` or `structural_simplify` on the system before creating a `DiscreteProblem`") @@ -496,11 +495,10 @@ function DiffEqBase.ODEProblem(sys::JumpSystem, u0map, tspan::Union{Tuple, Nothi osys = ODESystem(equations(sys).x[4], get_iv(sys), unknowns(sys), parameters(sys); observed = observed(sys), name = nameof(sys), description = description(sys), systems = get_systems(sys), defaults = defaults(sys), - continuous_events = continuous_events(sys), parameter_dependencies = parameter_dependencies(sys), metadata = get_metadata(sys), gui_metadata = get_gui_metadata(sys)) osys = complete(osys) - return ODEProblem(osys, u0map, tspan, parammap; check_length, kwargs...) + return ODEProblem(osys, u0map, tspan, parammap; check_length = false, kwargs...) else _, u0, p = process_SciMLProblem(EmptySciMLFunction, sys, u0map, parammap; t = tspan === nothing ? nothing : tspan[1], use_union, tofloat = false, @@ -508,7 +506,7 @@ function DiffEqBase.ODEProblem(sys::JumpSystem, u0map, tspan::Union{Tuple, Nothi f = (du, u, p, t) -> (du .= 0; nothing) observedfun = ObservedFunctionCache(sys; eval_expression, eval_module) df = ODEFunction(f; sys, observed = observedfun) - return ODEProblem(df, u0, tspan, p; check_length, kwargs...) + return ODEProblem(df, u0, tspan, p; kwargs...) end end From a77fcd4e184133ff766f4ba4fc8ccb4f72f8e569 Mon Sep 17 00:00:00 2001 From: Herman Sletmoen Date: Tue, 5 Nov 2024 21:47:21 +0100 Subject: [PATCH 0298/1337] Update perturbation example with Symbolics' new Taylor series functions --- docs/src/examples/perturbation.md | 195 ++++++++---------------------- 1 file changed, 50 insertions(+), 145 deletions(-) diff --git a/docs/src/examples/perturbation.md b/docs/src/examples/perturbation.md index 3ebdc3488a..b2ee4bef32 100644 --- a/docs/src/examples/perturbation.md +++ b/docs/src/examples/perturbation.md @@ -1,183 +1,88 @@ # [Symbolic-Numeric Perturbation Theory for ODEs](@id perturb_diff) -## Prelims +In the [Mixed Symbolic-Numeric Perturbation Theory tutorial](https://symbolics.juliasymbolics.org/stable/tutorials/perturbation/), we discussed how to solve algebraic equations using **Symbolics.jl**. Here we extend the method to differential equations. The procedure is similar, but the Taylor series coefficients now become functions of an independent variable (usually time). -In the previous tutorial, [Mixed Symbolic-Numeric Perturbation Theory](https://symbolics.juliasymbolics.org/stable/examples/perturbation/), we discussed how to solve algebraic equations using **Symbolics.jl**. Here, our goal is to extend the method to differential equations. First, we import the following helper functions that were introduced in [Mixed Symbolic/Numerical Methods for Perturbation Theory - Algebraic Equations](https://symbolics.juliasymbolics.org/stable/examples/perturbation/): - -```@example perturbation -using Symbolics - -def_taylor(x, ps) = sum([a * x^(i - 1) for (i, a) in enumerate(ps)]) - -function collect_powers(eq, x, ns; max_power = 100) - eq = substitute(expand(eq), Dict(x^j => 0 for j in (last(ns) + 1):max_power)) - - eqs = [] - for i in ns - powers = Dict(x^j => (i == j ? 1 : 0) for j in 1:last(ns)) - e = substitute(eq, powers) - - # manually remove zeroth order from higher orders - if 0 in ns && i != 0 - e = e - eqs[1] - end - - push!(eqs, e) - end - eqs -end - -function solve_coef(eqs, ps) - vals = Dict() - - for i in 1:length(ps) - eq = substitute(eqs[i], vals) - vals[ps[i]] = Symbolics.symbolic_linear_solve(eq ~ 0, ps[i]) - end - vals -end -``` - -## The Trajectory of a Ball! - -In the first two examples, we applied the perturbation method to algebraic problems. However, the main power of the perturbation method is to solve differential equations (usually ODEs, but also occasionally PDEs). Surprisingly, the main procedure developed to solve algebraic problems works well for differential equations. In fact, we will use the same two helper functions, `collect_powers` and `solve_coef`. The main difference is in the way we expand the dependent variables. For algebraic problems, the coefficients of $\epsilon$ are constants; whereas, for differential equations, they are functions of the dependent variable (usually time). - -As the first ODE example, we have chosen a simple and well-behaved problem, which is a variation of a standard first-year physics problem: what is the trajectory of an object (say, a ball, or a rocket) thrown vertically at velocity $v$ from the surface of a planet? Assuming a constant acceleration of gravity, $g$, every burgeoning physicist knows the answer: $x(t) = x(0) + vt - \frac{1}{2}gt^2$. However, what happens if $g$ is not constant? Specifically, $g$ is inversely proportional to the distant from the center of the planet. If $v$ is large and the projectile travels a large fraction of the radius of the planet, the assumption of constant gravity does not hold anymore. However, unless $v$ is large compared to the escape velocity, the correction is usually small. After simplifications and change of variables to dimensionless, the problem becomes +## Free fall in a varying gravitational field +Our first ODE example is a well-known physics problem: what is the altitude $x(t)$ of an object (say, a ball or a rocket) thrown vertically with initial velocity $ẋ(0)$ from the surface of a planet with mass $M$ and radius $R$? According to Newton's second law and law of gravity, it is the solution of the ODE ```math - \ddot{x}(t) = -\frac{1}{(1 + \epsilon x(t))^2} +ẍ = -\frac{GM}{(R+x)^2} = -\frac{GM}{R^2} \frac{1}{\left(1+ϵ\frac{x}{R}\right)^2}. ``` +In the last equality, we introduced a perturbative expansion parameter $ϵ$. When $ϵ=1$, we recover the original problem. When $ϵ=0$, the problem reduces to the trivial problem $ẍ = -g$ with constant gravitational acceleration $g = GM/R^2$ and solution $x(t) = x(0) + ẋ(0) t - \frac{1}{2} g t^2$. This is a good setup for perturbation theory. -with the initial conditions $x(0) = 0$, and $\dot{x}(0) = 1$. Note that for $\epsilon = 0$, this equation transforms back to the standard one. Let's start with defining the variables - +To make the problem dimensionless, we redefine $x \leftarrow x / R$ and $t \leftarrow t / \sqrt{R^3/GM}$. Then the ODE becomes ```@example perturbation +using ModelingToolkit using ModelingToolkit: t_nounits as t, D_nounits as D -order = 2 -n = order + 1 -@variables ϵ (y(t))[1:n] (∂∂y(t))[1:n] -``` - -Next, we define $x$. - -```@example perturbation -x = def_taylor(ϵ, y) -``` - -We need the second derivative of `x`. It may seem that we can do this using `Differential(t)`; however, this operation needs to wait for a few steps because we need to manipulate the differentials as separate variables. Instead, we define dummy variables `∂∂y` as the placeholder for the second derivatives and define - -```@example perturbation -∂∂x = def_taylor(ϵ, ∂∂y) -``` - -as the second derivative of `x`. After rearrangement, our governing equation is $\ddot{x}(t)(1 + \epsilon x(t))^{-2} + 1 = 0$, or - -```@example perturbation -eq = ∂∂x * (1 + ϵ * x)^2 + 1 -``` - -The next two steps are the same as the ones for algebraic equations (note that we pass `1:n` to `collect_powers` because the zeroth order term is needed here) - -```@example perturbation -eqs = collect_powers(eq, ϵ, 0:order) -``` - -and, - -```@example perturbation -vals = solve_coef(eqs, ∂∂y) +@variables ϵ x(t) +eq = D(D(x)) ~ -(1 + ϵ*x)^(-2) ``` -Our system of ODEs is forming. Now is the time to convert `∂∂`s to the correct **Symbolics.jl** form by substitution: - +Next, expand $x(t)$ in a series up to second order in $ϵ$: ```@example perturbation -subs = Dict(∂∂y[i] => D(D(y[i])) for i in eachindex(y)) -eqs = [substitute(first(v), subs) ~ substitute(last(v), subs) for v in vals] +using Symbolics +@variables y(t)[0:2] # coefficients +x_series = series(y, ϵ) ``` -We are nearly there! From this point on, the rest is standard ODE solving procedures. Potentially, we can use a symbolic ODE solver to find a closed form solution to this problem. However, **Symbolics.jl** currently does not support this functionality. Instead, we solve the problem numerically. We form an `ODESystem`, lower the order (convert second derivatives to first), generate an `ODEProblem` (after passing the correct initial conditions), and, finally, solve it. - +Insert this into the equation and collect perturbed equations to each order: ```@example perturbation -using ModelingToolkit, DifferentialEquations - -@mtkbuild sys = ODESystem(eqs, t) -unknowns(sys) +eq_pert = substitute(eq, x => x_series) +eqs_pert = taylor_coeff(eq_pert, ϵ, 0:2) ``` - +!!! note + The 0-th order equation can be solved analytically, but ModelingToolkit does currently not feature automatic analytical solution of ODEs, so we proceed with solving it numerically. +These are the ODEs we want to solve. Now construct an `ODESystem`, which automatically inserts dummy derivatives for the velocities: ```@example perturbation -# the initial conditions -# everything is zero except the initial velocity -u0 = Dict([unknowns(sys) .=> 0; D(y[1]) => 1]) - -prob = ODEProblem(sys, u0, (0, 3.0)) -sol = solve(prob; dtmax = 0.01); +@mtkbuild sys = ODESystem(eqs_pert, t) ``` -Finally, we calculate the solution to the problem as a function of `ϵ` by substituting the solution to the ODE system back into the defining equation for `x`. Note that `𝜀` is a number, compared to `ϵ`, which is a symbolic variable. - +To solve the `ODESystem`, we generate an `ODEProblem` with initial conditions $x(0) = 0$, and $ẋ(0) = 1$, and solve it: ```@example perturbation -X = 𝜀 -> sum([𝜀^(i - 1) * sol[yi] for (i, yi) in enumerate(y)]) +using DifferentialEquations +u0 = Dict([unknowns(sys) .=> 0.0; D(y[0]) => 1.0]) # nonzero initial velocity +prob = ODEProblem(sys, u0, (0.0, 3.0)) +sol = solve(prob) ``` - -Using `X`, we can plot the trajectory for a range of $𝜀$s. - +This is the solution for the coefficients in the series for $x(t)$ and their derivatives. Finally, we calculate the solution to the original problem by summing the series for different $ϵ$: ```@example perturbation using Plots - -plot(sol.t, hcat([X(𝜀) for 𝜀 in 0.0:0.1:0.5]...)) -``` - -As expected, as `𝜀` becomes larger (meaning the gravity is less with altitude), the object goes higher and stays up for a longer duration. Of course, we could have solved the problem directly using as ODE solver. One of the benefits of the perturbation method is that we need to run the ODE solver only once and then can just calculate the answer for different values of `𝜀`; whereas, if we had used the direct method, we would need to run the solver once for each value of `𝜀`. - -## A Weakly Nonlinear Oscillator - -For the next example, we have chosen a simple example from a very important class of problems, the nonlinear oscillators. As we will see, perturbation theory has difficulty providing a good solution to this problem, but the process is instructive. This example closely follows the chapter 7.6 of *Nonlinear Dynamics and Chaos* by Steven Strogatz. - -The goal is to solve $\ddot{x} + 2\epsilon\dot{x} + x = 0$, where the dot signifies time-derivatives and the initial conditions are $x(0) = 0$ and $\dot{x}(0) = 1$. If $\epsilon = 0$, the problem reduces to the simple linear harmonic oscillator with the exact solution $x(t) = \sin(t)$. We follow the same steps as the previous example. - -```@example perturbation -order = 2 -n = order + 1 -@variables ϵ (y(t))[1:n] (∂y)[1:n] (∂∂y)[1:n] -x = def_taylor(ϵ, y) -∂x = def_taylor(ϵ, ∂y) -∂∂x = def_taylor(ϵ, ∂∂y) +p = plot() +for ϵᵢ in 0.0:0.1:1.0 + plot!(p, sol, idxs = substitute(x_series, ϵ => ϵᵢ), label = "ϵ = $ϵᵢ") +end +p ``` +This makes sense: for larger $ϵ$, gravity weakens with altitude, and the trajectory goes higher for a fixed initial velocity. -This time we also need the first derivative terms. Continuing, +An advantage of the perturbative method is that we run the ODE solver only once and calculate trajectories for several $ϵ$ for free. Had we solved the full unperturbed ODE directly, we would need to do repeat it for every $ϵ$. -```@example perturbation -eq = ∂∂x + 2 * ϵ * ∂x + x -eqs = collect_powers(eq, ϵ, 0:n) -vals = solve_coef(eqs, ∂∂y) -``` +## Weakly nonlinear oscillator -Next, we need to replace `∂`s and `∂∂`s with their **Symbolics.jl** counterparts: +Our second example applies perturbation theory to nonlinear oscillators -- a very important class of problems. As we will see, perturbation theory has difficulty providing a good solution to this problem, but the process is nevertheless instructive. This example closely follows chapter 7.6 of *Nonlinear Dynamics and Chaos* by Steven Strogatz. +The goal is to solve the ODE ```@example perturbation -subs1 = Dict(∂y[i] => D(y[i]) for i in eachindex(y)) -subs2 = Dict(∂∂y[i] => D(D(y[i])) for i in eachindex(y)) -subs = subs1 ∪ subs2 -eqs = [substitute(first(v), subs) ~ substitute(last(v), subs) for v in vals] +eq = D(D(x)) + 2*ϵ*D(x) + x ~ 0 ``` +with initial conditions $x(0) = 0$ and $ẋ(0) = 1$. With $ϵ = 0$, the problem reduces to the simple linear harmonic oscillator with the exact solution $x(t) = \sin(t)$. -We continue with converting 'eqs' to an `ODEProblem`, solving it, and finally plot the results against the exact solution to the original problem, which is $x(t, \epsilon) = (1 - \epsilon)^{-1/2} e^{-\epsilon t} \sin((1- \epsilon^2)^{1/2}t)$, - +We follow the same steps as in the previous example to construct the `ODESystem`: ```@example perturbation -@mtkbuild sys = ODESystem(eqs, t) +eq_pert = substitute(eq, x => x_series) +eqs_pert = taylor_coeff(eq_pert, ϵ, 0:2) +@mtkbuild sys = ODESystem(eqs_pert, t) ``` +We solve and plot it as in the previous example, and compare the solution with $ϵ=0.1$ to the exact solution $x(t, ϵ) = e^{-ϵ t} \sin(\sqrt{(1-ϵ^2)}\,t) / \sqrt{1-ϵ^2}$ of the unperturbed equation: ```@example perturbation -# the initial conditions -u0 = Dict([unknowns(sys) .=> 0; D(y[1]) => 1]) - -prob = ODEProblem(sys, u0, (0, 50.0)) -sol = solve(prob; dtmax = 0.01) - -X = 𝜀 -> sum([𝜀^(i - 1) * sol[yi] for (i, yi) in enumerate(y)]) -T = sol.t -Y = 𝜀 -> exp.(-𝜀 * T) .* sin.(sqrt(1 - 𝜀^2) * T) / sqrt(1 - 𝜀^2) # exact solution +u0 = Dict([unknowns(sys) .=> 0.0; D(y[0]) => 1.0]) # nonzero initial velocity +prob = ODEProblem(sys, u0, (0.0, 50.0)) +sol = solve(prob) +plot(sol, idxs = substitute(x_series, ϵ => 0.1); label = "Perturbative (ϵ=0.1)") -plot(sol.t, [Y(0.1), X(0.1)]) +x_exact(t, ϵ) = exp(-ϵ * t) * sin(√(1 - ϵ^2) * t) / √(1 - ϵ^2) +plot!(sol.t, x_exact.(sol.t, 0.1); label = "Exact (ϵ=0.1)") ``` -The figure is similar to Figure 7.6.2 in *Nonlinear Dynamics and Chaos*. The two curves fit well for the first couple of cycles, but then the perturbation method curve diverges from the true solution. The main reason is that the problem has two or more time-scales that introduce secular terms in the solution. One solution is to explicitly account for the two time scales and use an analytic method called *two-timing*. +This is similar to Figure 7.6.2 in *Nonlinear Dynamics and Chaos*. The two curves fit well for the first couple of cycles, but then the perturbative solution diverges from the exact solution. The main reason is that the problem has two or more time-scales that introduce secular terms in the solution. One solution is to explicitly account for the two time scales and use an analytic method called *two-timing*, but this is outside the scope of this example. From feac63a6c123c1658b35fd04c76ac02cbffad0dc Mon Sep 17 00:00:00 2001 From: Sam Isaacson Date: Tue, 5 Nov 2024 17:27:11 -0500 Subject: [PATCH 0299/1337] update get_continuous_events to only apply to systems that have them --- src/systems/callbacks.jl | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/systems/callbacks.jl b/src/systems/callbacks.jl index 9ca69bf151..9dd78af65e 100644 --- a/src/systems/callbacks.jl +++ b/src/systems/callbacks.jl @@ -1,7 +1,9 @@ #################################### system operations ##################################### -get_continuous_events(sys::AbstractSystem) = SymbolicContinuousCallback[] -get_continuous_events(sys::AbstractTimeDependentSystem) = getfield(sys, :continuous_events) has_continuous_events(sys::AbstractSystem) = isdefined(sys, :continuous_events) +function get_continuous_events(sys::AbstractSystem) + has_continuous_events(sys) || return SymbolicContinuousCallback[] + getfield(sys, :discrete_events) +end has_discrete_events(sys::AbstractSystem) = isdefined(sys, :discrete_events) function get_discrete_events(sys::AbstractSystem) From e4ed1367908e3a40b8a344c4612b1517b1f948ef Mon Sep 17 00:00:00 2001 From: Sam Isaacson Date: Tue, 5 Nov 2024 17:27:36 -0500 Subject: [PATCH 0300/1337] typo --- 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 9dd78af65e..0b6b69e272 100644 --- a/src/systems/callbacks.jl +++ b/src/systems/callbacks.jl @@ -2,7 +2,7 @@ has_continuous_events(sys::AbstractSystem) = isdefined(sys, :continuous_events) function get_continuous_events(sys::AbstractSystem) has_continuous_events(sys) || return SymbolicContinuousCallback[] - getfield(sys, :discrete_events) + getfield(sys, :continuous_events) end has_discrete_events(sys::AbstractSystem) = isdefined(sys, :discrete_events) From fec5b00ce854e58f014997caf7bcf336c82708f8 Mon Sep 17 00:00:00 2001 From: Fredrik Bagge Carlson Date: Thu, 7 Nov 2024 05:18:10 +0100 Subject: [PATCH 0301/1337] add name of sys in show method --- src/systems/abstractsystem.jl | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index c53c900d27..5c0348c060 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -1908,7 +1908,8 @@ function Base.show( # Print name and description desc = description(sys) - printstyled(io, "Model ", nameof(sys), ":"; bold) + name = nameof(sys) + printstyled(io, "Model ", name, ":"; bold) !isempty(desc) && print(io, " ", desc) # Print subsystems @@ -1916,7 +1917,7 @@ function Base.show( nsubs = length(subs) nrows = min(nsubs, limit ? rows : nsubs) nrows > 0 && printstyled(io, "\nSubsystems ($(nsubs)):"; bold) - nrows > 0 && hint && print(io, " see hierarchy(sys)") + nrows > 0 && hint && print(io, " see hierarchy($name)") for i in 1:nrows sub = subs[i] name = String(nameof(sub)) @@ -1940,9 +1941,9 @@ function Base.show( next = n_expanded_connection_equations(sys) ntot = neqs + next ntot > 0 && printstyled(io, "\nEquations ($ntot):"; bold) - neqs > 0 && print(io, "\n $neqs standard", hint ? ": see equations(sys)" : "") + neqs > 0 && print(io, "\n $neqs standard", hint ? ": see equations($name)" : "") next > 0 && print(io, "\n $next connecting", - hint ? ": see equations(expand_connections(sys))" : "") + hint ? ": see equations(expand_connections($name))" : "") #Base.print_matrix(io, eqs) # usually too long and not useful to print all equations end @@ -1953,7 +1954,7 @@ function Base.show( nvars == 0 && continue # skip header = titlecase(String(nameof(varfunc))) # e.g. "Unknowns" printstyled(io, "\n$header ($nvars):"; bold) - hint && print(io, " see $(nameof(varfunc))(sys)") + hint && print(io, " see $(nameof(varfunc))($name)") nrows = min(nvars, limit ? rows : nvars) defs = has_defaults(sys) ? defaults(sys) : nothing for i in 1:nrows @@ -1982,7 +1983,7 @@ function Base.show( # Print observed nobs = has_observed(sys) ? length(observed(sys)) : 0 nobs > 0 && printstyled(io, "\nObserved ($nobs):"; bold) - nobs > 0 && hint && print(io, " see observed(sys)") + nobs > 0 && hint && print(io, " see observed($name)") return nothing end From a1b249f6fd9864c51364b4ebe571a04faec87fb8 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 6 Nov 2024 16:56:48 +0530 Subject: [PATCH 0302/1337] feat: add circular dependency checking, substitution limit --- src/systems/problem_utils.jl | 127 +++++++++++++++++++++++++++++++++-- test/initial_values.jl | 25 +++++++ 2 files changed, 146 insertions(+), 6 deletions(-) diff --git a/src/systems/problem_utils.jl b/src/systems/problem_utils.jl index 977f637d47..142d2f6d71 100644 --- a/src/systems/problem_utils.jl +++ b/src/systems/problem_utils.jl @@ -250,6 +250,25 @@ function add_parameter_dependencies!(sys::AbstractSystem, varmap::AbstractDict) add_observed_equations!(varmap, parameter_dependencies(sys)) end +struct UnexpectedSymbolicValueInVarmap <: Exception + sym::Any + val::Any +end + +function Base.showerror(io::IO, err::UnexpectedSymbolicValueInVarmap) + println(io, + """ + Found symbolic value $(err.val) for variable $(err.sym). You may be missing an \ + initial condition or have cyclic initial conditions. If this is intended, pass \ + `symbolic_u0 = true`. In case the initial conditions are not cyclic but \ + require more substitutions to resolve, increase `substitution_limit`. To report \ + cycles in initial conditions of unknowns/parameters, pass \ + `warn_cyclic_dependency = true`. If the cycles are still not reported, you \ + may need to pass a larger value for `circular_dependency_max_cycle_length` \ + or `circular_dependency_max_cycles`. + """) +end + """ $(TYPEDSIGNATURES) @@ -278,6 +297,12 @@ function better_varmap_to_vars(varmap::AbstractDict, vars::Vector; isempty(missing_vars) || throw(MissingVariablesError(missing_vars)) end vals = map(x -> varmap[x], vars) + if !allow_symbolic + for (sym, val) in zip(vars, vals) + symbolic_type(val) == NotSymbolic() && continue + throw(UnexpectedSymbolicValueInVarmap(sym, val)) + end + end if container_type <: Union{AbstractDict, Tuple, Nothing, SciMLBase.NullParameters} container_type = Array @@ -298,6 +323,57 @@ function better_varmap_to_vars(varmap::AbstractDict, vars::Vector; end end +""" + $(TYPEDSIGNATURES) + +Check if any of the substitution rules in `varmap` lead to cycles involving +variables in `vars`. Return a vector of vectors containing all the variables +in each cycle. + +Keyword arguments: +- `max_cycle_length`: The maximum length (number of variables) of detected cycles. +- `max_cycles`: The maximum number of cycles to report. +""" +function check_substitution_cycles( + varmap::AbstractDict, vars; max_cycle_length = length(varmap), max_cycles = 10) + # ordered set so that `vars` are the first `k` in the list + allvars = OrderedSet{Any}(vars) + union!(allvars, keys(varmap)) + allvars = collect(allvars) + var_to_idx = Dict(allvars .=> eachindex(allvars)) + graph = SimpleDiGraph(length(allvars)) + + buffer = Set() + for (k, v) in varmap + kidx = var_to_idx[k] + if symbolic_type(v) != NotSymbolic() + vars!(buffer, v) + for var in buffer + haskey(var_to_idx, var) || continue + add_edge!(graph, kidx, var_to_idx[var]) + end + elseif v isa AbstractArray + for val in v + vars!(buffer, val) + end + for var in buffer + haskey(var_to_idx, var) || continue + add_edge!(graph, kidx, var_to_idx[var]) + end + end + empty!(buffer) + end + + # detect at most 100 cycles involving at most `length(varmap)` vertices + cycles = Graphs.simplecycles_limited_length(graph, max_cycle_length, max_cycles) + # only count those which contain variables in `vars` + filter!(Base.Fix1(any, <=(length(vars))), cycles) + + map(cycles) do cycle + map(Base.Fix1(getindex, allvars), cycle) + end +end + """ $(TYPEDSIGNATURES) @@ -305,10 +381,10 @@ Performs symbolic substitution on the values in `varmap` for the keys in `vars`, `varmap` itself as the set of substitution rules. If an entry in `vars` is not a key in `varmap`, it is ignored. """ -function evaluate_varmap!(varmap::AbstractDict, vars) +function evaluate_varmap!(varmap::AbstractDict, vars; limit = 100) for k in vars haskey(varmap, k) || continue - varmap[k] = fixpoint_sub(varmap[k], varmap) + varmap[k] = fixpoint_sub(varmap[k], varmap; maxiters = limit) end end @@ -407,6 +483,14 @@ Keyword arguments: length of `u0` vector for consistency. If `false`, do not check with equations. This is forwarded to `check_eqs_u0` - `symbolic_u0` allows the returned `u0` to be an array of symbolics. +- `warn_cyclic_dependency`: Whether to emit a warning listing out cycles in initial + conditions provided for unknowns and parameters. +- `circular_dependency_max_cycle_length`: Maximum length of cycle to check for. + Only applicable if `warn_cyclic_dependency == true`. +- `circular_dependency_max_cycles`: Maximum number of cycles to check for. + Only applicable if `warn_cyclic_dependency == true`. +- `substitution_limit`: The number times to substitute initial conditions into each + other to attempt to arrive at a numeric value. All other keyword arguments are passed as-is to `constructor`. """ @@ -416,7 +500,11 @@ function process_SciMLProblem( warn_initialize_determined = true, initialization_eqs = [], eval_expression = false, eval_module = @__MODULE__, fully_determined = false, check_initialization_units = false, tofloat = true, use_union = false, - u0_constructor = identity, du0map = nothing, check_length = true, symbolic_u0 = false, kwargs...) + u0_constructor = identity, du0map = nothing, check_length = true, + symbolic_u0 = false, warn_cyclic_dependency = false, + circular_dependency_max_cycle_length = length(all_symbols(sys)), + circular_dependency_max_cycles = 10, + substitution_limit = 100, kwargs...) dvs = unknowns(sys) ps = parameters(sys) iv = has_iv(sys) ? get_iv(sys) : nothing @@ -466,7 +554,8 @@ function process_SciMLProblem( initializeprob = ModelingToolkit.InitializationProblem( sys, t, u0map, pmap; guesses, warn_initialize_determined, initialization_eqs, eval_expression, eval_module, fully_determined, - check_units = check_initialization_units) + warn_cyclic_dependency, check_units = check_initialization_units, + circular_dependency_max_cycle_length, circular_dependency_max_cycles) initializeprobmap = getu(initializeprob, unknowns(sys)) punknowns = [p @@ -503,7 +592,20 @@ function process_SciMLProblem( add_observed!(sys, op) add_parameter_dependencies!(sys, op) - evaluate_varmap!(op, dvs) + if warn_cyclic_dependency + cycles = check_substitution_cycles( + op, dvs; max_cycle_length = circular_dependency_max_cycle_length, + max_cycles = circular_dependency_max_cycles) + if !isempty(cycles) + buffer = IOBuffer() + for cycle in cycles + println(buffer, cycle) + end + msg = String(take!(buffer)) + @warn "Cycles in unknowns:\n$msg" + end + end + evaluate_varmap!(op, dvs; limit = substitution_limit) u0 = better_varmap_to_vars( op, dvs; tofloat = true, use_union = false, @@ -515,7 +617,20 @@ function process_SciMLProblem( check_eqs_u0(eqs, dvs, u0; check_length, kwargs...) - evaluate_varmap!(op, ps) + if warn_cyclic_dependency + cycles = check_substitution_cycles( + op, ps; max_cycle_length = circular_dependency_max_cycle_length, + max_cycles = circular_dependency_max_cycles) + if !isempty(cycles) + buffer = IOBuffer() + for cycle in cycles + println(buffer, cycle) + end + msg = String(take!(buffer)) + @warn "Cycles in parameters:\n$msg" + end + end + evaluate_varmap!(op, ps; limit = substitution_limit) if is_split(sys) p = MTKParameters(sys, op) else diff --git a/test/initial_values.jl b/test/initial_values.jl index fc386242a6..a2931e22b6 100644 --- a/test/initial_values.jl +++ b/test/initial_values.jl @@ -149,3 +149,28 @@ end pmap = [c1 => 5.0, c2 => 1.0, c3 => 1.2] oprob = ODEProblem(osys, u0map, (0.0, 10.0), pmap) end + +@testset "Cyclic dependency checking and substitution limits" begin + @variables x(t) y(t) + @mtkbuild sys = ODESystem( + [D(x) ~ x, D(y) ~ y], t; initialization_eqs = [x ~ 2y + 3, y ~ 2x], + guesses = [x => 2y, y => 2x]) + @test_warn ["Cycle", "unknowns", "x", "y"] try + ODEProblem(sys, [], (0.0, 1.0), warn_cyclic_dependency = true) + catch + end + @test_throws ModelingToolkit.UnexpectedSymbolicValueInVarmap ODEProblem( + sys, [x => 2y + 1, y => 2x], (0.0, 1.0); build_initializeprob = false) + + @parameters p q + @mtkbuild sys = ODESystem( + [D(x) ~ x * p, D(y) ~ y * q], t; guesses = [p => 1.0, q => 2.0]) + # "unknowns" because they are initialization unknowns + @test_warn ["Cycle", "unknowns", "p", "q"] try + ODEProblem(sys, [x => 1, y => 2], (0.0, 1.0), + [p => 2q, q => 3p]; warn_cyclic_dependency = true) + catch + end + @test_throws ModelingToolkit.UnexpectedSymbolicValueInVarmap ODEProblem( + sys, [x => 1, y => 2], (0.0, 1.0), [p => 2q, q => 3p]) +end From 5a6da3cdf29104aa7e81528d5fe6299c4f432def Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 6 Nov 2024 16:56:55 +0530 Subject: [PATCH 0303/1337] build: bump Symbolics compat `maxiters` keyword to `fixpoint_sub` is required --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 656fef6173..67401c601a 100644 --- a/Project.toml +++ b/Project.toml @@ -131,7 +131,7 @@ SpecialFunctions = "0.7, 0.8, 0.9, 0.10, 1.0, 2" StaticArrays = "0.10, 0.11, 0.12, 1.0" SymbolicIndexingInterface = "0.3.31" SymbolicUtils = "3.7" -Symbolics = "6.15.2" +Symbolics = "6.15.4" URIs = "1" UnPack = "0.1, 1.0" Unitful = "1.1" From 7ca38cb306269df20388ed9f47eb5ea24e10ff52 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 6 Nov 2024 16:57:22 +0530 Subject: [PATCH 0304/1337] refactor: format --- docs/src/examples/perturbation.md | 22 +++++++++++++++++++--- src/systems/abstractsystem.jl | 4 ++-- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/docs/src/examples/perturbation.md b/docs/src/examples/perturbation.md index b2ee4bef32..cd1e843f55 100644 --- a/docs/src/examples/perturbation.md +++ b/docs/src/examples/perturbation.md @@ -5,20 +5,24 @@ In the [Mixed Symbolic-Numeric Perturbation Theory tutorial](https://symbolics.j ## Free fall in a varying gravitational field Our first ODE example is a well-known physics problem: what is the altitude $x(t)$ of an object (say, a ball or a rocket) thrown vertically with initial velocity $ẋ(0)$ from the surface of a planet with mass $M$ and radius $R$? According to Newton's second law and law of gravity, it is the solution of the ODE + ```math ẍ = -\frac{GM}{(R+x)^2} = -\frac{GM}{R^2} \frac{1}{\left(1+ϵ\frac{x}{R}\right)^2}. ``` + In the last equality, we introduced a perturbative expansion parameter $ϵ$. When $ϵ=1$, we recover the original problem. When $ϵ=0$, the problem reduces to the trivial problem $ẍ = -g$ with constant gravitational acceleration $g = GM/R^2$ and solution $x(t) = x(0) + ẋ(0) t - \frac{1}{2} g t^2$. This is a good setup for perturbation theory. To make the problem dimensionless, we redefine $x \leftarrow x / R$ and $t \leftarrow t / \sqrt{R^3/GM}$. Then the ODE becomes + ```@example perturbation using ModelingToolkit using ModelingToolkit: t_nounits as t, D_nounits as D @variables ϵ x(t) -eq = D(D(x)) ~ -(1 + ϵ*x)^(-2) +eq = D(D(x)) ~ -(1 + ϵ * x)^(-2) ``` Next, expand $x(t)$ in a series up to second order in $ϵ$: + ```@example perturbation using Symbolics @variables y(t)[0:2] # coefficients @@ -26,25 +30,32 @@ x_series = series(y, ϵ) ``` Insert this into the equation and collect perturbed equations to each order: + ```@example perturbation eq_pert = substitute(eq, x => x_series) eqs_pert = taylor_coeff(eq_pert, ϵ, 0:2) ``` + !!! note + The 0-th order equation can be solved analytically, but ModelingToolkit does currently not feature automatic analytical solution of ODEs, so we proceed with solving it numerically. -These are the ODEs we want to solve. Now construct an `ODESystem`, which automatically inserts dummy derivatives for the velocities: + These are the ODEs we want to solve. Now construct an `ODESystem`, which automatically inserts dummy derivatives for the velocities: + ```@example perturbation @mtkbuild sys = ODESystem(eqs_pert, t) ``` To solve the `ODESystem`, we generate an `ODEProblem` with initial conditions $x(0) = 0$, and $ẋ(0) = 1$, and solve it: + ```@example perturbation using DifferentialEquations u0 = Dict([unknowns(sys) .=> 0.0; D(y[0]) => 1.0]) # nonzero initial velocity prob = ODEProblem(sys, u0, (0.0, 3.0)) sol = solve(prob) ``` + This is the solution for the coefficients in the series for $x(t)$ and their derivatives. Finally, we calculate the solution to the original problem by summing the series for different $ϵ$: + ```@example perturbation using Plots p = plot() @@ -53,6 +64,7 @@ for ϵᵢ in 0.0:0.1:1.0 end p ``` + This makes sense: for larger $ϵ$, gravity weakens with altitude, and the trajectory goes higher for a fixed initial velocity. An advantage of the perturbative method is that we run the ODE solver only once and calculate trajectories for several $ϵ$ for free. Had we solved the full unperturbed ODE directly, we would need to do repeat it for every $ϵ$. @@ -62,12 +74,15 @@ An advantage of the perturbative method is that we run the ODE solver only once Our second example applies perturbation theory to nonlinear oscillators -- a very important class of problems. As we will see, perturbation theory has difficulty providing a good solution to this problem, but the process is nevertheless instructive. This example closely follows chapter 7.6 of *Nonlinear Dynamics and Chaos* by Steven Strogatz. The goal is to solve the ODE + ```@example perturbation -eq = D(D(x)) + 2*ϵ*D(x) + x ~ 0 +eq = D(D(x)) + 2 * ϵ * D(x) + x ~ 0 ``` + with initial conditions $x(0) = 0$ and $ẋ(0) = 1$. With $ϵ = 0$, the problem reduces to the simple linear harmonic oscillator with the exact solution $x(t) = \sin(t)$. We follow the same steps as in the previous example to construct the `ODESystem`: + ```@example perturbation eq_pert = substitute(eq, x => x_series) eqs_pert = taylor_coeff(eq_pert, ϵ, 0:2) @@ -75,6 +90,7 @@ eqs_pert = taylor_coeff(eq_pert, ϵ, 0:2) ``` We solve and plot it as in the previous example, and compare the solution with $ϵ=0.1$ to the exact solution $x(t, ϵ) = e^{-ϵ t} \sin(\sqrt{(1-ϵ^2)}\,t) / \sqrt{1-ϵ^2}$ of the unperturbed equation: + ```@example perturbation u0 = Dict([unknowns(sys) .=> 0.0; D(y[0]) => 1.0]) # nonzero initial velocity prob = ODEProblem(sys, u0, (0.0, 50.0)) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index 10401d91c5..9dda345f14 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -3001,8 +3001,8 @@ By default, the resulting system inherits `sys`'s name and description. See also [`compose`](@ref). """ function extend(sys::AbstractSystem, basesys::AbstractSystem; - name::Symbol = nameof(sys), description = description(sys), - gui_metadata = get_gui_metadata(sys)) + name::Symbol = nameof(sys), description = description(sys), + gui_metadata = get_gui_metadata(sys)) T = SciMLBase.parameterless_type(basesys) ivs = independent_variables(basesys) if !(sys isa T) From a023f7e694f2cb52674fcafbf1fa7bd4483c6bc8 Mon Sep 17 00:00:00 2001 From: Benjamin Chung Date: Sun, 27 Oct 2024 14:05:55 -0700 Subject: [PATCH 0305/1337] Add support for initialize and finalize callbacks --- src/systems/callbacks.jl | 177 ++++++++++++++++++++++++++++++--------- 1 file changed, 138 insertions(+), 39 deletions(-) diff --git a/src/systems/callbacks.jl b/src/systems/callbacks.jl index c427ae02a1..99d001146f 100644 --- a/src/systems/callbacks.jl +++ b/src/systems/callbacks.jl @@ -114,6 +114,8 @@ initialization. """ struct SymbolicContinuousCallback eqs::Vector{Equation} + initialize::Union{Vector{Equation}, FunctionalAffect} + finalize::Union{Vector{Equation}, FunctionalAffect} affect::Union{Vector{Equation}, FunctionalAffect} affect_neg::Union{Vector{Equation}, FunctionalAffect, Nothing} rootfind::SciMLBase.RootfindOpt @@ -122,9 +124,12 @@ struct SymbolicContinuousCallback eqs::Vector{Equation}, affect = NULL_AFFECT, affect_neg = affect, + initialize = NULL_AFFECT, + finalize = NULL_AFFECT, rootfind = SciMLBase.LeftRootFind, reinitializealg = SciMLBase.CheckInit()) - new(eqs, make_affect(affect), make_affect(affect_neg), rootfind, reinitializealg) + new(eqs, initialize, finalize, make_affect(affect), + make_affect(affect_neg), rootfind, reinitializealg) end # Default affect to nothing end make_affect(affect) = affect @@ -133,17 +138,80 @@ 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) 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 = cb.affect isa AbstractVector ? foldr(hash, cb.affect, init = s) : hash(cb.affect, s) - s = cb.affect_neg isa AbstractVector ? foldr(hash, cb.affect_neg, init = s) : - hash(cb.affect_neg, 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) end +function Base.show(io::IO, cb::SymbolicContinuousCallback) + indent = get(io, :indent, 0) + iio = IOContext(io, :indent => indent + 1) + print(io, "SymbolicContinuousCallback(") + print(iio, "Equations:") + show(iio, equations(cb)) + print(iio, "; ") + if affects(cb) != NULL_AFFECT + print(iio, "Affect:") + show(iio, affects(cb)) + print(iio, ", ") + end + if affect_negs(cb) != NULL_AFFECT + print(iio, "Negative-edge affect:") + show(iio, affect_negs(cb)) + print(iio, ", ") + end + if initialize_affects(cb) != NULL_AFFECT + print(iio, "Initialization affect:") + show(iio, initialize_affects(cb)) + print(iio, ", ") + end + if finalize_affects(cb) != NULL_AFFECT + print(iio, "Finalization affect:") + show(iio, finalize_affects(cb)) + end + print(iio, ")") +end + +function Base.show(io::IO, mime::MIME"text/plain", cb::SymbolicContinuousCallback) + indent = get(io, :indent, 0) + iio = IOContext(io, :indent => indent + 1) + println(io, "SymbolicContinuousCallback:") + println(iio, "Equations:") + show(iio, mime, equations(cb)) + print(iio, "\n") + if affects(cb) != NULL_AFFECT + println(iio, "Affect:") + show(iio, mime, affects(cb)) + print(iio, "\n") + end + if affect_negs(cb) != NULL_AFFECT + println(iio, "Negative-edge affect:") + show(iio, mime, affect_negs(cb)) + print(iio, "\n") + end + if initialize_affects(cb) != NULL_AFFECT + println(iio, "Initialization affect:") + show(iio, mime, initialize_affects(cb)) + print(iio, "\n") + end + if finalize_affects(cb) != NULL_AFFECT + println(iio, "Finalization affect:") + show(iio, mime, finalize_affects(cb)) + print(iio, "\n") + end +end + to_equation_vector(eq::Equation) = [eq] to_equation_vector(eqs::Vector{Equation}) = eqs function to_equation_vector(eqs::Vector{Any}) @@ -156,15 +224,18 @@ function SymbolicContinuousCallback(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; - affect_neg = affect, rootfind = SciMLBase.LeftRootFind) +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, rootfind = rootfind) + 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, rootfind = SciMLBase.LeftRootFind) + affect_neg = affect, initialize=NULL_AFFECT, finalize=NULL_AFFECT, + rootfind = SciMLBase.LeftRootFind) SymbolicContinuousCallback( - eqs = eqs, affect = affect, affect_neg = affect_neg, rootfind = rootfind) + eqs = eqs, affect = affect, affect_neg = affect_neg, initialize=initialize, finalize=finalize, rootfind = rootfind) end SymbolicContinuousCallbacks(cb::SymbolicContinuousCallback) = [cb] @@ -199,15 +270,28 @@ function reinitialization_algs(cbs::Vector{SymbolicContinuousCallback}) reinitialization_alg, vcat, cbs, init = SciMLBase.DAEInitializationAlgorithm[]) end +initialize_affects(cb::SymbolicContinuousCallback) = cb.initialize +function initialize_affects(cbs::Vector{SymbolicContinuousCallback}) + mapreduce(initialize_affects, vcat, cbs, init = Equation[]) +end + +finalize_affects(cb::SymbolicContinuousCallback) = cb.initialize +function finalize_affects(cbs::Vector{SymbolicContinuousCallback}) + mapreduce(finalize_affects, vcat, cbs, init = Equation[]) +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_callback(cb::SymbolicContinuousCallback, s)::SymbolicContinuousCallback - SymbolicContinuousCallback( - namespace_equation.(equations(cb), (s,)), - namespace_affects(affects(cb), s); - affect_neg = namespace_affects(affect_negs(cb), s)) + 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) end """ @@ -589,22 +673,25 @@ function generate_single_rootfinding_callback( rf_oop(u, parameter_values(integ), t) end end - + user_initfun = (affect_function.initialize == NULL_AFFECT) ? 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 = SciMLBase.INITIALIZE_DEFAULT + initfn = user_initfun end return ContinuousCallback( cond, affect_function.affect, affect_function.affect_neg, rootfind = cb.rootfind, initialize = initfn, + finalize = (affect_function.finalize == NULL_AFFECT) ? SciMLBase.FINALIZE_DEFAULT : (c, u, t, i) -> affect_function.finalize(i), initializealg = reinitialization_alg(cb)) end @@ -626,13 +713,14 @@ function generate_vector_rootfinding_callback( _, rf_ip = generate_custom_function( sys, rhss, dvs, ps; expression = Val{false}, kwargs...) - affect_functions = @NamedTuple{affect::Function, affect_neg::Union{Function, Nothing}}[compile_affect_fn( - cb, - sys, - dvs, - ps, - kwargs) - for cb in cbs] + 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 @@ -658,26 +746,31 @@ function generate_vector_rootfinding_callback( affect_neg(integ) end end - if has_index_cache(sys) && (ic = get_index_cache(sys)) !== nothing - save_idxs = mapreduce( - cb -> get(ic.callback_to_clocks, cb, Int[]), vcat, cbs; init = Int[]) - initfn = if isempty(save_idxs) - SciMLBase.INITIALIZE_DEFAULT + function handle_optional_setup_fn(funs, default) + if all(isnothing, funs) + return default else - let save_idxs = save_idxs - function (cb, u, t, integrator) - for idx in save_idxs - SciMLBase.save_discretes!(integrator, idx) + 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 - else - initfn = SciMLBase.INITIALIZE_DEFAULT end + + initialize = handle_optional_setup_fn( + map(fn -> fn.initialize, affect_functions), SciMLBase.INITIALIZE_DEFAULT) + 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 = initfn, initializealg = reinitialization) + initialize = initialize, finalize = finalize, initializealg = reinitialization) end """ @@ -687,15 +780,21 @@ function compile_affect_fn(cb, sys::AbstractODESystem, 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 - elseif isnothing(eq_neg_aff) - affect_neg = nothing else - affect_neg = compile_affect( - eq_neg_aff, cb, sys, dvs, ps; expression = Val{false}, kwargs...) + affect_neg = compile_optional_affect(eq_neg_aff) end - (affect = affect, affect_neg = affect_neg) + initialize = compile_optional_affect(initialize_affects(cb), NULL_AFFECT) + finalize = compile_optional_affect(finalize_affects(cb), NULL_AFFECT) + (affect = affect, affect_neg = affect_neg, initialize = initialize, finalize = finalize) end function generate_rootfinding_callback(cbs, sys::AbstractODESystem, dvs = unknowns(sys), From b4893a3ef5b767eafc2d34409781241397de9509 Mon Sep 17 00:00:00 2001 From: Benjamin Chung Date: Mon, 28 Oct 2024 01:59:41 -0700 Subject: [PATCH 0306/1337] Fix initial condition --- src/systems/callbacks.jl | 36 +++++++++++++++++++++++++++++++----- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/src/systems/callbacks.jl b/src/systems/callbacks.jl index 99d001146f..fe2dd8c14f 100644 --- a/src/systems/callbacks.jl +++ b/src/systems/callbacks.jl @@ -673,7 +673,7 @@ function generate_single_rootfinding_callback( rf_oop(u, parameter_values(integ), t) end end - user_initfun = (affect_function.initialize == NULL_AFFECT) ? SciMLBase.INITIALIZE_DEFAULT : + 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 @@ -691,7 +691,7 @@ function generate_single_rootfinding_callback( return ContinuousCallback( cond, affect_function.affect, affect_function.affect_neg, rootfind = cb.rootfind, initialize = initfn, - finalize = (affect_function.finalize == NULL_AFFECT) ? SciMLBase.FINALIZE_DEFAULT : (c, u, t, i) -> affect_function.finalize(i), + finalize = isnothing(affect_function.finalize) ? SciMLBase.FINALIZE_DEFAULT : (c, u, t, i) -> affect_function.finalize(i), initializealg = reinitialization_alg(cb)) end @@ -763,9 +763,35 @@ function generate_vector_rootfinding_callback( end end end - - initialize = handle_optional_setup_fn( - map(fn -> fn.initialize, affect_functions), SciMLBase.INITIALIZE_DEFAULT) + initialize = nothing + if has_index_cache(sys) && (ic = get_index_cache(sys)) !== nothing + initialize = handle_optional_setup_fn(map((cb, fn) -> begin + if (save_idxs = get(ic.callback_to_clocks, cb, nothing)) !== nothing + let save_idxs = save_idxs + if !isnothing(fn.initialize) + (i) -> begin + for idx in save_idxs + SciMLBase.save_discretes!(i, idx) + end + fn.initialize(i) + end + else + (i) -> begin + for idx in save_idxs + SciMLBase.save_discretes!(i, idx) + end + end + end + end + else + fn.initialize + end + end, cbs, affect_functions), 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( From aa0ab70d75f33b97a13a42e740a59107add84e3e Mon Sep 17 00:00:00 2001 From: Benjamin Chung Date: Mon, 28 Oct 2024 02:15:10 -0700 Subject: [PATCH 0307/1337] Fix finalization --- src/systems/callbacks.jl | 2 +- test/symbolic_events.jl | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/systems/callbacks.jl b/src/systems/callbacks.jl index fe2dd8c14f..1729796a14 100644 --- a/src/systems/callbacks.jl +++ b/src/systems/callbacks.jl @@ -275,7 +275,7 @@ function initialize_affects(cbs::Vector{SymbolicContinuousCallback}) mapreduce(initialize_affects, vcat, cbs, init = Equation[]) end -finalize_affects(cb::SymbolicContinuousCallback) = cb.initialize +finalize_affects(cb::SymbolicContinuousCallback) = cb.finalize function finalize_affects(cbs::Vector{SymbolicContinuousCallback}) mapreduce(finalize_affects, vcat, cbs, init = Equation[]) end diff --git a/test/symbolic_events.jl b/test/symbolic_events.jl index 26593f980c..1e9321770e 100644 --- a/test/symbolic_events.jl +++ b/test/symbolic_events.jl @@ -970,6 +970,20 @@ end @test sol[c] == [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0] 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], 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) + @test sol[x][1] ≈ 1.0 + @test sol[x][2] ≈ 1.5 # the initialize affect has been applied + @test seen == true +end + @testset "Bump" begin @variables x(t) [irreducible = true] y(t) [irreducible = true] eqs = [x ~ y, D(x) ~ -1] From 3e5cbde5ae297a69b554334b05ce3dd92114e6f4 Mon Sep 17 00:00:00 2001 From: Benjamin Chung Date: Mon, 28 Oct 2024 02:17:35 -0700 Subject: [PATCH 0308/1337] Add a note to the docstring about initialize and finalize --- src/systems/callbacks.jl | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/systems/callbacks.jl b/src/systems/callbacks.jl index 1729796a14..03869e9779 100644 --- a/src/systems/callbacks.jl +++ b/src/systems/callbacks.jl @@ -111,6 +111,9 @@ DAEs will be reinitialized using `reinitializealg` (which defaults to `SciMLBase 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. + +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} From 9e638232e4fa7d24f412111656814af62b7c0d27 Mon Sep 17 00:00:00 2001 From: Benjamin Chung Date: Mon, 28 Oct 2024 02:20:36 -0700 Subject: [PATCH 0309/1337] Test VCC initialization/finalization --- test/symbolic_events.jl | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/test/symbolic_events.jl b/test/symbolic_events.jl index 1e9321770e..727e190c72 100644 --- a/test/symbolic_events.jl +++ b/test/symbolic_events.jl @@ -982,6 +982,25 @@ end @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], Equation[], 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], Equation[], 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 end @testset "Bump" begin From 6424ca9da6597a7fc8c1a6da1dee233535f6e602 Mon Sep 17 00:00:00 2001 From: Benjamin Chung Date: Mon, 28 Oct 2024 18:28:21 -0700 Subject: [PATCH 0310/1337] Initialize and finalize for discrete callbacks --- src/systems/callbacks.jl | 88 ++++++++++++++++++++++++++++------------ test/symbolic_events.jl | 48 ++++++++++++++++++++++ 2 files changed, 111 insertions(+), 25 deletions(-) diff --git a/src/systems/callbacks.jl b/src/systems/callbacks.jl index 03869e9779..18497a0d1e 100644 --- a/src/systems/callbacks.jl +++ b/src/systems/callbacks.jl @@ -328,13 +328,16 @@ struct SymbolicDiscreteCallback # TODO: Iterative condition::Any affects::Any + initialize::Any + finalize::Any reinitializealg::SciMLBase.DAEInitializationAlgorithm function SymbolicDiscreteCallback( - condition, affects = NULL_AFFECT, reinitializealg = SciMLBase.CheckInit()) + condition, affects = NULL_AFFECT; reinitializealg = SciMLBase.CheckInit(), + initialize=NULL_AFFECT, finalize=NULL_AFFECT) c = scalarize_condition(condition) a = scalarize_affects(affects) - new(c, a, reinitializealg) + new(c, a, scalarize_affects(initialize), scalarize_affects(finalize), reinitializealg) end # Default affect to nothing end @@ -373,11 +376,16 @@ function Base.show(io::IO, db::SymbolicDiscreteCallback) end function Base.:(==)(e1::SymbolicDiscreteCallback, e2::SymbolicDiscreteCallback) - isequal(e1.condition, e2.condition) && isequal(e1.affects, e2.affects) + 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) - cb.affects isa AbstractVector ? foldr(hash, cb.affects, init = s) : hash(cb.affects, 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 @@ -397,10 +405,23 @@ function reinitialization_algs(cbs::Vector{SymbolicDiscreteCallback}) 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 - af = affects(cb) - af = af isa AbstractVector ? namespace_affect.(af, Ref(s)) : namespace_affect(af, s) - SymbolicDiscreteCallback(namespace_condition(condition(cb), s), af) + 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)] @@ -773,10 +794,10 @@ function generate_vector_rootfinding_callback( let save_idxs = save_idxs if !isnothing(fn.initialize) (i) -> begin + fn.initialize(i) for idx in save_idxs SciMLBase.save_discretes!(i, idx) end - fn.initialize(i) end else (i) -> begin @@ -809,20 +830,13 @@ function compile_affect_fn(cb, sys::AbstractODESystem, 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 - affect_neg = compile_optional_affect(eq_neg_aff) + affect_neg = _compile_optional_affect(NULL_AFFECT, eq_neg_aff, cb, sys, dvs, ps; kwargs...) end - initialize = compile_optional_affect(initialize_affects(cb), NULL_AFFECT) - finalize = compile_optional_affect(finalize_affects(cb), NULL_AFFECT) + 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 @@ -914,31 +928,48 @@ 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 + else + 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) 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 + 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 end else - initfn = SciMLBase.INITIALIZE_DEFAULT + 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, initializealg = reinitialization_alg(cb)) + cond, as; initialize = initfn, finalize=finfun, initializealg = reinitialization_alg(cb)) else # Periodic return PeriodicCallback( - as, cond; initialize = initfn, initializealg = reinitialization_alg(cb)) + as, cond; initialize = initfn, finalize=finfun, initializealg = reinitialization_alg(cb)) end end @@ -951,20 +982,27 @@ function generate_discrete_callback(cb, sys, dvs, ps; postprocess_affect_expr! = 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 + 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 end else - initfn = SciMLBase.INITIALIZE_DEFAULT + 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, initializealg = reinitialization_alg(cb)) + c, as; initialize = initfn, finalize = finfun, initializealg = reinitialization_alg(cb)) end end diff --git a/test/symbolic_events.jl b/test/symbolic_events.jl index 727e190c72..54180ad7c6 100644 --- a/test/symbolic_events.jl +++ b/test/symbolic_events.jl @@ -1001,6 +1001,54 @@ end @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 From e0e0e460a61b5ccc9c20841745b275338995f090 Mon Sep 17 00:00:00 2001 From: Benjamin Chung Date: Mon, 28 Oct 2024 18:29:20 -0700 Subject: [PATCH 0311/1337] Format --- src/systems/callbacks.jl | 149 +++++++++++++++++++++++---------------- test/symbolic_events.jl | 60 +++++++++------- 2 files changed, 121 insertions(+), 88 deletions(-) diff --git a/src/systems/callbacks.jl b/src/systems/callbacks.jl index 18497a0d1e..0b758a6e86 100644 --- a/src/systems/callbacks.jl +++ b/src/systems/callbacks.jl @@ -131,7 +131,7 @@ struct SymbolicContinuousCallback finalize = NULL_AFFECT, rootfind = SciMLBase.LeftRootFind, reinitializealg = SciMLBase.CheckInit()) - new(eqs, initialize, finalize, make_affect(affect), + new(eqs, initialize, finalize, make_affect(affect), make_affect(affect_neg), rootfind, reinitializealg) end # Default affect to nothing end @@ -227,18 +227,19 @@ function SymbolicContinuousCallback(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) +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) + 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, + 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) + eqs = eqs, affect = affect, affect_neg = affect_neg, + initialize = initialize, finalize = finalize, rootfind = rootfind) end SymbolicContinuousCallbacks(cb::SymbolicContinuousCallback) = [cb] @@ -334,10 +335,11 @@ struct SymbolicDiscreteCallback function SymbolicDiscreteCallback( condition, affects = NULL_AFFECT; reinitializealg = SciMLBase.CheckInit(), - initialize=NULL_AFFECT, finalize=NULL_AFFECT) + initialize = NULL_AFFECT, finalize = NULL_AFFECT) c = scalarize_condition(condition) a = scalarize_affects(affects) - new(c, a, scalarize_affects(initialize), scalarize_affects(finalize), reinitializealg) + new(c, a, scalarize_affects(initialize), + scalarize_affects(finalize), reinitializealg) end # Default affect to nothing end @@ -376,14 +378,17 @@ function Base.show(io::IO, db::SymbolicDiscreteCallback) 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) + 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 = 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 @@ -405,7 +410,6 @@ function reinitialization_algs(cbs::Vector{SymbolicDiscreteCallback}) 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[]) @@ -418,10 +422,13 @@ 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) + 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))) + 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)] @@ -698,7 +705,7 @@ function generate_single_rootfinding_callback( end end user_initfun = isnothing(affect_function.initialize) ? SciMLBase.INITIALIZE_DEFAULT : - (c, u, t, i) -> affect_function.initialize(i) + (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 @@ -715,7 +722,8 @@ function generate_single_rootfinding_callback( 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), + finalize = isnothing(affect_function.finalize) ? SciMLBase.FINALIZE_DEFAULT : + (c, u, t, i) -> affect_function.finalize(i), initializealg = reinitialization_alg(cb)) end @@ -742,8 +750,8 @@ function generate_vector_rootfinding_callback( 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] + 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) @@ -789,31 +797,37 @@ function generate_vector_rootfinding_callback( end initialize = nothing if has_index_cache(sys) && (ic = get_index_cache(sys)) !== nothing - initialize = handle_optional_setup_fn(map((cb, fn) -> begin - if (save_idxs = get(ic.callback_to_clocks, cb, nothing)) !== nothing - let save_idxs = save_idxs - if !isnothing(fn.initialize) - (i) -> begin - fn.initialize(i) - for idx in save_idxs - SciMLBase.save_discretes!(i, idx) - end - end - else - (i) -> begin - for idx in save_idxs - SciMLBase.save_discretes!(i, idx) + initialize = handle_optional_setup_fn( + map( + (cb, fn) -> begin + if (save_idxs = get(ic.callback_to_clocks, cb, nothing)) !== nothing + let save_idxs = save_idxs + if !isnothing(fn.initialize) + (i) -> begin + fn.initialize(i) + for idx in save_idxs + SciMLBase.save_discretes!(i, idx) + end + end + else + (i) -> begin + for idx in save_idxs + SciMLBase.save_discretes!(i, idx) + end + end end end + else + fn.initialize end - end - else - fn.initialize - end - end, cbs, affect_functions), SciMLBase.INITIALIZE_DEFAULT) - + end, + cbs, + affect_functions), + SciMLBase.INITIALIZE_DEFAULT) + else - initialize = handle_optional_setup_fn(map(fn -> fn.initialize, affect_functions), SciMLBase.INITIALIZE_DEFAULT) + initialize = handle_optional_setup_fn( + map(fn -> fn.initialize, affect_functions), SciMLBase.INITIALIZE_DEFAULT) end finalize = handle_optional_setup_fn( @@ -833,10 +847,13 @@ function compile_affect_fn(cb, sys::AbstractODESystem, dvs, ps, 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...) + 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...) + 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 @@ -928,7 +945,6 @@ 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 @@ -942,13 +958,15 @@ function generate_timed_callback(cb, sys, dvs, ps; postprocess_affect_expr! = no 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...) + 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 + initfn = let save_idxs = save_idxs - initfun=user_initfun + initfun = user_initfun function (cb, u, t, integrator) if !isnothing(initfun) initfun(integrator) @@ -959,17 +977,21 @@ function generate_timed_callback(cb, sys, dvs, ps; postprocess_affect_expr! = no end end else - initfn = isnothing(user_initfun) ? SciMLBase.INITIALIZE_DEFAULT : (_,_,_,i) -> user_initfun(i) + initfn = isnothing(user_initfun) ? SciMLBase.INITIALIZE_DEFAULT : + (_, _, _, i) -> user_initfun(i) end - finfun = isnothing(user_finfun) ? SciMLBase.FINALIZE_DEFAULT : (_,_,_,i) -> user_finfun(i) + 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)) + cond, as; initialize = initfn, finalize = finfun, + initializealg = reinitialization_alg(cb)) else # Periodic return PeriodicCallback( - as, cond; initialize = initfn, finalize=finfun, initializealg = reinitialization_alg(cb)) + as, cond; initialize = initfn, finalize = finfun, + initializealg = reinitialization_alg(cb)) end end @@ -983,8 +1005,10 @@ function generate_discrete_callback(cb, sys, dvs, ps; postprocess_affect_expr! = 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...) + 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 @@ -998,11 +1022,14 @@ function generate_discrete_callback(cb, sys, dvs, ps; postprocess_affect_expr! = end end else - initfn = isnothing(user_initfun) ? SciMLBase.INITIALIZE_DEFAULT : (_,_,_,i) -> user_initfun(i) + initfn = isnothing(user_initfun) ? SciMLBase.INITIALIZE_DEFAULT : + (_, _, _, i) -> user_initfun(i) end - finfun = isnothing(user_finfun) ? SciMLBase.FINALIZE_DEFAULT : (_,_,_,i) -> user_finfun(i) + finfun = isnothing(user_finfun) ? SciMLBase.FINALIZE_DEFAULT : + (_, _, _, i) -> user_finfun(i) return DiscreteCallback( - c, as; initialize = initfn, finalize = finfun, initializealg = reinitialization_alg(cb)) + c, as; initialize = initfn, finalize = finfun, + initializealg = reinitialization_alg(cb)) end end diff --git a/test/symbolic_events.jl b/test/symbolic_events.jl index 54180ad7c6..b58d5911f4 100644 --- a/test/symbolic_events.jl +++ b/test/symbolic_events.jl @@ -970,84 +970,90 @@ end @test sol[c] == [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0] 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], Equation[], initialize=[x~1.5], finalize=f) + 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) @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) + 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], Equation[], initialize=[x~1.5], finalize=f) - inited = false + 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) + 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], Equation[], initialize=a, finalize=b) + 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], Equation[], 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 inited == true @test finaled == true #periodic - inited = false + inited = false finaled = false - cb3 = ModelingToolkit.SymbolicDiscreteCallback(1.0, [x ~ 2], initialize=a, finalize=b) + 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 inited == true @test finaled == true - @test isapprox(sol[x][3], 0.0, atol=1e-9) + @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 + inited = false finaled = false - cb3 = ModelingToolkit.SymbolicDiscreteCallback(1.0, f, initialize=a, finalize=b) + 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 inited == true #preset seen = false - inited = false + inited = false finaled = false - cb3 = ModelingToolkit.SymbolicDiscreteCallback([1.0], f, initialize=a, finalize=b) + 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 inited == true @test finaled == true #equational seen = false - inited = false + inited = false finaled = false - cb3 = ModelingToolkit.SymbolicDiscreteCallback(t == 1.0, f, initialize=a, finalize=b) + 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) + sol = solve(prob, Tsit5(); tstops = 1.0) @test seen == true - @test inited == true + @test inited == true @test finaled == true end From 5a127174bf71bdcff2e075d11df34ce3d4779999 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 7 Nov 2024 15:40:21 +0530 Subject: [PATCH 0312/1337] build: bump version --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 67401c601a..f2c1209b93 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "ModelingToolkit" uuid = "961ee093-0014-501f-94e3-6117800e7a78" authors = ["Yingbo Ma ", "Chris Rackauckas and contributors"] -version = "9.49.0" +version = "9.50.0" [deps] AbstractTrees = "1520ce14-60c1-5f80-bbc7-55ef81b5835c" From 32aeac69c6d083602777ee6fd331f115d8e3d7de Mon Sep 17 00:00:00 2001 From: Sam Isaacson Date: Thu, 7 Nov 2024 07:00:01 -0500 Subject: [PATCH 0313/1337] update master --- test/jumpsystem.jl | 34 ++++++++++++++++++++++++---------- 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/test/jumpsystem.jl b/test/jumpsystem.jl index 76d11f6205..8a3c2adccf 100644 --- a/test/jumpsystem.jl +++ b/test/jumpsystem.jl @@ -301,7 +301,7 @@ end # basic VariableRateJump test let N = 1000 # number of simulations for testing solve accuracy - Random.seed!(rng, 1111) + 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]) @@ -427,20 +427,34 @@ end let @variables X(t) Y(t) @parameters k1 k2 - rate1 = k1 * X - affect1! = [X ~ X - 1] - rate2 = k1 - affect2! = [Y ~ Y + 1] + vrj1 = VariableRateJump(k1 * X, [X ~ X - 1]) + vrj2 = VariableRateJump(k2, [Y ~ Y + 1]) eqs = [D(X) ~ k2, D(Y) ~ -k2/100*Y] - vrj1 = VariableRateJump(rate1, affect1!) - vrj2 = VariableRateJump(rate2, affect2!) @named jsys = JumpSystem([vrj1, vrj2, eqs[1], eqs[2]], t, [X, Y], [k1, k2]) jsys = complete(jsys) - u0 = [X => 0.0, Y => 0.0] - p = [k1 => 1.0, k2 => 20.0] + X0 = 0.0; Y0 = 0.0 + u0 = [X => X0, Y => Y0] + k1val = 1.0; k2val = 20.0 + p = [k1 => k1val, k2 => k2val] tspan = (0.0, 20.0) oprob = ODEProblem(jsys, u0, tspan, p) jprob = JumpProblem(jsys, oprob; rng) - sol = solve(jprob, Tsit5()) + + times = range(0.0, 20.0, length = 100) + Nsims = 2000 + X = zeros(length(times)) + Y = similar(X) + for n in 1:Nsims + sol = solve(jprob, Tsit5()) + X .+= Array(sol(times; idxs = X)) + Y .+= Array(sol(times; idxs = Y)) + end + X ./= Nsims; Y ./= Nsims; + + Xact(t) = X0 * exp(-k1val * t) + (k2val / k1val) * (1 - exp(-k1val * t)) + Yact(t) = Y0 * exp(-k2val/100 * t) + (k1 / (k2val/100)) * (1 - exp(-k2val/100 * t)) + + @test all(abs.(X .- Xact.(times)) .<= 0.05 .* X) + @test all(abs.(Y .- Yact.(times)) .<= 0.05 .* Y) end \ No newline at end of file From 551cf0d9803f1a500f1d1933d13ffd3a9994a3fe Mon Sep 17 00:00:00 2001 From: Venkateshprasad <32921645+ven-k@users.noreply.github.com> Date: Thu, 7 Nov 2024 17:19:41 +0000 Subject: [PATCH 0314/1337] feat: support multiple extend statements in MTKModel --- docs/src/basics/MTKLanguage.md | 7 ++++--- src/systems/abstractsystem.jl | 13 +++++++++++-- src/systems/model_parsing.jl | 22 ++++++++++++++-------- test/model_parsing.jl | 21 +++++++++++++++++++++ 4 files changed, 50 insertions(+), 13 deletions(-) diff --git a/docs/src/basics/MTKLanguage.md b/docs/src/basics/MTKLanguage.md index 537e1b0349..06250f90b4 100644 --- a/docs/src/basics/MTKLanguage.md +++ b/docs/src/basics/MTKLanguage.md @@ -147,9 +147,9 @@ julia> ModelingToolkit.getdefault(model_c1.v) 2.0 ``` -#### `@extend` begin block +#### `@extend` statement -Partial systems can be extended in a higher system in two ways: +One or more partial systems can be extended in a higher system with `@extend` statements. This can be done in two ways: - `@extend PartialSystem(var1 = value1)` @@ -313,7 +313,8 @@ end - `:components`: The list of sub-components in the form of [[name, sub_component_name],...]. - `:constants`: Dictionary of constants mapped to its metadata. - `:defaults`: Dictionary of variables and default values specified in the `@defaults`. - - `:extend`: The list of extended unknowns, name given to the base system, and name of the base system. + - `:extend`: The list of extended unknowns, parameters and components, name given to the base system, and name of the base system. + When multiple extend statements are present, latter two are returned as lists. - `:structural_parameters`: Dictionary of structural parameters mapped to their metadata. - `:parameters`: Dictionary of symbolic parameters mapped to their metadata. For parameter arrays, length is added to the metadata as `:size`. diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index 21a3600749..e715dc6eea 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -918,7 +918,7 @@ Mark a system as completed. A completed system is a system which is done being defined/modified and is ready for structural analysis or other transformations. This allows for analyses and optimizations to be performed which require knowing the global structure of the system. - + One property to note is that if a system is complete, the system will no longer namespace its subsystems or variables, i.e. `isequal(complete(sys).v.i, v.i)`. """ @@ -1933,7 +1933,7 @@ function Base.show( end end limited = nrows < nsubs - limited && print(io, "\n ⋮") # too many to print + limited && print(io, "\n ⋮") # too many to print # Print equations eqs = equations(sys) @@ -3043,10 +3043,19 @@ function extend(sys::AbstractSystem, basesys::AbstractSystem; return T(args...; kwargs...) end +function extend(sys, basesys::Vector{T}) where {T <: AbstractSystem} + foldl(extend, basesys, init = sys) +end + function Base.:(&)(sys::AbstractSystem, basesys::AbstractSystem; kwargs...) extend(sys, basesys; kwargs...) end +function Base.:(&)( + sys::AbstractSystem, basesys::Vector{T}; kwargs...) where {T <: AbstractSystem} + extend(sys, basesys; kwargs...) +end + """ $(SIGNATURES) diff --git a/src/systems/model_parsing.jl b/src/systems/model_parsing.jl index e8955b5b84..5b79eaa91b 100644 --- a/src/systems/model_parsing.jl +++ b/src/systems/model_parsing.jl @@ -50,7 +50,7 @@ function _model_macro(mod, name, expr, isconnector) :structural_parameters => Dict{Symbol, Dict}() ) comps = Union{Symbol, Expr}[] - ext = Ref{Any}(nothing) + ext = [] eqs = Expr[] icon = Ref{Union{String, URI}}() ps, sps, vs, = [], [], [] @@ -115,10 +115,10 @@ function _model_macro(mod, name, expr, isconnector) sys = :($ODESystem($(flatten_equations)(equations), $iv, variables, parameters; name, systems, gui_metadata = $gui_metadata, defaults)) - if ext[] === nothing + if length(ext) == 0 push!(exprs.args, :(var"#___sys___" = $sys)) else - push!(exprs.args, :(var"#___sys___" = $extend($sys, $(ext[])))) + push!(exprs.args, :(var"#___sys___" = $extend($sys, [$(ext...)]))) end isconnector && push!(exprs.args, @@ -240,7 +240,7 @@ function unit_handled_variable_value(meta, varname) end # This function parses various variable/parameter definitions. -# +# # The comments indicate the syntax matched by a block; either when parsed directly # when it is called recursively for parsing a part of an expression. # These variable definitions are part of test suite in `test/model_parsing.jl` @@ -286,7 +286,7 @@ function parse_variable_def!(dict, mod, arg, varclass, kwargs, where_types; # `(l2(t)[1:N, 1:M] = 2), [description = "l is more than 1D, with arbitrary length"]` # `(l3(t)[1:3] = 3), [description = "l2 is 1D"]` # `(l4(t)[1:N] = 4), [description = "l2 is 1D, with arbitrary length"]` - # + # # Condition 2 parses: # `(l5(t)[1:3]::Int = 5), [description = "l3 is 1D and has a type"]` # `(l6(t)[1:N]::Int = 6), [description = "l3 is 1D and has a type, with arbitrary length"]` @@ -373,7 +373,7 @@ function parse_variable_def!(dict, mod, arg, varclass, kwargs, where_types; # Condition 1 is recursively called by: # `par5[1:3]::BigFloat` # `par6(t)[1:3]::BigFloat` - # + # # Condition 2 parses: # `b2(t)[1:2]` # `a2[1:2]` @@ -791,11 +791,17 @@ function _parse_extend!(ext, a, b, dict, expr, kwargs, vars, implicit_arglist) end end - ext[] = a + push!(ext, a) push!(b.args, Expr(:kw, :name, Meta.quot(a))) push!(expr.args, :($a = $b)) - dict[:extend] = [Symbol.(vars.args), a, b.args[1]] + if !haskey(dict, :extend) + dict[:extend] = [Symbol.(vars.args), a, b.args[1]] + else + push!(dict[:extend][1], Symbol.(vars.args)...) + dict[:extend][2] = vcat(dict[:extend][2], a) + dict[:extend][3] = vcat(dict[:extend][3], b.args[1]) + end push!(expr.args, :(@unpack $vars = $a)) end diff --git a/test/model_parsing.jl b/test/model_parsing.jl index 9cdd8712d4..fd223800e3 100644 --- a/test/model_parsing.jl +++ b/test/model_parsing.jl @@ -945,6 +945,15 @@ end end end +@mtkmodel MidModelB begin + @parameters begin + b + end + @components begin + inmodel_b = InnerModel() + end +end + @mtkmodel OuterModel begin @extend MidModel() @equations begin @@ -958,3 +967,15 @@ end @named out = OuterModel() @test OuterModel.structure[:extend][1] == [:inmodel] end + +@mtkmodel MultipleExtend begin + @extend MidModel() + @extend MidModelB() +end + +@testset "Multiple extend statements" begin + @named multiple_extend = MultipleExtend() + @test collect(nameof.(multiple_extend.systems)) == [:inmodel_b, :inmodel] + @test MultipleExtend.structure[:extend][1] == [:inmodel, :b, :inmodel_b] + @test tosymbol.(parameters(multiple_extend)) == [:b, :inmodel_b₊p, :inmodel₊p] +end From 8eab77a1377052dd5c49a783d2da4b6cf7110e59 Mon Sep 17 00:00:00 2001 From: Sam Isaacson Date: Thu, 7 Nov 2024 14:46:58 -0500 Subject: [PATCH 0315/1337] test passes --- src/systems/jumps/jumpsystem.jl | 2 +- test/jumpsystem.jl | 30 ++++++++++++++---------------- 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/src/systems/jumps/jumpsystem.jl b/src/systems/jumps/jumpsystem.jl index e497d86141..e745d0c627 100644 --- a/src/systems/jumps/jumpsystem.jl +++ b/src/systems/jumps/jumpsystem.jl @@ -288,7 +288,7 @@ function assemble_vrj( outputidxs = [unknowntoid[var] for var in outputvars] affect = eval_or_rgf(generate_affect_function(js, vrj.affect!, outputidxs); eval_expression, eval_module) - VariableRateJump(rate, affect) + VariableRateJump(rate, affect; save_positions = vrj.save_positions) end function assemble_vrj_expr(js, vrj, unknowntoid) diff --git a/test/jumpsystem.jl b/test/jumpsystem.jl index 8a3c2adccf..75bac26011 100644 --- a/test/jumpsystem.jl +++ b/test/jumpsystem.jl @@ -427,8 +427,8 @@ end let @variables X(t) Y(t) @parameters k1 k2 - vrj1 = VariableRateJump(k1 * X, [X ~ X - 1]) - vrj2 = VariableRateJump(k2, [Y ~ Y + 1]) + vrj1 = VariableRateJump(k1 * X, [X ~ X - 1]; save_positions = (false, false)) + vrj2 = VariableRateJump(k1, [Y ~ Y + 1]; save_positions = (false, false)) eqs = [D(X) ~ k2, D(Y) ~ -k2/100*Y] @named jsys = JumpSystem([vrj1, vrj2, eqs[1], eqs[2]], t, [X, Y], [k1, k2]) jsys = complete(jsys) @@ -436,25 +436,23 @@ let u0 = [X => X0, Y => Y0] k1val = 1.0; k2val = 20.0 p = [k1 => k1val, k2 => k2val] - tspan = (0.0, 20.0) + tspan = (0.0, 10.0) oprob = ODEProblem(jsys, u0, tspan, p) - jprob = JumpProblem(jsys, oprob; rng) + jprob = JumpProblem(jsys, oprob; rng, save_positions = (false, false)) - times = range(0.0, 20.0, length = 100) + times = range(0.0, tspan[2], length = 100) Nsims = 2000 - X = zeros(length(times)) - Y = similar(X) + Xv = zeros(length(times)) + Yv = copy(Xv) for n in 1:Nsims - sol = solve(jprob, Tsit5()) - X .+= Array(sol(times; idxs = X)) - Y .+= Array(sol(times; idxs = Y)) + sol = solve(jprob, Tsit5(); saveat = times) + Xv .+= sol[1,:] #sol(times; idxs = X) + Yv .+= sol[2,:] #sol(times; idxs = Y) end - X ./= Nsims; Y ./= Nsims; + Xv ./= Nsims; Yv ./= Nsims; Xact(t) = X0 * exp(-k1val * t) + (k2val / k1val) * (1 - exp(-k1val * t)) - Yact(t) = Y0 * exp(-k2val/100 * t) + (k1 / (k2val/100)) * (1 - exp(-k2val/100 * t)) - - @test all(abs.(X .- Xact.(times)) .<= 0.05 .* X) - @test all(abs.(Y .- Yact.(times)) .<= 0.05 .* Y) - + Yact(t) = Y0 * exp(-k2val/100 * t) + (k1val / (k2val/100)) * (1 - exp(-k2val/100 * t)) + @test all(abs.(Xv .- Xact.(times)) .<= 0.05 .* Xv) + @test all(abs.(Yv .- Yact.(times)) .<= 0.05 .* Yv) end \ No newline at end of file From 79344744e5a4e38771f49509245356b94e4a9ecf Mon Sep 17 00:00:00 2001 From: Sam Isaacson Date: Thu, 7 Nov 2024 14:47:08 -0500 Subject: [PATCH 0316/1337] updates --- test/jumpsystem.jl | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/jumpsystem.jl b/test/jumpsystem.jl index 75bac26011..7f049b0aa9 100644 --- a/test/jumpsystem.jl +++ b/test/jumpsystem.jl @@ -446,8 +446,10 @@ let Yv = copy(Xv) for n in 1:Nsims sol = solve(jprob, Tsit5(); saveat = times) - Xv .+= sol[1,:] #sol(times; idxs = X) - Yv .+= sol[2,:] #sol(times; idxs = Y) + + # use direct indexing as much faster than symbolic indexing + Xv .+= sol[1,:] + Yv .+= sol[2,:] end Xv ./= Nsims; Yv ./= Nsims; From 0a223169fa74f22b033c90e88e4b64874b46aaa4 Mon Sep 17 00:00:00 2001 From: Sam Isaacson Date: Thu, 7 Nov 2024 17:50:13 -0500 Subject: [PATCH 0317/1337] more sampled --- test/jumpsystem.jl | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/test/jumpsystem.jl b/test/jumpsystem.jl index 7f049b0aa9..204f4f2fb5 100644 --- a/test/jumpsystem.jl +++ b/test/jumpsystem.jl @@ -441,13 +441,11 @@ let jprob = JumpProblem(jsys, oprob; rng, save_positions = (false, false)) times = range(0.0, tspan[2], length = 100) - Nsims = 2000 + Nsims = 4000 Xv = zeros(length(times)) Yv = copy(Xv) for n in 1:Nsims sol = solve(jprob, Tsit5(); saveat = times) - - # use direct indexing as much faster than symbolic indexing Xv .+= sol[1,:] Yv .+= sol[2,:] end From caa236ae4141d1c3ef3f9b5b43bf23061ff5452b Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 7 Nov 2024 13:12:25 +0530 Subject: [PATCH 0318/1337] fix: fix discrete callback condition with array variable --- src/systems/callbacks.jl | 5 +++-- test/symbolic_events.jl | 21 +++++++++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/src/systems/callbacks.jl b/src/systems/callbacks.jl index 0b758a6e86..ef1b2e3cd6 100644 --- a/src/systems/callbacks.jl +++ b/src/systems/callbacks.jl @@ -550,7 +550,7 @@ end """ compile_condition(cb::SymbolicDiscreteCallback, sys, dvs, ps; expression, kwargs...) -Returns a function `condition(u,p,t)` returning the `condition(cb)`. +Returns a function `condition(u,t,integrator)` returning the `condition(cb)`. Notes @@ -571,7 +571,8 @@ function compile_condition(cb::SymbolicDiscreteCallback, sys, dvs, ps; end expr = build_function( condit, u, t, p...; expression = Val{true}, - wrap_code = condition_header(sys) .∘ wrap_array_vars(sys, condit; dvs, ps) .∘ + wrap_code = condition_header(sys) .∘ + wrap_array_vars(sys, condit; dvs, ps, inputs = true) .∘ wrap_parameter_dependencies(sys, !(condit isa AbstractArray)), kwargs...) if expression == Val{true} diff --git a/test/symbolic_events.jl b/test/symbolic_events.jl index b58d5911f4..61690acdce 100644 --- a/test/symbolic_events.jl +++ b/test/symbolic_events.jl @@ -1075,3 +1075,24 @@ end prob = ODEProblem(pend, [x => 1], (0.0, 3.0), guesses = [y => x]) @test all(≈(0.0; atol = 1e-9), solve(prob, Rodas5())[[x, y]][end]) end + +@testset "Issue#3154 Array variable in discrete condition" begin + @mtkmodel DECAY begin + @parameters begin + unrelated[1:2] = zeros(2) + k = 0.0 + end + @variables begin + x(t) = 10.0 + end + @equations begin + D(x) ~ -k * x + end + @discrete_events begin + (t == 1.0) => [k ~ 1.0] + end + end + @mtkbuild decay = DECAY() + prob = ODEProblem(decay, [], (0.0, 10.0), []) + @test_nowarn solve(prob, Tsit5(), tstops = [1.0]) +end From 094d736fe1951fc46f8209739a22905ef34a64b8 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 8 Nov 2024 13:01:30 +0530 Subject: [PATCH 0319/1337] test: specify seed for SDDE solve --- test/dde.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/dde.jl b/test/dde.jl index aa4536e604..2030a90d06 100644 --- a/test/dde.jl +++ b/test/dde.jl @@ -74,7 +74,7 @@ pmul = [1.0, prob = SDDEProblem(hayes_modelf, hayes_modelg, [1.0], h, tspan, pmul; constant_lags = (pmul[1],)); -sol = solve(prob, RKMil()) +sol = solve(prob, RKMil(), seed = 100) @variables x(..) @parameters a=-4.0 b=-2.0 c=10.0 α=-1.3 β=-1.2 γ=1.1 @@ -87,7 +87,7 @@ eqs = [D(x(t)) ~ a * x(t) + b * x(t - τ) + c + (α * x(t) + γ) * η] @test equations(sys) == [D(x(t)) ~ a * x(t) + b * x(t - τ) + c] @test isequal(ModelingToolkit.get_noiseeqs(sys), [α * x(t) + γ]) prob_mtk = SDDEProblem(sys, [x(t) => 1.0 + t], tspan; constant_lags = (τ,)); -@test_nowarn sol_mtk = solve(prob_mtk, RKMil()) +@test_nowarn sol_mtk = solve(prob_mtk, RKMil(), seed = 100) prob_sa = SDDEProblem( sys, [x(t) => 1.0 + t], tspan; constant_lags = (τ,), u0_constructor = SVector{1}) From de628fa336af97fec346338dc4720d206d54ee20 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 8 Nov 2024 17:19:06 +0530 Subject: [PATCH 0320/1337] feat: cache start system and solver in HomotopyContinuation interface --- ext/MTKHomotopyContinuationExt.jl | 36 +++++++++++++++--------- src/systems/nonlinear/nonlinearsystem.jl | 7 ++++- test/extensions/homotopy_continuation.jl | 13 ++------- 3 files changed, 30 insertions(+), 26 deletions(-) diff --git a/ext/MTKHomotopyContinuationExt.jl b/ext/MTKHomotopyContinuationExt.jl index ebd8afbfc1..96adf2a959 100644 --- a/ext/MTKHomotopyContinuationExt.jl +++ b/ext/MTKHomotopyContinuationExt.jl @@ -32,14 +32,16 @@ function is_polynomial(x, wrt) end if operation(x) == (^) b, p = arguments(x) - is_pow_integer = symtype(p) <: Integer - if !is_pow_integer - if symbolic_type(p) == NotSymbolic() - @warn "In $x: Exponent $p is not an integer" - else - @warn "In $x: Exponent $p is not an integer. Use `@parameters p::Integer` to declare integer parameters." - end + if symbolic_type(p) != NotSymbolic() + @warn "In $x: Exponent $p cannot be symbolic" + is_pow_integer = false + elseif !(p isa Integer) + @warn "In $x: Exponent $p is not an integer" + is_pow_integer = false + else + is_pow_integer = true end + exponent_has_unknowns = contains_variable(p, wrt) if exponent_has_unknowns @warn "In $x: Exponent $p cannot contain unknowns of the system." @@ -179,6 +181,8 @@ Create a `HomotopyContinuationProblem` from a `NonlinearSystem` with polynomial The problem will be solved by HomotopyContinuation.jl. The resultant `NonlinearSolution` will contain the polynomial root closest to the point specified by `u0map` (if real roots exist for the system). + +Keyword arguments are forwarded to `HomotopyContinuation.solver_startsystems`. """ function MTK.HomotopyContinuationProblem( sys::NonlinearSystem, u0map, parammap = nothing; eval_expression = false, @@ -223,20 +227,24 @@ function MTK.HomotopyContinuationProblem( obsfn = MTK.ObservedFunctionCache(sys; eval_expression, eval_module) - return MTK.HomotopyContinuationProblem(u0, mtkhsys, denominator, sys, obsfn) + solver_and_starts = HomotopyContinuation.solver_startsolutions(mtkhsys; kwargs...) + return MTK.HomotopyContinuationProblem( + u0, mtkhsys, denominator, sys, obsfn, solver_and_starts) end """ $(TYPEDSIGNATURES) Solve a `HomotopyContinuationProblem`. Ignores the algorithm passed to it, and always -uses `HomotopyContinuation.jl`. All keyword arguments except the ones listed below are -forwarded to `HomotopyContinuation.solve`. The original solution as returned by +uses `HomotopyContinuation.jl`. The original solution as returned by `HomotopyContinuation.jl` will be available in the `.original` field of the returned `NonlinearSolution`. -All keyword arguments have their default values in HomotopyContinuation.jl, except -`show_progress` which defaults to `false`. +All keyword arguments except the ones listed below are forwarded to +`HomotopyContinuation.solve`. Note that the solver and start solutions are precomputed, +and only keyword arguments related to the solve process are valid. All keyword +arguments have their default values in HomotopyContinuation.jl, except `show_progress` +which defaults to `false`. Extra keyword arguments: - `denominator_abstol`: In case `prob` is solving a rational function, roots which cause @@ -244,8 +252,8 @@ Extra keyword arguments: """ function CommonSolve.solve(prob::MTK.HomotopyContinuationProblem, alg = nothing; show_progress = false, denominator_abstol = 1e-7, kwargs...) - sol = HomotopyContinuation.solve( - prob.homotopy_continuation_system; show_progress, kwargs...) + solver, starts = prob.solver_and_starts + sol = HomotopyContinuation.solve(solver, starts; show_progress, kwargs...) realsols = HomotopyContinuation.results(sol; only_real = true) if isempty(realsols) u = state_values(prob) diff --git a/src/systems/nonlinear/nonlinearsystem.jl b/src/systems/nonlinear/nonlinearsystem.jl index 44e77d5a0f..c6b2610ae7 100644 --- a/src/systems/nonlinear/nonlinearsystem.jl +++ b/src/systems/nonlinear/nonlinearsystem.jl @@ -690,7 +690,7 @@ A type of Nonlinear problem which specializes on polynomial systems and uses HomotopyContinuation.jl to solve the system. Requires importing HomotopyContinuation.jl to create and solve. """ -struct HomotopyContinuationProblem{uType, H, D, O} <: +struct HomotopyContinuationProblem{uType, H, D, O, SS} <: SciMLBase.AbstractNonlinearProblem{uType, true} """ The initial values of states in the system. If there are multiple real roots of @@ -716,6 +716,11 @@ struct HomotopyContinuationProblem{uType, H, D, O} <: A function which generates and returns observed expressions for the given system. """ obsfn::O + """ + The HomotopyContinuation.jl solver and start system, obtained through + `HomotopyContinuation.solver_startsystems`. + """ + solver_and_starts::SS end function HomotopyContinuationProblem(::AbstractSystem, _u0, _p; kwargs...) diff --git a/test/extensions/homotopy_continuation.jl b/test/extensions/homotopy_continuation.jl index 9bd64fa7fb..056700b805 100644 --- a/test/extensions/homotopy_continuation.jl +++ b/test/extensions/homotopy_continuation.jl @@ -61,20 +61,11 @@ end @test sol.retcode == ReturnCode.ConvergenceFailure end -@testset "Parametric exponent" begin - @variables x = 1.0 - @parameters n::Integer = 4 - @mtkbuild sys = NonlinearSystem([x^n + x^2 - 1 ~ 0]) - prob = HomotopyContinuationProblem(sys, []) - sol = solve(prob; threading = false) - @test SciMLBase.successful_retcode(sol) -end - @testset "Polynomial check and warnings" begin @variables x = 1.0 - @parameters n = 4 + @parameters n::Integer = 4 @mtkbuild sys = NonlinearSystem([x^n + x^2 - 1 ~ 0]) - @test_warn ["Exponent", "not an integer", "@parameters"] @test_throws "not a polynomial" HomotopyContinuationProblem( + @test_warn ["Exponent", "cannot be symbolic"] @test_throws "not a polynomial" HomotopyContinuationProblem( sys, []) @mtkbuild sys = NonlinearSystem([x^1.5 + x^2 - 1 ~ 0]) @test_warn ["Exponent", "not an integer"] @test_throws "not a polynomial" HomotopyContinuationProblem( From db7386bd51e8181f17553f41251dfb69f001434b Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 8 Nov 2024 20:04:20 +0530 Subject: [PATCH 0321/1337] fix: use `full_equations` when generating `HomotopyContinuationProblem` --- ext/MTKHomotopyContinuationExt.jl | 10 +++++++++- test/extensions/homotopy_continuation.jl | 22 ++++++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/ext/MTKHomotopyContinuationExt.jl b/ext/MTKHomotopyContinuationExt.jl index 96adf2a959..d714f3cbfb 100644 --- a/ext/MTKHomotopyContinuationExt.jl +++ b/ext/MTKHomotopyContinuationExt.jl @@ -192,7 +192,12 @@ function MTK.HomotopyContinuationProblem( end dvs = unknowns(sys) - eqs = equations(sys) + # we need to consider `full_equations` because observed also should be + # polynomials (if used in equations) and we don't know if observed is used + # in denominator. + # This is not the most efficient, and would be improved significantly with + # CSE/hashconsing. + eqs = full_equations(sys) denoms = [] eqs2 = map(eqs) do eq @@ -216,6 +221,9 @@ function MTK.HomotopyContinuationProblem( end sys2 = MTK.@set sys.eqs = eqs2 + # remove observed equations to avoid adding them in codegen + MTK.@set! sys2.observed = Equation[] + MTK.@set! sys2.substitutions = nothing nlfn, u0, p = MTK.process_SciMLProblem(NonlinearFunction{true}, sys2, u0map, parammap; jac = true, eval_expression, eval_module) diff --git a/test/extensions/homotopy_continuation.jl b/test/extensions/homotopy_continuation.jl index 056700b805..5c4470befa 100644 --- a/test/extensions/homotopy_continuation.jl +++ b/test/extensions/homotopy_continuation.jl @@ -76,6 +76,11 @@ end @mtkbuild sys = NonlinearSystem([((x^2) / sin(x))^2 + x ~ 0]) @test_warn ["Unrecognized", "sin"] @test_throws "not a polynomial" HomotopyContinuationProblem( sys, []) + + @variables y = 2.0 + @mtkbuild sys = NonlinearSystem([x^2 + y^2 + 2 ~ 0, y ~ sin(x)]) + @test_warn ["Unrecognized", "sin"] @test_throws "not a polynomial" HomotopyContinuationProblem( + sys, []) end @testset "Rational functions" begin @@ -122,4 +127,21 @@ end end end @test prob.denominator([2.0, 4.0], p)[1] <= 1e-8 + + @testset "Rational function in observed" begin + @variables x=1 y=1 + @mtkbuild sys = NonlinearSystem([x^2 + y^2 - 2x - 2 ~ 0, y ~ (x - 1) / (x - 2)]) + prob = HomotopyContinuationProblem(sys, []) + @test any(prob.denominator([2.0], parameter_values(prob)) .≈ 0.0) + @test_nowarn solve(prob; threading = false) + end +end + +@test "Non-polynomial observed not used in equations" begin + @variables x=1 y + @mtkbuild sys = NonlinearSystem([x^2 - 2 ~ 0, y ~ sin(x)]) + prob = HomotopyContinuationProblem(sys, []) + sol = @test_nowarn solve(prob; threading = false) + @test sol[x] ≈ √2.0 + @test sol[y] ≈ sin(√2.0) end From 2c95e1941cd53cc080ccaa877c15f6dc9813d972 Mon Sep 17 00:00:00 2001 From: Sam Isaacson Date: Fri, 8 Nov 2024 10:26:48 -0500 Subject: [PATCH 0322/1337] losen test tolerance --- test/jumpsystem.jl | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/test/jumpsystem.jl b/test/jumpsystem.jl index 204f4f2fb5..9569208474 100644 --- a/test/jumpsystem.jl +++ b/test/jumpsystem.jl @@ -425,11 +425,12 @@ end # PDMP test let + Random.seed!(rng, 1111) @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)) - eqs = [D(X) ~ k2, D(Y) ~ -k2/100*Y] + 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) X0 = 0.0; Y0 = 0.0 @@ -443,7 +444,7 @@ let times = range(0.0, tspan[2], length = 100) Nsims = 4000 Xv = zeros(length(times)) - Yv = copy(Xv) + Yv = zeros(length(times)) for n in 1:Nsims sol = solve(jprob, Tsit5(); saveat = times) Xv .+= sol[1,:] @@ -452,7 +453,7 @@ let Xv ./= Nsims; Yv ./= Nsims; Xact(t) = X0 * exp(-k1val * t) + (k2val / k1val) * (1 - exp(-k1val * t)) - Yact(t) = Y0 * exp(-k2val/100 * t) + (k1val / (k2val/100)) * (1 - exp(-k2val/100 * t)) + Yact(t) = Y0 * exp(-k2val/10 * t) + (k1val / (k2val/10)) * (1 - exp(-k2val/10 * t)) @test all(abs.(Xv .- Xact.(times)) .<= 0.05 .* Xv) - @test all(abs.(Yv .- Yact.(times)) .<= 0.05 .* Yv) + @test all(abs.(Yv .- Yact.(times)) .<= 0.1 .* Yv) end \ No newline at end of file From 03fd5fad23be1f02b1aeee4bd48dcc8e9811d818 Mon Sep 17 00:00:00 2001 From: Alex Hirzel Date: Fri, 8 Nov 2024 21:15:40 -0500 Subject: [PATCH 0323/1337] Update stochastic_diffeq.md - fix Brownian coefficient this changes the coefficient from 0.1 to 0.3 to match the rest of the text and code of this example. --- docs/src/tutorials/stochastic_diffeq.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/src/tutorials/stochastic_diffeq.md b/docs/src/tutorials/stochastic_diffeq.md index a6543dc977..79c71a2e8d 100644 --- a/docs/src/tutorials/stochastic_diffeq.md +++ b/docs/src/tutorials/stochastic_diffeq.md @@ -23,9 +23,9 @@ where the magnitude of the noise scales with (0.3 times) the magnitude of each o ```math \begin{aligned} -\frac{dx}{dt} &= (\sigma (y-x)) &+ 0.1x\frac{dB}{dt} \\ -\frac{dy}{dt} &= (x(\rho-z) - y) &+ 0.1y\frac{dB}{dt} \\ -\frac{dz}{dt} &= (xy - \beta z) &+ 0.1z\frac{dB}{dt} \\ +\frac{dx}{dt} &= (\sigma (y-x)) &+ 0.3x\frac{dB}{dt} \\ +\frac{dy}{dt} &= (x(\rho-z) - y) &+ 0.3y\frac{dB}{dt} \\ +\frac{dz}{dt} &= (xy - \beta z) &+ 0.3z\frac{dB}{dt} \\ \end{aligned} ``` From d326ab494b5026b90f12f6d5648905d894818bfd Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Sat, 9 Nov 2024 21:31:10 +0530 Subject: [PATCH 0324/1337] refactor: allow parametric exponents --- Project.toml | 2 + ext/MTKHomotopyContinuationExt.jl | 129 +++++++++++++++++++---- src/ModelingToolkit.jl | 1 + test/extensions/homotopy_continuation.jl | 23 ++-- 4 files changed, 126 insertions(+), 29 deletions(-) diff --git a/Project.toml b/Project.toml index ea380ef6b1..0e804e1f63 100644 --- a/Project.toml +++ b/Project.toml @@ -21,6 +21,7 @@ Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f" DocStringExtensions = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae" DomainSets = "5b8099bc-c8ec-5219-889f-1d9e522a28bf" DynamicQuantities = "06fc5a27-2a28-4c7c-a15d-362465fb6821" +EnumX = "4e289a0a-7415-4d19-859d-a7e5c4648b56" ExprTools = "e2ba6199-217a-4e67-a87a-7c52f15ade04" Expronicon = "6b7a57c9-7cc1-4fdf-b7f5-e857abae3636" FindFirstFunctions = "64ca27bc-2ba2-4a57-88aa-44e436879224" @@ -94,6 +95,7 @@ Distributions = "0.23, 0.24, 0.25" DocStringExtensions = "0.7, 0.8, 0.9" DomainSets = "0.6, 0.7" DynamicQuantities = "^0.11.2, 0.12, 0.13, 1" +EnumX = "1.0.4" ExprTools = "0.1.10" Expronicon = "0.8" FindFirstFunctions = "1" diff --git a/ext/MTKHomotopyContinuationExt.jl b/ext/MTKHomotopyContinuationExt.jl index d714f3cbfb..19c21483c3 100644 --- a/ext/MTKHomotopyContinuationExt.jl +++ b/ext/MTKHomotopyContinuationExt.jl @@ -15,12 +15,70 @@ function contains_variable(x, wrt) any(y -> occursin(y, x), wrt) end +""" +Possible reasons why a term is not polynomial +""" +MTK.EnumX.@enumx NonPolynomialReason begin + NonIntegerExponent + ExponentContainsUnknowns + BaseNotPolynomial + UnrecognizedOperation +end + +function display_reason(reason::NonPolynomialReason.T, sym) + if reason == NonPolynomialReason.NonIntegerExponent + pow = arguments(sym)[2] + "In $sym: Exponent $pow is not an integer" + elseif reason == NonPolynomialReason.ExponentContainsUnknowns + pow = arguments(sym)[2] + "In $sym: Exponent $pow contains unknowns of the system" + elseif reason == NonPolynomialReason.BaseNotPolynomial + base = arguments(sym)[1] + "In $sym: Base $base is not a polynomial in the unknowns" + elseif reason == NonPolynomialReason.UnrecognizedOperation + op = operation(sym) + """ + In $sym: Operation $op is not recognized. Allowed polynomial operations are \ + `*, /, +, -, ^`. + """ + else + error("This should never happen. Please open an issue in ModelingToolkit.jl.") + end +end + +mutable struct PolynomialData + non_polynomial_terms::Vector{BasicSymbolic} + reasons::Vector{NonPolynomialReason.T} + has_parametric_exponent::Bool +end + +PolynomialData() = PolynomialData(BasicSymbolic[], NonPolynomialReason.T[], false) + +struct NotPolynomialError <: Exception + eq::Equation + data::PolynomialData +end + +function Base.showerror(io::IO, err::NotPolynomialError) + println(io, + "Equation $(err.eq) is not a polynomial in the unknowns for the following reasons:") + for (term, reason) in zip(err.data.non_polynomial_terms, err.data.reasons) + println(io, display_reason(reason, term)) + end +end + +function is_polynomial!(data, y, wrt) + process_polynomial!(data, y, wrt) + isempty(data.reasons) +end + """ $(TYPEDSIGNATURES) -Check if `x` is polynomial with respect to the variables in `wrt`. +Return information about the polynmial `x` with respect to variables in `wrt`, +writing said information to `data`. """ -function is_polynomial(x, wrt) +function process_polynomial!(data::PolynomialData, x, wrt) x = unwrap(x) symbolic_type(x) == NotSymbolic() && return true iscall(x) || return true @@ -28,31 +86,33 @@ function is_polynomial(x, wrt) any(isequal(x), wrt) && return true if operation(x) in (*, +, -, /) - return all(y -> is_polynomial(y, wrt), arguments(x)) + return all(y -> is_polynomial!(data, y, wrt), arguments(x)) end if operation(x) == (^) b, p = arguments(x) + is_pow_integer = symtype(p) <: Integer + if !is_pow_integer + push!(data.non_polynomial_terms, x) + push!(data.reasons, NonPolynomialReason.NonIntegerExponent) + end if symbolic_type(p) != NotSymbolic() - @warn "In $x: Exponent $p cannot be symbolic" - is_pow_integer = false - elseif !(p isa Integer) - @warn "In $x: Exponent $p is not an integer" - is_pow_integer = false - else - is_pow_integer = true + data.has_parametric_exponent = true end exponent_has_unknowns = contains_variable(p, wrt) if exponent_has_unknowns - @warn "In $x: Exponent $p cannot contain unknowns of the system." + push!(data.non_polynomial_terms, x) + push!(data.reasons, NonPolynomialReason.ExponentContainsUnknowns) end - base_polynomial = is_polynomial(b, wrt) + base_polynomial = is_polynomial!(data, b, wrt) if !base_polynomial - @warn "In $x: Base is not a polynomial" + push!(data.non_polynomial_terms, x) + push!(data.reasons, NonPolynomialReason.BaseNotPolynomial) end return base_polynomial && !exponent_has_unknowns && is_pow_integer end - @warn "In $x: Unrecognized operation $(operation(x)). Allowed polynomial operations are `*, +, -, ^`" + push!(data.non_polynomial_terms, x) + push!(data.reasons, NonPolynomialReason.UnrecognizedOperation) return false end @@ -182,11 +242,17 @@ The problem will be solved by HomotopyContinuation.jl. The resultant `NonlinearS will contain the polynomial root closest to the point specified by `u0map` (if real roots exist for the system). -Keyword arguments are forwarded to `HomotopyContinuation.solver_startsystems`. +Keyword arguments: +- `eval_expression`: Whether to `eval` the generated functions or use a `RuntimeGeneratedFunction`. +- `eval_module`: The module to use for `eval`/`@RuntimeGeneratedFunction` +- `warn_parametric_exponent`: Whether to warn if the system contains a parametric + exponent preventing the homotopy from being cached. + +All other keyword arguments are forwarded to `HomotopyContinuation.solver_startsystems`. """ function MTK.HomotopyContinuationProblem( sys::NonlinearSystem, u0map, parammap = nothing; eval_expression = false, - eval_module = ModelingToolkit, kwargs...) + eval_module = ModelingToolkit, warn_parametric_exponent = true, kwargs...) if !iscomplete(sys) error("A completed `NonlinearSystem` is required. Call `complete` or `structural_simplify` on the system before creating a `HomotopyContinuationProblem`") end @@ -200,9 +266,14 @@ function MTK.HomotopyContinuationProblem( eqs = full_equations(sys) denoms = [] + has_parametric_exponents = false eqs2 = map(eqs) do eq - if !is_polynomial(eq.lhs, dvs) || !is_polynomial(eq.rhs, dvs) - error("Equation $eq is not a polynomial in the unknowns. See warnings for further details.") + data = PolynomialData() + process_polynomial!(data, eq.lhs, dvs) + process_polynomial!(data, eq.rhs, dvs) + has_parametric_exponents |= data.has_parametric_exponent + if !isempty(data.non_polynomial_terms) + throw(NotPolynomialError(eq, data)) end num, den = handle_rational_polynomials(eq.rhs - eq.lhs, dvs) @@ -235,7 +306,18 @@ function MTK.HomotopyContinuationProblem( obsfn = MTK.ObservedFunctionCache(sys; eval_expression, eval_module) - solver_and_starts = HomotopyContinuation.solver_startsolutions(mtkhsys; kwargs...) + if has_parametric_exponents + if warn_parametric_exponent + @warn """ + The system has parametric exponents, preventing caching of the homotopy. \ + This will cause `solve` to be slower. Pass `warn_parametric_exponent \ + = false` to turn off this warning + """ + end + solver_and_starts = nothing + else + solver_and_starts = HomotopyContinuation.solver_startsolutions(mtkhsys; kwargs...) + end return MTK.HomotopyContinuationProblem( u0, mtkhsys, denominator, sys, obsfn, solver_and_starts) end @@ -260,8 +342,13 @@ Extra keyword arguments: """ function CommonSolve.solve(prob::MTK.HomotopyContinuationProblem, alg = nothing; show_progress = false, denominator_abstol = 1e-7, kwargs...) - solver, starts = prob.solver_and_starts - sol = HomotopyContinuation.solve(solver, starts; show_progress, kwargs...) + if prob.solver_and_starts === nothing + sol = HomotopyContinuation.solve( + prob.homotopy_continuation_system; show_progress, kwargs...) + else + solver, starts = prob.solver_and_starts + sol = HomotopyContinuation.solve(solver, starts; show_progress, kwargs...) + end realsols = HomotopyContinuation.results(sol; only_real = true) if isempty(realsols) u = state_values(prob) diff --git a/src/ModelingToolkit.jl b/src/ModelingToolkit.jl index bdb9168fa0..de8e69c41f 100644 --- a/src/ModelingToolkit.jl +++ b/src/ModelingToolkit.jl @@ -55,6 +55,7 @@ using RecursiveArrayTools import Graphs: SimpleDiGraph, add_edge!, incidence_matrix import BlockArrays: BlockedArray, Block, blocksize, blocksizes import CommonSolve +import EnumX using RuntimeGeneratedFunctions using RuntimeGeneratedFunctions: drop_expr diff --git a/test/extensions/homotopy_continuation.jl b/test/extensions/homotopy_continuation.jl index 5c4470befa..172ee58890 100644 --- a/test/extensions/homotopy_continuation.jl +++ b/test/extensions/homotopy_continuation.jl @@ -61,25 +61,32 @@ end @test sol.retcode == ReturnCode.ConvergenceFailure end -@testset "Polynomial check and warnings" begin +@testset "Parametric exponents" begin @variables x = 1.0 @parameters n::Integer = 4 @mtkbuild sys = NonlinearSystem([x^n + x^2 - 1 ~ 0]) - @test_warn ["Exponent", "cannot be symbolic"] @test_throws "not a polynomial" HomotopyContinuationProblem( - sys, []) + prob = @test_warn ["parametric", "exponent"] HomotopyContinuationProblem(sys, []) + @test prob.solver_and_starts === nothing + @test_nowarn HomotopyContinuationProblem(sys, []; warn_parametric_exponent = false) + sol = solve(prob; threading = false) + @test SciMLBase.successful_retcode(sol) +end + +@testset "Polynomial check and warnings" begin + @variables x = 1.0 @mtkbuild sys = NonlinearSystem([x^1.5 + x^2 - 1 ~ 0]) - @test_warn ["Exponent", "not an integer"] @test_throws "not a polynomial" HomotopyContinuationProblem( + @test_throws ["Exponent", "not an integer", "not a polynomial"] HomotopyContinuationProblem( sys, []) @mtkbuild sys = NonlinearSystem([x^x - x ~ 0]) - @test_warn ["Exponent", "unknowns"] @test_throws "not a polynomial" HomotopyContinuationProblem( + @test_throws ["Exponent", "unknowns", "not a polynomial"] HomotopyContinuationProblem( sys, []) @mtkbuild sys = NonlinearSystem([((x^2) / sin(x))^2 + x ~ 0]) - @test_warn ["Unrecognized", "sin"] @test_throws "not a polynomial" HomotopyContinuationProblem( + @test_throws ["recognized", "sin", "not a polynomial"] HomotopyContinuationProblem( sys, []) @variables y = 2.0 @mtkbuild sys = NonlinearSystem([x^2 + y^2 + 2 ~ 0, y ~ sin(x)]) - @test_warn ["Unrecognized", "sin"] @test_throws "not a polynomial" HomotopyContinuationProblem( + @test_throws ["recognized", "sin", "not a polynomial"] HomotopyContinuationProblem( sys, []) end @@ -137,7 +144,7 @@ end end end -@test "Non-polynomial observed not used in equations" begin +@testset "Non-polynomial observed not used in equations" begin @variables x=1 y @mtkbuild sys = NonlinearSystem([x^2 - 2 ~ 0, y ~ sin(x)]) prob = HomotopyContinuationProblem(sys, []) From d2849786277bbb03232672542d10565437f1e18a Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Sun, 10 Nov 2024 01:35:19 +0530 Subject: [PATCH 0325/1337] test: move BifurcationKit to the end of the group --- test/runtests.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/runtests.jl b/test/runtests.jl index c7edbfaaf5..192ead935c 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -109,9 +109,9 @@ end if GROUP == "All" || GROUP == "Extensions" activate_extensions_env() - @safetestset "BifurcationKit Extension Test" include("extensions/bifurcationkit.jl") @safetestset "HomotopyContinuation Extension Test" include("extensions/homotopy_continuation.jl") @safetestset "Auto Differentiation Test" include("extensions/ad.jl") @safetestset "LabelledArrays Test" include("labelledarrays.jl") + @safetestset "BifurcationKit Extension Test" include("extensions/bifurcationkit.jl") end end From 5f0a4eacbe7b46e727e2f0d165b0460ede788e98 Mon Sep 17 00:00:00 2001 From: Sam Isaacson Date: Mon, 11 Nov 2024 12:13:36 -0500 Subject: [PATCH 0326/1337] finish tests --- test/jumpsystem.jl | 68 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 67 insertions(+), 1 deletion(-) diff --git a/test/jumpsystem.jl b/test/jumpsystem.jl index 9569208474..7de84ea5a3 100644 --- a/test/jumpsystem.jl +++ b/test/jumpsystem.jl @@ -1,5 +1,5 @@ using ModelingToolkit, DiffEqBase, JumpProcesses, Test, LinearAlgebra -using Random, StableRNGs +using Random, StableRNGs, NonlinearSolve using OrdinaryDiffEq using ModelingToolkit: t_nounits as t, D_nounits as D MT = ModelingToolkit @@ -456,4 +456,70 @@ let Yact(t) = Y0 * exp(-k2val/10 * t) + (k1val / (k2val/10)) * (1 - exp(-k2val/10 * t)) @test all(abs.(Xv .- Xact.(times)) .<= 0.05 .* Xv) @test all(abs.(Yv .- Yact.(times)) .<= 0.1 .* Yv) +end + +# that mixes ODEs and jump types, and then contin events +let + Random.seed!(rng, 1111) + @variables X(t) Y(t) + @parameters α β + vrj = VariableRateJump(β * X, [X ~ X - 1]; save_positions = (false, false)) + crj = ConstantRateJump(β*Y, [Y ~ Y - 1]) + maj = MassActionJump(α, [0 => 1], [Y => 1]) + eqs = [D(X) ~ α*(1 + Y)] + @named jsys = JumpSystem([maj, crj, vrj, eqs[1]], t, [X, Y], [α, β]) + jsys = complete(jsys) + + p = (α = 6.0, β = 2.0, X₀ = 2.0, Y₀ = 1.0) + u0map = [X => p.X₀, Y => p.Y₀] + pmap = [α => p.α, β => p.β] + tspan = (0.0, 20.0) + oprob = ODEProblem(jsys, u0map, tspan, pmap) + jprob = JumpProblem(jsys, oprob; rng, save_positions = (false, false)) + + times = range(0.0, tspan[2], length = 100) + Nsims = 4000 + Xv = zeros(length(times)) + Yv = zeros(length(times)) + for n in 1:Nsims + sol = solve(jprob, Tsit5(); saveat = times) + Xv .+= sol[1,:] + Yv .+= sol[2,:] + end + Xv ./= Nsims; Yv ./= Nsims; + function Yf(t, p) + @unpack α, β, Y₀ = p + return (α / β) + (Y₀ - α / β) * exp(-β * t) + end + function Xf(t, p) + @unpack α, β, X₀, Y₀ = p + return (α / β) + (α^2 / β^2) + α * (Y₀ - α / β) * t * exp(-β * t) + (X₀ - α / β - α^2 / β^2) * exp(-β * t) + end + Xact = [Xf(t,p) for t in times] + Yact = [Yf(t,p) for t in times] + @test all(abs.(Xv .- Xact) .<= 0.05 .* Xv) + @test all(abs.(Yv .- Yact) .<= 0.05 .* Yv) + + Xss = (p.α / p.β) + (p.α^2 / p.β^2) + function affect!(integ, u, p, ctx) + savevalues!(integ, true) + terminate!(integ) + nothing + end + cevents = [t ~ .2] => (affect!, [], [], [], nothing) + @named jsys = JumpSystem([maj, crj, vrj, eqs[1]], t, [X, Y], [α, β]; + continuous_events = cevents) + jsys = complete(jsys) + tspan = (0.0, 200.0) + oprob = ODEProblem(jsys, u0map, tspan, pmap) + jprob = JumpProblem(jsys, oprob; rng, save_positions = (false, false)) + Xsamp = 0.0 + Nsims = 4000 + for n in 1:Nsims + sol = solve(jprob, Tsit5(), saveat = tspan[2]) + @test sol.retcode == ReturnCode.Terminated + Xsamp += sol[1,end] + end + Xsamp /= Nsims + @test abs(Xsamp - Xf(.2,p) < .05 * Xf(.2,p)) end \ No newline at end of file From 350f9df1540cf888efefb48b9ca940e5841c81af Mon Sep 17 00:00:00 2001 From: Sam Isaacson Date: Mon, 11 Nov 2024 12:26:57 -0500 Subject: [PATCH 0327/1337] bug fix --- test/jumpsystem.jl | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/test/jumpsystem.jl b/test/jumpsystem.jl index 7de84ea5a3..7841e3b4ff 100644 --- a/test/jumpsystem.jl +++ b/test/jumpsystem.jl @@ -469,14 +469,12 @@ let eqs = [D(X) ~ α*(1 + Y)] @named jsys = JumpSystem([maj, crj, vrj, eqs[1]], t, [X, Y], [α, β]) jsys = complete(jsys) - p = (α = 6.0, β = 2.0, X₀ = 2.0, Y₀ = 1.0) u0map = [X => p.X₀, Y => p.Y₀] pmap = [α => p.α, β => p.β] tspan = (0.0, 20.0) oprob = ODEProblem(jsys, u0map, tspan, pmap) jprob = JumpProblem(jsys, oprob; rng, save_positions = (false, false)) - times = range(0.0, tspan[2], length = 100) Nsims = 4000 Xv = zeros(length(times)) @@ -487,20 +485,20 @@ let Yv .+= sol[2,:] end Xv ./= Nsims; Yv ./= Nsims; + function Yf(t, p) - @unpack α, β, Y₀ = p + local α, β, X₀, Y₀ = p return (α / β) + (Y₀ - α / β) * exp(-β * t) end function Xf(t, p) - @unpack α, β, X₀, Y₀ = p + local α, β, X₀, Y₀ = p return (α / β) + (α^2 / β^2) + α * (Y₀ - α / β) * t * exp(-β * t) + (X₀ - α / β - α^2 / β^2) * exp(-β * t) end Xact = [Xf(t,p) for t in times] Yact = [Yf(t,p) for t in times] @test all(abs.(Xv .- Xact) .<= 0.05 .* Xv) @test all(abs.(Yv .- Yact) .<= 0.05 .* Yv) - - Xss = (p.α / p.β) + (p.α^2 / p.β^2) + function affect!(integ, u, p, ctx) savevalues!(integ, true) terminate!(integ) From 4c6dea1d1f16977ba47753fe5fad64d7fff98ee5 Mon Sep 17 00:00:00 2001 From: Sam Isaacson Date: Mon, 11 Nov 2024 13:10:12 -0500 Subject: [PATCH 0328/1337] format --- src/systems/callbacks.jl | 8 ++--- src/systems/jumps/jumpsystem.jl | 8 ++--- test/jumpsystem.jl | 63 ++++++++++++++++++--------------- 3 files changed, 43 insertions(+), 36 deletions(-) diff --git a/src/systems/callbacks.jl b/src/systems/callbacks.jl index dcc48286af..e5db9a71b6 100644 --- a/src/systems/callbacks.jl +++ b/src/systems/callbacks.jl @@ -678,8 +678,8 @@ function compile_affect(eqs::Vector{Equation}, cb, sys, dvs, ps; outputidxs = no end end -function generate_rootfinding_callback(sys::AbstractTimeDependentSystem, dvs = unknowns(sys), - ps = parameters(sys); kwargs...) +function generate_rootfinding_callback(sys::AbstractTimeDependentSystem, + dvs = unknowns(sys), ps = parameters(sys); kwargs...) cbs = continuous_events(sys) isempty(cbs) && return nothing generate_rootfinding_callback(cbs, sys, dvs, ps; kwargs...) @@ -860,8 +860,8 @@ function compile_affect_fn(cb, sys::AbstractTimeDependentSystem, 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); kwargs...) +function generate_rootfinding_callback(cbs, sys::AbstractTimeDependentSystem, + dvs = unknowns(sys), ps = parameters(sys); kwargs...) eqs = map(cb -> flatten_equations(cb.eqs), cbs) num_eqs = length.(eqs) total_eqs = sum(num_eqs) diff --git a/src/systems/jumps/jumpsystem.jl b/src/systems/jumps/jumpsystem.jl index e745d0c627..e5e17fb5f9 100644 --- a/src/systems/jumps/jumpsystem.jl +++ b/src/systems/jumps/jumpsystem.jl @@ -140,7 +140,7 @@ struct JumpSystem{U <: ArrayPartition} <: AbstractTimeDependentSystem end new{U}(tag, ap, iv, unknowns, ps, var_to_name, observed, name, description, systems, defaults, - connector_type, cevents, devents, parameter_dependencies, metadata, + connector_type, cevents, devents, parameter_dependencies, metadata, gui_metadata, complete, index_cache, isscheduled) end end @@ -494,17 +494,17 @@ function DiffEqBase.ODEProblem(sys::JumpSystem, u0map, tspan::Union{Tuple, Nothi if has_equations(sys) osys = ODESystem(equations(sys).x[4], get_iv(sys), unknowns(sys), parameters(sys); observed = observed(sys), name = nameof(sys), description = description(sys), - systems = get_systems(sys), defaults = defaults(sys), + systems = get_systems(sys), defaults = defaults(sys), parameter_dependencies = parameter_dependencies(sys), metadata = get_metadata(sys), gui_metadata = get_gui_metadata(sys)) osys = complete(osys) return ODEProblem(osys, u0map, tspan, parammap; check_length = false, kwargs...) else _, u0, p = process_SciMLProblem(EmptySciMLFunction, sys, u0map, parammap; - t = tspan === nothing ? nothing : tspan[1], use_union, tofloat = false, + t = tspan === nothing ? nothing : tspan[1], use_union, tofloat = false, check_length = false) f = (du, u, p, t) -> (du .= 0; nothing) - observedfun = ObservedFunctionCache(sys; eval_expression, eval_module) + observedfun = ObservedFunctionCache(sys; eval_expression, eval_module) df = ODEFunction(f; sys, observed = observedfun) return ODEProblem(df, u0, tspan, p; kwargs...) end diff --git a/test/jumpsystem.jl b/test/jumpsystem.jl index 7841e3b4ff..d27a54598b 100644 --- a/test/jumpsystem.jl +++ b/test/jumpsystem.jl @@ -301,7 +301,7 @@ end # basic VariableRateJump test let N = 1000 # number of simulations for testing solve accuracy - Random.seed!(rng, 1111) + 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]) @@ -425,17 +425,19 @@ end # PDMP test let - Random.seed!(rng, 1111) + Random.seed!(rng, 1111) @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)) - eqs = [D(X) ~ k2, D(Y) ~ -k2/10*Y] + 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) - X0 = 0.0; Y0 = 0.0 + X0 = 0.0 + Y0 = 0.0 u0 = [X => X0, Y => Y0] - k1val = 1.0; k2val = 20.0 + k1val = 1.0 + k2val = 20.0 p = [k1 => k1val, k2 => k2val] tspan = (0.0, 10.0) oprob = ODEProblem(jsys, u0, tspan, p) @@ -447,27 +449,30 @@ let Yv = zeros(length(times)) for n in 1:Nsims sol = solve(jprob, Tsit5(); saveat = times) - Xv .+= sol[1,:] - Yv .+= sol[2,:] + Xv .+= sol[1, :] + Yv .+= sol[2, :] end - Xv ./= Nsims; Yv ./= Nsims; + Xv ./= Nsims + Yv ./= Nsims Xact(t) = X0 * exp(-k1val * t) + (k2val / k1val) * (1 - exp(-k1val * t)) - Yact(t) = Y0 * exp(-k2val/10 * t) + (k1val / (k2val/10)) * (1 - exp(-k2val/10 * t)) + function Yact(t) + Y0 * exp(-k2val / 10 * t) + (k1val / (k2val / 10)) * (1 - exp(-k2val / 10 * t)) + end @test all(abs.(Xv .- Xact.(times)) .<= 0.05 .* Xv) - @test all(abs.(Yv .- Yact.(times)) .<= 0.1 .* Yv) + @test all(abs.(Yv .- Yact.(times)) .<= 0.1 .* Yv) end # that mixes ODEs and jump types, and then contin events let - Random.seed!(rng, 1111) + Random.seed!(rng, 1111) @variables X(t) Y(t) @parameters α β vrj = VariableRateJump(β * X, [X ~ X - 1]; save_positions = (false, false)) - crj = ConstantRateJump(β*Y, [Y ~ Y - 1]) + crj = ConstantRateJump(β * Y, [Y ~ Y - 1]) maj = MassActionJump(α, [0 => 1], [Y => 1]) - eqs = [D(X) ~ α*(1 + Y)] - @named jsys = JumpSystem([maj, crj, vrj, eqs[1]], t, [X, Y], [α, β]) + eqs = [D(X) ~ α * (1 + Y)] + @named jsys = JumpSystem([maj, crj, vrj, eqs[1]], t, [X, Y], [α, β]) jsys = complete(jsys) p = (α = 6.0, β = 2.0, X₀ = 2.0, Y₀ = 1.0) u0map = [X => p.X₀, Y => p.Y₀] @@ -481,10 +486,11 @@ let Yv = zeros(length(times)) for n in 1:Nsims sol = solve(jprob, Tsit5(); saveat = times) - Xv .+= sol[1,:] - Yv .+= sol[2,:] + Xv .+= sol[1, :] + Yv .+= sol[2, :] end - Xv ./= Nsims; Yv ./= Nsims; + Xv ./= Nsims + Yv ./= Nsims function Yf(t, p) local α, β, X₀, Y₀ = p @@ -492,21 +498,22 @@ let end function Xf(t, p) local α, β, X₀, Y₀ = p - return (α / β) + (α^2 / β^2) + α * (Y₀ - α / β) * t * exp(-β * t) + (X₀ - α / β - α^2 / β^2) * exp(-β * t) + return (α / β) + (α^2 / β^2) + α * (Y₀ - α / β) * t * exp(-β * t) + + (X₀ - α / β - α^2 / β^2) * exp(-β * t) end - Xact = [Xf(t,p) for t in times] - Yact = [Yf(t,p) for t in times] + Xact = [Xf(t, p) for t in times] + Yact = [Yf(t, p) for t in times] @test all(abs.(Xv .- Xact) .<= 0.05 .* Xv) - @test all(abs.(Yv .- Yact) .<= 0.05 .* Yv) - + @test all(abs.(Yv .- Yact) .<= 0.05 .* Yv) + function affect!(integ, u, p, ctx) savevalues!(integ, true) terminate!(integ) nothing end - cevents = [t ~ .2] => (affect!, [], [], [], nothing) - @named jsys = JumpSystem([maj, crj, vrj, eqs[1]], t, [X, Y], [α, β]; - continuous_events = cevents) + cevents = [t ~ 0.2] => (affect!, [], [], [], nothing) + @named jsys = JumpSystem([maj, crj, vrj, eqs[1]], t, [X, Y], [α, β]; + continuous_events = cevents) jsys = complete(jsys) tspan = (0.0, 200.0) oprob = ODEProblem(jsys, u0map, tspan, pmap) @@ -516,8 +523,8 @@ let for n in 1:Nsims sol = solve(jprob, Tsit5(), saveat = tspan[2]) @test sol.retcode == ReturnCode.Terminated - Xsamp += sol[1,end] + Xsamp += sol[1, end] end Xsamp /= Nsims - @test abs(Xsamp - Xf(.2,p) < .05 * Xf(.2,p)) -end \ No newline at end of file + @test abs(Xsamp - Xf(0.2, p) < 0.05 * Xf(0.2, p)) +end From efee2f9207cb5bbf808e7a3e3370ca45b85029e1 Mon Sep 17 00:00:00 2001 From: Sam Isaacson Date: Mon, 11 Nov 2024 17:23:43 -0500 Subject: [PATCH 0329/1337] try tweaking test --- test/jumpsystem.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/jumpsystem.jl b/test/jumpsystem.jl index d27a54598b..adeb751899 100644 --- a/test/jumpsystem.jl +++ b/test/jumpsystem.jl @@ -434,7 +434,7 @@ let @named jsys = JumpSystem([vrj1, vrj2, eqs[1], eqs[2]], t, [X, Y], [k1, k2]) jsys = complete(jsys) X0 = 0.0 - Y0 = 0.0 + Y0 = 3.0 u0 = [X => X0, Y => Y0] k1val = 1.0 k2val = 20.0 From ddf0612b2815d4d179b3bff50f8930794e01589f Mon Sep 17 00:00:00 2001 From: Sam Isaacson Date: Mon, 11 Nov 2024 17:33:11 -0500 Subject: [PATCH 0330/1337] seed default rng too --- test/jumpsystem.jl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/jumpsystem.jl b/test/jumpsystem.jl index adeb751899..b187a7a7ae 100644 --- a/test/jumpsystem.jl +++ b/test/jumpsystem.jl @@ -426,6 +426,7 @@ end # PDMP test let Random.seed!(rng, 1111) + Random.seed!(Random.default_rng(), 1111) @variables X(t) Y(t) @parameters k1 k2 vrj1 = VariableRateJump(k1 * X, [X ~ X - 1]; save_positions = (false, false)) @@ -466,6 +467,7 @@ end # that mixes ODEs and jump types, and then contin events let Random.seed!(rng, 1111) + Random.seed!(Random.default_rng(), 1111) @variables X(t) Y(t) @parameters α β vrj = VariableRateJump(β * X, [X ~ X - 1]; save_positions = (false, false)) From 08decab292d1bb596d70c42d618f43cdf91cc586 Mon Sep 17 00:00:00 2001 From: Sam Isaacson Date: Mon, 11 Nov 2024 20:44:26 -0500 Subject: [PATCH 0331/1337] try to make tests deterministic --- test/jumpsystem.jl | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/test/jumpsystem.jl b/test/jumpsystem.jl index b187a7a7ae..c63d4e3d15 100644 --- a/test/jumpsystem.jl +++ b/test/jumpsystem.jl @@ -425,8 +425,8 @@ end # PDMP test let - Random.seed!(rng, 1111) - Random.seed!(Random.default_rng(), 1111) + seed = 1111 + Random.seed!(rng, seed) @variables X(t) Y(t) @parameters k1 k2 vrj1 = VariableRateJump(k1 * X, [X ~ X - 1]; save_positions = (false, false)) @@ -449,9 +449,10 @@ let Xv = zeros(length(times)) Yv = zeros(length(times)) for n in 1:Nsims - sol = solve(jprob, Tsit5(); saveat = times) + sol = solve(jprob, Tsit5(); saveat = times, seed) Xv .+= sol[1, :] Yv .+= sol[2, :] + seed += 1 end Xv ./= Nsims Yv ./= Nsims @@ -466,8 +467,8 @@ end # that mixes ODEs and jump types, and then contin events let - Random.seed!(rng, 1111) - Random.seed!(Random.default_rng(), 1111) + seed = 1111 + Random.seed!(rng, seed) @variables X(t) Y(t) @parameters α β vrj = VariableRateJump(β * X, [X ~ X - 1]; save_positions = (false, false)) @@ -487,9 +488,10 @@ let Xv = zeros(length(times)) Yv = zeros(length(times)) for n in 1:Nsims - sol = solve(jprob, Tsit5(); saveat = times) + sol = solve(jprob, Tsit5(); saveat = times, seed) Xv .+= sol[1, :] Yv .+= sol[2, :] + seed += 1 end Xv ./= Nsims Yv ./= Nsims @@ -523,9 +525,10 @@ let Xsamp = 0.0 Nsims = 4000 for n in 1:Nsims - sol = solve(jprob, Tsit5(), saveat = tspan[2]) + sol = solve(jprob, Tsit5(), saveat = tspan[2], seed) @test sol.retcode == ReturnCode.Terminated Xsamp += sol[1, end] + seed += 1 end Xsamp /= Nsims @test abs(Xsamp - Xf(0.2, p) < 0.05 * Xf(0.2, p)) From 456eb885a6b04dceef9e4059587a298595db3128 Mon Sep 17 00:00:00 2001 From: Herman Sletmoen Date: Tue, 12 Nov 2024 11:27:17 +0100 Subject: [PATCH 0332/1337] Update perturbation.md --- docs/src/examples/perturbation.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/src/examples/perturbation.md b/docs/src/examples/perturbation.md index cd1e843f55..407248709d 100644 --- a/docs/src/examples/perturbation.md +++ b/docs/src/examples/perturbation.md @@ -39,7 +39,8 @@ eqs_pert = taylor_coeff(eq_pert, ϵ, 0:2) !!! note The 0-th order equation can be solved analytically, but ModelingToolkit does currently not feature automatic analytical solution of ODEs, so we proceed with solving it numerically. - These are the ODEs we want to solve. Now construct an `ODESystem`, which automatically inserts dummy derivatives for the velocities: + +These are the ODEs we want to solve. Now construct an `ODESystem`, which automatically inserts dummy derivatives for the velocities: ```@example perturbation @mtkbuild sys = ODESystem(eqs_pert, t) From 0ec1fa39e15ae81c569ee2c93e7731ffe2825265 Mon Sep 17 00:00:00 2001 From: Sam Isaacson Date: Tue, 12 Nov 2024 05:39:01 -0500 Subject: [PATCH 0333/1337] Update test/jumpsystem.jl --- test/jumpsystem.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/jumpsystem.jl b/test/jumpsystem.jl index c63d4e3d15..0fd6dc4af0 100644 --- a/test/jumpsystem.jl +++ b/test/jumpsystem.jl @@ -525,7 +525,7 @@ let Xsamp = 0.0 Nsims = 4000 for n in 1:Nsims - sol = solve(jprob, Tsit5(), saveat = tspan[2], seed) + sol = solve(jprob, Tsit5(); saveat = tspan[2], seed) @test sol.retcode == ReturnCode.Terminated Xsamp += sol[1, end] seed += 1 From 3bc5d236053cebf4b08315f64379c3666d5c9e84 Mon Sep 17 00:00:00 2001 From: Benjamin Chung Date: Tue, 12 Nov 2024 16:29:47 -0800 Subject: [PATCH 0334/1337] Fix merge issues --- src/systems/callbacks.jl | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/systems/callbacks.jl b/src/systems/callbacks.jl index c2c895626c..fab573cffb 100644 --- a/src/systems/callbacks.jl +++ b/src/systems/callbacks.jl @@ -247,8 +247,6 @@ struct SymbolicContinuousCallback initialize = NULL_AFFECT, finalize = NULL_AFFECT, rootfind = SciMLBase.LeftRootFind, - initialize = NULL_AFFECT, - finalize = NULL_AFFECT, reinitializealg = SciMLBase.CheckInit()) new(eqs, initialize, finalize, make_affect(affect), make_affect(affect_neg), rootfind, reinitializealg) @@ -387,16 +385,6 @@ function affect_negs(cbs::Vector{SymbolicContinuousCallback}) mapreduce(affect_negs, vcat, cbs, init = Equation[]) end -initialize_affects(cb::SymbolicContinuousCallback) = cb.initialize -function initialize_affects(cbs::Vector{SymbolicContinuousCallback}) - mapreduce(initialize_affects, vcat, cbs, init = Equation[]) -end - -finalize_affects(cb::SymbolicContinuousCallback) = cb.initialize -function finalize_affects(cbs::Vector{SymbolicContinuousCallback}) - mapreduce(finalize_affects, vcat, cbs, init = Equation[]) -end - reinitialization_alg(cb::SymbolicContinuousCallback) = cb.reinitializealg function reinitialization_algs(cbs::Vector{SymbolicContinuousCallback}) mapreduce( From e69ef194e685fb94016541255030b75764acbbb5 Mon Sep 17 00:00:00 2001 From: Benjamin Chung Date: Tue, 12 Nov 2024 23:45:49 -0800 Subject: [PATCH 0335/1337] Clean up usage of custom init --- src/systems/callbacks.jl | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/src/systems/callbacks.jl b/src/systems/callbacks.jl index fab573cffb..9c8187f6b7 100644 --- a/src/systems/callbacks.jl +++ b/src/systems/callbacks.jl @@ -922,18 +922,11 @@ function generate_vector_rootfinding_callback( (cb, fn) -> begin if (save_idxs = get(ic.callback_to_clocks, cb, nothing)) !== nothing let save_idxs = save_idxs - if !isnothing(fn.initialize) - (i) -> begin - fn.initialize(i) - for idx in save_idxs - SciMLBase.save_discretes!(i, idx) - end - end - else - (i) -> begin - for idx in save_idxs - SciMLBase.save_discretes!(i, idx) - end + 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 From 3c6b37ed3852d3590afe9b29f9690b0824f84a09 Mon Sep 17 00:00:00 2001 From: Benjamin Chung Date: Tue, 12 Nov 2024 23:46:52 -0800 Subject: [PATCH 0336/1337] Simplify setup function construction --- src/systems/callbacks.jl | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/src/systems/callbacks.jl b/src/systems/callbacks.jl index 9c8187f6b7..b9423b8f07 100644 --- a/src/systems/callbacks.jl +++ b/src/systems/callbacks.jl @@ -918,24 +918,21 @@ function generate_vector_rootfinding_callback( initialize = nothing if has_index_cache(sys) && (ic = get_index_cache(sys)) !== nothing initialize = handle_optional_setup_fn( - map( - (cb, fn) -> begin - 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 + 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 - else - fn.initialize end - end, - cbs, - affect_functions), + else + fn.initialize + end + end, SciMLBase.INITIALIZE_DEFAULT) else From 8edc6b0927737608d7d510e6ce0bd99ed2a153c3 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 13 Nov 2024 14:07:55 +0530 Subject: [PATCH 0337/1337] feat: support directly generating observed functions for tuples --- Project.toml | 2 +- src/systems/abstractsystem.jl | 5 ++++- src/systems/diffeqs/odesystem.jl | 13 ++++++++++++- test/symbolic_indexing_interface.jl | 19 +++++++++++++++++++ 4 files changed, 36 insertions(+), 3 deletions(-) diff --git a/Project.toml b/Project.toml index 0e804e1f63..7e64764e63 100644 --- a/Project.toml +++ b/Project.toml @@ -131,7 +131,7 @@ SimpleNonlinearSolve = "0.1.0, 1, 2" SparseArrays = "1" SpecialFunctions = "0.7, 0.8, 0.9, 0.10, 1.0, 2" StaticArrays = "0.10, 0.11, 0.12, 1.0" -SymbolicIndexingInterface = "0.3.31" +SymbolicIndexingInterface = "0.3.35" SymbolicUtils = "3.7" Symbolics = "6.15.4" URIs = "1" diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index e715dc6eea..44681e1db6 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -818,6 +818,8 @@ function SymbolicIndexingInterface.is_observed(sys::AbstractSystem, sym) !is_independent_variable(sys, sym) && symbolic_type(sym) != NotSymbolic() end +SymbolicIndexingInterface.supports_tuple_observed(::AbstractSystem) = true + function SymbolicIndexingInterface.observed( sys::AbstractSystem, sym; eval_expression = false, eval_module = @__MODULE__) if has_index_cache(sys) && (ic = get_index_cache(sys)) !== nothing @@ -827,7 +829,8 @@ function SymbolicIndexingInterface.observed( throw(ArgumentError("Symbol $sym does not exist in the system")) end sym = _sym - elseif sym isa AbstractArray && symbolic_type(sym) isa NotSymbolic && + elseif (sym isa Tuple || + (sym isa AbstractArray && symbolic_type(sym) isa NotSymbolic)) && any(x -> x isa Symbol, sym) sym = map(sym) do s if s isa Symbol diff --git a/src/systems/diffeqs/odesystem.jl b/src/systems/diffeqs/odesystem.jl index 4d37339518..29423baf5e 100644 --- a/src/systems/diffeqs/odesystem.jl +++ b/src/systems/diffeqs/odesystem.jl @@ -427,6 +427,10 @@ function build_explicit_observed_function(sys, ts; param_only = false, op = Operator, throw = true) + is_tuple = ts isa Tuple + if is_tuple + ts = collect(ts) + end if (isscalar = symbolic_type(ts) !== NotSymbolic()) ts = [ts] end @@ -573,9 +577,16 @@ function build_explicit_observed_function(sys, ts; # Need to keep old method of building the function since it uses `output_type`, # which can't be provided to `build_function` + return_value = if isscalar + ts[1] + elseif is_tuple + MakeTuple(Tuple(ts)) + else + MakeArray(ts, output_type) + end oop_fn = Func(args, [], pre(Let(obsexprs, - isscalar ? ts[1] : MakeArray(ts, output_type), + return_value, false))) |> array_wrapper[1] |> oop_mtkp_wrapper |> toexpr oop_fn = expression ? oop_fn : eval_or_rgf(oop_fn; eval_expression, eval_module) diff --git a/test/symbolic_indexing_interface.jl b/test/symbolic_indexing_interface.jl index 0f42ed7d34..161d926cb2 100644 --- a/test/symbolic_indexing_interface.jl +++ b/test/symbolic_indexing_interface.jl @@ -8,6 +8,7 @@ using SciMLStructures: Tunable eqs = [D(x) ~ a * y + t, D(y) ~ b * t] @named odesys = ODESystem(eqs, t, [x, y], [a, b]; observed = [xy ~ x + y]) odesys = complete(odesys) + @test SymbolicIndexingInterface.supports_tuple_observed(odesys) @test all(is_variable.((odesys,), [x, y, 1, 2, :x, :y])) @test all(.!is_variable.((odesys,), [a, b, t, 3, 0, :a, :b])) @test variable_index.((odesys,), [x, y, a, b, t, 1, 2, :x, :y, :a, :b]) == @@ -33,6 +34,14 @@ using SciMLStructures: Tunable @test default_values(odesys)[y] == 2.0 @test isequal(default_values(odesys)[xy], x + y) + prob = ODEProblem(odesys, [], (0.0, 1.0), [a => 1.0, b => 2.0]) + getter = getu(odesys, (x + 1, x + 2)) + @test getter(prob) isa Tuple + @test_nowarn @inferred getter(prob) + getter = getp(odesys, (a + 1, a + 2)) + @test getter(prob) isa Tuple + @test_nowarn @inferred getter(prob) + @named odesys = ODESystem( eqs, t, [x, y], [a, b]; defaults = [xy => 3.0], observed = [xy ~ x + y]) odesys = complete(odesys) @@ -99,6 +108,7 @@ end 0 ~ x * y - β * z] @named ns = NonlinearSystem(eqs, [x, y, z], [σ, ρ, β]) ns = complete(ns) + @test SymbolicIndexingInterface.supports_tuple_observed(ns) @test !is_time_dependent(ns) ps = ModelingToolkit.MTKParameters(ns, [σ => 1.0, ρ => 2.0, β => 3.0]) pobs = parameter_observed(ns, σ + ρ) @@ -107,6 +117,15 @@ end pobs = parameter_observed(ns, [σ + ρ, ρ + β]) @test isempty(get_all_timeseries_indexes(ns, [σ + ρ, ρ + β])) @test pobs(ps) == [3.0, 5.0] + + prob = NonlinearProblem( + ns, [x => 1.0, y => 2.0, z => 3.0], [σ => 1.0, ρ => 2.0, β => 3.0]) + getter = getu(ns, (x + 1, x + 2)) + @test getter(prob) isa Tuple + @test_nowarn @inferred getter(prob) + getter = getp(ns, (σ + 1, σ + 2)) + @test getter(prob) isa Tuple + @test_nowarn @inferred getter(prob) end @testset "PDESystem" begin From 82d55dc3f38ca924a5e2870a96a53c0863072595 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 14 Nov 2024 11:48:42 +0530 Subject: [PATCH 0338/1337] fix: fix `getproperty` syntax on `structural_simplify(complete(sys))` --- src/systems/abstractsystem.jl | 4 ++-- test/components.jl | 20 ++++++++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index 44681e1db6..f396a9830a 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -1122,7 +1122,7 @@ function Base.propertynames(sys::AbstractSystem; private = false) return fieldnames(typeof(sys)) else if has_parent(sys) && (parent = get_parent(sys); parent !== nothing) - sys = parent + return propertynames(parent; private) end names = Symbol[] for s in get_systems(sys) @@ -1144,7 +1144,7 @@ end function Base.getproperty(sys::AbstractSystem, name::Symbol; namespace = !iscomplete(sys)) if has_parent(sys) && (parent = get_parent(sys); parent !== nothing) - sys = parent + return getproperty(parent, name; namespace) end wrap(getvar(sys, name; namespace = namespace)) end diff --git a/test/components.jl b/test/components.jl index a284c9b0c8..298f9fceb9 100644 --- a/test/components.jl +++ b/test/components.jl @@ -350,3 +350,23 @@ end @test_throws ArgumentError simp.inner₊p @test_throws ArgumentError outer.inner₊p end + +@testset "`getproperty` on `structural_simplify(complete(sys))`" begin + @mtkmodel Foo begin + @variables begin + x(t) + end + end + @mtkmodel Bar begin + @components begin + foo = Foo() + end + @equations begin + D(foo.x) ~ foo.x + end + end + @named bar = Bar() + cbar = complete(bar) + ss = structural_simplify(cbar) + @test isequal(cbar.foo.x, ss.foo.x) +end From 1431f9013b0228141f3ab557faaf17176a0bc3ad Mon Sep 17 00:00:00 2001 From: Benjamin Chung Date: Tue, 12 Nov 2024 16:44:06 -0800 Subject: [PATCH 0339/1337] Document build_explicit_observed_function and allow user-defined array construction --- src/systems/diffeqs/odesystem.jl | 33 ++++++++++++++++++++++++++++---- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/src/systems/diffeqs/odesystem.jl b/src/systems/diffeqs/odesystem.jl index 29423baf5e..ba3a4977ba 100644 --- a/src/systems/diffeqs/odesystem.jl +++ b/src/systems/diffeqs/odesystem.jl @@ -411,8 +411,32 @@ ODESystem(eq::Equation, args...; kwargs...) = ODESystem([eq], args...; kwargs... """ $(SIGNATURES) -Build the observed function assuming the observed equations are all explicit, -i.e. there are no cycles. +Generates a function that computes the observed value(s) `ts` in the system `sys` assuming that there are no cycles in the equations. + +The return value will be either: +* a single function if the input is a scalar or if the input is a Vector but `return_inplace` is false +* the out of place and in-place functions `(ip, oop)` if `return_inplace` is true and the input is a `Vector` + +The function(s) will be: +* `RuntimeGeneratedFunction`s by default, +* A Julia `Expr` if `expression` is true, +* A directly evaluated Julia function in the module `eval_module` if `eval_expression` is true + +The signatures will be of the form `g(...)` with arguments: +* `output` for in-place functions +* `unknowns` if `params_only` is `false` +* `inputs` if `inputs` is an array of symbolic inputs that should be available in `ts` +* `p...` unconditionally; note that in the case of `MTKParameters` more than one parameters argument may be present, so it must be splatted +* `t` if the system is time-dependent; for example `NonlinearSystem` will not have `t` +For example, a function `g(op, unknowns, p, inputs, t)` will be the in-place function generated if `return_inplace` is true, `ts` is a vector, an array of inputs `inputs` is given, and `params_only` is false for a time-dependent system. + +Options not otherwise specified are: +* `output_type = Array` the type of the array generated by the out-of-place vector-valued function +* `checkbounds = true` checks bounds if true when destructuring parameters +* `op = Operator` sets the recursion terminator for the walk done by `vars` to identify the variables that appear in `ts`. See the documentation for `vars` for more detail. +* `throw = true` if true, throw an error when generating a function for `ts` that reference variables that do not exist +* `drop_expr` is deprecated. +* `mkarray`; only used if the output is an array (that is, `!isscalar(ts)`). Called as `mkarray(ts, output_type)` where `ts` are the expressions to put in the array and `output_type` is the argument of the same name passed to build_explicit_observed_function. """ function build_explicit_observed_function(sys, ts; inputs = nothing, @@ -426,7 +450,8 @@ function build_explicit_observed_function(sys, ts; return_inplace = false, param_only = false, op = Operator, - throw = true) + throw = true, + mkarray = MakeArray) is_tuple = ts isa Tuple if is_tuple ts = collect(ts) @@ -582,7 +607,7 @@ function build_explicit_observed_function(sys, ts; elseif is_tuple MakeTuple(Tuple(ts)) else - MakeArray(ts, output_type) + mkarray(ts, output_type) end oop_fn = Func(args, [], pre(Let(obsexprs, From dbf06cd421c29ce21185df2cd1668952cb88fc96 Mon Sep 17 00:00:00 2001 From: Benjamin Chung Date: Thu, 14 Nov 2024 17:17:11 -0800 Subject: [PATCH 0340/1337] Remove deprecated argument to build_explicit_observed_function --- src/systems/diffeqs/odesystem.jl | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/systems/diffeqs/odesystem.jl b/src/systems/diffeqs/odesystem.jl index ba3a4977ba..1f333b5e91 100644 --- a/src/systems/diffeqs/odesystem.jl +++ b/src/systems/diffeqs/odesystem.jl @@ -445,8 +445,7 @@ function build_explicit_observed_function(sys, ts; eval_module = @__MODULE__, output_type = Array, checkbounds = true, - drop_expr = drop_expr, - ps = parameters(sys), + ps = parameters(sys), return_inplace = false, param_only = false, op = Operator, From 7b73213151436cb3c02f687a4c9d35eaaba9fa7f Mon Sep 17 00:00:00 2001 From: Benjamin Chung Date: Thu, 14 Nov 2024 17:17:29 -0800 Subject: [PATCH 0341/1337] Reformat docstring --- src/systems/diffeqs/odesystem.jl | 58 ++++++++++++++++++++------------ 1 file changed, 36 insertions(+), 22 deletions(-) diff --git a/src/systems/diffeqs/odesystem.jl b/src/systems/diffeqs/odesystem.jl index 1f333b5e91..b60220aeea 100644 --- a/src/systems/diffeqs/odesystem.jl +++ b/src/systems/diffeqs/odesystem.jl @@ -409,34 +409,48 @@ end ODESystem(eq::Equation, args...; kwargs...) = ODESystem([eq], args...; kwargs...) """ -$(SIGNATURES) + build_explicit_observed_function(sys, ts; kwargs...) -> Function(s) + +Generates a function that computes the observed value(s) `ts` in the system `sys`, while making the assumption that there are no cycles in the equations. + +## Arguments +- `sys`: The system for which to generate the function +- `ts`: The symbolic observed values whose value should be computed + +## Keywords +- `return_inplace = false`: If true and the observed value is a vector, then return both the in place and out of place methods. +- `expression = false`: Generates a Julia `Expr`` computing the observed value if `expression` is true +- `eval_expression = false`: If true and `expression = false`, evaluates the returned function in the module `eval_module` +- `output_type = Array` the type of the array generated by a out-of-place vector-valued function +- `param_only = false` if true, only allow the generated function to access system parameters +- `inputs = nothing` additinoal symbolic variables that should be provided to the generated function +- `checkbounds = true` checks bounds if true when destructuring parameters +- `op = Operator` sets the recursion terminator for the walk done by `vars` to identify the variables that appear in `ts`. See the documentation for `vars` for more detail. +- `throw = true` if true, throw an error when generating a function for `ts` that reference variables that do not exist. +- `mkarray`; only used if the output is an array (that is, `!isscalar(ts)`). Called as `mkarray(ts, output_type)` where `ts` are the expressions to put in +the array and `output_type` is the argument of the same name passed to build_explicit_observed_function. + +## Returns -Generates a function that computes the observed value(s) `ts` in the system `sys` assuming that there are no cycles in the equations. - The return value will be either: -* a single function if the input is a scalar or if the input is a Vector but `return_inplace` is false -* the out of place and in-place functions `(ip, oop)` if `return_inplace` is true and the input is a `Vector` +* a single function `f_oop` if the input is a scalar or if the input is a Vector but `return_inplace` is false +* the out of place and in-place functions `(f_ip, f_oop)` if `return_inplace` is true and the input is a `Vector` -The function(s) will be: +The function(s) `f_oop` (and potentially `f_ip`) will be: * `RuntimeGeneratedFunction`s by default, * A Julia `Expr` if `expression` is true, -* A directly evaluated Julia function in the module `eval_module` if `eval_expression` is true +* A directly evaluated Julia function in the module `eval_module` if `eval_expression` is true and `expression` is false. The signatures will be of the form `g(...)` with arguments: -* `output` for in-place functions -* `unknowns` if `params_only` is `false` -* `inputs` if `inputs` is an array of symbolic inputs that should be available in `ts` -* `p...` unconditionally; note that in the case of `MTKParameters` more than one parameters argument may be present, so it must be splatted -* `t` if the system is time-dependent; for example `NonlinearSystem` will not have `t` -For example, a function `g(op, unknowns, p, inputs, t)` will be the in-place function generated if `return_inplace` is true, `ts` is a vector, an array of inputs `inputs` is given, and `params_only` is false for a time-dependent system. - -Options not otherwise specified are: -* `output_type = Array` the type of the array generated by the out-of-place vector-valued function -* `checkbounds = true` checks bounds if true when destructuring parameters -* `op = Operator` sets the recursion terminator for the walk done by `vars` to identify the variables that appear in `ts`. See the documentation for `vars` for more detail. -* `throw = true` if true, throw an error when generating a function for `ts` that reference variables that do not exist -* `drop_expr` is deprecated. -* `mkarray`; only used if the output is an array (that is, `!isscalar(ts)`). Called as `mkarray(ts, output_type)` where `ts` are the expressions to put in the array and `output_type` is the argument of the same name passed to build_explicit_observed_function. + +- `output` for in-place functions +- `unknowns` if `param_only` is `false` +- `inputs` if `inputs` is an array of symbolic inputs that should be available in `ts` +- `p...` unconditionally; note that in the case of `MTKParameters` more than one parameters argument may be present, so it must be splatted +- `t` if the system is time-dependent; for example `NonlinearSystem` will not have `t` + +For example, a function `g(op, unknowns, p..., inputs, t)` will be the in-place function generated if `return_inplace` is true, `ts` is a vector, +an array of inputs `inputs` is given, and `param_only` is false for a time-dependent system. """ function build_explicit_observed_function(sys, ts; inputs = nothing, @@ -445,7 +459,7 @@ function build_explicit_observed_function(sys, ts; eval_module = @__MODULE__, output_type = Array, checkbounds = true, - ps = parameters(sys), + ps = parameters(sys), return_inplace = false, param_only = false, op = Operator, From 14996d7db8065c3716e8ad54333bbe5cbbf7af45 Mon Sep 17 00:00:00 2001 From: Benjamin Chung Date: Thu, 14 Nov 2024 17:59:02 -0800 Subject: [PATCH 0342/1337] Fix docstring to account for tuples --- src/systems/diffeqs/odesystem.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/systems/diffeqs/odesystem.jl b/src/systems/diffeqs/odesystem.jl index b60220aeea..5ad275906d 100644 --- a/src/systems/diffeqs/odesystem.jl +++ b/src/systems/diffeqs/odesystem.jl @@ -427,7 +427,7 @@ Generates a function that computes the observed value(s) `ts` in the system `sys - `checkbounds = true` checks bounds if true when destructuring parameters - `op = Operator` sets the recursion terminator for the walk done by `vars` to identify the variables that appear in `ts`. See the documentation for `vars` for more detail. - `throw = true` if true, throw an error when generating a function for `ts` that reference variables that do not exist. -- `mkarray`; only used if the output is an array (that is, `!isscalar(ts)`). Called as `mkarray(ts, output_type)` where `ts` are the expressions to put in +- `mkarray`; only used if the output is an array (that is, `!isscalar(ts)` and `ts` is not a tuple, in which case the result will always be a tuple). Called as `mkarray(ts, output_type)` where `ts` are the expressions to put in the array and `output_type` is the argument of the same name passed to build_explicit_observed_function. ## Returns From 1e9181ff128dde4faf9f1915cc1d6434d062d2ae Mon Sep 17 00:00:00 2001 From: Benjamin Chung Date: Thu, 14 Nov 2024 20:24:59 -0800 Subject: [PATCH 0343/1337] Update input_output_handling.jl to remove use of drop_expr --- test/input_output_handling.jl | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/input_output_handling.jl b/test/input_output_handling.jl index 07a26d2d1a..c3a0a63f6c 100644 --- a/test/input_output_handling.jl +++ b/test/input_output_handling.jl @@ -140,8 +140,7 @@ if VERSION >= v"1.8" # :opaque_closure not supported before A, B, C, D = matrices obsf = ModelingToolkit.build_explicit_observed_function(ssys, [y], - inputs = [torque.tau.u], - drop_expr = identity) + inputs = [torque.tau.u]) x = randn(size(A, 1)) u = randn(size(B, 2)) p = (getindex.( From eaf72ac5505851839f37112326cb80f8206fc28d Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 15 Nov 2024 11:54:58 +0530 Subject: [PATCH 0344/1337] test: remove use of deprecated kwarg --- test/input_output_handling.jl | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/input_output_handling.jl b/test/input_output_handling.jl index 07a26d2d1a..c3a0a63f6c 100644 --- a/test/input_output_handling.jl +++ b/test/input_output_handling.jl @@ -140,8 +140,7 @@ if VERSION >= v"1.8" # :opaque_closure not supported before A, B, C, D = matrices obsf = ModelingToolkit.build_explicit_observed_function(ssys, [y], - inputs = [torque.tau.u], - drop_expr = identity) + inputs = [torque.tau.u]) x = randn(size(A, 1)) u = randn(size(B, 2)) p = (getindex.( From 3bd65e095447058aaac756fb81312408c5caa84e Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 15 Nov 2024 12:58:40 +0530 Subject: [PATCH 0345/1337] fix: handle observed in `remake_initializeprob` --- src/systems/nonlinear/initializesystem.jl | 16 +++++- test/initializationsystem.jl | 70 +++++++++++++++++++++++ 2 files changed, 83 insertions(+), 3 deletions(-) diff --git a/src/systems/nonlinear/initializesystem.jl b/src/systems/nonlinear/initializesystem.jl index eefe393acc..eff19afb07 100644 --- a/src/systems/nonlinear/initializesystem.jl +++ b/src/systems/nonlinear/initializesystem.jl @@ -225,7 +225,7 @@ function SciMLBase.remake_initializeprob(sys::ODESystem, odefn, u0, t0, p) u0idxs = Int[] u0vals = [] for sym in variable_symbols(oldinitprob) - if is_variable(sys, sym) + if is_variable(sys, sym) || has_observed_with_lhs(sys, sym) u0 !== missing || continue idx = variable_index(oldinitprob, sym) push!(u0idxs, idx) @@ -247,8 +247,18 @@ function SciMLBase.remake_initializeprob(sys::ODESystem, odefn, u0, t0, p) end end end - newu0 = remake_buffer(oldinitprob.f.sys, state_values(oldinitprob), u0idxs, u0vals) - newp = remake_buffer(oldinitprob.f.sys, parameter_values(oldinitprob), pidxs, pvals) + if isempty(u0idxs) + newu0 = state_values(oldinitprob) + else + newu0 = remake_buffer( + oldinitprob.f.sys, state_values(oldinitprob), u0idxs, u0vals) + end + if isempty(pidxs) + newp = parameter_values(oldinitprob) + else + newp = remake_buffer( + oldinitprob.f.sys, parameter_values(oldinitprob), pidxs, pvals) + end initprob = remake(oldinitprob; u0 = newu0, p = newp) return initprob, odefn.update_initializeprob!, odefn.initializeprobmap, odefn.initializeprobpmap diff --git a/test/initializationsystem.jl b/test/initializationsystem.jl index c9783c9659..f39ec3c2f2 100644 --- a/test/initializationsystem.jl +++ b/test/initializationsystem.jl @@ -877,3 +877,73 @@ end sol = solve(prob, Rodas5P()) @test SciMLBase.successful_retcode(sol) end + +@testset "Issue#3205" begin + using ModelingToolkitStandardLibrary.Electrical + import ModelingToolkitStandardLibrary.Mechanical.Rotational as MR + using ModelingToolkitStandardLibrary.Blocks + using SciMLBase + + function dc_motor(R1 = 0.5) + R = R1 # [Ohm] armature resistance + L = 4.5e-3 # [H] armature inductance + k = 0.5 # [N.m/A] motor constant + J = 0.02 # [kg.m²] inertia + f = 0.01 # [N.m.s/rad] friction factor + tau_L_step = -0.3 # [N.m] amplitude of the load torque step + + @named ground = Ground() + @named source = Voltage() + @named ref = Blocks.Step(height = 0.2, start_time = 0) + @named pi_controller = Blocks.LimPI(k = 1.1, T = 0.035, u_max = 10, Ta = 0.035) + @named feedback = Blocks.Feedback() + @named R1 = Resistor(R = R) + @named L1 = Inductor(L = L) + @named emf = EMF(k = k) + @named fixed = MR.Fixed() + @named load = MR.Torque() + @named load_step = Blocks.Step(height = tau_L_step, start_time = 3) + @named inertia = MR.Inertia(J = J) + @named friction = MR.Damper(d = f) + @named speed_sensor = MR.SpeedSensor() + + connections = [connect(fixed.flange, emf.support, friction.flange_b) + connect(emf.flange, friction.flange_a, inertia.flange_a) + connect(inertia.flange_b, load.flange) + connect(inertia.flange_b, speed_sensor.flange) + connect(load_step.output, load.tau) + connect(ref.output, feedback.input1) + connect(speed_sensor.w, :y, feedback.input2) + connect(feedback.output, pi_controller.err_input) + connect(pi_controller.ctr_output, :u, source.V) + connect(source.p, R1.p) + connect(R1.n, L1.p) + connect(L1.n, emf.p) + connect(emf.n, source.n, ground.g)] + + @named model = ODESystem(connections, t, + systems = [ + ground, + ref, + pi_controller, + feedback, + source, + R1, + L1, + emf, + fixed, + load, + load_step, + inertia, + friction, + speed_sensor + ]) + end + + model = dc_motor() + sys = structural_simplify(model) + + prob = ODEProblem(sys, [sys.L1.i => 0.0], (0, 6.0)) + + @test_nowarn remake(prob, p = prob.p) +end From 894c135f797acb8384408bc3e4b9b23231adef90 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 8 Oct 2024 15:14:04 +0530 Subject: [PATCH 0346/1337] fix: detect observed variables and dependent parameters dependent on discrete parameters --- src/systems/abstractsystem.jl | 13 ++++-- src/systems/diffeqs/odesystem.jl | 11 ++++- src/systems/index_cache.jl | 76 ++++++++++++++++++++------------ test/odesystem.jl | 11 +++++ 4 files changed, 77 insertions(+), 34 deletions(-) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index f396a9830a..15fc54e9e9 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -731,7 +731,7 @@ end function has_observed_with_lhs(sys, sym) has_observed(sys) || return false if has_index_cache(sys) && (ic = get_index_cache(sys)) !== nothing - return any(isequal(sym), ic.observed_syms) + return haskey(ic.observed_syms_to_timeseries, sym) else return any(isequal(sym), [eq.lhs for eq in observed(sys)]) end @@ -740,7 +740,7 @@ end function has_parameter_dependency_with_lhs(sys, sym) has_parameter_dependencies(sys) || return false if has_index_cache(sys) && (ic = get_index_cache(sys)) !== nothing - return any(isequal(sym), ic.dependent_pars) + return haskey(ic.dependent_pars_to_timeseries, unwrap(sym)) else return any(isequal(sym), [eq.lhs for eq in parameter_dependencies(sys)]) end @@ -762,11 +762,16 @@ for traitT in [ allsyms = vars(sym; op = Symbolics.Operator) for s in allsyms s = unwrap(s) - if is_variable(sys, s) || is_independent_variable(sys, s) || - has_observed_with_lhs(sys, s) + if is_variable(sys, s) || is_independent_variable(sys, s) push!(ts_idxs, ContinuousTimeseries()) elseif is_timeseries_parameter(sys, s) push!(ts_idxs, timeseries_parameter_index(sys, s).timeseries_idx) + elseif has_index_cache(sys) && (ic = get_index_cache(sys)) !== nothing + if (ts = get(ic.observed_syms_to_timeseries, s, nothing)) !== nothing + union!(ts_idxs, ts) + elseif (ts = get(ic.dependent_pars_to_timeseries, s, nothing)) !== nothing + union!(ts_idxs, ts) + end end end end diff --git a/src/systems/diffeqs/odesystem.jl b/src/systems/diffeqs/odesystem.jl index 5ad275906d..b8dee2bac7 100644 --- a/src/systems/diffeqs/odesystem.jl +++ b/src/systems/diffeqs/odesystem.jl @@ -490,7 +490,16 @@ function build_explicit_observed_function(sys, ts; ivs = independent_variables(sys) dep_vars = scalarize(setdiff(vars, ivs)) - obs = param_only ? Equation[] : observed(sys) + obs = observed(sys) + if param_only + if has_index_cache(sys) && (ic = get_index_cache(sys)) !== nothing + obs = filter(obs) do eq + !(ContinuousTimeseries() in ic.observed_syms_to_timeseries[eq.lhs]) + end + else + obs = Equation[] + end + end cs = collect_constants(obs) if !isempty(cs) > 0 diff --git a/src/systems/index_cache.jl b/src/systems/index_cache.jl index 00f7837407..7a376aa1f2 100644 --- a/src/systems/index_cache.jl +++ b/src/systems/index_cache.jl @@ -38,6 +38,7 @@ const UnknownIndexMap = Dict{ BasicSymbolic, Union{Int, UnitRange{Int}, AbstractArray{Int}}} const TunableIndexMap = Dict{BasicSymbolic, Union{Int, UnitRange{Int}, Base.ReshapedArray{Int, N, UnitRange{Int}} where {N}}} +const TimeseriesSetType = Set{Union{ContinuousTimeseries, Int}} struct IndexCache unknown_idx::UnknownIndexMap @@ -48,8 +49,9 @@ struct IndexCache tunable_idx::TunableIndexMap constant_idx::ParamIndexMap nonnumeric_idx::NonnumericMap - observed_syms::Set{BasicSymbolic} - dependent_pars::Set{Union{BasicSymbolic, CallWithMetadata}} + observed_syms_to_timeseries::Dict{BasicSymbolic, TimeseriesSetType} + dependent_pars_to_timeseries::Dict{ + Union{BasicSymbolic, CallWithMetadata}, TimeseriesSetType} discrete_buffer_sizes::Vector{Vector{BufferTemplate}} tunable_buffer_size::BufferTemplate constant_buffer_sizes::Vector{BufferTemplate} @@ -91,20 +93,6 @@ function IndexCache(sys::AbstractSystem) end end - observed_syms = Set{BasicSymbolic}() - for eq in observed(sys) - if symbolic_type(eq.lhs) != NotSymbolic() - sym = eq.lhs - ttsym = default_toterm(sym) - rsym = renamespace(sys, sym) - rttsym = renamespace(sys, ttsym) - push!(observed_syms, sym) - push!(observed_syms, ttsym) - push!(observed_syms, rsym) - push!(observed_syms, rttsym) - end - end - tunable_buffers = Dict{Any, Set{BasicSymbolic}}() constant_buffers = Dict{Any, Set{BasicSymbolic}}() nonnumeric_buffers = Dict{Any, Set{Union{BasicSymbolic, CallWithMetadata}}}() @@ -267,29 +255,59 @@ function IndexCache(sys::AbstractSystem) end end - for sym in Iterators.flatten((keys(unk_idxs), keys(disc_idxs), keys(tunable_idxs), - keys(const_idxs), keys(nonnumeric_idxs), - observed_syms, independent_variable_symbols(sys))) - if hasname(sym) && (!iscall(sym) || operation(sym) !== getindex) - symbol_to_variable[getname(sym)] = sym - end - end - - dependent_pars = Set{Union{BasicSymbolic, CallWithMetadata}}() + dependent_pars_to_timeseries = Dict{ + Union{BasicSymbolic, CallWithMetadata}, TimeseriesSetType}() for eq in parameter_dependencies(sys) sym = eq.lhs + vs = vars(eq.rhs) + timeseries = TimeseriesSetType() + for v in vs + if (idx = get(disc_idxs, v, nothing)) !== nothing + push!(timeseries, idx.clock_idx) + end + end ttsym = default_toterm(sym) rsym = renamespace(sys, sym) rttsym = renamespace(sys, ttsym) - for s in [sym, ttsym, rsym, rttsym] - push!(dependent_pars, s) + for s in (sym, ttsym, rsym, rttsym) + dependent_pars_to_timeseries[s] = timeseries if hasname(s) && (!iscall(s) || operation(s) != getindex) symbol_to_variable[getname(s)] = sym end end end + observed_syms_to_timeseries = Dict{BasicSymbolic, TimeseriesSetType}() + for eq in observed(sys) + if symbolic_type(eq.lhs) != NotSymbolic() + sym = eq.lhs + vs = vars(eq.rhs) + timeseries = TimeseriesSetType() + for v in vs + if (idx = get(disc_idxs, v, nothing)) !== nothing + push!(timeseries, idx.clock_idx) + elseif haskey(unk_idxs, v) || haskey(observed_syms_to_timeseries, v) + push!(timeseries, ContinuousTimeseries()) + end + end + ttsym = default_toterm(sym) + rsym = renamespace(sys, sym) + rttsym = renamespace(sys, ttsym) + for s in (sym, ttsym, rsym, rttsym) + observed_syms_to_timeseries[s] = timeseries + end + end + end + + for sym in Iterators.flatten((keys(unk_idxs), keys(disc_idxs), keys(tunable_idxs), + keys(const_idxs), keys(nonnumeric_idxs), + keys(observed_syms_to_timeseries), independent_variable_symbols(sys))) + if hasname(sym) && (!iscall(sym) || operation(sym) !== getindex) + symbol_to_variable[getname(sym)] = sym + end + end + return IndexCache( unk_idxs, disc_idxs, @@ -297,8 +315,8 @@ function IndexCache(sys::AbstractSystem) tunable_idxs, const_idxs, nonnumeric_idxs, - observed_syms, - dependent_pars, + observed_syms_to_timeseries, + dependent_pars_to_timeseries, disc_buffer_templates, BufferTemplate(Real, tunable_buffer_size), const_buffer_sizes, diff --git a/test/odesystem.jl b/test/odesystem.jl index d7a2658f63..fb185fdcb0 100644 --- a/test/odesystem.jl +++ b/test/odesystem.jl @@ -1504,3 +1504,14 @@ end sys2 = complete(sys; split = false) @test ModelingToolkit.get_index_cache(sys2) === nothing end + +# https://github.com/SciML/SciMLBase.jl/issues/786 +@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]]) + prob = ODEProblem(sys, [x => 0.0], (0.0, 2pi), [c => 1.0]) + sol = solve(prob, Tsit5()) + @test sol[obs] ≈ 1:7 +end From bcd0edb1de1790c63200cd6f4229ebf0c183fdac Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 28 Oct 2024 18:04:21 +0530 Subject: [PATCH 0347/1337] fix: fix timeseries detection for shifted variables in DDEs --- src/systems/abstractsystem.jl | 4 ++++ src/systems/index_cache.jl | 27 +++++++++++++++++++-------- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index 15fc54e9e9..db2fcb4f10 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -766,6 +766,10 @@ for traitT in [ push!(ts_idxs, ContinuousTimeseries()) elseif is_timeseries_parameter(sys, s) push!(ts_idxs, timeseries_parameter_index(sys, s).timeseries_idx) + elseif is_time_dependent(sys) && iscall(s) && issym(operation(s)) && + is_variable(sys, operation(s)(get_iv(sys))) + # DDEs case, to detect x(t - k) + push!(ts_idxs, ContinuousTimeseries()) elseif has_index_cache(sys) && (ic = get_index_cache(sys)) !== nothing if (ts = get(ic.observed_syms_to_timeseries, s, nothing)) !== nothing union!(ts_idxs, ts) diff --git a/src/systems/index_cache.jl b/src/systems/index_cache.jl index 7a376aa1f2..307ac71a56 100644 --- a/src/systems/index_cache.jl +++ b/src/systems/index_cache.jl @@ -262,9 +262,11 @@ function IndexCache(sys::AbstractSystem) sym = eq.lhs vs = vars(eq.rhs) timeseries = TimeseriesSetType() - for v in vs - if (idx = get(disc_idxs, v, nothing)) !== nothing - push!(timeseries, idx.clock_idx) + if is_time_dependent(sys) + for v in vs + if (idx = get(disc_idxs, v, nothing)) !== nothing + push!(timeseries, idx.clock_idx) + end end end ttsym = default_toterm(sym) @@ -284,11 +286,20 @@ function IndexCache(sys::AbstractSystem) sym = eq.lhs vs = vars(eq.rhs) timeseries = TimeseriesSetType() - for v in vs - if (idx = get(disc_idxs, v, nothing)) !== nothing - push!(timeseries, idx.clock_idx) - elseif haskey(unk_idxs, v) || haskey(observed_syms_to_timeseries, v) - push!(timeseries, ContinuousTimeseries()) + if is_time_dependent(sys) + for v in vs + if (idx = get(disc_idxs, v, nothing)) !== nothing + push!(timeseries, idx.clock_idx) + elseif haskey(unk_idxs, v) + push!(timeseries, ContinuousTimeseries()) + elseif haskey(observed_syms_to_timeseries, v) + union!(timeseries, observed_syms_to_timeseries[v]) + elseif haskey(dependent_pars_to_timeseries, v) + union!(timeseries, dependent_pars_to_timeseries[v]) + elseif iscall(v) && issym(operation(v)) && + is_variable(sys, operation(v)(get_iv(sys))) + push!(timeseries, ContinuousTimeseries()) + end end end ttsym = default_toterm(sym) From 87c16598cf244f55c5ea4d483ae6ec553ef7da87 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 29 Oct 2024 15:36:54 +0530 Subject: [PATCH 0348/1337] fix: handle timeseries detection for constant observed --- src/systems/index_cache.jl | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/systems/index_cache.jl b/src/systems/index_cache.jl index 307ac71a56..1e3490ee69 100644 --- a/src/systems/index_cache.jl +++ b/src/systems/index_cache.jl @@ -284,23 +284,21 @@ function IndexCache(sys::AbstractSystem) for eq in observed(sys) if symbolic_type(eq.lhs) != NotSymbolic() sym = eq.lhs - vs = vars(eq.rhs) + vs = vars(eq.rhs; op = Nothing) timeseries = TimeseriesSetType() if is_time_dependent(sys) for v in vs if (idx = get(disc_idxs, v, nothing)) !== nothing push!(timeseries, idx.clock_idx) - elseif haskey(unk_idxs, v) - push!(timeseries, ContinuousTimeseries()) elseif haskey(observed_syms_to_timeseries, v) union!(timeseries, observed_syms_to_timeseries[v]) elseif haskey(dependent_pars_to_timeseries, v) union!(timeseries, dependent_pars_to_timeseries[v]) - elseif iscall(v) && issym(operation(v)) && - is_variable(sys, operation(v)(get_iv(sys))) - push!(timeseries, ContinuousTimeseries()) end end + if isempty(timeseries) + push!(timeseries, ContinuousTimeseries()) + end end ttsym = default_toterm(sym) rsym = renamespace(sys, sym) From 55c52175830b91a83d70939e95941d359b70ed03 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 15 Nov 2024 15:46:53 +0530 Subject: [PATCH 0349/1337] ci: prevent codecov from failing CI --- .github/workflows/Downstream.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/Downstream.yml b/.github/workflows/Downstream.yml index 7a7556efa3..a6d5e84656 100644 --- a/.github/workflows/Downstream.yml +++ b/.github/workflows/Downstream.yml @@ -72,4 +72,4 @@ jobs: with: file: lcov.info token: ${{ secrets.CODECOV_TOKEN }} - fail_ci_if_error: true + fail_ci_if_error: false From 085358b7573c66a8c9ba2a651a70514681eee6d0 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 14 Nov 2024 17:48:34 +0530 Subject: [PATCH 0350/1337] feat: add support for bounds of array variables --- src/variables.jl | 34 +++++++++++++++++++++++----- test/test_variable_metadata.jl | 41 ++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 5 deletions(-) diff --git a/src/variables.jl b/src/variables.jl index fcfc7f9b1e..d11c6c1834 100644 --- a/src/variables.jl +++ b/src/variables.jl @@ -254,7 +254,6 @@ end ## Bounds ====================================================================== struct VariableBounds end Symbolics.option_to_metadata_type(::Val{:bounds}) = VariableBounds -getbounds(x::Num) = getbounds(Symbolics.unwrap(x)) """ getbounds(x) @@ -266,10 +265,35 @@ Create parameters with bounds like this @parameters p [bounds=(-1, 1)] ``` """ -function getbounds(x) +function getbounds(x::Union{Num, Symbolics.Arr, SymbolicUtils.Symbolic}) + x = unwrap(x) p = Symbolics.getparent(x, nothing) - p === nothing || (x = p) - Symbolics.getmetadata(x, VariableBounds, (-Inf, Inf)) + if p === nothing + bounds = Symbolics.getmetadata(x, VariableBounds, (-Inf, Inf)) + if symbolic_type(x) == ArraySymbolic() && Symbolics.shape(x) != Symbolics.Unknown() + bounds = map(bounds) do b + b isa AbstractArray && return b + return fill(b, size(x)) + end + end + else + # if we reached here, `x` is the result of calling `getindex` + bounds = @something Symbolics.getmetadata(x, VariableBounds, nothing) getbounds(p) + idxs = arguments(x)[2:end] + bounds = map(bounds) do b + if b isa AbstractArray + if Symbolics.shape(p) != Symbolics.Unknown() && size(p) != size(b) + throw(DimensionMismatch("Expected array variable $p with shape $(size(p)) to have bounds of identical size. Found $bounds of size $(size(bounds)).")) + end + return b[idxs...] + elseif symbolic_type(x) == ArraySymbolic() + return fill(b, size(x)) + else + return b + end + end + end + return bounds end """ @@ -280,7 +304,7 @@ See also [`getbounds`](@ref). """ function hasbounds(x) b = getbounds(x) - isfinite(b[1]) || isfinite(b[2]) + any(isfinite.(b[1]) .|| isfinite.(b[2])) end ## Disturbance ================================================================= diff --git a/test/test_variable_metadata.jl b/test/test_variable_metadata.jl index 7f9799edb3..7b093be366 100644 --- a/test/test_variable_metadata.jl +++ b/test/test_variable_metadata.jl @@ -10,6 +10,47 @@ using ModelingToolkit @test !hasbounds(y) @test !haskey(ModelingToolkit.dump_variable_metadata(y), :bounds) +@variables y[1:3] +@test !hasbounds(y) +@test getbounds(y)[1] == [-Inf, -Inf, -Inf] +@test getbounds(y)[2] == [Inf, Inf, Inf] +for i in eachindex(y) + @test !hasbounds(y[i]) + b = getbounds(y[i]) + @test b[1] == -Inf && b[2] == Inf +end + +@variables y[1:3] [bounds = (-1, 1)] +@test hasbounds(y) +@test getbounds(y)[1] == -ones(3) +@test getbounds(y)[2] == ones(3) +for i in eachindex(y) + @test hasbounds(y[i]) + b = getbounds(y[i]) + @test b[1] == -1.0 && b[2] == 1.0 +end +@test getbounds(y[1:2])[1] == -ones(2) +@test getbounds(y[1:2])[2] == ones(2) + +@variables y[1:2, 1:2] [bounds = (-1, [1.0 Inf; 2.0 3.0])] +@test hasbounds(y) +@test getbounds(y)[1] == [-1 -1; -1 -1] +@test getbounds(y)[2] == [1.0 Inf; 2.0 3.0] +for i in eachindex(y) + @test hasbounds(y[i]) + b = getbounds(y[i]) + @test b[1] == -1 && b[2] == [1.0 Inf; 2.0 3.0][i] +end + +@variables y[1:2] [bounds = (-Inf, [1.0, Inf])] +@test hasbounds(y) +@test getbounds(y)[1] == [-Inf, -Inf] +@test getbounds(y)[2] == [1.0, Inf] +@test hasbounds(y[1]) +@test getbounds(y[1]) == (-Inf, 1.0) +@test !hasbounds(y[2]) +@test getbounds(y[2]) == (-Inf, Inf) + # Guess @variables y [guess = 0] @test getguess(y) === 0 From 9017fbf95081ec485b32f5d44bef32ca5ff7fe86 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 15 Nov 2024 11:11:02 +0530 Subject: [PATCH 0351/1337] docs: update documentation for bounds metadata --- docs/src/basics/Variable_metadata.md | 38 ++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/docs/src/basics/Variable_metadata.md b/docs/src/basics/Variable_metadata.md index 6462c3a469..e9bdabfe38 100644 --- a/docs/src/basics/Variable_metadata.md +++ b/docs/src/basics/Variable_metadata.md @@ -86,6 +86,44 @@ hasbounds(u) getbounds(u) ``` +Bounds can also be specified for array variables. A scalar array bound is applied to each +element of the array. A bound may also be specified as an array, in which case the size of +the array must match the size of the symbolic variable. + +```@example metadata +@variables x[1:2, 1:2] [bounds = (-1, 1)] +hasbounds(x) +``` + +```@example metadata +getbounds(x) +``` + +```@example metadata +getbounds(x[1, 1]) +``` + +```@example metadata +getbounds(x[1:2, 1]) +``` + +```@example metadata +@variables x[1:2] [bounds = (-Inf, [1.0, Inf])] +hasbounds(x) +``` + +```@example metadata +getbounds(x) +``` + +```@example metadata +getbounds(x[2]) +``` + +```@example metadata +hasbounds(x[2]) +``` + ## Guess Specify an initial guess for custom initial conditions of an `ODESystem`. From 6dbc375e3d65309ba85b7cf046b92da0e202d649 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 15 Nov 2024 16:03:17 +0530 Subject: [PATCH 0352/1337] feat: respect array variable bounds in `OptimizationSystem` --- src/systems/optimization/optimizationsystem.jl | 4 ++-- test/optimizationsystem.jl | 9 +++++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/systems/optimization/optimizationsystem.jl b/src/systems/optimization/optimizationsystem.jl index a0fa07be47..39c5c1e7a7 100644 --- a/src/systems/optimization/optimizationsystem.jl +++ b/src/systems/optimization/optimizationsystem.jl @@ -101,8 +101,8 @@ function OptimizationSystem(op, unknowns, ps; gui_metadata = nothing) name === nothing && throw(ArgumentError("The `name` keyword must be provided. Please consider using the `@named` macro")) - constraints = value.(scalarize(constraints)) - unknowns′ = value.(scalarize(unknowns)) + constraints = value.(reduce(vcat, scalarize(constraints); init = [])) + unknowns′ = value.(reduce(vcat, scalarize(unknowns); init = [])) ps′ = value.(ps) op′ = value(scalarize(op)) diff --git a/test/optimizationsystem.jl b/test/optimizationsystem.jl index dfa11fca37..a8e2be936d 100644 --- a/test/optimizationsystem.jl +++ b/test/optimizationsystem.jl @@ -358,4 +358,13 @@ end @mtkbuild sys = OptimizationSystem(obj, [x, y, z], []; constraints = cons) @test is_variable(sys, z) @test !is_variable(sys, y) + + @variables x[1:3] [bounds = ([-Inf, -1.0, -2.0], [Inf, 1.0, 2.0])] + obj = x[1]^2 + x[2]^2 + x[3]^2 + cons = [x[2] ~ 2x[1] + 3, x[3] ~ x[1] + x[2]] + @mtkbuild sys = OptimizationSystem(obj, [x], []; constraints = cons) + @test length(unknowns(sys)) == 2 + @test !is_variable(sys, x[1]) + @test is_variable(sys, x[2]) + @test is_variable(sys, x[3]) end From b0e4c7aeb3ac5ad6472f87af92d30bc52c7fe78f Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 15 Nov 2024 19:26:43 +0530 Subject: [PATCH 0353/1337] fix: enable using `MTKParameters` with `generate_control_function` --- src/inputoutput.jl | 10 ++++++---- test/input_output_handling.jl | 24 ++++++++++++++++++------ 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/src/inputoutput.jl b/src/inputoutput.jl index 9ed0984050..4a99ec11f5 100644 --- a/src/inputoutput.jl +++ b/src/inputoutput.jl @@ -235,18 +235,20 @@ function generate_control_function(sys::AbstractODESystem, inputs = unbound_inpu # TODO: add an optional check on the ordering of observed equations u = map(x -> time_varying_as_func(value(x), sys), dvs) p = map(x -> time_varying_as_func(value(x), sys), ps) + p = reorder_parameters(sys, p) t = get_iv(sys) # pre = has_difference ? (ex -> ex) : get_postprocess_fbody(sys) - args = (u, inputs, p, t) + args = (u, inputs, p..., t) if implicit_dae ddvs = map(Differential(get_iv(sys)), dvs) args = (ddvs, args...) end process = get_postprocess_fbody(sys) f = build_function(rhss, args...; postprocess_fbody = process, - expression = Val{true}, wrap_code = wrap_array_vars(sys, rhss; dvs, ps) .∘ + expression = Val{true}, wrap_code = wrap_mtkparameters(sys, false, 3) .∘ + wrap_array_vars(sys, rhss; dvs, ps, inputs) .∘ wrap_parameter_dependencies(sys, false), kwargs...) f = eval_or_rgf.(f; eval_expression, eval_module) @@ -426,7 +428,7 @@ function add_input_disturbance(sys, dist::DisturbanceModel, inputs = nothing; kw augmented_sys = ODESystem(eqs, t, systems = [dsys], name = gensym(:outer)) augmented_sys = extend(augmented_sys, sys) - (f_oop, f_ip), dvs, p = generate_control_function(augmented_sys, all_inputs, + (f_oop, f_ip), dvs, p, io_sys = generate_control_function(augmented_sys, all_inputs, [d]; kwargs...) - (f_oop, f_ip), augmented_sys, dvs, p + (f_oop, f_ip), augmented_sys, dvs, p, io_sys end diff --git a/test/input_output_handling.jl b/test/input_output_handling.jl index c3a0a63f6c..9550a87f31 100644 --- a/test/input_output_handling.jl +++ b/test/input_output_handling.jl @@ -160,12 +160,12 @@ eqs = [ ] @named sys = ODESystem(eqs, t) -f, dvs, ps = ModelingToolkit.generate_control_function(sys, simplify = true) +f, dvs, ps, io_sys = ModelingToolkit.generate_control_function(sys, simplify = true) @test isequal(dvs[], x) @test isempty(ps) -p = [] +p = nothing x = [rand()] u = [rand()] @test f[1](x, u, p, 1) == -x + u @@ -221,10 +221,10 @@ eqs = [connect_sd(sd, mass1, mass2) @named _model = ODESystem(eqs, t) @named model = compose(_model, mass1, mass2, sd); -f, dvs, ps = ModelingToolkit.generate_control_function(model, simplify = true) +f, dvs, ps, io_sys = ModelingToolkit.generate_control_function(model, simplify = true) @test length(dvs) == 4 @test length(ps) == length(parameters(model)) -p = ModelingToolkit.varmap_to_vars(ModelingToolkit.defaults(model), ps) +p = MTKParameters(io_sys, [io_sys.u => NaN]) x = ModelingToolkit.varmap_to_vars( merge(ModelingToolkit.defaults(model), Dict(D.(unknowns(model)) .=> 0.0)), dvs) @@ -288,7 +288,7 @@ model_outputs = [model.inertia1.w, model.inertia2.w, model.inertia1.phi, model.i @named dmodel = Blocks.StateSpace([0.0], [1.0], [1.0], [0.0]) # An integrating disturbance @named dist = ModelingToolkit.DisturbanceModel(model.torque.tau.u, dmodel) -(f_oop, f_ip), outersys, dvs, p = ModelingToolkit.add_input_disturbance(model, dist) +(f_oop, f_ip), outersys, dvs, p, io_sys = ModelingToolkit.add_input_disturbance(model, dist) @unpack u, d = outersys matrices, ssys = linearize(outersys, [u, d], model_outputs) @@ -302,7 +302,7 @@ x_add = ModelingToolkit.varmap_to_vars(merge(Dict(dvs .=> 0), Dict(dstate => 1)) x0 = randn(5) x1 = copy(x0) + x_add # add disturbance state perturbation u = randn(1) -pn = ModelingToolkit.varmap_to_vars(def, p) +pn = MTKParameters(io_sys, []) xp0 = f_oop(x0, u, pn, 0) xp1 = f_oop(x1, u, pn, 0) @@ -401,3 +401,15 @@ end f, dvs, ps = ModelingToolkit.generate_control_function(sys, simplify = true) @test f[1]([0.5], nothing, nothing, 0.0) == [1.0] end + +@testset "With callable symbolic" begin + @variables x(t)=0 u(t)=0 [input = true] + @parameters p(::Real) = (x -> 2x) + eqs = [D(x) ~ -x + p(u)] + @named sys = ODESystem(eqs, t) + f, dvs, ps, io_sys = ModelingToolkit.generate_control_function(sys, simplify = true) + p = MTKParameters(io_sys, []) + u = [1.0] + x = [1.0] + @test_nowarn f[1](x, u, p, 0.0) +end From 47e26794ecfebdf82a3f5bc05bdb1d730fed1dcb Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Sun, 17 Nov 2024 22:59:47 +0530 Subject: [PATCH 0354/1337] feat: support polynomials of invertible functions in HomotopyContinuationExt --- ext/MTKHomotopyContinuationExt.jl | 207 ++++++++++++++++++++--- src/systems/nonlinear/nonlinearsystem.jl | 8 +- test/extensions/homotopy_continuation.jl | 48 +++++- 3 files changed, 230 insertions(+), 33 deletions(-) diff --git a/ext/MTKHomotopyContinuationExt.jl b/ext/MTKHomotopyContinuationExt.jl index 19c21483c3..fa5d1bfcd4 100644 --- a/ext/MTKHomotopyContinuationExt.jl +++ b/ext/MTKHomotopyContinuationExt.jl @@ -54,16 +54,73 @@ end PolynomialData() = PolynomialData(BasicSymbolic[], NonPolynomialReason.T[], false) +abstract type PolynomialTransformationError <: Exception end + +struct MultivarTerm <: PolynomialTransformationError + term::Any + vars::Any +end + +function Base.showerror(io::IO, err::MultivarTerm) + println(io, + "Cannot convert system to polynomial: Found term $(err.term) which is a function of multiple unknowns $(err.vars).") +end + +struct MultipleTermsOfSameVar <: PolynomialTransformationError + terms::Any + var::Any +end + +function Base.showerror(io::IO, err::MultipleTermsOfSameVar) + println(io, + "Cannot convert system to polynomial: Found multiple non-polynomial terms $(err.terms) involving the same unknown $(err.var).") +end + +struct SymbolicSolveFailure <: PolynomialTransformationError + term::Any + var::Any +end + +function Base.showerror(io::IO, err::SymbolicSolveFailure) + println(io, + "Cannot convert system to polynomial: Unable to symbolically solve $(err.term) for $(err.var).") +end + +struct NemoNotLoaded <: PolynomialTransformationError end + +function Base.showerror(io::IO, err::NemoNotLoaded) + println(io, + "ModelingToolkit may be able to solve this system as a polynomial system if `Nemo` is loaded. Run `import Nemo` and try again.") +end + +struct VariablesAsPolyAndNonPoly <: PolynomialTransformationError + vars::Any +end + +function Base.showerror(io::IO, err::VariablesAsPolyAndNonPoly) + println(io, + "Cannot convert convert system to polynomial: Variables $(err.vars) occur in both polynomial and non-polynomial terms in the system.") +end + struct NotPolynomialError <: Exception - eq::Equation - data::PolynomialData + transformation_err::Union{PolynomialTransformationError, Nothing} + eq::Vector{Equation} + data::Vector{PolynomialData} end function Base.showerror(io::IO, err::NotPolynomialError) - println(io, - "Equation $(err.eq) is not a polynomial in the unknowns for the following reasons:") - for (term, reason) in zip(err.data.non_polynomial_terms, err.data.reasons) - println(io, display_reason(reason, term)) + if err.transformation_err !== nothing + Base.showerror(io, err.transformation_err) + end + for (eq, data) in zip(err.eq, err.data) + if isempty(data.non_polynomial_terms) + continue + end + println(io, + "Equation $(eq) is not a polynomial in the unknowns for the following reasons:") + for (term, reason) in zip(data.non_polynomial_terms, data.reasons) + println(io, display_reason(reason, term)) + end end end @@ -86,7 +143,9 @@ function process_polynomial!(data::PolynomialData, x, wrt) any(isequal(x), wrt) && return true if operation(x) in (*, +, -, /) - return all(y -> is_polynomial!(data, y, wrt), arguments(x)) + # `map` because `all` will early exit, but we want to search + # through everything to get all the non-polynomial terms + return all(map(y -> is_polynomial!(data, y, wrt), arguments(x))) end if operation(x) == (^) b, p = arguments(x) @@ -105,10 +164,6 @@ function process_polynomial!(data::PolynomialData, x, wrt) push!(data.reasons, NonPolynomialReason.ExponentContainsUnknowns) end base_polynomial = is_polynomial!(data, b, wrt) - if !base_polynomial - push!(data.non_polynomial_terms, x) - push!(data.reasons, NonPolynomialReason.BaseNotPolynomial) - end return base_polynomial && !exponent_has_unknowns && is_pow_integer end push!(data.non_polynomial_terms, x) @@ -234,6 +289,12 @@ end SymbolicIndexingInterface.parameter_values(s::MTKHomotopySystem) = s.p +struct PolynomialTransformationData + new_var::BasicSymbolic + term::BasicSymbolic + inv_term::Vector +end + """ $(TYPEDSIGNATURES) @@ -265,18 +326,95 @@ function MTK.HomotopyContinuationProblem( # CSE/hashconsing. eqs = full_equations(sys) - denoms = [] - has_parametric_exponents = false - eqs2 = map(eqs) do eq + polydata = map(eqs) do eq data = PolynomialData() process_polynomial!(data, eq.lhs, dvs) process_polynomial!(data, eq.rhs, dvs) - has_parametric_exponents |= data.has_parametric_exponent - if !isempty(data.non_polynomial_terms) - throw(NotPolynomialError(eq, data)) + data + end + + has_parametric_exponents = any(d -> d.has_parametric_exponent, polydata) + + all_non_poly_terms = mapreduce(d -> d.non_polynomial_terms, vcat, polydata) + unique!(all_non_poly_terms) + + var_to_nonpoly = Dict{BasicSymbolic, PolynomialTransformationData}() + + is_poly = true + transformation_err = nothing + for t in all_non_poly_terms + # if the term involves multiple unknowns, we can't invert it + dvs_in_term = map(x -> occursin(x, t), dvs) + if count(dvs_in_term) > 1 + transformation_err = MultivarTerm(t, dvs[dvs_in_term]) + is_poly = false + break + end + # we already have a substitution solving for `var` + var = dvs[findfirst(dvs_in_term)] + if haskey(var_to_nonpoly, var) && !isequal(var_to_nonpoly[var].term, t) + transformation_err = MultipleTermsOfSameVar([t, var_to_nonpoly[var].term], var) + is_poly = false + break + end + # we want to solve `term - new_var` for `var` + new_var = gensym(Symbol(var)) + new_var = unwrap(only(@variables $new_var)) + invterm = Symbolics.ia_solve( + t - new_var, var; complex_roots = false, periodic_roots = false, warns = false) + # if we can't invert it, quit + if invterm === nothing || isempty(invterm) + transformation_err = SymbolicSolveFailure(t, var) + is_poly = false + break + end + # `ia_solve` returns lazy terms i.e. `asin(1.0)` instead of `pi/2` + # this just evaluates the constant expressions + invterm = Symbolics.substitute.(invterm, (Dict(),)) + # RootsOf implies Symbolics couldn't solve the inner polynomial because + # `Nemo` wasn't loaded. + if any(x -> MTK.iscall(x) && MTK.operation(x) == Symbolics.RootsOf, invterm) + transformation_err = NemoNotLoaded() + is_poly = false + break end - num, den = handle_rational_polynomials(eq.rhs - eq.lhs, dvs) + var_to_nonpoly[var] = PolynomialTransformationData(new_var, t, invterm) + end + + if !is_poly + throw(NotPolynomialError(transformation_err, eqs, polydata)) + end + + subrules = Dict() + combinations = Vector[] + new_dvs = [] + for x in dvs + if haskey(var_to_nonpoly, x) + _data = var_to_nonpoly[x] + subrules[_data.term] = _data.new_var + push!(combinations, _data.inv_term) + push!(new_dvs, _data.new_var) + else + push!(combinations, [x]) + push!(new_dvs, x) + end + end + all_solutions = collect.(collect(Iterators.product(combinations...))) + denoms = [] + eqs2 = map(eqs) do eq + t = eq.rhs - eq.lhs + t = Symbolics.fixpoint_sub(t, subrules; maxiters = length(dvs)) + # the substituted variable occurs outside the substituted term + poly_and_nonpoly = map(dvs) do x + haskey(var_to_nonpoly, x) && occursin(x, t) + end + if any(poly_and_nonpoly) + throw(NotPolynomialError( + VariablesAsPolyAndNonPoly(dvs[poly_and_nonpoly]), eqs, polydata)) + end + + num, den = handle_rational_polynomials(t, new_dvs) # make factors different elements, otherwise the nonzero factors artificially # inflate the error of the zero factor. if iscall(den) && operation(den) == * @@ -292,16 +430,19 @@ function MTK.HomotopyContinuationProblem( end sys2 = MTK.@set sys.eqs = eqs2 + MTK.@set! sys2.unknowns = new_dvs # remove observed equations to avoid adding them in codegen MTK.@set! sys2.observed = Equation[] MTK.@set! sys2.substitutions = nothing - nlfn, u0, p = MTK.process_SciMLProblem(NonlinearFunction{true}, sys2, u0map, parammap; - jac = true, eval_expression, eval_module) + _, u0, p = MTK.process_SciMLProblem( + MTK.EmptySciMLFunction, sys, u0map, parammap; eval_expression, eval_module) + nlfn = NonlinearFunction{true}(sys2; jac = true, eval_expression, eval_module) - denominator = MTK.build_explicit_observed_function(sys, denoms) + denominator = MTK.build_explicit_observed_function(sys2, denoms) + unpack_solution = MTK.build_explicit_observed_function(sys2, all_solutions) - hvars = symbolics_to_hc.(dvs) + hvars = symbolics_to_hc.(new_dvs) mtkhsys = MTKHomotopySystem(nlfn.f, p, nlfn.jac, hvars, length(eqs)) obsfn = MTK.ObservedFunctionCache(sys; eval_expression, eval_module) @@ -319,7 +460,7 @@ function MTK.HomotopyContinuationProblem( solver_and_starts = HomotopyContinuation.solver_startsolutions(mtkhsys; kwargs...) end return MTK.HomotopyContinuationProblem( - u0, mtkhsys, denominator, sys, obsfn, solver_and_starts) + u0, mtkhsys, denominator, sys, obsfn, solver_and_starts, unpack_solution) end """ @@ -353,25 +494,35 @@ function CommonSolve.solve(prob::MTK.HomotopyContinuationProblem, if isempty(realsols) u = state_values(prob) retcode = SciMLBase.ReturnCode.ConvergenceFailure + resid = prob.homotopy_continuation_system(u) else T = eltype(state_values(prob)) - distance, idx = findmin(realsols) do result + distance = T(Inf) + u = state_values(prob) + resid = nothing + for result in realsols if any(<=(denominator_abstol), prob.denominator(real.(result.solution), parameter_values(prob))) - return T(Inf) + continue + end + for truesol in prob.unpack_solution(result.solution, parameter_values(prob)) + dist = norm(truesol - state_values(prob)) + if dist < distance + distance = dist + u = T.(real.(truesol)) + resid = T.(real.(prob.homotopy_continuation_system(result.solution))) + end end - norm(result.solution - state_values(prob)) end # all roots cause denominator to be zero if isinf(distance) u = state_values(prob) + resid = prob.homotopy_continuation_system(u) retcode = SciMLBase.ReturnCode.Infeasible else - u = real.(realsols[idx].solution) retcode = SciMLBase.ReturnCode.Success end end - resid = prob.homotopy_continuation_system(u) return SciMLBase.build_solution( prob, :HomotopyContinuation, u, resid; retcode, original = sol) diff --git a/src/systems/nonlinear/nonlinearsystem.jl b/src/systems/nonlinear/nonlinearsystem.jl index c6b2610ae7..94b44e0a6f 100644 --- a/src/systems/nonlinear/nonlinearsystem.jl +++ b/src/systems/nonlinear/nonlinearsystem.jl @@ -690,7 +690,7 @@ A type of Nonlinear problem which specializes on polynomial systems and uses HomotopyContinuation.jl to solve the system. Requires importing HomotopyContinuation.jl to create and solve. """ -struct HomotopyContinuationProblem{uType, H, D, O, SS} <: +struct HomotopyContinuationProblem{uType, H, D, O, SS, U} <: SciMLBase.AbstractNonlinearProblem{uType, true} """ The initial values of states in the system. If there are multiple real roots of @@ -721,6 +721,12 @@ struct HomotopyContinuationProblem{uType, H, D, O, SS} <: `HomotopyContinuation.solver_startsystems`. """ solver_and_starts::SS + """ + A function which takes a solution of the transformed system, and returns a vector + of solutions for the original system. This is utilized when converting systems + to polynomials. + """ + unpack_solution::U end function HomotopyContinuationProblem(::AbstractSystem, _u0, _p; kwargs...) diff --git a/test/extensions/homotopy_continuation.jl b/test/extensions/homotopy_continuation.jl index 172ee58890..81c252c84a 100644 --- a/test/extensions/homotopy_continuation.jl +++ b/test/extensions/homotopy_continuation.jl @@ -75,19 +75,59 @@ end @testset "Polynomial check and warnings" begin @variables x = 1.0 @mtkbuild sys = NonlinearSystem([x^1.5 + x^2 - 1 ~ 0]) - @test_throws ["Exponent", "not an integer", "not a polynomial"] HomotopyContinuationProblem( + @test_throws ["Cannot convert", "Unable", "symbolically solve", + "Exponent", "not an integer", "not a polynomial"] HomotopyContinuationProblem( sys, []) @mtkbuild sys = NonlinearSystem([x^x - x ~ 0]) - @test_throws ["Exponent", "unknowns", "not a polynomial"] HomotopyContinuationProblem( + @test_throws ["Cannot convert", "Unable", "symbolically solve", + "Exponent", "unknowns", "not a polynomial"] HomotopyContinuationProblem( sys, []) @mtkbuild sys = NonlinearSystem([((x^2) / sin(x))^2 + x ~ 0]) - @test_throws ["recognized", "sin", "not a polynomial"] HomotopyContinuationProblem( + @test_throws ["Cannot convert", "both polynomial", "non-polynomial", + "recognized", "sin", "not a polynomial"] HomotopyContinuationProblem( sys, []) @variables y = 2.0 @mtkbuild sys = NonlinearSystem([x^2 + y^2 + 2 ~ 0, y ~ sin(x)]) - @test_throws ["recognized", "sin", "not a polynomial"] HomotopyContinuationProblem( + @test_throws ["Cannot convert", "recognized", "sin", "not a polynomial"] HomotopyContinuationProblem( sys, []) + + @mtkbuild sys = NonlinearSystem([x^2 + y^2 - 2 ~ 0, sin(x + y) ~ 0]) + @test_throws ["Cannot convert", "function of multiple unknowns"] HomotopyContinuationProblem( + sys, []) + + @mtkbuild sys = NonlinearSystem([sin(x)^2 + 1 ~ 0, cos(y) - cos(x) - 1 ~ 0]) + @test_throws ["Cannot convert", "multiple non-polynomial terms", "same unknown"] HomotopyContinuationProblem( + sys, []) + + @mtkbuild sys = NonlinearSystem([sin(x^2)^2 + sin(x^2) - 1 ~ 0]) + @test_throws ["import Nemo"] HomotopyContinuationProblem(sys, []) +end + +import Nemo + +@testset "With Nemo" begin + @variables x = 2.0 + @mtkbuild sys = NonlinearSystem([sin(x^2)^2 + sin(x^2) - 1 ~ 0]) + prob = HomotopyContinuationProblem(sys, []) + @test prob[1] ≈ 2.0 + sol = solve(prob; threading = false) + _x = sol[1] + @test sin(_x^2)^2 + sin(_x^2) - 1≈0.0 atol=1e-12 +end + +@testset "Function of polynomial" begin + @variables x=0.25 y=0.125 + a = sin(x^2 - 4x + 1) + b = cos(3log(y) + 4) + @mtkbuild sys = NonlinearSystem([(a^2 - 4a * b + 4b^2) / (a - 0.25) ~ 0 + (a^2 - 0.75a + 0.125) ~ 0]) + prob = HomotopyContinuationProblem(sys, []) + @test prob[x] ≈ 0.25 + @test prob[y] ≈ 0.125 + sol = solve(prob; threading = false) + @test sol[a]≈0.5 atol=1e-6 + @test sol[b]≈0.25 atol=1e-6 end @testset "Rational functions" begin From daa3fee063055687c5ee5766344f50c73dea3fd0 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 18 Nov 2024 14:03:01 +0530 Subject: [PATCH 0355/1337] fix: fix `DAEProblem` with array parameters --- src/systems/diffeqs/abstractodesystem.jl | 4 +++- src/systems/problem_utils.jl | 3 +-- test/odesystem.jl | 9 +++++++++ 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index 518b48e929..eeb3838f99 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -221,9 +221,11 @@ function generate_function(sys::AbstractODESystem, dvs = unknowns(sys), pre, sol_states = get_substitutions_and_solved_unknowns(sys) if implicit_dae + # inputs = [] makes `wrap_array_vars` offset by 1 since there is an extra + # argument build_function(rhss, ddvs, u, p..., t; postprocess_fbody = pre, states = sol_states, - wrap_code = wrap_code .∘ wrap_array_vars(sys, rhss; dvs, ps) .∘ + wrap_code = wrap_code .∘ wrap_array_vars(sys, rhss; dvs, ps, inputs = []) .∘ wrap_parameter_dependencies(sys, false), kwargs...) else diff --git a/src/systems/problem_utils.jl b/src/systems/problem_utils.jl index 142d2f6d71..ff1d69e2bc 100644 --- a/src/systems/problem_utils.jl +++ b/src/systems/problem_utils.jl @@ -641,8 +641,7 @@ function process_SciMLProblem( ddvs = map(Differential(iv), dvs) du0map = to_varmap(du0map, ddvs) merge!(op, du0map) - - du0 = varmap_to_vars(du0map, ddvs; toterm = identity, + du0 = varmap_to_vars(op, ddvs; toterm = identity, tofloat = true) kwargs = merge(kwargs, (; ddvs)) else diff --git a/test/odesystem.jl b/test/odesystem.jl index fb185fdcb0..c36c10f4f8 100644 --- a/test/odesystem.jl +++ b/test/odesystem.jl @@ -1515,3 +1515,12 @@ end sol = solve(prob, Tsit5()) @test sol[obs] ≈ 1:7 end + +@testset "DAEProblem with array parameters" begin + @variables x(t)=1.0 y(t) [guess = 1.0] + @parameters p[1:2] = [1.0, 2.0] + @mtkbuild sys = ODESystem([D(x) ~ x, y^2 ~ x + sum(p)], t) + prob = DAEProblem(sys, [D(x) => x, D(y) => D(x) / 2y], [], (0.0, 1.0)) + sol = solve(prob, DImplicitEuler()) + @test sol[x]≈sol[y^2 - sum(p)] atol=1e-5 +end From f83ff6c90d223b09134593bdfb75b0e5c81a950c Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 18 Nov 2024 14:03:56 +0530 Subject: [PATCH 0356/1337] refactor: remove unused `DiscreteSaveAffect` --- src/systems/diffeqs/abstractodesystem.jl | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index eeb3838f99..ca69c038d9 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -792,12 +792,6 @@ function DiffEqBase.ODEProblem{false}(sys::AbstractODESystem, args...; kwargs... ODEProblem{false, SciMLBase.FullSpecialize}(sys, args...; kwargs...) end -struct DiscreteSaveAffect{F, S} <: Function - f::F - s::S -end -(d::DiscreteSaveAffect)(args...) = d.f(args..., d.s) - function DiffEqBase.ODEProblem{iip, specialize}(sys::AbstractODESystem, u0map = [], tspan = get_tspan(sys), parammap = DiffEqBase.NullParameters(); From e0b22f885b1623f1ac9147ec6493405d3f2c63b7 Mon Sep 17 00:00:00 2001 From: Christopher Rackauckas Date: Mon, 18 Nov 2024 07:52:45 -0100 Subject: [PATCH 0357/1337] Update Project.toml --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 7e64764e63..7bac4f96fc 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "ModelingToolkit" uuid = "961ee093-0014-501f-94e3-6117800e7a78" authors = ["Yingbo Ma ", "Chris Rackauckas and contributors"] -version = "9.50.0" +version = "9.51.0" [deps] AbstractTrees = "1520ce14-60c1-5f80-bbc7-55ef81b5835c" From 898810c1e99477a9eaac1ddd694655604821fbd8 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 18 Nov 2024 00:12:16 +0530 Subject: [PATCH 0358/1337] test: add Nemo to extension deps --- test/extensions/Project.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/test/extensions/Project.toml b/test/extensions/Project.toml index d11b88a6e9..37614d9bfb 100644 --- a/test/extensions/Project.toml +++ b/test/extensions/Project.toml @@ -6,6 +6,7 @@ ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210" HomotopyContinuation = "f213a82b-91d6-5c5d-acf7-10f1c761b327" LabelledArrays = "2ee39098-c373-598a-b85f-a56591580800" ModelingToolkit = "961ee093-0014-501f-94e3-6117800e7a78" +Nemo = "2edaba10-b0f1-5616-af89-8c11ac63239a" OrdinaryDiffEq = "1dea7af3-3e70-54e6-95c3-0bf5283fa5ed" SciMLSensitivity = "1ed8b502-d754-442c-8d5d-10ac956f44a1" SciMLStructures = "53ae85a6-f571-4167-b2af-e1d143709226" From 13500f22fc62913b7f12a4170585b08047936cd7 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 18 Nov 2024 16:01:34 +0530 Subject: [PATCH 0359/1337] build: bump Symbolics compat required for HCExtension, using `ia_solve` without `Nemo` --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 7e64764e63..ff6d2ffa38 100644 --- a/Project.toml +++ b/Project.toml @@ -133,7 +133,7 @@ SpecialFunctions = "0.7, 0.8, 0.9, 0.10, 1.0, 2" StaticArrays = "0.10, 0.11, 0.12, 1.0" SymbolicIndexingInterface = "0.3.35" SymbolicUtils = "3.7" -Symbolics = "6.15.4" +Symbolics = "6.19" URIs = "1" UnPack = "0.1, 1.0" Unitful = "1.1" From f6d581c9fdf6f14096cf46073e9923f540a59f09 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Nov 2024 11:35:06 +0000 Subject: [PATCH 0360/1337] build(deps): bump codecov/codecov-action from 4 to 5 Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 4 to 5. - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/codecov/codecov-action/compare/v4...v5) --- updated-dependencies: - dependency-name: codecov/codecov-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/Documentation.yml | 2 +- .github/workflows/Downstream.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/Documentation.yml b/.github/workflows/Documentation.yml index dceee5f1d2..f21acd9d25 100644 --- a/.github/workflows/Documentation.yml +++ b/.github/workflows/Documentation.yml @@ -31,7 +31,7 @@ jobs: JULIA_DEBUG: "Documenter" run: DISPLAY=:0 xvfb-run -s '-screen 0 1024x768x24' julia --project=docs/ --code-coverage=user docs/make.jl - uses: julia-actions/julia-processcoverage@v1 - - uses: codecov/codecov-action@v4 + - uses: codecov/codecov-action@v5 with: file: lcov.info token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/Downstream.yml b/.github/workflows/Downstream.yml index a6d5e84656..e407da6129 100644 --- a/.github/workflows/Downstream.yml +++ b/.github/workflows/Downstream.yml @@ -68,7 +68,7 @@ jobs: exit(0) # Exit immediately, as a success end - uses: julia-actions/julia-processcoverage@v1 - - uses: codecov/codecov-action@v4 + - uses: codecov/codecov-action@v5 with: file: lcov.info token: ${{ secrets.CODECOV_TOKEN }} From 061f3c848157935bb3b747105185eee03894b277 Mon Sep 17 00:00:00 2001 From: Christopher Rackauckas Date: Mon, 18 Nov 2024 10:37:54 -0100 Subject: [PATCH 0361/1337] Update .github/workflows/Downstream.yml --- .github/workflows/Downstream.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/Downstream.yml b/.github/workflows/Downstream.yml index e407da6129..f0aefa87f6 100644 --- a/.github/workflows/Downstream.yml +++ b/.github/workflows/Downstream.yml @@ -70,6 +70,6 @@ jobs: - uses: julia-actions/julia-processcoverage@v1 - uses: codecov/codecov-action@v5 with: - file: lcov.info + files: lcov.info token: ${{ secrets.CODECOV_TOKEN }} fail_ci_if_error: false From 720b848d24d4f80ac646d43a699c5435f155cfc0 Mon Sep 17 00:00:00 2001 From: Christopher Rackauckas Date: Mon, 18 Nov 2024 10:38:00 -0100 Subject: [PATCH 0362/1337] Update .github/workflows/Documentation.yml --- .github/workflows/Documentation.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/Documentation.yml b/.github/workflows/Documentation.yml index f21acd9d25..14bea532b3 100644 --- a/.github/workflows/Documentation.yml +++ b/.github/workflows/Documentation.yml @@ -33,6 +33,6 @@ jobs: - uses: julia-actions/julia-processcoverage@v1 - uses: codecov/codecov-action@v5 with: - file: lcov.info + files: lcov.info token: ${{ secrets.CODECOV_TOKEN }} fail_ci_if_error: true From 8dccf9db87d6353406f5663f99371e0c23666d2a Mon Sep 17 00:00:00 2001 From: Christopher Rackauckas Date: Mon, 18 Nov 2024 18:11:06 -0100 Subject: [PATCH 0363/1337] Update odesystem.jl --- test/odesystem.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/odesystem.jl b/test/odesystem.jl index c36c10f4f8..90d1c4c578 100644 --- a/test/odesystem.jl +++ b/test/odesystem.jl @@ -1521,6 +1521,6 @@ end @parameters p[1:2] = [1.0, 2.0] @mtkbuild sys = ODESystem([D(x) ~ x, y^2 ~ x + sum(p)], t) prob = DAEProblem(sys, [D(x) => x, D(y) => D(x) / 2y], [], (0.0, 1.0)) - sol = solve(prob, DImplicitEuler()) + sol = solve(prob, DFBDF(), abstol=1e-8, reltol=1e-8) @test sol[x]≈sol[y^2 - sum(p)] atol=1e-5 end From 85ca1a9f8cb8a426cab4302e0517e484951d19df Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 18 Nov 2024 14:04:14 +0530 Subject: [PATCH 0364/1337] feat: add symbolic tstops support to `ODESystem` --- src/systems/abstractsystem.jl | 17 ++++++++ src/systems/diffeqs/abstractodesystem.jl | 49 +++++++++++++++++++++++- src/systems/diffeqs/odesystem.jl | 15 ++++++-- test/odesystem.jl | 28 ++++++++++++++ 4 files changed, 103 insertions(+), 6 deletions(-) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index db2fcb4f10..8f021c9a1d 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -1051,6 +1051,7 @@ for prop in [:eqs :split_idxs :parent :is_dde + :tstops :index_cache :is_scalar_noise :isscheduled] @@ -1377,6 +1378,14 @@ function namespace_initialization_equations( map(eq -> namespace_equation(eq, sys; ivs), eqs) end +function namespace_tstops(sys::AbstractSystem) + tstops = symbolic_tstops(sys) + isempty(tstops) && return tstops + map(tstops) do val + namespace_expr(val, sys) + end +end + function namespace_equation(eq::Equation, sys, n = nameof(sys); @@ -1632,6 +1641,14 @@ function initialization_equations(sys::AbstractSystem) end end +function symbolic_tstops(sys::AbstractSystem) + tstops = get_tstops(sys) + systems = get_systems(sys) + isempty(systems) && return tstops + tstops = [tstops; reduce(vcat, namespace_tstops.(get_systems(sys)); init = [])] + return tstops +end + function preface(sys::AbstractSystem) has_preface(sys) || return nothing pre = get_preface(sys) diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index ca69c038d9..64a24b8ba3 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -758,6 +758,39 @@ function DAEFunctionExpr(sys::AbstractODESystem, args...; kwargs...) DAEFunctionExpr{true}(sys, args...; kwargs...) end +struct SymbolicTstops{F} + fn::F +end + +function (st::SymbolicTstops)(p, tspan) + unique!(sort!(reduce(vcat, st.fn(p..., tspan...)))) +end + +function SymbolicTstops( + sys::AbstractSystem; eval_expression = false, eval_module = @__MODULE__) + tstops = symbolic_tstops(sys) + isempty(tstops) && return nothing + t0 = gensym(:t0) + t1 = gensym(:t1) + tstops = map(tstops) do val + if is_array_of_symbolics(val) || val isa AbstractArray + collect(val) + else + term(:, t0, unwrap(val), t1; type = AbstractArray{Real}) + end + end + rps = reorder_parameters(sys, parameters(sys)) + tstops, _ = build_function(tstops, + rps..., + t0, + t1; + expression = Val{true}, + wrap_code = wrap_array_vars(sys, tstops; dvs = nothing) .∘ + wrap_parameter_dependencies(sys, false)) + tstops = eval_or_rgf(tstops; eval_expression, eval_module) + return SymbolicTstops(tstops) +end + """ ```julia DiffEqBase.ODEProblem{iip}(sys::AbstractODESystem, u0map, tspan, @@ -817,6 +850,11 @@ function DiffEqBase.ODEProblem{iip, specialize}(sys::AbstractODESystem, u0map = kwargs1 = merge(kwargs1, (callback = cbs,)) end + tstops = SymbolicTstops(sys; eval_expression, eval_module) + if tstops !== nothing + kwargs1 = merge(kwargs1, (; tstops)) + end + return ODEProblem{iip}(f, u0, tspan, p, pt; kwargs1..., kwargs...) end get_callback(prob::ODEProblem) = prob.kwargs[:callback] @@ -843,7 +881,7 @@ end function DiffEqBase.DAEProblem{iip}(sys::AbstractODESystem, du0map, u0map, tspan, parammap = DiffEqBase.NullParameters(); warn_initialize_determined = true, - check_length = true, kwargs...) where {iip} + check_length = true, eval_expression = false, eval_module = @__MODULE__, kwargs...) where {iip} if !iscomplete(sys) error("A completed system is required. Call `complete` or `structural_simplify` on the system before creating a `DAEProblem`") end @@ -856,8 +894,15 @@ function DiffEqBase.DAEProblem{iip}(sys::AbstractODESystem, du0map, u0map, tspan differential_vars = map(Base.Fix2(in, diffvars), sts) kwargs = filter_kwargs(kwargs) + kwargs1 = (;) + + tstops = SymbolicTstops(sys; eval_expression, eval_module) + if tstops !== nothing + kwargs1 = merge(kwargs1, (; tstops)) + end + DAEProblem{iip}(f, du0, u0, tspan, p; differential_vars = differential_vars, - kwargs...) + kwargs..., kwargs1...) end function generate_history(sys::AbstractODESystem, u0; expression = Val{false}, kwargs...) diff --git a/src/systems/diffeqs/odesystem.jl b/src/systems/diffeqs/odesystem.jl index b8dee2bac7..2b0bd8c8d7 100644 --- a/src/systems/diffeqs/odesystem.jl +++ b/src/systems/diffeqs/odesystem.jl @@ -149,6 +149,11 @@ struct ODESystem <: AbstractODESystem """ is_dde::Bool """ + A list of points to provide to the solver as tstops. Uses the same syntax as discrete + events. + """ + tstops::Vector{Any} + """ Cache for intermediate tearing state. """ tearing_state::Any @@ -187,7 +192,7 @@ struct ODESystem <: AbstractODESystem connector_type, preface, cevents, devents, parameter_dependencies, metadata = nothing, gui_metadata = nothing, is_dde = false, - tearing_state = nothing, + tstops = [], tearing_state = nothing, substitutions = nothing, complete = false, index_cache = nothing, discrete_subsystems = nothing, solved_unknowns = nothing, split_idxs = nothing, parent = nothing; checks::Union{Bool, Int} = true) @@ -206,7 +211,7 @@ struct ODESystem <: AbstractODESystem ctrl_jac, Wfact, Wfact_t, name, description, systems, defaults, guesses, torn_matching, initializesystem, initialization_eqs, schedule, connector_type, preface, cevents, devents, parameter_dependencies, metadata, - gui_metadata, is_dde, tearing_state, substitutions, complete, index_cache, + gui_metadata, is_dde, tstops, tearing_state, substitutions, complete, index_cache, discrete_subsystems, solved_unknowns, split_idxs, parent) end end @@ -233,7 +238,8 @@ function ODESystem(deqs::AbstractVector{<:Equation}, iv, dvs, ps; checks = true, metadata = nothing, gui_metadata = nothing, - is_dde = nothing) + is_dde = nothing, + tstops = []) name === nothing && throw(ArgumentError("The `name` keyword must be provided. Please consider using the `@named` macro")) @assert all(control -> any(isequal.(control, ps)), controls) "All controls must also be parameters." @@ -299,7 +305,7 @@ function ODESystem(deqs::AbstractVector{<:Equation}, iv, dvs, ps; defaults, guesses, nothing, initializesystem, initialization_eqs, schedule, connector_type, preface, cont_callbacks, disc_callbacks, parameter_dependencies, - metadata, gui_metadata, is_dde, checks = checks) + metadata, gui_metadata, is_dde, tstops, checks = checks) end function ODESystem(eqs, iv; kwargs...) @@ -402,6 +408,7 @@ function flatten(sys::ODESystem, noeqs = false) description = description(sys), initialization_eqs = initialization_equations(sys), is_dde = is_dde(sys), + tstops = symbolic_tstops(sys), checks = false) end end diff --git a/test/odesystem.jl b/test/odesystem.jl index 90d1c4c578..7f292798df 100644 --- a/test/odesystem.jl +++ b/test/odesystem.jl @@ -1524,3 +1524,31 @@ end sol = solve(prob, DFBDF(), abstol=1e-8, reltol=1e-8) @test sol[x]≈sol[y^2 - sum(p)] atol=1e-5 end + +@testset "Symbolic tstops" begin + @variables x(t) = 1.0 + @parameters p=0.15 q=0.25 r[1:2]=[0.35, 0.45] + @mtkbuild sys = ODESystem( + [D(x) ~ p * x + q * t + sum(r)], t; tstops = [0.5p, [0.1, 0.2], [p + 2q], r]) + prob = ODEProblem(sys, [], (0.0, 5.0)) + sol = solve(prob) + expected_tstops = unique!(sort!(vcat(0.0:0.075:5.0, 0.1, 0.2, 0.65, 0.35, 0.45))) + @test all(x -> any(isapprox(x, atol = 1e-6), sol.t), expected_tstops) + prob2 = remake(prob; tspan = (0.0, 10.0)) + sol2 = solve(prob2) + expected_tstops = unique!(sort!(vcat(0.0:0.075:10.0, 0.1, 0.2, 0.65, 0.35, 0.45))) + @test all(x -> any(isapprox(x, atol = 1e-6), sol2.t), expected_tstops) + + @variables y(t) [guess = 1.0] + @mtkbuild sys = ODESystem([D(x) ~ p * x + q * t + sum(r), y^3 ~ 2x + 1], + t; tstops = [0.5p, [0.1, 0.2], [p + 2q], r]) + prob = DAEProblem( + sys, [D(y) => 2D(x) / 3y^2, D(x) => p * x + q * t + sum(r)], [], (0.0, 5.0)) + sol = solve(prob, DImplicitEuler()) + expected_tstops = unique!(sort!(vcat(0.0:0.075:5.0, 0.1, 0.2, 0.65, 0.35, 0.45))) + @test all(x -> any(isapprox(x, atol = 1e-6), sol.t), expected_tstops) + prob2 = remake(prob; tspan = (0.0, 10.0)) + sol2 = solve(prob2, DImplicitEuler()) + expected_tstops = unique!(sort!(vcat(0.0:0.075:10.0, 0.1, 0.2, 0.65, 0.35, 0.45))) + @test all(x -> any(isapprox(x, atol = 1e-6), sol2.t), expected_tstops) +end From c4a5616971f1bf25891aa264b508d3fea7c3d6f9 Mon Sep 17 00:00:00 2001 From: Fredrik Bagge Carlson Date: Tue, 19 Nov 2024 09:27:50 +0100 Subject: [PATCH 0365/1337] add capability to trace MTK dynamics with InfiniteOpt --- Project.toml | 3 + ext/MTKInfiniteOptExt.jl | 19 ++++++ test/extensions/Project.toml | 3 + test/extensions/test_infiniteopt.jl | 102 ++++++++++++++++++++++++++++ test/runtests.jl | 1 + 5 files changed, 128 insertions(+) create mode 100644 ext/MTKInfiniteOptExt.jl create mode 100644 test/extensions/test_infiniteopt.jl diff --git a/Project.toml b/Project.toml index 18b4d55992..5848298418 100644 --- a/Project.toml +++ b/Project.toml @@ -64,6 +64,7 @@ BifurcationKit = "0f109fa4-8a5d-4b75-95aa-f515264e7665" ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4" DeepDiffs = "ab62b9b5-e342-54a8-a765-a90f495de1a6" HomotopyContinuation = "f213a82b-91d6-5c5d-acf7-10f1c761b327" +InfiniteOpt = "20393b10-9daf-11e9-18c9-8db751c92c57" LabelledArrays = "2ee39098-c373-598a-b85f-a56591580800" [extensions] @@ -72,6 +73,7 @@ MTKChainRulesCoreExt = "ChainRulesCore" MTKDeepDiffsExt = "DeepDiffs" MTKHomotopyContinuationExt = "HomotopyContinuation" MTKLabelledArraysExt = "LabelledArrays" +MTKInfiniteOptExt = "InfiniteOpt" [compat] AbstractTrees = "0.3, 0.4" @@ -104,6 +106,7 @@ FunctionWrappers = "1.1" FunctionWrappersWrappers = "0.1" Graphs = "1.5.2" HomotopyContinuation = "2.11" +InfiniteOpt = "0.5" InteractiveUtils = "1" JuliaFormatter = "1.0.47" JumpProcesses = "9.13.1" diff --git a/ext/MTKInfiniteOptExt.jl b/ext/MTKInfiniteOptExt.jl new file mode 100644 index 0000000000..88bf724d14 --- /dev/null +++ b/ext/MTKInfiniteOptExt.jl @@ -0,0 +1,19 @@ +module MTKInfiniteOptExt +import ModelingToolkit +import SymbolicUtils +import NaNMath +import InfiniteOpt + +# This file contains method definitions to make it possible to trace through functions generated by MTK using JuMP variables + +for ff in [acos, log1p, acosh, log2, asin, lgamma, tan, atanh, cos, log, sin, log10, sqrt] + f = nameof(ff) + # These need to be defined so that JuMP can trace through functions built by Symbolics + @eval NaNMath.$f(x::GeneralVariableRef) = Base.$f(x) +end + +# JuMP variables and Symbolics variables never compare equal. When tracing through dynamics, a function argument can be either a JuMP variable or A Symbolics variable, it can never be both. +Base.isequal(::SymbolicUtils.BasicSymbolic{Real}, ::InfiniteOpt.GeneralVariableRef) = false +Base.isequal(::InfiniteOpt.GeneralVariableRef, ::SymbolicUtils.BasicSymbolic{Real}) = false + +end diff --git a/test/extensions/Project.toml b/test/extensions/Project.toml index 37614d9bfb..5f7afe222a 100644 --- a/test/extensions/Project.toml +++ b/test/extensions/Project.toml @@ -4,6 +4,9 @@ ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4" ChainRulesTestUtils = "cdddcdb0-9152-4a09-a978-84456f9df70a" ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210" HomotopyContinuation = "f213a82b-91d6-5c5d-acf7-10f1c761b327" +InfiniteOpt = "20393b10-9daf-11e9-18c9-8db751c92c57" +Ipopt = "b6b21f68-93f8-5de0-b562-5493be1d77c9" +JuMP = "4076af6c-e467-56ae-b986-b466b2749572" LabelledArrays = "2ee39098-c373-598a-b85f-a56591580800" ModelingToolkit = "961ee093-0014-501f-94e3-6117800e7a78" Nemo = "2edaba10-b0f1-5616-af89-8c11ac63239a" diff --git a/test/extensions/test_infiniteopt.jl b/test/extensions/test_infiniteopt.jl new file mode 100644 index 0000000000..e45aa0f2fd --- /dev/null +++ b/test/extensions/test_infiniteopt.jl @@ -0,0 +1,102 @@ +using ModelingToolkit, InfiniteOpt, JuMP, Ipopt +using ModelingToolkit: D_nounits as D, t_nounits as t, varmap_to_vars + +@mtkmodel Pendulum begin + @parameters begin + g = 9.8 + L = 0.4 + K = 1.2 + m = 0.3 + end + @variables begin + θ(t) # state + ω(t) # state + τ(t) = 0 # input + y(t) # output + end + @equations begin + D(θ) ~ ω + D(ω) ~ -g / L * sin(θ) - K / m * ω + τ / m / L^2 + y ~ θ * 180 / π + end +end +@named model = Pendulum() +model = complete(model) + +inputs = [model.τ] +(f_oop, f_ip), dvs, psym, io_sys = ModelingToolkit.generate_control_function( + model, inputs, split = false) + +outputs = [model.y] +f_obs = ModelingToolkit.build_explicit_observed_function(io_sys, outputs; inputs = inputs) + +expected_state_order = [model.θ, model.ω] +permutation = [findfirst(isequal(x), expected_state_order) for x in dvs] # This maps our expected state order to the actual state order + +## + +ub = varmap_to_vars([model.θ => 2pi, model.ω => 10], dvs) +lb = varmap_to_vars([model.θ => -2pi, model.ω => -10], dvs) +xf = varmap_to_vars([model.θ => pi, model.ω => 0], dvs) +nx = length(dvs) +nu = length(inputs) +ny = length(outputs) + +## +m = InfiniteModel(optimizer_with_attributes(Ipopt.Optimizer, + "print_level" => 0, "acceptable_tol" => 1e-3, "constr_viol_tol" => 1e-5, "max_iter" => 1000, + "tol" => 1e-5, "mu_strategy" => "monotone", "nlp_scaling_method" => "gradient-based", + "alpha_for_y" => "safer-min-dual-infeas", "bound_mult_init_method" => "mu-based", "print_user_options" => "yes")); + +@infinite_parameter(m, τ in [0, 1], num_supports=51, + derivative_method=OrthogonalCollocation(4)) # Time variable +guess_xs = [t -> pi, t -> 0.1][permutation] +guess_us = [t -> 0.1] +InfiniteOpt.@variables(m, + begin + # state variables + (lb[i] <= x[i = 1:nx] <= ub[i], Infinite(τ), start = guess_xs[i]) # state variables + -10 <= u[i = 1:nu] <= 10, Infinite(τ), (start = guess_us[i]) # control variables + 0 <= tf <= 10, (start = 5) # Final time + 0.2 <= L <= 0.6, (start = 0.4) # Length parameter + end) + +# Trace the dynamics +x0, p = ModelingToolkit.get_u0_p(io_sys, [model.θ => 0, model.ω => 0], [model.L => L]) + +xp = f_oop(x, u, p, τ) +cp = f_obs(x, u, p, τ) # Test that it's possible to trace through an observed function + +@objective(m, Min, tf) +@constraint(m, [i = 1:nx], x[i](0)==x0[i]) # Initial condition +@constraint(m, [i = 1:nx], x[i](1)==xf[i]) # Terminal state + +x_scale = varmap_to_vars([model.θ => 1 + model.ω => 1], dvs) + +# Add dynamics constraints +@constraint(m, [i = 1:nx], (∂(x[i], τ) - tf * xp[i]) / x_scale[i]==0) + +optimize!(m) + +# Extract the optimal solution +opt_tf = value(tf) +opt_time = opt_tf * value(τ) +opt_x = [value(x[i]) for i in permutation] +opt_u = [value(u[i]) for i in 1:nu] +opt_L = value(L) + +# Plot the results +# using Plots +# plot(opt_time, opt_x[1], label = "θ", xlabel = "Time [s]", layout=3) +# plot!(opt_time, opt_x[2], label = "ω", sp=2) +# plot!(opt_time, opt_u[1], label = "τ", sp=3) + +using Test +@test opt_x[1][end]≈pi atol=1e-3 +@test opt_x[2][end]≈0 atol=1e-3 + +@test opt_x[1][1]≈0 atol=1e-3 +@test opt_x[2][1]≈0 atol=1e-3 + +@test opt_L≈0.2 atol=1e-3 # Smallest permissible length is optimal diff --git a/test/runtests.jl b/test/runtests.jl index 192ead935c..25b692f678 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -113,5 +113,6 @@ end @safetestset "Auto Differentiation Test" include("extensions/ad.jl") @safetestset "LabelledArrays Test" include("labelledarrays.jl") @safetestset "BifurcationKit Extension Test" include("extensions/bifurcationkit.jl") + @safetestset "InfiniteOpt Extension Test" include("extensions/test_infiniteopt.jl") end end From d4f7e8a1392b4aa8b6c3507c706d91ac968eb9d6 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 19 Nov 2024 13:30:37 +0530 Subject: [PATCH 0366/1337] fix: handle observeds in `get_all_timeseries_indexes` for `split = false` systems --- src/systems/abstractsystem.jl | 18 +++++++++++++----- test/symbolic_indexing_interface.jl | 11 +++++++++++ 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index db2fcb4f10..6d59c5b8ea 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -770,11 +770,19 @@ for traitT in [ is_variable(sys, operation(s)(get_iv(sys))) # DDEs case, to detect x(t - k) push!(ts_idxs, ContinuousTimeseries()) - elseif has_index_cache(sys) && (ic = get_index_cache(sys)) !== nothing - if (ts = get(ic.observed_syms_to_timeseries, s, nothing)) !== nothing - union!(ts_idxs, ts) - elseif (ts = get(ic.dependent_pars_to_timeseries, s, nothing)) !== nothing - union!(ts_idxs, ts) + else + if has_index_cache(sys) && (ic = get_index_cache(sys)) !== nothing + if (ts = get(ic.observed_syms_to_timeseries, s, nothing)) !== nothing + union!(ts_idxs, ts) + elseif (ts = get(ic.dependent_pars_to_timeseries, s, nothing)) !== + nothing + union!(ts_idxs, ts) + end + else + # for split=false systems + if has_observed_with_lhs(sys, sym) + push!(ts_idxs, ContinuousTimeseries()) + end end end end diff --git a/test/symbolic_indexing_interface.jl b/test/symbolic_indexing_interface.jl index 161d926cb2..e46c197dde 100644 --- a/test/symbolic_indexing_interface.jl +++ b/test/symbolic_indexing_interface.jl @@ -213,3 +213,14 @@ end prob = ODEProblem(sys, [], (0.0, 1.0)) @test prob.ps[b] == prob.ps[:b] end + +@testset "`get_all_timeseries_indexes` with non-split systems" begin + @variables x(t) y(t) z(t) + @parameters a + @named sys = ODESystem([D(x) ~ a * x, y ~ 2x, z ~ 0.0], t) + sys = structural_simplify(sys, split = false) + for sym in [x, y, z, x + y, x + a, y / x] + @test only(get_all_timeseries_indexes(sys, sym)) == ContinuousTimeseries() + end + @test isempty(get_all_timeseries_indexes(sys, a)) +end From 0b9d51dbac41d7bf3138fa3f6c5710a79043330a Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 19 Nov 2024 14:39:40 +0530 Subject: [PATCH 0367/1337] refactor: format --- test/odesystem.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/odesystem.jl b/test/odesystem.jl index 90d1c4c578..02eb80ef85 100644 --- a/test/odesystem.jl +++ b/test/odesystem.jl @@ -1521,6 +1521,6 @@ end @parameters p[1:2] = [1.0, 2.0] @mtkbuild sys = ODESystem([D(x) ~ x, y^2 ~ x + sum(p)], t) prob = DAEProblem(sys, [D(x) => x, D(y) => D(x) / 2y], [], (0.0, 1.0)) - sol = solve(prob, DFBDF(), abstol=1e-8, reltol=1e-8) + sol = solve(prob, DFBDF(), abstol = 1e-8, reltol = 1e-8) @test sol[x]≈sol[y^2 - sum(p)] atol=1e-5 end From a631481e026c5ee39219dc8868f1f3e34a2269f0 Mon Sep 17 00:00:00 2001 From: David Widmann Date: Tue, 19 Nov 2024 10:51:51 +0100 Subject: [PATCH 0368/1337] Remove unused variables --- src/systems/diffeqs/abstractodesystem.jl | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index ca69c038d9..3d8df99f0a 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -71,8 +71,6 @@ function calculate_jacobian(sys::AbstractODESystem; rhs = [eq.rhs - eq.lhs for eq in full_equations(sys)] #need du terms on rhs for differentiating wrt du - iv = get_iv(sys) - if sparse jac = sparsejacobian(rhs, dvs, simplify = simplify) else @@ -94,8 +92,6 @@ function calculate_control_jacobian(sys::AbstractODESystem; end rhs = [eq.rhs for eq in full_equations(sys)] - - iv = get_iv(sys) ctrls = controls(sys) if sparse From 00bfd1d539977588416cfb6de8cdcbcbf8ce2c24 Mon Sep 17 00:00:00 2001 From: Christopher Rackauckas Date: Tue, 19 Nov 2024 09:36:52 -0100 Subject: [PATCH 0369/1337] Update MTKInfiniteOptExt.jl --- ext/MTKInfiniteOptExt.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ext/MTKInfiniteOptExt.jl b/ext/MTKInfiniteOptExt.jl index 88bf724d14..79463f7e71 100644 --- a/ext/MTKInfiniteOptExt.jl +++ b/ext/MTKInfiniteOptExt.jl @@ -15,5 +15,6 @@ end # JuMP variables and Symbolics variables never compare equal. When tracing through dynamics, a function argument can be either a JuMP variable or A Symbolics variable, it can never be both. Base.isequal(::SymbolicUtils.BasicSymbolic{Real}, ::InfiniteOpt.GeneralVariableRef) = false Base.isequal(::InfiniteOpt.GeneralVariableRef, ::SymbolicUtils.BasicSymbolic{Real}) = false - +Base.isequal(::SymbolicUtils.Symbolic, ::Union{JuMP.GenericAffExpr, JuMP.GenericQuadExpr, InfiniteOpt.AbstractInfOptExpr}) = false +Base.isequal(::Union{JuMP.GenericAffExpr, JuMP.GenericQuadExpr, InfiniteOpt.AbstractInfOptExpr}, ::SymbolicUtils.Symbolic) = false end From a40177226295769049f51d926bb6fca3b23ec800 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 19 Nov 2024 16:13:35 +0530 Subject: [PATCH 0370/1337] fix: set defaults in `modelingtoolkitize(::SDEProblem)` --- src/systems/diffeqs/modelingtoolkitize.jl | 16 ++++++++++++- test/modelingtoolkitize.jl | 28 +++++++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/src/systems/diffeqs/modelingtoolkitize.jl b/src/systems/diffeqs/modelingtoolkitize.jl index 68a970d8b5..2e0623bea1 100644 --- a/src/systems/diffeqs/modelingtoolkitize.jl +++ b/src/systems/diffeqs/modelingtoolkitize.jl @@ -277,10 +277,24 @@ function modelingtoolkitize(prob::DiffEqBase.SDEProblem; kwargs...) else Vector(vec(params)) end + sts = Vector(vec(vars)) + default_u0 = Dict(sts .=> vec(collect(prob.u0))) + default_p = if has_p + if prob.p isa AbstractDict + Dict(v => prob.p[k] for (k, v) in pairs(_params)) + elseif prob.p isa MTKParameters + Dict(params .=> reduce(vcat, prob.p)) + else + Dict(params .=> vec(collect(prob.p))) + end + else + Dict() + end - de = SDESystem(deqs, neqs, t, Vector(vec(vars)), params; + de = SDESystem(deqs, neqs, t, sts, params; name = gensym(:MTKizedSDE), tspan = prob.tspan, + defaults = merge(default_u0, default_p), kwargs...) de diff --git a/test/modelingtoolkitize.jl b/test/modelingtoolkitize.jl index 621bf530b7..3bcfdecc8b 100644 --- a/test/modelingtoolkitize.jl +++ b/test/modelingtoolkitize.jl @@ -441,3 +441,31 @@ prob = NonlinearLeastSquaresProblem( sys = modelingtoolkitize(prob) @test length(equations(sys)) == 3 @test length(equations(structural_simplify(sys; fully_determined = false))) == 0 + +@testset "`modelingtoolkitize(::SDEProblem)` sets defaults" begin + function sdeg!(du, u, p, t) + du[1] = 0.3 * u[1] + du[2] = 0.3 * u[2] + du[3] = 0.3 * u[3] + end + function sdef!(du, u, p, t) + x, y, z = u + sigma, rho, beta = p + du[1] = sigma * (y - x) + du[2] = x * (rho - z) - y + du[3] = x * y - beta * z + end + u0 = [1.0, 0.0, 0.0] + tspan = (0.0, 100.0) + p = [10.0, 28.0, 2.66] + sprob = SDEProblem(sdef!, sdeg!, u0, tspan, p) + sys = complete(modelingtoolkitize(sprob)) + @test length(ModelingToolkit.defaults(sys)) == 6 + sprob2 = SDEProblem(sys, [], tspan) + + truevals = similar(u0) + sprob.f(truevals, u0, p, tspan[1]) + mtkvals = similar(u0) + sprob2.f(mtkvals, sprob2.u0, sprob2.p, tspan[1]) + @test mtkvals ≈ truevals +end From 73daad1904da5c6d44af94d6272ba6e2416777d1 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 19 Nov 2024 19:09:41 +0530 Subject: [PATCH 0371/1337] feat: add function to parse variable from string --- src/systems/abstractsystem.jl | 73 +++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index db2fcb4f10..9c427a2a12 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -3285,6 +3285,79 @@ function dump_unknowns(sys::AbstractSystem) end end +# syntax: +# varname = "D(" varname ")" | arrvar | maybe_dummy_var +# arrvar = maybe_dummy_var "[idxs...]" +# idxs = int | int "," idxs +# maybe_dummy_var = namespacedvar | namespacedvar "(t)" | +# namespacedvar "(t)" "ˍ" ts | namespacedvar "ˍ" ts | +# namespacedvar "ˍ" ts "(t)" +# ts = "t" | "t" ts +# namespacedvar = ident "₊" namespacedvar | ident "." namespacedvar | ident +# +# I'd write a regex to validate this, but https://xkcd.com/1171/ +function parse_variable(sys::AbstractSystem, str::AbstractString) + str = strip(str) + derivative_level = 0 + while startswith(str, "D(") && endswith(str, ")") + derivative_level += 1 + str = _string_view_inner(str, 2, 1) + end + + arr_idxs = nothing + if endswith(str, ']') + open_idx = only(findfirst('[', str)) + idxs_range = nextind(str, open_idx):prevind(str, lastindex(str)) + idxs_str = view(str, idxs_range) + str = view(str, firstindex(str):prevind(str, open_idx)) + arr_idxs = map(Base.Fix1(parse, Int), eachsplit(idxs_str, ",")) + end + + if endswith(str, "(t)") + str = _string_view_inner(str, 0, 3) + end + + dummyderivative_level = 0 + if (dd_idx = findfirst('ˍ', str)) !== nothing + t_idx = nextind(str, dd_idx) + while checkbounds(Bool, str, t_idx) + if str[t_idx] != 't' + throw(ArgumentError("Dummy derivative must be 'ˍ' followed by one or more 't'.")) + end + dummyderivative_level += 1 + t_idx = nextind(str, t_idx) + end + str = view(str, firstindex(str):prevind(str, dd_idx)) + end + + if endswith(str, "(t)") + str = _string_view_inner(str, 0, 3) + end + + cur = sys + for ident in eachsplit(str, ('.', NAMESPACE_SEPARATOR)) + ident = Symbol(ident) + hasproperty(cur, ident) || + throw(ArgumentError("System $(nameof(cur)) does not have a subsystem/variable named $(ident)")) + cur = getproperty(cur, ident) + end + + if arr_idxs !== nothing + cur = cur[arr_idxs...] + end + + for i in 1:(derivative_level + dummyderivative_level) + cur = Differential(get_iv(sys))(cur) + end + + return cur +end + +function _string_view_inner(str, startoffset, endoffset) + view(str, + nextind(str, firstindex(str), startoffset):prevind(str, lastindex(str), endoffset)) +end + ### Functions for accessing algebraic/differential equations in systems ### """ From fc13ecd8e726694042be7ca823a04941b3f6236a Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 19 Nov 2024 19:23:54 +0530 Subject: [PATCH 0372/1337] test: add tests for `parse_variable` --- test/variable_utils.jl | 99 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 98 insertions(+), 1 deletion(-) diff --git a/test/variable_utils.jl b/test/variable_utils.jl index d76f2f1209..100e2bc1ad 100644 --- a/test/variable_utils.jl +++ b/test/variable_utils.jl @@ -1,6 +1,7 @@ using ModelingToolkit, Test -using ModelingToolkit: value, vars +using ModelingToolkit: value, vars, parse_variable using SymbolicUtils: <ₑ + @parameters α β δ expr = (((1 / β - 1) + δ) / α)^(1 / (α - 1)) ref = sort([β, δ, α], lt = <ₑ) @@ -41,3 +42,99 @@ ts = collect_ivs([eq]) res = vars(fn([x, y], z)) @test length(res) == 3 end + +@testset "parse_variable" begin + @mtkmodel Lorenz begin + @variables begin + x(t) + y(t) + z(t) + end + @parameters begin + σ + ρ + β + end + @equations begin + D(D(x)) ~ σ * (y - x) + D(y) ~ x * (ρ - z) - y + D(z) ~ x * y - β * z + end + end + @mtkmodel ArrSys begin + @variables begin + x(t)[1:2] + end + @parameters begin + p[1:2, 1:2] + end + @equations begin + D(D(x)) ~ p * x + end + end + @mtkmodel Outer begin + @components begin + 😄 = Lorenz() + arr = ArrSys() + end + end + + @mtkbuild sys = Outer() + for (str, var) in [ + # unicode system, scalar variable + ("😄.x", sys.😄.x), + ("😄.x(t)", sys.😄.x), + ("😄₊x", sys.😄.x), + ("😄₊x(t)", sys.😄.x), + # derivative + ("D(😄.x)", D(sys.😄.x)), + ("D(😄.x(t))", D(sys.😄.x)), + ("D(😄₊x)", D(sys.😄.x)), + ("D(😄₊x(t))", D(sys.😄.x)), + # other derivative + ("😄.xˍt", D(sys.😄.x)), + ("😄.x(t)ˍt", D(sys.😄.x)), + ("😄₊xˍt", D(sys.😄.x)), + ("😄₊x(t)ˍt", D(sys.😄.x)), + # scalar parameter + ("😄.σ", sys.😄.σ), + ("😄₊σ", sys.😄.σ), + # array variable + ("arr.x", sys.arr.x), + ("arr₊x", sys.arr.x), + ("arr.x(t)", sys.arr.x), + ("arr₊x(t)", sys.arr.x), + # getindex + ("arr.x[1]", sys.arr.x[1]), + ("arr₊x[1]", sys.arr.x[1]), + ("arr.x(t)[1]", sys.arr.x[1]), + ("arr₊x(t)[1]", sys.arr.x[1]), + # derivative + ("D(arr.x(t))", D(sys.arr.x)), + ("D(arr₊x(t))", D(sys.arr.x)), + ("D(arr.x[1])", D(sys.arr.x[1])), + ("D(arr₊x[1])", D(sys.arr.x[1])), + ("D(arr.x(t)[1])", D(sys.arr.x[1])), + ("D(arr₊x(t)[1])", D(sys.arr.x[1])), + # other derivative + ("arr.xˍt", D(sys.arr.x)), + ("arr₊xˍt", D(sys.arr.x)), + ("arr.xˍt(t)", D(sys.arr.x)), + ("arr₊xˍt(t)", D(sys.arr.x)), + ("arr.xˍt[1]", D(sys.arr.x[1])), + ("arr₊xˍt[1]", D(sys.arr.x[1])), + ("arr.xˍt(t)[1]", D(sys.arr.x[1])), + ("arr₊xˍt(t)[1]", D(sys.arr.x[1])), + ("arr.x(t)ˍt", D(sys.arr.x)), + ("arr₊x(t)ˍt", D(sys.arr.x)), + ("arr.x(t)ˍt[1]", D(sys.arr.x[1])), + ("arr₊x(t)ˍt[1]", D(sys.arr.x[1])), + # array parameter + ("arr.p", sys.arr.p), + ("arr₊p", sys.arr.p), + ("arr.p[1, 2]", sys.arr.p[1, 2]), + ("arr₊p[1, 2]", sys.arr.p[1, 2]) + ] + isequal(parse_variable(sys, str), var) + end +end From 8712f991b7096d8715e131a9edf1c72fdf9b8ac2 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 19 Nov 2024 19:26:30 +0530 Subject: [PATCH 0373/1337] docs: add docstring for `parse_variable` --- src/systems/abstractsystem.jl | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index 9c427a2a12..ba78a40a58 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -3285,18 +3285,25 @@ function dump_unknowns(sys::AbstractSystem) end end -# syntax: -# varname = "D(" varname ")" | arrvar | maybe_dummy_var -# arrvar = maybe_dummy_var "[idxs...]" -# idxs = int | int "," idxs -# maybe_dummy_var = namespacedvar | namespacedvar "(t)" | -# namespacedvar "(t)" "ˍ" ts | namespacedvar "ˍ" ts | -# namespacedvar "ˍ" ts "(t)" -# ts = "t" | "t" ts -# namespacedvar = ident "₊" namespacedvar | ident "." namespacedvar | ident -# -# I'd write a regex to validate this, but https://xkcd.com/1171/ +""" + $(TYPEDSIGNATURES) + +Return the variable in `sys` referred to by its string representation `str`. +Roughly supports the following CFG: + +``` +varname = "D(" varname ")" | arrvar | maybe_dummy_var +arrvar = maybe_dummy_var "[idxs...]" +idxs = int | int "," idxs +maybe_dummy_var = namespacedvar | namespacedvar "(t)" | + namespacedvar "(t)" "ˍ" ts | namespacedvar "ˍ" ts | + namespacedvar "ˍ" ts "(t)" +ts = "t" | "t" ts +namespacedvar = ident "₊" namespacedvar | ident "." namespacedvar | ident +``` +""" function parse_variable(sys::AbstractSystem, str::AbstractString) + # I'd write a regex to validate `str`, but https://xkcd.com/1171/ str = strip(str) derivative_level = 0 while startswith(str, "D(") && endswith(str, ")") From dddcd2c9c568dfe22e49a8b81afda21b3bee2d58 Mon Sep 17 00:00:00 2001 From: Christopher Rackauckas Date: Tue, 19 Nov 2024 17:46:53 -0100 Subject: [PATCH 0374/1337] Update Project.toml --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 18b4d55992..e1bcbef173 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "ModelingToolkit" uuid = "961ee093-0014-501f-94e3-6117800e7a78" authors = ["Yingbo Ma ", "Chris Rackauckas and contributors"] -version = "9.51.0" +version = "9.52.0" [deps] AbstractTrees = "1520ce14-60c1-5f80-bbc7-55ef81b5835c" From 3c728a399b8edf733d7be568750c32b68d828d09 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 20 Nov 2024 11:53:25 +0530 Subject: [PATCH 0375/1337] docs: add `parse_variable` to documentation --- docs/src/basics/FAQ.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/src/basics/FAQ.md b/docs/src/basics/FAQ.md index bb3d01fc30..10671299c6 100644 --- a/docs/src/basics/FAQ.md +++ b/docs/src/basics/FAQ.md @@ -82,6 +82,16 @@ parameter_index(sys, sym) Note that while the variable index will be an integer, the parameter index is a struct of type `ParameterIndex` whose internals should not be relied upon. +## Can I index with strings? + +Strings are not considered symbolic variables, and thus cannot directly be used for symbolic +indexing. However, ModelingToolkit does provide a method to parse the string representation of +a variable, given the system in which that variable exists. + +```@docs +ModelingToolkit.parse_variable +``` + ## Transforming value maps to arrays ModelingToolkit.jl allows (and recommends) input maps like `[x => 2.0, y => 3.0]` From afd55cbe3f197836a438163fba084e06bc9cc623 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 20 Nov 2024 11:53:57 +0530 Subject: [PATCH 0376/1337] feat: support arbitrary independent variables in `parse_variable` --- src/systems/abstractsystem.jl | 31 ++++++------ test/variable_utils.jl | 95 +++++++++++++++++------------------ 2 files changed, 62 insertions(+), 64 deletions(-) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index ba78a40a58..36bd5ac800 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -3295,14 +3295,18 @@ Roughly supports the following CFG: varname = "D(" varname ")" | arrvar | maybe_dummy_var arrvar = maybe_dummy_var "[idxs...]" idxs = int | int "," idxs -maybe_dummy_var = namespacedvar | namespacedvar "(t)" | - namespacedvar "(t)" "ˍ" ts | namespacedvar "ˍ" ts | - namespacedvar "ˍ" ts "(t)" -ts = "t" | "t" ts +maybe_dummy_var = namespacedvar | namespacedvar "(" iv ")" | + namespacedvar "(" iv ")" "ˍ" ts | namespacedvar "ˍ" ts | + namespacedvar "ˍ" ts "(" iv ")" +ts = iv | iv ts namespacedvar = ident "₊" namespacedvar | ident "." namespacedvar | ident ``` + +Where `iv` is the independent variable, `int` is an integer and `ident` is an identifier. """ function parse_variable(sys::AbstractSystem, str::AbstractString) + iv = has_iv(sys) ? string(getname(get_iv(sys))) : nothing + # I'd write a regex to validate `str`, but https://xkcd.com/1171/ str = strip(str) derivative_level = 0 @@ -3320,25 +3324,22 @@ function parse_variable(sys::AbstractSystem, str::AbstractString) arr_idxs = map(Base.Fix1(parse, Int), eachsplit(idxs_str, ",")) end - if endswith(str, "(t)") - str = _string_view_inner(str, 0, 3) + if iv !== nothing && endswith(str, "($iv)") + str = _string_view_inner(str, 0, 2 + length(iv)) end dummyderivative_level = 0 - if (dd_idx = findfirst('ˍ', str)) !== nothing - t_idx = nextind(str, dd_idx) - while checkbounds(Bool, str, t_idx) - if str[t_idx] != 't' - throw(ArgumentError("Dummy derivative must be 'ˍ' followed by one or more 't'.")) - end + if iv !== nothing && (dd_idx = findfirst('ˍ', str)) !== nothing + t_idx = findnext(iv, str, dd_idx) + while t_idx !== nothing dummyderivative_level += 1 - t_idx = nextind(str, t_idx) + t_idx = findnext(iv, str, nextind(str, last(t_idx))) end str = view(str, firstindex(str):prevind(str, dd_idx)) end - if endswith(str, "(t)") - str = _string_view_inner(str, 0, 3) + if iv !== nothing && endswith(str, "($iv)") + str = _string_view_inner(str, 0, 2 + length(iv)) end cur = sys diff --git a/test/variable_utils.jl b/test/variable_utils.jl index 100e2bc1ad..ebbc3c0d3b 100644 --- a/test/variable_utils.jl +++ b/test/variable_utils.jl @@ -43,98 +43,95 @@ ts = collect_ivs([eq]) @test length(res) == 3 end -@testset "parse_variable" begin - @mtkmodel Lorenz begin +@testset "parse_variable with iv: $iv" for iv in [t, only(@independent_variables tt)] + D = Differential(iv) + function Lorenz(; name) @variables begin - x(t) - y(t) - z(t) + x(iv) + y(iv) + z(iv) end @parameters begin σ ρ β end - @equations begin - D(D(x)) ~ σ * (y - x) - D(y) ~ x * (ρ - z) - y - D(z) ~ x * y - β * z - end + sys = ODESystem( + [D(D(x)) ~ σ * (y - x) + D(y) ~ x * (ρ - z) - y + D(z) ~ x * y - β * z], iv; name) end - @mtkmodel ArrSys begin + function ArrSys(; name) @variables begin - x(t)[1:2] + x(iv)[1:2] end @parameters begin p[1:2, 1:2] end - @equations begin - D(D(x)) ~ p * x - end + sys = ODESystem([D(D(x)) ~ p * x], iv; name) end - @mtkmodel Outer begin - @components begin - 😄 = Lorenz() - arr = ArrSys() - end + function Outer(; name) + @named 😄 = Lorenz() + @named arr = ArrSys() + sys = ODESystem(Equation[], iv; name, systems = [😄, arr]) end @mtkbuild sys = Outer() for (str, var) in [ # unicode system, scalar variable ("😄.x", sys.😄.x), - ("😄.x(t)", sys.😄.x), + ("😄.x($iv)", sys.😄.x), ("😄₊x", sys.😄.x), - ("😄₊x(t)", sys.😄.x), + ("😄₊x($iv)", sys.😄.x), # derivative ("D(😄.x)", D(sys.😄.x)), - ("D(😄.x(t))", D(sys.😄.x)), + ("D(😄.x($iv))", D(sys.😄.x)), ("D(😄₊x)", D(sys.😄.x)), - ("D(😄₊x(t))", D(sys.😄.x)), + ("D(😄₊x($iv))", D(sys.😄.x)), # other derivative - ("😄.xˍt", D(sys.😄.x)), - ("😄.x(t)ˍt", D(sys.😄.x)), - ("😄₊xˍt", D(sys.😄.x)), - ("😄₊x(t)ˍt", D(sys.😄.x)), + ("😄.xˍ$iv", D(sys.😄.x)), + ("😄.x($iv)ˍ$iv", D(sys.😄.x)), + ("😄₊xˍ$iv", D(sys.😄.x)), + ("😄₊x($iv)ˍ$iv", D(sys.😄.x)), # scalar parameter ("😄.σ", sys.😄.σ), ("😄₊σ", sys.😄.σ), # array variable ("arr.x", sys.arr.x), ("arr₊x", sys.arr.x), - ("arr.x(t)", sys.arr.x), - ("arr₊x(t)", sys.arr.x), + ("arr.x($iv)", sys.arr.x), + ("arr₊x($iv)", sys.arr.x), # getindex ("arr.x[1]", sys.arr.x[1]), ("arr₊x[1]", sys.arr.x[1]), - ("arr.x(t)[1]", sys.arr.x[1]), - ("arr₊x(t)[1]", sys.arr.x[1]), + ("arr.x($iv)[1]", sys.arr.x[1]), + ("arr₊x($iv)[1]", sys.arr.x[1]), # derivative - ("D(arr.x(t))", D(sys.arr.x)), - ("D(arr₊x(t))", D(sys.arr.x)), + ("D(arr.x($iv))", D(sys.arr.x)), + ("D(arr₊x($iv))", D(sys.arr.x)), ("D(arr.x[1])", D(sys.arr.x[1])), ("D(arr₊x[1])", D(sys.arr.x[1])), - ("D(arr.x(t)[1])", D(sys.arr.x[1])), - ("D(arr₊x(t)[1])", D(sys.arr.x[1])), + ("D(arr.x($iv)[1])", D(sys.arr.x[1])), + ("D(arr₊x($iv)[1])", D(sys.arr.x[1])), # other derivative - ("arr.xˍt", D(sys.arr.x)), - ("arr₊xˍt", D(sys.arr.x)), - ("arr.xˍt(t)", D(sys.arr.x)), - ("arr₊xˍt(t)", D(sys.arr.x)), - ("arr.xˍt[1]", D(sys.arr.x[1])), - ("arr₊xˍt[1]", D(sys.arr.x[1])), - ("arr.xˍt(t)[1]", D(sys.arr.x[1])), - ("arr₊xˍt(t)[1]", D(sys.arr.x[1])), - ("arr.x(t)ˍt", D(sys.arr.x)), - ("arr₊x(t)ˍt", D(sys.arr.x)), - ("arr.x(t)ˍt[1]", D(sys.arr.x[1])), - ("arr₊x(t)ˍt[1]", D(sys.arr.x[1])), + ("arr.xˍ$iv", D(sys.arr.x)), + ("arr₊xˍ$iv", D(sys.arr.x)), + ("arr.xˍ$iv($iv)", D(sys.arr.x)), + ("arr₊xˍ$iv($iv)", D(sys.arr.x)), + ("arr.xˍ$iv[1]", D(sys.arr.x[1])), + ("arr₊xˍ$iv[1]", D(sys.arr.x[1])), + ("arr.xˍ$iv($iv)[1]", D(sys.arr.x[1])), + ("arr₊xˍ$iv($iv)[1]", D(sys.arr.x[1])), + ("arr.x($iv)ˍ$iv", D(sys.arr.x)), + ("arr₊x($iv)ˍ$iv", D(sys.arr.x)), + ("arr.x($iv)ˍ$iv[1]", D(sys.arr.x[1])), + ("arr₊x($iv)ˍ$iv[1]", D(sys.arr.x[1])), # array parameter ("arr.p", sys.arr.p), ("arr₊p", sys.arr.p), ("arr.p[1, 2]", sys.arr.p[1, 2]), ("arr₊p[1, 2]", sys.arr.p[1, 2]) ] - isequal(parse_variable(sys, str), var) + @test isequal(parse_variable(sys, str), var) end end From 2186298df1c35bffe38ea34be5ebcd097441486f Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 20 Nov 2024 18:24:42 +0530 Subject: [PATCH 0377/1337] feat: support `Differential(t)` syntax in `parse_variable` --- src/systems/abstractsystem.jl | 15 ++++++++++++--- test/variable_utils.jl | 10 ++++++++++ 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index 36bd5ac800..37a7a74f2d 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -3292,7 +3292,7 @@ Return the variable in `sys` referred to by its string representation `str`. Roughly supports the following CFG: ``` -varname = "D(" varname ")" | arrvar | maybe_dummy_var +varname = "D(" varname ")" | "Differential(" iv ")(" varname ")" | arrvar | maybe_dummy_var arrvar = maybe_dummy_var "[idxs...]" idxs = int | int "," idxs maybe_dummy_var = namespacedvar | namespacedvar "(" iv ")" | @@ -3310,9 +3310,18 @@ function parse_variable(sys::AbstractSystem, str::AbstractString) # I'd write a regex to validate `str`, but https://xkcd.com/1171/ str = strip(str) derivative_level = 0 - while startswith(str, "D(") && endswith(str, ")") + while ((cond1 = startswith(str, "D(")) || startswith(str, "Differential(")) && endswith(str, ")") + if cond1 + derivative_level += 1 + str = _string_view_inner(str, 2, 1) + continue + end + _tmpstr = _string_view_inner(str, 13, 1) + if !startswith(_tmpstr, "$iv)(") + throw(ArgumentError("Expected differential with respect to independent variable $iv in $str")) + end derivative_level += 1 - str = _string_view_inner(str, 2, 1) + str = _string_view_inner(_tmpstr, length(iv) + 2, 0) end arr_idxs = nothing diff --git a/test/variable_utils.jl b/test/variable_utils.jl index ebbc3c0d3b..ecc2421955 100644 --- a/test/variable_utils.jl +++ b/test/variable_utils.jl @@ -88,6 +88,10 @@ end ("D(😄.x($iv))", D(sys.😄.x)), ("D(😄₊x)", D(sys.😄.x)), ("D(😄₊x($iv))", D(sys.😄.x)), + ("Differential($iv)(😄.x)", D(sys.😄.x)), + ("Differential($iv)(😄.x($iv))", D(sys.😄.x)), + ("Differential($iv)(😄₊x)", D(sys.😄.x)), + ("Differential($iv)(😄₊x($iv))", D(sys.😄.x)), # other derivative ("😄.xˍ$iv", D(sys.😄.x)), ("😄.x($iv)ˍ$iv", D(sys.😄.x)), @@ -113,6 +117,12 @@ end ("D(arr₊x[1])", D(sys.arr.x[1])), ("D(arr.x($iv)[1])", D(sys.arr.x[1])), ("D(arr₊x($iv)[1])", D(sys.arr.x[1])), + ("Differential($iv)(arr.x($iv))", D(sys.arr.x)), + ("Differential($iv)(arr₊x($iv))", D(sys.arr.x)), + ("Differential($iv)(arr.x[1])", D(sys.arr.x[1])), + ("Differential($iv)(arr₊x[1])", D(sys.arr.x[1])), + ("Differential($iv)(arr.x($iv)[1])", D(sys.arr.x[1])), + ("Differential($iv)(arr₊x($iv)[1])", D(sys.arr.x[1])), # other derivative ("arr.xˍ$iv", D(sys.arr.x)), ("arr₊xˍ$iv", D(sys.arr.x)), From 72009993b81bc0e7908e35c20775b0c03589beb1 Mon Sep 17 00:00:00 2001 From: Fredrik Bagge Carlson Date: Wed, 20 Nov 2024 18:39:08 +0100 Subject: [PATCH 0378/1337] rm lgamma --- ext/MTKInfiniteOptExt.jl | 16 +++++++++++----- test/odesystem.jl | 2 +- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/ext/MTKInfiniteOptExt.jl b/ext/MTKInfiniteOptExt.jl index 79463f7e71..3b482a1f03 100644 --- a/ext/MTKInfiniteOptExt.jl +++ b/ext/MTKInfiniteOptExt.jl @@ -3,18 +3,24 @@ import ModelingToolkit import SymbolicUtils import NaNMath import InfiniteOpt +import InfiniteOpt: JuMP, GeneralVariableRef # This file contains method definitions to make it possible to trace through functions generated by MTK using JuMP variables -for ff in [acos, log1p, acosh, log2, asin, lgamma, tan, atanh, cos, log, sin, log10, sqrt] +for ff in [acos, log1p, acosh, log2, asin, tan, atanh, cos, log, sin, log10, sqrt] f = nameof(ff) # These need to be defined so that JuMP can trace through functions built by Symbolics @eval NaNMath.$f(x::GeneralVariableRef) = Base.$f(x) end # JuMP variables and Symbolics variables never compare equal. When tracing through dynamics, a function argument can be either a JuMP variable or A Symbolics variable, it can never be both. -Base.isequal(::SymbolicUtils.BasicSymbolic{Real}, ::InfiniteOpt.GeneralVariableRef) = false -Base.isequal(::InfiniteOpt.GeneralVariableRef, ::SymbolicUtils.BasicSymbolic{Real}) = false -Base.isequal(::SymbolicUtils.Symbolic, ::Union{JuMP.GenericAffExpr, JuMP.GenericQuadExpr, InfiniteOpt.AbstractInfOptExpr}) = false -Base.isequal(::Union{JuMP.GenericAffExpr, JuMP.GenericQuadExpr, InfiniteOpt.AbstractInfOptExpr}, ::SymbolicUtils.Symbolic) = false +function Base.isequal(::SymbolicUtils.Symbolic, + ::Union{JuMP.GenericAffExpr, JuMP.GenericQuadExpr, InfiniteOpt.AbstractInfOptExpr}) + false +end +function Base.isequal( + ::Union{JuMP.GenericAffExpr, JuMP.GenericQuadExpr, InfiniteOpt.AbstractInfOptExpr}, + ::SymbolicUtils.Symbolic) + false +end end diff --git a/test/odesystem.jl b/test/odesystem.jl index 90d1c4c578..02eb80ef85 100644 --- a/test/odesystem.jl +++ b/test/odesystem.jl @@ -1521,6 +1521,6 @@ end @parameters p[1:2] = [1.0, 2.0] @mtkbuild sys = ODESystem([D(x) ~ x, y^2 ~ x + sum(p)], t) prob = DAEProblem(sys, [D(x) => x, D(y) => D(x) / 2y], [], (0.0, 1.0)) - sol = solve(prob, DFBDF(), abstol=1e-8, reltol=1e-8) + sol = solve(prob, DFBDF(), abstol = 1e-8, reltol = 1e-8) @test sol[x]≈sol[y^2 - sum(p)] atol=1e-5 end From e3e20cfeacce1adb67c24a5240df741b813823b6 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 22 Nov 2024 14:26:33 +0530 Subject: [PATCH 0379/1337] build: bump minor version --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index e1bcbef173..dcad7c5e4d 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "ModelingToolkit" uuid = "961ee093-0014-501f-94e3-6117800e7a78" authors = ["Yingbo Ma ", "Chris Rackauckas and contributors"] -version = "9.52.0" +version = "9.53.0" [deps] AbstractTrees = "1520ce14-60c1-5f80-bbc7-55ef81b5835c" From 9733460668a662b41fd4cf87102d08521c1e4f92 Mon Sep 17 00:00:00 2001 From: vyudu Date: Fri, 22 Nov 2024 13:25:30 -0500 Subject: [PATCH 0380/1337] init --- src/systems/diffeqs/abstractodesystem.jl | 110 +++++++++++++++++++++++ 1 file changed, 110 insertions(+) diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index 8d9e0b5381..2579ebafcf 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -466,6 +466,116 @@ function DiffEqBase.ODEFunction{iip, specialize}(sys::AbstractODESystem, initializeprobpmap = initializeprobpmap) end +""" +```julia +SciMLBase.BVProblem{iip}(sys::AbstractODESystem, u0map, tspan, + parammap = DiffEqBase.NullParameters(); + version = nothing, tgrad = false, + jac = true, sparse = true, + simplify = false, + kwargs...) where {iip} +``` + +Create a `BVProblem` from the [`ODESystem`](@ref). The arguments `dvs` and +`ps` are used to set the order of the dependent variable and parameter vectors, +respectively. +""" +function SciMLBase.BVProblem(sys::AbstractODESystem, args...; kwargs...) + BVProblem{true}(sys, args...; kwargs...) +end + +function SciMLBase.BVProblem(sys::AbstractODESystem, + u0map::StaticArray, + args...; + kwargs...) + BVProblem{false, SciMLBase.FullSpecialize}(sys, u0map, args...; kwargs...) +end + +function SciMLBase.BVProblem{true}(sys::AbstractODESystem, args...; kwargs...) + BVProblem{true, SciMLBase.AutoSpecialize}(sys, args...; kwargs...) +end + +function SciMLBase.BVProblem{false}(sys::AbstractODESystem, args...; kwargs...) + BVProblem{false, SciMLBase.FullSpecialize}(sys, args...; kwargs...) +end + +function SciMLBase.BVProblem{iip, specialize}(sys::AbstractODESystem, u0map = [], + tspan = get_tspan(sys), + parammap = DiffEqBase.NullParameters(); + version = nothing, tgrad = false, + jac = true, sparse = true, + sparsity = true, + callback = nothing, + check_length = true, + warn_initialize_determined = true, + eval_expression = false, + eval_module = @__MODULE__, + kwargs...) where {iip, specialize} + + if !iscomplete(sys) + error("A completed system is required. Call `complete` or `structural_simplify` on the system before creating an `BVProblem`") + end + + f, u0, p = process_SciMLProblem(ODEFunction{iip, specialize}, sys, u0map, parammap; + t = tspan !== nothing ? tspan[1] : tspan, + check_length, warn_initialize_determined, eval_expression, eval_module, jac, sparse, sparsity, kwargs...) + + # if jac + # jac_gen = generate_jacobian(sys, dvs, ps; + # simplify = simplify, sparse = sparse, + # expression = Val{true}, + # expression_module = eval_module, + # checkbounds = checkbounds, kwargs...) + # jac_oop, jac_iip = eval_or_rgf.(jac_gen; eval_expression, eval_module) + + # _jac(u, p, t) = jac_oop(u, p, t) + # _jac(J, u, p, t) = jac_iip(J, u, p, t) + # _jac(u, p::Tuple{Vararg{Number}}, t) = jac_oop(u, p, t) + # _jac(J, u, p::Tuple{Vararg{Number}}, t) = jac_iip(J, u, p, t) + # _jac(u, p::Tuple, t) = jac_oop(u, p..., t) + # _jac(J, u, p::Tuple, t) = jac_iip(J, u, p..., t) + # _jac(u, p::MTKParameters, t) = jac_oop(u, p..., t) + # _jac(J, u, p::MTKParameters, t) = jac_iip(J, u, p..., t) + # else + # _jac = nothing + # end + + # jac_prototype = if sparse + # uElType = u0 === nothing ? Float64 : eltype(u0) + # if jac + # similar(calculate_jacobian(sys, sparse = sparse), uElType) + # else + # similar(jacobian_sparsity(sys), uElType) + # end + # else + # nothing + # end + + # f.jac = _jac + # f.jac_prototype = jac_prototype + # f.sparsity = jac ? jacobian_sparsity(sys) : nothing + + cbs = process_events(sys; callback, eval_expression, eval_module, kwargs...) + + kwargs = filter_kwargs(kwargs) + + kwargs1 = (;) + if cbs !== nothing + kwargs1 = merge(kwargs1, (callback = cbs,)) + end + + # Define the boundary conditions + bc = if iip + (residual, u, p, t) -> (residual = u[1] - u0) + else + (u, p, t) -> (u[1] - u0) + end + + return BVProblem{iip}(f, bc, u0, tspan, p; kwargs1..., kwargs...) +end + +get_callback(prob::BVProblem) = prob.kwargs[:callback] + """ ```julia DiffEqBase.DAEFunction{iip}(sys::AbstractODESystem, dvs = unknowns(sys), From d0748e6467285627531071e4377f43c9489cebfa Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 25 Nov 2024 13:04:00 +0530 Subject: [PATCH 0381/1337] fix: fix nonnumeric parameters in `@mtkmodel` --- src/systems/model_parsing.jl | 2 +- test/model_parsing.jl | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/systems/model_parsing.jl b/src/systems/model_parsing.jl index 5b79eaa91b..5402659b1c 100644 --- a/src/systems/model_parsing.jl +++ b/src/systems/model_parsing.jl @@ -923,7 +923,7 @@ function parse_variable_arg(dict, mod, arg, varclass, kwargs, where_types) end push!(varexpr.args, metadata_expr) - return vv isa Num ? name : :($name...), varexpr + return symbolic_type(vv) == ScalarSymbolic() ? name : :($name...), varexpr else return vv end diff --git a/test/model_parsing.jl b/test/model_parsing.jl index fd223800e3..6ab7636b32 100644 --- a/test/model_parsing.jl +++ b/test/model_parsing.jl @@ -979,3 +979,14 @@ end @test MultipleExtend.structure[:extend][1] == [:inmodel, :b, :inmodel_b] @test tosymbol.(parameters(multiple_extend)) == [:b, :inmodel_b₊p, :inmodel₊p] end + +struct CustomStruct end +@testset "Nonnumeric parameters" begin + @mtkmodel MyModel begin + @parameters begin + p::CustomStruct + end + end + @named sys = MyModel(p = CustomStruct()) + @test ModelingToolkit.defaults(sys)[@nonamespace sys.p] == CustomStruct() +end From 472921dc215bd2ecf8785c4ca5a630e9095e42c5 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 25 Nov 2024 14:31:12 +0530 Subject: [PATCH 0382/1337] fix: support callable parameters provided to discretes list of callback --- src/systems/index_cache.jl | 29 +++++++++++++++++++---------- test/index_cache.jl | 26 ++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 10 deletions(-) diff --git a/src/systems/index_cache.jl b/src/systems/index_cache.jl index 1e3490ee69..ab0dd08764 100644 --- a/src/systems/index_cache.jl +++ b/src/systems/index_cache.jl @@ -40,10 +40,12 @@ const TunableIndexMap = Dict{BasicSymbolic, Union{Int, UnitRange{Int}, Base.ReshapedArray{Int, N, UnitRange{Int}} where {N}}} const TimeseriesSetType = Set{Union{ContinuousTimeseries, Int}} +const SymbolicParam = Union{BasicSymbolic, CallWithMetadata} + struct IndexCache unknown_idx::UnknownIndexMap # sym => (bufferidx, idx_in_buffer) - discrete_idx::Dict{BasicSymbolic, DiscreteIndex} + discrete_idx::Dict{SymbolicParam, DiscreteIndex} # sym => (clockidx, idx_in_clockbuffer) callback_to_clocks::Dict{Any, Vector{Int}} tunable_idx::TunableIndexMap @@ -56,13 +58,13 @@ struct IndexCache tunable_buffer_size::BufferTemplate constant_buffer_sizes::Vector{BufferTemplate} nonnumeric_buffer_sizes::Vector{BufferTemplate} - symbol_to_variable::Dict{Symbol, Union{BasicSymbolic, CallWithMetadata}} + symbol_to_variable::Dict{Symbol, SymbolicParam} end function IndexCache(sys::AbstractSystem) unks = solved_unknowns(sys) unk_idxs = UnknownIndexMap() - symbol_to_variable = Dict{Symbol, Union{BasicSymbolic, CallWithMetadata}}() + symbol_to_variable = Dict{Symbol, SymbolicParam}() let idx = 1 for sym in unks @@ -95,7 +97,7 @@ function IndexCache(sys::AbstractSystem) tunable_buffers = Dict{Any, Set{BasicSymbolic}}() constant_buffers = Dict{Any, Set{BasicSymbolic}}() - nonnumeric_buffers = Dict{Any, Set{Union{BasicSymbolic, CallWithMetadata}}}() + nonnumeric_buffers = Dict{Any, Set{SymbolicParam}}() function insert_by_type!(buffers::Dict{Any, S}, sym, ctype) where {S} sym = unwrap(sym) @@ -103,10 +105,10 @@ function IndexCache(sys::AbstractSystem) push!(buf, sym) end - disc_param_callbacks = Dict{BasicSymbolic, Set{Int}}() + disc_param_callbacks = Dict{SymbolicParam, Set{Int}}() events = vcat(continuous_events(sys), discrete_events(sys)) for (i, event) in enumerate(events) - discs = Set{BasicSymbolic}() + discs = Set{SymbolicParam}() affs = affects(event) if !(affs isa AbstractArray) affs = [affs] @@ -130,26 +132,32 @@ function IndexCache(sys::AbstractSystem) isequal(only(arguments(sym)), get_iv(sys)) clocks = get!(() -> Set{Int}(), disc_param_callbacks, sym) push!(clocks, i) - else + elseif is_variable_floatingpoint(sym) insert_by_type!(constant_buffers, sym, symtype(sym)) + else + stype = symtype(sym) + if stype <: FnType + stype = fntype_to_function_type(stype) + end + insert_by_type!(nonnumeric_buffers, sym, stype) end end end clock_partitions = unique(collect(values(disc_param_callbacks))) disc_symtypes = unique(symtype.(keys(disc_param_callbacks))) disc_symtype_idx = Dict(disc_symtypes .=> eachindex(disc_symtypes)) - disc_syms_by_symtype = [BasicSymbolic[] for _ in disc_symtypes] + disc_syms_by_symtype = [SymbolicParam[] for _ in disc_symtypes] for sym in keys(disc_param_callbacks) push!(disc_syms_by_symtype[disc_symtype_idx[symtype(sym)]], sym) end - disc_syms_by_symtype_by_partition = [Vector{BasicSymbolic}[] for _ in disc_symtypes] + disc_syms_by_symtype_by_partition = [Vector{SymbolicParam}[] for _ in disc_symtypes] for (i, buffer) in enumerate(disc_syms_by_symtype) for partition in clock_partitions push!(disc_syms_by_symtype_by_partition[i], [sym for sym in buffer if disc_param_callbacks[sym] == partition]) end end - disc_idxs = Dict{BasicSymbolic, DiscreteIndex}() + disc_idxs = Dict{SymbolicParam, DiscreteIndex}() callback_to_clocks = Dict{ Union{SymbolicContinuousCallback, SymbolicDiscreteCallback}, Set{Int}}() for (typei, disc_syms_by_partition) in enumerate(disc_syms_by_symtype_by_partition) @@ -191,6 +199,7 @@ function IndexCache(sys::AbstractSystem) end haskey(disc_idxs, p) && continue haskey(constant_buffers, ctype) && p in constant_buffers[ctype] && continue + haskey(nonnumeric_buffers, ctype) && p in nonnumeric_buffers[ctype] && continue insert_by_type!( if ctype <: Real || ctype <: AbstractArray{<:Real} if istunable(p, true) && Symbolics.shape(p) != Symbolics.Unknown() && diff --git a/test/index_cache.jl b/test/index_cache.jl index c479075797..455203d759 100644 --- a/test/index_cache.jl +++ b/test/index_cache.jl @@ -92,3 +92,29 @@ end reorder_dimension_by_tunables!(dst, sys, src, [r, q, p]; dim = 2) @test dst ≈ stack([vcat(4ones(4), 3ones(3), 1.0) for i in 1:5]; dims = 1) end + +mutable struct ParamTest + y::Any +end +(pt::ParamTest)(x) = pt.y - x +@testset "Issue#3215: Callable discrete parameter" begin + function update_affect!(integ, u, p, ctx) + integ.p[p.p_1].y = integ.t + end + + tp1 = typeof(ParamTest(1)) + @parameters (p_1::tp1)(..) = ParamTest(1) + @variables x(ModelingToolkit.t_nounits) = 0 + + event1 = [1.0, 2, 3] => (update_affect!, [], [p_1], [p_1], nothing) + + @named sys = ODESystem([ + ModelingToolkit.D_nounits(x) ~ p_1(x) + ], + ModelingToolkit.t_nounits; + discrete_events = [event1] + ) + ss = @test_nowarn complete(sys) + @test length(parameters(ss)) == 1 + @test !is_timeseries_parameter(ss, p_1) +end From 585de359502db53e50609d656e97d3696e2d6300 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 25 Nov 2024 15:05:31 +0530 Subject: [PATCH 0383/1337] fix: fix SDEs with noise dependent on observed variables --- .../symbolics_tearing.jl | 8 ++++++++ src/systems/systems.jl | 2 ++ test/sdesystem.jl | 19 +++++++++++++++++++ 3 files changed, 29 insertions(+) diff --git a/src/structural_transformation/symbolics_tearing.jl b/src/structural_transformation/symbolics_tearing.jl index a854acb9b1..8161a9572b 100644 --- a/src/structural_transformation/symbolics_tearing.jl +++ b/src/structural_transformation/symbolics_tearing.jl @@ -88,6 +88,14 @@ function tearing_sub(expr, dict, s) s ? simplify(expr) : expr end +function tearing_substitute_expr(sys::AbstractSystem, expr; simplify = false) + empty_substitutions(sys) && return expr + substitutions = get_substitutions(sys) + @unpack subs = substitutions + solved = Dict(eq.lhs => eq.rhs for eq in subs) + return tearing_sub(expr, solved, simplify) +end + """ $(TYPEDSIGNATURES) diff --git a/src/systems/systems.jl b/src/systems/systems.jl index 848980605f..4205a4d207 100644 --- a/src/systems/systems.jl +++ b/src/systems/systems.jl @@ -152,6 +152,8 @@ function __structural_simplify(sys::AbstractSystem, io = nothing; simplify = fal noise_eqs = sorted_g_rows is_scalar_noise = false end + + noise_eqs = StructuralTransformations.tearing_substitute_expr(ode_sys, noise_eqs) return SDESystem(full_equations(ode_sys), noise_eqs, get_iv(ode_sys), unknowns(ode_sys), parameters(ode_sys); name = nameof(ode_sys), is_scalar_noise) diff --git a/test/sdesystem.jl b/test/sdesystem.jl index c258a4142b..78d7a0418b 100644 --- a/test/sdesystem.jl +++ b/test/sdesystem.jl @@ -780,3 +780,22 @@ end prob = @test_nowarn SDEProblem(sys, nothing, (0.0, 1.0)) @test_nowarn solve(prob, ImplicitEM()) end + +@testset "Issue#3212: Noise dependent on observed" begin + sts = @variables begin + x(t) = 1.0 + input(t) + [input = true] + end + ps = @parameters a = 2 + @brownian η + + eqs = [D(x) ~ -a * x + (input + 1) * η + input ~ 0.0] + + sys = System(eqs, t, sts, ps; name = :name) + sys = structural_simplify(sys) + @test ModelingToolkit.get_noiseeqs(sys) ≈ [1.0] + prob = SDEProblem(sys, [], (0.0, 1.0), []) + @test_nowarn solve(prob, RKMil()) +end From 398849613e4fd4bc7f9bcdc0960d54c2bb2c270e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Miclu=C8=9Ba-C=C3=A2mpeanu?= Date: Mon, 25 Nov 2024 13:06:07 +0200 Subject: [PATCH 0384/1337] fix: fix InitializationProblems that have vectors in u0map --- src/systems/diffeqs/abstractodesystem.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index 1dc2198cc2..18aac952f4 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -1356,6 +1356,7 @@ function InitializationProblem{iip, specialize}(sys::AbstractODESystem, u0T = promote_type(u0T, typeof(fullmap[eq.lhs])) end if u0T != Union{} + u0T = eltype(u0T) u0map = Dict(k => if symbolic_type(v) == NotSymbolic() && !is_array_of_symbolics(v) v isa AbstractArray ? u0T.(v) : u0T(v) else From d4e0d0f1634d6b322ef0cfa4ee110e0f9d7cd693 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 25 Nov 2024 12:37:39 +0530 Subject: [PATCH 0385/1337] feat: enable `structural_simplify` to automatically handle non-fully-determined systems Uses the result of `check_consistency` --- src/structural_transformation/utils.jl | 58 +++++++++++++++++++------- src/systems/systemstructure.jl | 9 +++- 2 files changed, 49 insertions(+), 18 deletions(-) diff --git a/src/structural_transformation/utils.jl b/src/structural_transformation/utils.jl index 8b2ac1f69c..405a3ebed3 100644 --- a/src/structural_transformation/utils.jl +++ b/src/structural_transformation/utils.jl @@ -58,7 +58,41 @@ end ### ### Structural check ### -function check_consistency(state::TransformationState, orig_inputs) + +""" + $(TYPEDSIGNATURES) + +Check if the `state` represents a singular system, and return the unmatched variables. +""" +function singular_check(state::TransformationState) + @unpack graph, var_to_diff = state.structure + fullvars = get_fullvars(state) + # This is defined to check if Pantelides algorithm terminates. For more + # details, check the equation (15) of the original paper. + extended_graph = (@set graph.fadjlist = Vector{Int}[graph.fadjlist; + map(collect, edges(var_to_diff))]) + extended_var_eq_matching = maximal_matching(extended_graph) + + nvars = ndsts(graph) + unassigned_var = [] + for (vj, eq) in enumerate(extended_var_eq_matching) + vj > nvars && break + if eq === unassigned && !isempty(𝑑neighbors(graph, vj)) + push!(unassigned_var, fullvars[vj]) + end + end + return unassigned_var +end + +""" + $(TYPEDSIGNATURES) + +Check the consistency of `state`, given the inputs `orig_inputs`. If `nothrow == false`, +throws an error if the system is under-/over-determined or singular. In this case, if the +function returns it will return `true`. If `nothrow == true`, it will return `false` +instead of throwing an error. The singular case will print a warning. +""" +function check_consistency(state::TransformationState, orig_inputs; nothrow = false) fullvars = get_fullvars(state) neqs = n_concrete_eqs(state) @unpack graph, var_to_diff = state.structure @@ -72,6 +106,7 @@ function check_consistency(state::TransformationState, orig_inputs) is_balanced = n_highest_vars == neqs if neqs > 0 && !is_balanced + nothrow && return false varwhitelist = var_to_diff .== nothing var_eq_matching = maximal_matching(graph, eq -> true, v -> varwhitelist[v]) # not assigned # Just use `error_reporting` to do conditional @@ -85,20 +120,7 @@ function check_consistency(state::TransformationState, orig_inputs) error_reporting(state, bad_idxs, n_highest_vars, iseqs, orig_inputs) end - # This is defined to check if Pantelides algorithm terminates. For more - # details, check the equation (15) of the original paper. - extended_graph = (@set graph.fadjlist = Vector{Int}[graph.fadjlist; - map(collect, edges(var_to_diff))]) - extended_var_eq_matching = maximal_matching(extended_graph) - - nvars = ndsts(graph) - unassigned_var = [] - for (vj, eq) in enumerate(extended_var_eq_matching) - vj > nvars && break - if eq === unassigned && !isempty(𝑑neighbors(graph, vj)) - push!(unassigned_var, fullvars[vj]) - end - end + unassigned_var = singular_check(state) if !isempty(unassigned_var) || !is_balanced io = IOBuffer() @@ -107,10 +129,14 @@ function check_consistency(state::TransformationState, orig_inputs) errmsg = "The system is structurally singular! " * "Here are the problematic variables: \n" * unassigned_var_str + if nothrow + @warn errmsg + return false + end throw(InvalidSystemException(errmsg)) end - return nothing + return true end ### diff --git a/src/systems/systemstructure.jl b/src/systems/systemstructure.jl index 71bdf6fe30..1c0ad8b8ae 100644 --- a/src/systems/systemstructure.jl +++ b/src/systems/systemstructure.jl @@ -677,7 +677,11 @@ function _structural_simplify!(state::TearingState, io; simplify = false, check_consistency = true, fully_determined = true, warn_initialize_determined = false, dummy_derivative = true, kwargs...) - check_consistency &= fully_determined + if fully_determined isa Bool + check_consistency &= fully_determined + else + check_consistency = true + end has_io = io !== nothing orig_inputs = Set() if has_io @@ -690,7 +694,8 @@ function _structural_simplify!(state::TearingState, io; simplify = false, end sys, mm = ModelingToolkit.alias_elimination!(state; kwargs...) if check_consistency - ModelingToolkit.check_consistency(state, orig_inputs) + fully_determined = ModelingToolkit.check_consistency( + state, orig_inputs; nothrow = fully_determined === nothing) end if fully_determined && dummy_derivative sys = ModelingToolkit.dummy_derivative( From beb16ec624e1f68a834a0aff7e7731c489e6fabc Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 25 Nov 2024 12:38:02 +0530 Subject: [PATCH 0386/1337] feat: simplify initialization system with `fully_determined=nothing` by default --- src/systems/diffeqs/abstractodesystem.jl | 6 +++++- src/systems/problem_utils.jl | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index 1dc2198cc2..4019a868d8 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -1295,7 +1295,7 @@ function InitializationProblem{iip, specialize}(sys::AbstractODESystem, check_length = true, warn_initialize_determined = true, initialization_eqs = [], - fully_determined = false, + fully_determined = nothing, check_units = true, kwargs...) where {iip, specialize} if !iscomplete(sys) @@ -1313,6 +1313,10 @@ function InitializationProblem{iip, specialize}(sys::AbstractODESystem, sys; u0map, initialization_eqs, check_units, pmap = parammap); fully_determined) end + if !isempty(StructuralTransformations.singular_check(get_tearing_state(isys))) + @warn "Since the initialization system is singular, the guess values may significantly affect the initial values of the ODE" + end + uninit = setdiff(unknowns(sys), [unknowns(isys); getfield.(observed(isys), :lhs)]) # TODO: throw on uninitialized arrays diff --git a/src/systems/problem_utils.jl b/src/systems/problem_utils.jl index ff1d69e2bc..1cf110df13 100644 --- a/src/systems/problem_utils.jl +++ b/src/systems/problem_utils.jl @@ -498,7 +498,7 @@ function process_SciMLProblem( constructor, sys::AbstractSystem, u0map, pmap; build_initializeprob = true, implicit_dae = false, t = nothing, guesses = AnyDict(), warn_initialize_determined = true, initialization_eqs = [], - eval_expression = false, eval_module = @__MODULE__, fully_determined = false, + eval_expression = false, eval_module = @__MODULE__, fully_determined = nothing, check_initialization_units = false, tofloat = true, use_union = false, u0_constructor = identity, du0map = nothing, check_length = true, symbolic_u0 = false, warn_cyclic_dependency = false, From 6632a2995ad99e129622da4c4910e0a36d2c93da Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 25 Nov 2024 12:38:13 +0530 Subject: [PATCH 0387/1337] test: test that singular initialization systems throw a warning --- test/initializationsystem.jl | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/test/initializationsystem.jl b/test/initializationsystem.jl index f39ec3c2f2..6d3e677c1e 100644 --- a/test/initializationsystem.jl +++ b/test/initializationsystem.jl @@ -947,3 +947,14 @@ end @test_nowarn remake(prob, p = prob.p) end + +@testset "Singular initialization prints a warning" begin + @parameters g + @variables x(t) y(t) [state_priority = 10] λ(t) + eqs = [D(D(x)) ~ λ * x + D(D(y)) ~ λ * y - g + x^2 + y^2 ~ 1] + @mtkbuild pend = ODESystem(eqs, t) + @test_warn ["structurally singular", "initialization", "guess"] ODEProblem( + pend, [x => 1, y => 0], (0.0, 1.5), [g => 1], guesses = [λ => 1]) +end From 1892f0a10900e124587ab034e5842a1f4ec565da Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 25 Nov 2024 12:43:28 +0530 Subject: [PATCH 0388/1337] refactor: format --- src/systems/abstractsystem.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index 497b5e6476..3a9bbd33e1 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -3335,7 +3335,8 @@ function parse_variable(sys::AbstractSystem, str::AbstractString) # I'd write a regex to validate `str`, but https://xkcd.com/1171/ str = strip(str) derivative_level = 0 - while ((cond1 = startswith(str, "D(")) || startswith(str, "Differential(")) && endswith(str, ")") + while ((cond1 = startswith(str, "D(")) || startswith(str, "Differential(")) && + endswith(str, ")") if cond1 derivative_level += 1 str = _string_view_inner(str, 2, 1) From 2b7a6b695a33a09634a6039f27eabf414f2c7530 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 25 Nov 2024 18:03:16 +0530 Subject: [PATCH 0389/1337] refactor: only display singular warning if `warn_initialize_determined` --- src/structural_transformation/utils.jl | 7 +++---- src/systems/diffeqs/abstractodesystem.jl | 13 +++++++++++-- test/initializationsystem.jl | 2 +- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/structural_transformation/utils.jl b/src/structural_transformation/utils.jl index 405a3ebed3..bd24a1d017 100644 --- a/src/structural_transformation/utils.jl +++ b/src/structural_transformation/utils.jl @@ -123,16 +123,15 @@ function check_consistency(state::TransformationState, orig_inputs; nothrow = fa unassigned_var = singular_check(state) if !isempty(unassigned_var) || !is_balanced + if nothrow + return false + end io = IOBuffer() Base.print_array(io, unassigned_var) unassigned_var_str = String(take!(io)) errmsg = "The system is structurally singular! " * "Here are the problematic variables: \n" * unassigned_var_str - if nothrow - @warn errmsg - return false - end throw(InvalidSystemException(errmsg)) end diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index 4019a868d8..5c13572aef 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -1313,8 +1313,17 @@ function InitializationProblem{iip, specialize}(sys::AbstractODESystem, sys; u0map, initialization_eqs, check_units, pmap = parammap); fully_determined) end - if !isempty(StructuralTransformations.singular_check(get_tearing_state(isys))) - @warn "Since the initialization system is singular, the guess values may significantly affect the initial values of the ODE" + ts = get_tearing_state(isys) + if warn_initialize_determined && + (unassigned_vars = StructuralTransformations.singular_check(ts); !isempty(unassigned_vars)) + errmsg = """ + The initialization system is structurally singular. Guess values may \ + significantly affect the initial values of the ODE. The problematic variables \ + are $unassigned_vars. + + Note that the identification of problematic variables is a best-effort heuristic. + """ + @warn errmsg end uninit = setdiff(unknowns(sys), [unknowns(isys); getfield.(observed(isys), :lhs)]) diff --git a/test/initializationsystem.jl b/test/initializationsystem.jl index 6d3e677c1e..920a73a723 100644 --- a/test/initializationsystem.jl +++ b/test/initializationsystem.jl @@ -955,6 +955,6 @@ end D(D(y)) ~ λ * y - g x^2 + y^2 ~ 1] @mtkbuild pend = ODESystem(eqs, t) - @test_warn ["structurally singular", "initialization", "guess"] ODEProblem( + @test_warn ["structurally singular", "initialization", "Guess", "heuristic"] ODEProblem( pend, [x => 1, y => 0], (0.0, 1.5), [g => 1], guesses = [λ => 1]) end From 9c7fbc7fda33b687553f51549af2e66f536cc407 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 22 Nov 2024 14:23:43 +0530 Subject: [PATCH 0390/1337] test: make initializaton tests runnable from OrdinaryDiffEq --- test/runtests.jl | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/runtests.jl b/test/runtests.jl index 192ead935c..0ba23fc4f7 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -33,7 +33,6 @@ end @safetestset "Dynamic Quantities Test" include("dq_units.jl") @safetestset "Unitful Quantities Test" include("units.jl") @safetestset "Mass Matrix Test" include("mass_matrix.jl") - @safetestset "InitializationSystem Test" include("initializationsystem.jl") @safetestset "Guess Propagation" include("guess_propagation.jl") @safetestset "Hierarchical Initialization Equations" include("hierarchical_initialization_eqs.jl") @safetestset "Reduction Test" include("reduction.jl") @@ -64,6 +63,10 @@ end end end + if GROUP == "All" || GROUP == "InterfaceI" || GROUP == "Initialization" + @safetestset "InitializationSystem Test" include("initializationsystem.jl") + end + if GROUP == "All" || GROUP == "InterfaceII" @testset "InterfaceII" begin @safetestset "IndexCache Test" include("index_cache.jl") From 7eba3438d96d3a0a1c275edd3f61c47e49b2788f Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 26 Nov 2024 17:56:51 +0530 Subject: [PATCH 0391/1337] test: also move guess propagation, hierarchical initialization equations and initial values testsets --- test/runtests.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/runtests.jl b/test/runtests.jl index 0ba23fc4f7..4f6c20e04b 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -33,8 +33,6 @@ end @safetestset "Dynamic Quantities Test" include("dq_units.jl") @safetestset "Unitful Quantities Test" include("units.jl") @safetestset "Mass Matrix Test" include("mass_matrix.jl") - @safetestset "Guess Propagation" include("guess_propagation.jl") - @safetestset "Hierarchical Initialization Equations" include("hierarchical_initialization_eqs.jl") @safetestset "Reduction Test" include("reduction.jl") @safetestset "Split Parameters Test" include("split_parameters.jl") @safetestset "StaticArrays Test" include("static_arrays.jl") @@ -57,14 +55,16 @@ end @safetestset "Constants Test" include("constants.jl") @safetestset "Parameter Dependency Test" include("parameter_dependencies.jl") @safetestset "Generate Custom Function Test" include("generate_custom_function.jl") - @safetestset "Initial Values Test" include("initial_values.jl") @safetestset "Equation Type Accessors Test" include("equation_type_accessors.jl") @safetestset "Equations with complex values" include("complex.jl") end end if GROUP == "All" || GROUP == "InterfaceI" || GROUP == "Initialization" + @safetestset "Guess Propagation" include("guess_propagation.jl") + @safetestset "Hierarchical Initialization Equations" include("hierarchical_initialization_eqs.jl") @safetestset "InitializationSystem Test" include("initializationsystem.jl") + @safetestset "Initial Values Test" include("initial_values.jl") end if GROUP == "All" || GROUP == "InterfaceII" From d73342d458fe6ca0b85fbfb6c6f8cb274dfd5ba0 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 27 Nov 2024 11:27:37 +0530 Subject: [PATCH 0392/1337] test: run `Initialization` as its own test group --- .github/workflows/Tests.yml | 1 + test/runtests.jl | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/Tests.yml b/.github/workflows/Tests.yml index 609ab2fb3d..a95f19a085 100644 --- a/.github/workflows/Tests.yml +++ b/.github/workflows/Tests.yml @@ -32,6 +32,7 @@ jobs: group: - InterfaceI - InterfaceII + - Initialization - SymbolicIndexingInterface - Extended - Extensions diff --git a/test/runtests.jl b/test/runtests.jl index 4f6c20e04b..06a167bdd9 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -60,7 +60,7 @@ end end end - if GROUP == "All" || GROUP == "InterfaceI" || GROUP == "Initialization" + if GROUP == "All" || GROUP == "Initialization" @safetestset "Guess Propagation" include("guess_propagation.jl") @safetestset "Hierarchical Initialization Equations" include("hierarchical_initialization_eqs.jl") @safetestset "InitializationSystem Test" include("initializationsystem.jl") From 237cb82ef42252c6619179befa918ad580647ee9 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 27 Nov 2024 13:10:31 +0530 Subject: [PATCH 0393/1337] refactor: format --- test/runtests.jl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/runtests.jl b/test/runtests.jl index 06a167bdd9..244393d351 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -61,10 +61,10 @@ end end if GROUP == "All" || GROUP == "Initialization" - @safetestset "Guess Propagation" include("guess_propagation.jl") - @safetestset "Hierarchical Initialization Equations" include("hierarchical_initialization_eqs.jl") - @safetestset "InitializationSystem Test" include("initializationsystem.jl") - @safetestset "Initial Values Test" include("initial_values.jl") + @safetestset "Guess Propagation" include("guess_propagation.jl") + @safetestset "Hierarchical Initialization Equations" include("hierarchical_initialization_eqs.jl") + @safetestset "InitializationSystem Test" include("initializationsystem.jl") + @safetestset "Initial Values Test" include("initial_values.jl") end if GROUP == "All" || GROUP == "InterfaceII" From 5b702cab0620e79b92637929bb773ef2a2805a21 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 21 Nov 2024 17:37:24 +0530 Subject: [PATCH 0394/1337] fix: propagate `initializeprobpmap` and `update_initializeprob!` to `DAEFunction` --- src/systems/diffeqs/abstractodesystem.jl | 6 +++++- test/initializationsystem.jl | 17 +++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index b6d750c621..cbf835f48d 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -498,6 +498,8 @@ function DiffEqBase.DAEFunction{iip}(sys::AbstractODESystem, dvs = unknowns(sys) checkbounds = false, initializeprob = nothing, initializeprobmap = nothing, + initializeprobpmap = nothing, + update_initializeprob! = nothing, kwargs...) where {iip} if !iscomplete(sys) error("A completed system is required. Call `complete` or `structural_simplify` on the system before creating a `DAEFunction`") @@ -551,7 +553,9 @@ function DiffEqBase.DAEFunction{iip}(sys::AbstractODESystem, dvs = unknowns(sys) jac_prototype = jac_prototype, observed = observedfun, initializeprob = initializeprob, - initializeprobmap = initializeprobmap) + initializeprobmap = initializeprobmap, + initializeprobpmap = initializeprobpmap, + update_initializeprob! = update_initializeprob!) end function DiffEqBase.DDEFunction(sys::AbstractODESystem, args...; kwargs...) diff --git a/test/initializationsystem.jl b/test/initializationsystem.jl index 920a73a723..770f9f12bd 100644 --- a/test/initializationsystem.jl +++ b/test/initializationsystem.jl @@ -958,3 +958,20 @@ end @test_warn ["structurally singular", "initialization", "Guess", "heuristic"] ODEProblem( pend, [x => 1, y => 0], (0.0, 1.5), [g => 1], guesses = [λ => 1]) end + +@testset "DAEProblem initialization" begin + @variables x(t) [guess = 1.0] y(t) [guess = 1.0] + @parameters p=missing [guess = 1.0] q=missing [guess = 1.0] + @mtkbuild sys = ODESystem( + [D(x) ~ p * y + q * t, x^3 + y^3 ~ 5], t; initialization_eqs = [p^2 + q^3 ~ 3]) + + # FIXME: solve for du0 + prob = DAEProblem( + sys, [D(x) => cbrt(4), D(y) => -1 / cbrt(4)], [x => 1.0], (0.0, 1.0), [p => 1.0]) + + integ = init(prob, DImplicitEuler()) + @test integ[x] ≈ 1.0 + @test integ[y]≈cbrt(4) rtol=1e-6 + @test integ.ps[p] ≈ 1.0 + @test integ.ps[q]≈cbrt(2) rtol=1e-6 +end From 57ea5fc3ffae436d516624a285b928b0d226be51 Mon Sep 17 00:00:00 2001 From: Andrew Leonard Date: Wed, 27 Nov 2024 08:47:15 -0500 Subject: [PATCH 0395/1337] add checks kwarg to OptimizationProblem, use in ConstraintSystem --- src/systems/optimization/optimizationsystem.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/systems/optimization/optimizationsystem.jl b/src/systems/optimization/optimizationsystem.jl index 39c5c1e7a7..176a2bdfed 100644 --- a/src/systems/optimization/optimizationsystem.jl +++ b/src/systems/optimization/optimizationsystem.jl @@ -284,6 +284,7 @@ function DiffEqBase.OptimizationProblem{iip}(sys::OptimizationSystem, u0map, linenumbers = true, parallel = SerialForm(), eval_expression = false, eval_module = @__MODULE__, use_union = false, + checks = true, kwargs...) where {iip} if !iscomplete(sys) error("A completed `OptimizationSystem` is required. Call `complete` or `structural_simplify` on the system before creating a `OptimizationProblem`") @@ -393,7 +394,7 @@ function DiffEqBase.OptimizationProblem{iip}(sys::OptimizationSystem, u0map, observedfun = ObservedFunctionCache(sys; eval_expression, eval_module) if length(cstr) > 0 - @named cons_sys = ConstraintsSystem(cstr, dvs, ps) + @named cons_sys = ConstraintsSystem(cstr, dvs, ps; checks) cons_sys = complete(cons_sys) cons, lcons_, ucons_ = generate_function(cons_sys, checkbounds = checkbounds, linenumbers = linenumbers, From d9943261b68954456de1de36865dc05d39722cb7 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 27 Nov 2024 20:05:45 +0530 Subject: [PATCH 0396/1337] fix: fix constraint function not wrapped in `OptimizationProblem` --- src/systems/optimization/optimizationsystem.jl | 9 +++++++-- test/optimizationsystem.jl | 8 ++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/systems/optimization/optimizationsystem.jl b/src/systems/optimization/optimizationsystem.jl index 176a2bdfed..801e7b05f3 100644 --- a/src/systems/optimization/optimizationsystem.jl +++ b/src/systems/optimization/optimizationsystem.jl @@ -399,7 +399,12 @@ function DiffEqBase.OptimizationProblem{iip}(sys::OptimizationSystem, u0map, cons, lcons_, ucons_ = generate_function(cons_sys, checkbounds = checkbounds, linenumbers = linenumbers, expression = Val{true}) - cons = eval_or_rgf.(cons; eval_expression, eval_module) + cons = let (cons_oop, cons_iip) = eval_or_rgf.(cons; eval_expression, eval_module) + _cons(u, p) = cons_oop(u, p) + _cons(resid, u, p) = cons_iip(resid, u, p) + _cons(u, p::MTKParameters) = cons_oop(u, p...) + _cons(resid, u, p::MTKParameters) = cons_iip(resid, u, p...) + end if cons_j _cons_j = let (cons_jac_oop, cons_jac_iip) = eval_or_rgf.( generate_jacobian(cons_sys; @@ -465,7 +470,7 @@ function DiffEqBase.OptimizationProblem{iip}(sys::OptimizationSystem, u0map, grad = _grad, hess = _hess, hess_prototype = hess_prototype, - cons = cons[2], + cons = cons, cons_j = _cons_j, cons_h = _cons_h, cons_jac_prototype = cons_jac_prototype, diff --git a/test/optimizationsystem.jl b/test/optimizationsystem.jl index a8e2be936d..23d07dcb85 100644 --- a/test/optimizationsystem.jl +++ b/test/optimizationsystem.jl @@ -368,3 +368,11 @@ end @test is_variable(sys, x[2]) @test is_variable(sys, x[3]) end + +@testset "Constraints work with nonnumeric parameters" begin + @variables x + @parameters p f(::Real) + @mtkbuild sys = OptimizationSystem(x^2 + f(x) * p, [x], [f, p]; constraints = [2.0 ≲ f(x) + p]) + prob = OptimizationProblem(sys, [x => 1.0], [p => 1.0, f => (x -> 2x)]) + @test abs(prob.f.cons(prob.u0, prob.p)[1]) ≈ 1.0 +end From d6add0eefe9f4d1232762e975cd023464c49cc01 Mon Sep 17 00:00:00 2001 From: Andrew Leonard Date: Wed, 27 Nov 2024 10:45:30 -0500 Subject: [PATCH 0397/1337] formatting --- test/optimizationsystem.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/optimizationsystem.jl b/test/optimizationsystem.jl index 23d07dcb85..bb59fb09d9 100644 --- a/test/optimizationsystem.jl +++ b/test/optimizationsystem.jl @@ -372,7 +372,8 @@ end @testset "Constraints work with nonnumeric parameters" begin @variables x @parameters p f(::Real) - @mtkbuild sys = OptimizationSystem(x^2 + f(x) * p, [x], [f, p]; constraints = [2.0 ≲ f(x) + p]) + @mtkbuild sys = OptimizationSystem( + x^2 + f(x) * p, [x], [f, p]; constraints = [2.0 ≲ f(x) + p]) prob = OptimizationProblem(sys, [x => 1.0], [p => 1.0, f => (x -> 2x)]) @test abs(prob.f.cons(prob.u0, prob.p)[1]) ≈ 1.0 end From f20c7941f18b88cd4b7d50754a83e414bfba6eff Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 28 Nov 2024 12:41:19 +0530 Subject: [PATCH 0398/1337] build: bump minor version --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index dcad7c5e4d..1cb204a41b 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "ModelingToolkit" uuid = "961ee093-0014-501f-94e3-6117800e7a78" authors = ["Yingbo Ma ", "Chris Rackauckas and contributors"] -version = "9.53.0" +version = "9.54.0" [deps] AbstractTrees = "1520ce14-60c1-5f80-bbc7-55ef81b5835c" From 28858155db1b668ea82547fc505da68131854f67 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 28 Nov 2024 13:20:27 +0530 Subject: [PATCH 0399/1337] feat: add automatic variable discovery for `OptimizationSystem` --- .../optimization/optimizationsystem.jl | 27 +++++++++++++++++++ src/utils.jl | 13 ++++++++- test/optimizationsystem.jl | 11 ++++++++ 3 files changed, 50 insertions(+), 1 deletion(-) diff --git a/src/systems/optimization/optimizationsystem.jl b/src/systems/optimization/optimizationsystem.jl index 801e7b05f3..3425216b5f 100644 --- a/src/systems/optimization/optimizationsystem.jl +++ b/src/systems/optimization/optimizationsystem.jl @@ -144,6 +144,33 @@ function OptimizationSystem(op, unknowns, ps; checks = checks) end +function OptimizationSystem(objective; constraints = [], kwargs...) + allunknowns = OrderedSet() + ps = OrderedSet() + collect_vars!(allunknowns, ps, objective, nothing) + for cons in constraints + collect_vars!(allunknowns, ps, cons, nothing) + end + for ssys in get(kwargs, :systems, OptimizationSystem[]) + collect_scoped_vars!(allunknowns, ps, ssys, nothing) + end + new_ps = OrderedSet() + for p in ps + if iscall(p) && operation(p) === getindex + par = arguments(p)[begin] + if Symbolics.shape(Symbolics.unwrap(par)) !== Symbolics.Unknown() && + all(par[i] in ps for i in eachindex(par)) + push!(new_ps, par) + else + push!(new_ps, p) + end + else + push!(new_ps, p) + end + end + return OptimizationSystem(objective, collect(allunknowns), collect(new_ps); constraints, kwargs...) +end + function flatten(sys::OptimizationSystem) systems = get_systems(sys) isempty(systems) && return sys diff --git a/src/utils.jl b/src/utils.jl index 830ec98e44..1555cd624e 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -516,6 +516,15 @@ function collect_scoped_vars!(unknowns, parameters, sys, iv; depth = 1, op = Dif end end end + if has_constraints(sys) + for eq in get_constraints(sys) + eqtype_supports_collect_vars(eq) || continue + collect_vars!(unknowns, parameters, eq, iv; depth, op) + end + end + if has_op(sys) + collect_vars!(unknowns, parameters, get_op(sys), iv; depth, op) + end newdepth = depth == -1 ? depth : depth + 1 for ssys in get_systems(sys) collect_scoped_vars!(unknowns, parameters, ssys, iv; depth = newdepth, op) @@ -544,9 +553,10 @@ Can be dispatched by higher-level libraries to indicate support. """ eqtype_supports_collect_vars(eq) = false eqtype_supports_collect_vars(eq::Equation) = true +eqtype_supports_collect_vars(eq::Inequality) = true eqtype_supports_collect_vars(eq::Pair) = true -function collect_vars!(unknowns, parameters, eq::Equation, iv; +function collect_vars!(unknowns, parameters, eq::Union{Equation, Inequality}, iv; depth = 0, op = Differential) collect_vars!(unknowns, parameters, eq.lhs, iv; depth, op) collect_vars!(unknowns, parameters, eq.rhs, iv; depth, op) @@ -559,6 +569,7 @@ function collect_vars!(unknowns, parameters, p::Pair, iv; depth = 0, op = Differ return nothing end + function collect_var!(unknowns, parameters, var, iv; depth = 0) isequal(var, iv) && return nothing check_scope_depth(getmetadata(var, SymScope, LocalScope()), depth) || return nothing diff --git a/test/optimizationsystem.jl b/test/optimizationsystem.jl index bb59fb09d9..c20613441a 100644 --- a/test/optimizationsystem.jl +++ b/test/optimizationsystem.jl @@ -377,3 +377,14 @@ end prob = OptimizationProblem(sys, [x => 1.0], [p => 1.0, f => (x -> 2x)]) @test abs(prob.f.cons(prob.u0, prob.p)[1]) ≈ 1.0 end + +@testset "Variable discovery" begin + @variables x1 x2 + @parameters p1 p2 + @named sys1 = OptimizationSystem(x1^2; constraints = [p1 * x1 ≲ 2.0]) + @named sys2 = OptimizationSystem(x2^2; constraints = [p2 * x2 ≲ 2.0], systems = [sys1]) + @test isequal(only(unknowns(sys1)), x1) + @test isequal(only(parameters(sys1)), p1) + @test all(y -> any(x -> isequal(x, y), unknowns(sys2)), [x2, sys1.x1]) + @test all(y -> any(x -> isequal(x, y), parameters(sys2)), [p2, sys1.p1]) +end From 6729cdaa202e272e922465f2d44f6dbca18870c3 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 28 Nov 2024 15:16:17 +0530 Subject: [PATCH 0400/1337] fix: retain observed equations after `structural_simplify` of `SDESystem` --- src/systems/systems.jl | 2 +- test/sdesystem.jl | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/systems/systems.jl b/src/systems/systems.jl index 4205a4d207..a54206d1dd 100644 --- a/src/systems/systems.jl +++ b/src/systems/systems.jl @@ -156,6 +156,6 @@ function __structural_simplify(sys::AbstractSystem, io = nothing; simplify = fal noise_eqs = StructuralTransformations.tearing_substitute_expr(ode_sys, noise_eqs) return SDESystem(full_equations(ode_sys), noise_eqs, get_iv(ode_sys), unknowns(ode_sys), parameters(ode_sys); - name = nameof(ode_sys), is_scalar_noise) + name = nameof(ode_sys), is_scalar_noise, observed = observed(ode_sys)) end end diff --git a/test/sdesystem.jl b/test/sdesystem.jl index 78d7a0418b..749aca86a7 100644 --- a/test/sdesystem.jl +++ b/test/sdesystem.jl @@ -799,3 +799,13 @@ end prob = SDEProblem(sys, [], (0.0, 1.0), []) @test_nowarn solve(prob, RKMil()) end + +@testset "Observed variables retained after `structural_simplify`" begin + @variables x(t) y(t) z(t) + @brownian a + @mtkbuild sys = System([D(x) ~ x + a, D(y) ~ y + a, z ~ x + y], t) + @test sys isa SDESystem + @test length(observed(sys)) == 1 + prob = SDEProblem(sys, [x => 1.0, y => 1.0], (0.0, 1.0)) + @test prob[z] ≈ 2.0 +end From 9589a1f347673cb85b639fbccdab508201d64402 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 21 Nov 2024 12:50:57 +0530 Subject: [PATCH 0401/1337] feat: store args and kwargs in `EmptySciMLFunction` --- src/systems/problem_utils.jl | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/systems/problem_utils.jl b/src/systems/problem_utils.jl index 1cf110df13..1cfb8eb40a 100644 --- a/src/systems/problem_utils.jl +++ b/src/systems/problem_utils.jl @@ -431,12 +431,16 @@ end $(TYPEDEF) A simple utility meant to be used as the `constructor` passed to `process_SciMLProblem` in -case constructing a SciMLFunction is not required. +case constructing a SciMLFunction is not required. The arguments passed to it are available +in the `args` field, and the keyword arguments in the `kwargs` field. """ -struct EmptySciMLFunction end +struct EmptySciMLFunction{A, K} + args::A + kwargs::K +end function EmptySciMLFunction(args...; kwargs...) - return nothing + return EmptySciMLFunction{typeof(args), typeof(kwargs)}(args, kwargs) end """ From 8d4f5423e424279ba0855bf79e008a25e6544a77 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 20 Nov 2024 12:54:33 +0530 Subject: [PATCH 0402/1337] feat: propagate `ODEProblem` guesses to `remake` --- src/systems/diffeqs/abstractodesystem.jl | 4 +- src/systems/nonlinear/initializesystem.jl | 90 ++++++----------------- src/systems/problem_utils.jl | 14 +++- test/initializationsystem.jl | 23 ++++++ 4 files changed, 60 insertions(+), 71 deletions(-) diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index cbf835f48d..af96c4fcfe 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -1310,11 +1310,11 @@ function InitializationProblem{iip, specialize}(sys::AbstractODESystem, elseif isempty(u0map) && get_initializesystem(sys) === nothing isys = structural_simplify( generate_initializesystem( - sys; initialization_eqs, check_units, pmap = parammap); fully_determined) + sys; initialization_eqs, check_units, pmap = parammap, guesses); fully_determined) else isys = structural_simplify( generate_initializesystem( - sys; u0map, initialization_eqs, check_units, pmap = parammap); fully_determined) + sys; u0map, initialization_eqs, check_units, pmap = parammap, guesses); fully_determined) end ts = get_tearing_state(isys) diff --git a/src/systems/nonlinear/initializesystem.jl b/src/systems/nonlinear/initializesystem.jl index eff19afb07..fe41e44d84 100644 --- a/src/systems/nonlinear/initializesystem.jl +++ b/src/systems/nonlinear/initializesystem.jl @@ -30,7 +30,8 @@ function generate_initializesystem(sys::ODESystem; # 1) process dummy derivatives and u0map into initialization system eqs_ics = eqs[idxs_alge] # start equation list with algebraic equations defs = copy(defaults(sys)) # copy so we don't modify sys.defaults - guesses = merge(get_guesses(sys), todict(guesses)) + additional_guesses = anydict(guesses) + guesses = merge(get_guesses(sys), additional_guesses) schedule = getfield(sys, :schedule) if !isnothing(schedule) for x in filter(x -> !isnothing(x[1]), schedule.dummy_sub) @@ -178,7 +179,7 @@ function generate_initializesystem(sys::ODESystem; for k in keys(defs) defs[k] = substitute(defs[k], paramsubs) end - meta = InitializationSystemMetadata(Dict{Any, Any}(u0map), Dict{Any, Any}(pmap)) + meta = InitializationSystemMetadata(anydict(u0map), anydict(pmap), additional_guesses) return NonlinearSystem(eqs_ics, vars, pars; @@ -193,6 +194,7 @@ end struct InitializationSystemMetadata u0map::Dict{Any, Any} pmap::Dict{Any, Any} + additional_guesses::Dict{Any, Any} end function is_parameter_solvable(p, pmap, defs, guesses) @@ -263,75 +265,29 @@ function SciMLBase.remake_initializeprob(sys::ODESystem, odefn, u0, t0, p) return initprob, odefn.update_initializeprob!, odefn.initializeprobmap, odefn.initializeprobpmap end - if u0 === missing || isempty(u0) - u0 = Dict() - elseif !(eltype(u0) <: Pair) - u0 = Dict(unknowns(sys) .=> u0) - end - if p === missing - p = Dict() + dvs = unknowns(sys) + ps = parameters(sys) + u0map = to_varmap(u0, dvs) + pmap = to_varmap(p, ps) + guesses = Dict() + if SciMLBase.has_initializeprob(odefn) + oldsys = odefn.initializeprob.f.sys + meta = get_metadata(oldsys) + if meta isa InitializationSystemMetadata + u0map = merge(meta.u0map, u0map) + pmap = merge(meta.pmap, pmap) + merge!(guesses, meta.additional_guesses) + end end if t0 === nothing t0 = 0.0 end - u0 = todict(u0) - defs = defaults(sys) - varmap = merge(defs, u0) - for k in collect(keys(varmap)) - if varmap[k] === nothing - delete!(varmap, k) - end - end - varmap = canonicalize_varmap(varmap) - missingvars = setdiff(unknowns(sys), collect(keys(varmap))) - setobserved = filter(keys(varmap)) do var - has_observed_with_lhs(sys, var) || has_observed_with_lhs(sys, default_toterm(var)) - end - p = todict(p) - guesses = ModelingToolkit.guesses(sys) - solvablepars = [par - for par in parameters(sys) - if is_parameter_solvable(par, p, defs, guesses)] - pvarmap = merge(defs, p) - setparobserved = filter(keys(pvarmap)) do var - has_parameter_dependency_with_lhs(sys, var) - end - if (((!isempty(missingvars) || !isempty(solvablepars) || - !isempty(setobserved) || !isempty(setparobserved)) && - ModelingToolkit.get_tearing_state(sys) !== nothing) || - !isempty(initialization_equations(sys))) - if SciMLBase.has_initializeprob(odefn) - oldsys = odefn.initializeprob.f.sys - meta = get_metadata(oldsys) - if meta isa InitializationSystemMetadata - u0 = merge(meta.u0map, u0) - p = merge(meta.pmap, p) - end - end - for k in collect(keys(u0)) - if u0[k] === nothing - delete!(u0, k) - end - end - for k in collect(keys(p)) - if p[k] === nothing - delete!(p, k) - end - end - - initprob = InitializationProblem(sys, t0, u0, p) - initprobmap = getu(initprob, unknowns(sys)) - punknowns = [p for p in all_variable_symbols(initprob) if is_parameter(sys, p)] - getpunknowns = getu(initprob, punknowns) - setpunknowns = setp(sys, punknowns) - initprobpmap = GetUpdatedMTKParameters(getpunknowns, setpunknowns) - reqd_syms = parameter_symbols(initprob) - update_initializeprob! = UpdateInitializeprob( - getu(sys, reqd_syms), setu(initprob, reqd_syms)) - return initprob, update_initializeprob!, initprobmap, initprobpmap - else - return nothing, nothing, nothing, nothing - end + filter_missing_values!(u0map) + filter_missing_values!(pmap) + f, _ = process_SciMLProblem(EmptySciMLFunction, sys, u0map, pmap; guesses, t = t0) + kws = f.kwargs + return get(kws, :initializeprob, nothing), get(kws, :update_initializeprob!, nothing), get(kws, :initializeprobmap, nothing), + get(kws, :initializeprobpmap, nothing) end """ diff --git a/src/systems/problem_utils.jl b/src/systems/problem_utils.jl index 1cfb8eb40a..9521df7872 100644 --- a/src/systems/problem_utils.jl +++ b/src/systems/problem_utils.jl @@ -4,12 +4,13 @@ const AnyDict = Dict{Any, Any} $(TYPEDSIGNATURES) If called without arguments, return `Dict{Any, Any}`. Otherwise, interpret the input -as a symbolic map and turn it into a `Dict{Any, Any}`. Handles `SciMLBase.NullParameters` -and `nothing`. +as a symbolic map and turn it into a `Dict{Any, Any}`. Handles `SciMLBase.NullParameters`, +`missing` and `nothing`. """ anydict() = AnyDict() anydict(::SciMLBase.NullParameters) = AnyDict() anydict(::Nothing) = AnyDict() +anydict(::Missing) = AnyDict() anydict(x::AnyDict) = x anydict(x) = AnyDict(x) @@ -388,6 +389,15 @@ function evaluate_varmap!(varmap::AbstractDict, vars; limit = 100) end end +""" + $(TYPEDSIGNATURES) + +Remove keys in `varmap` whose values are `nothing`. +""" +function filter_missing_values!(varmap::AbstractDict) + filter!(kvp -> kvp[2] !== nothing, varmap) +end + struct GetUpdatedMTKParameters{G, S} # `getu` functor which gets parameters that are unknowns during initialization getpunknowns::G diff --git a/test/initializationsystem.jl b/test/initializationsystem.jl index 770f9f12bd..edb4199356 100644 --- a/test/initializationsystem.jl +++ b/test/initializationsystem.jl @@ -975,3 +975,26 @@ end @test integ.ps[p] ≈ 1.0 @test integ.ps[q]≈cbrt(2) rtol=1e-6 end + +@testset "Guesses provided to `ODEProblem` are used in `remake`" begin + @variables x(t) y(t)=2x + @parameters p q=3x + @mtkbuild sys = ODESystem([D(x) ~ x * p + q, x^3 + y^3 ~ 3], t) + prob = ODEProblem( + sys, [], (0.0, 1.0), [p => 1.0]; guesses = [x => 1.0, y => 1.0, q => 1.0]) + @test prob[x] == 0.0 + @test prob[y] == 0.0 + @test prob.ps[p] == 1.0 + @test prob.ps[q] == 0.0 + integ = init(prob) + @test integ[x] ≈ 1 / cbrt(3) + @test integ[y] ≈ 2 / cbrt(3) + @test integ.ps[p] == 1.0 + @test integ.ps[q] ≈ 3 / cbrt(3) + prob2 = remake(prob; u0 = [y => 3x], p = [q => 2x]) + integ2 = init(prob2) + @test integ2[x] ≈ cbrt(3 / 28) + @test integ2[y] ≈ 3cbrt(3 / 28) + @test integ2.ps[p] == 1.0 + @test integ2.ps[q] ≈ 2cbrt(3 / 28) +end From 2641fd8a97cfc785d340e2fa7cea80a3a3815182 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 22 Nov 2024 14:04:13 +0530 Subject: [PATCH 0403/1337] fix: handle initial values passed as `Symbol`s --- src/systems/problem_utils.jl | 38 ++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/src/systems/problem_utils.jl b/src/systems/problem_utils.jl index 9521df7872..b837eef98e 100644 --- a/src/systems/problem_utils.jl +++ b/src/systems/problem_utils.jl @@ -52,6 +52,42 @@ function add_toterms(varmap::AbstractDict; toterm = default_toterm) return cp end +""" + $(TYPEDSIGNATURES) + +Turn any `Symbol` keys in `varmap` to the appropriate symbolic variables in `sys`. Any +symbols that cannot be converted are ignored. +""" +function symbols_to_symbolics!(sys::AbstractSystem, varmap::AbstractDict) + if is_split(sys) + ic = get_index_cache(sys) + for k in collect(keys(varmap)) + k isa Symbol || continue + newk = get(ic.symbol_to_variable, k, nothing) + newk === nothing && continue + varmap[newk] = varmap[k] + delete!(varmap, k) + end + else + syms = all_symbols(sys) + for k in collect(keys(varmap)) + k isa Symbol || continue + idx = findfirst(syms) do sym + hasname(sym) || return false + name = getname(sym) + return name == k + end + idx === nothing && continue + newk = syms[idx] + if iscall(newk) && operation(newk) === getindex + newk = arguments(newk)[1] + end + varmap[newk] = varmap[k] + delete!(varmap, k) + end + end +end + """ $(TYPEDSIGNATURES) @@ -530,8 +566,10 @@ function process_SciMLProblem( pType = typeof(pmap) _u0map = u0map u0map = to_varmap(u0map, dvs) + symbols_to_symbolics!(sys, u0map) _pmap = pmap pmap = to_varmap(pmap, ps) + symbols_to_symbolics!(sys, pmap) defs = add_toterms(recursive_unwrap(defaults(sys))) cmap, cs = get_cmap(sys) kwargs = NamedTuple(kwargs) From 26980151eacd8d3b1977f544f4a284022ce71ad9 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 22 Nov 2024 14:04:34 +0530 Subject: [PATCH 0404/1337] fix: handle `remake` with no pre-existing initializeprob --- src/systems/nonlinear/initializesystem.jl | 58 ++++++++++++++++++----- 1 file changed, 47 insertions(+), 11 deletions(-) diff --git a/src/systems/nonlinear/initializesystem.jl b/src/systems/nonlinear/initializesystem.jl index fe41e44d84..4abb345822 100644 --- a/src/systems/nonlinear/initializesystem.jl +++ b/src/systems/nonlinear/initializesystem.jl @@ -210,17 +210,16 @@ function is_parameter_solvable(p, pmap, defs, guesses) _val1 === nothing && _val2 !== nothing)) && _val3 !== nothing end -function SciMLBase.remake_initializeprob(sys::ODESystem, odefn, u0, t0, p) +function SciMLBase.remake_initialization_data(sys::ODESystem, odefn, u0, t0, p, newu0, newp) if u0 === missing && p === missing - return odefn.initializeprob, odefn.update_initializeprob!, odefn.initializeprobmap, - odefn.initializeprobpmap + return odefn.initialization_data end if !(eltype(u0) <: Pair) && !(eltype(p) <: Pair) oldinitprob = odefn.initializeprob - if oldinitprob === nothing || !SciMLBase.has_sys(oldinitprob.f) || - !(oldinitprob.f.sys isa NonlinearSystem) - return oldinitprob, odefn.update_initializeprob!, odefn.initializeprobmap, - odefn.initializeprobpmap + oldinitprob === nothing && return nothing + if !SciMLBase.has_sys(oldinitprob.f) || !(oldinitprob.f.sys isa NonlinearSystem) + return SciMLBase.OverrideInitData(oldinitprob, odefn.update_initializeprob!, + odefn.initializeprobmap, odefn.initializeprobpmap) end pidxs = ParameterIndex[] pvals = [] @@ -262,14 +261,17 @@ function SciMLBase.remake_initializeprob(sys::ODESystem, odefn, u0, t0, p) oldinitprob.f.sys, parameter_values(oldinitprob), pidxs, pvals) end initprob = remake(oldinitprob; u0 = newu0, p = newp) - return initprob, odefn.update_initializeprob!, odefn.initializeprobmap, - odefn.initializeprobpmap + return SciMLBase.OverrideInitData(initprob, odefn.update_initializeprob!, + odefn.initializeprobmap, odefn.initializeprobpmap) end dvs = unknowns(sys) ps = parameters(sys) u0map = to_varmap(u0, dvs) + symbols_to_symbolics!(sys, u0map) pmap = to_varmap(p, ps) + symbols_to_symbolics!(sys, pmap) guesses = Dict() + defs = defaults(sys) if SciMLBase.has_initializeprob(odefn) oldsys = odefn.initializeprob.f.sys meta = get_metadata(oldsys) @@ -278,6 +280,35 @@ function SciMLBase.remake_initializeprob(sys::ODESystem, odefn, u0, t0, p) pmap = merge(meta.pmap, pmap) merge!(guesses, meta.additional_guesses) end + else + # there is no initializeprob, so the original problem construction + # had no solvable parameters and had the differential variables + # specified in `u0map`. + if u0 === missing + # the user didn't pass `u0` to `remake`, so they want to retain + # existing values. Fill the differential variables in `u0map`, + # initialization will either be elided or solve for the algebraic + # variables + diff_idxs = isdiffeq.(equations(sys)) + for i in eachindex(dvs) + diff_idxs[i] || continue + u0map[dvs[i]] = newu0[i] + end + end + if p === missing + # the user didn't pass `p` to `remake`, so they want to retain + # existing values. Fill all parameters in `pmap` so that none of + # them are solvable. + for p in ps + pmap[p] = getp(sys, p)(newp) + end + end + # all non-solvable parameters need values regardless + for p in ps + haskey(pmap, p) && continue + is_parameter_solvable(p, pmap, defs, guesses) && continue + pmap[p] = getp(sys, p)(newp) + end end if t0 === nothing t0 = 0.0 @@ -286,8 +317,13 @@ function SciMLBase.remake_initializeprob(sys::ODESystem, odefn, u0, t0, p) filter_missing_values!(pmap) f, _ = process_SciMLProblem(EmptySciMLFunction, sys, u0map, pmap; guesses, t = t0) kws = f.kwargs - return get(kws, :initializeprob, nothing), get(kws, :update_initializeprob!, nothing), get(kws, :initializeprobmap, nothing), - get(kws, :initializeprobpmap, nothing) + initprob = get(kws, :initializeprob, nothing) + if initprob === nothing + return nothing + end + return SciMLBase.OverrideInitData(initprob, get(kws, :update_initializeprob!, nothing), + get(kws, :initializeprobmap, nothing), + get(kws, :initializeprobpmap, nothing)) end """ From 3383bcede7c841530a9d82f601ff70e40f32deca Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 22 Nov 2024 14:05:11 +0530 Subject: [PATCH 0405/1337] test: test `remake` without initializeprob and `Symbol` values --- test/initializationsystem.jl | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/test/initializationsystem.jl b/test/initializationsystem.jl index edb4199356..0b0dc42c1e 100644 --- a/test/initializationsystem.jl +++ b/test/initializationsystem.jl @@ -998,3 +998,37 @@ end @test integ2.ps[p] == 1.0 @test integ2.ps[q] ≈ 2cbrt(3 / 28) end + +@testset "Remake problem with no initializeprob" begin + @variables x(t) [guess = 1.0] y(t) [guess = 1.0] + @parameters p [guess = 1.0] q [guess = 1.0] + @mtkbuild sys = ODESystem( + [D(x) ~ p * x + q * y, y ~ 2x], t; parameter_dependencies = [q ~ 2p]) + prob = ODEProblem(sys, [x => 1.0], (0.0, 1.0), [p => 1.0]) + @test prob.f.initialization_data === nothing + prob2 = remake(prob; u0 = [x => 2.0]) + @test prob2[x] == 2.0 + @test prob2.f.initialization_data === nothing + prob3 = remake(prob; u0 = [y => 2.0]) + @test prob3.f.initialization_data !== nothing + @test init(prob3)[x] ≈ 1.0 + prob4 = remake(prob; p = [p => 1.0]) + @test prob4.f.initialization_data === nothing + prob5 = remake(prob; p = [p => missing, q => 2.0]) + @test prob5.f.initialization_data !== nothing + @test init(prob5).ps[p] ≈ 1.0 +end + +@testset "Variables provided as symbols" begin + @variables x(t) [guess = 1.0] y(t) [guess = 1.0] + @parameters p [guess = 1.0] q [guess = 1.0] + @mtkbuild sys = ODESystem( + [D(x) ~ p * x + q * y, y ~ 2x], t; parameter_dependencies = [q ~ 2p]) + prob = ODEProblem(sys, [:x => 1.0], (0.0, 1.0), [p => 1.0]) + @test prob.f.initialization_data === nothing + prob2 = remake(prob; u0 = [:x => 2.0]) + @test prob2.f.initialization_data === nothing + prob3 = remake(prob; u0 = [:y => 1.0]) + @test prob3.f.initialization_data !== nothing + @test init(prob3)[x] ≈ 0.5 +end From 83ed891cd7cf2319ffbef0c5bb870424de5ab6ff Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Sat, 30 Nov 2024 23:37:12 +0530 Subject: [PATCH 0406/1337] build: bump SciMLBase compat --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 093c632c42..1621669c2c 100644 --- a/Project.toml +++ b/Project.toml @@ -126,7 +126,7 @@ REPL = "1" RecursiveArrayTools = "3.26" Reexport = "0.2, 1" RuntimeGeneratedFunctions = "0.5.9" -SciMLBase = "2.57.1" +SciMLBase = "2.64" SciMLStructures = "1.0" Serialization = "1" Setfield = "0.7, 0.8, 1" From 10cc9c16854fc277b88967bbcd709bf42a3b14d2 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Sat, 30 Nov 2024 23:40:22 +0530 Subject: [PATCH 0407/1337] refactor: format --- src/systems/optimization/optimizationsystem.jl | 3 ++- src/utils.jl | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/systems/optimization/optimizationsystem.jl b/src/systems/optimization/optimizationsystem.jl index 3425216b5f..43e9294dd3 100644 --- a/src/systems/optimization/optimizationsystem.jl +++ b/src/systems/optimization/optimizationsystem.jl @@ -168,7 +168,8 @@ function OptimizationSystem(objective; constraints = [], kwargs...) push!(new_ps, p) end end - return OptimizationSystem(objective, collect(allunknowns), collect(new_ps); constraints, kwargs...) + return OptimizationSystem( + objective, collect(allunknowns), collect(new_ps); constraints, kwargs...) end function flatten(sys::OptimizationSystem) diff --git a/src/utils.jl b/src/utils.jl index 1555cd624e..416efd8f2c 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -569,7 +569,6 @@ function collect_vars!(unknowns, parameters, p::Pair, iv; depth = 0, op = Differ return nothing end - function collect_var!(unknowns, parameters, var, iv; depth = 0) isequal(var, iv) && return nothing check_scope_depth(getmetadata(var, SymScope, LocalScope()), depth) || return nothing From 12b02c0bc5881873c5bfc27c141895c1a006b7e2 Mon Sep 17 00:00:00 2001 From: Christopher Rackauckas Date: Sun, 1 Dec 2024 12:28:48 -0800 Subject: [PATCH 0408/1337] Fix initialization_eqs example It was trivially wrong, fixes https://github.com/SciML/ModelingToolkit.jl/issues/3245 --- docs/src/tutorials/initialization.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/tutorials/initialization.md b/docs/src/tutorials/initialization.md index c3dbf8bbb0..c01310eb06 100644 --- a/docs/src/tutorials/initialization.md +++ b/docs/src/tutorials/initialization.md @@ -113,7 +113,7 @@ the `initialization_eqs` keyword argument, for example: ```@example init prob = ODEProblem(pend, [x => 1], (0.0, 1.5), [g => 1], guesses = [λ => 0, y => 1], - initialization_eqs = [y ~ 1]) + initialization_eqs = [y ~ 0]) sol = solve(prob, Rodas5P()) plot(sol, idxs = (x, y)) ``` From b3da8137a9288b9c3eeff209a541dd41a5f97524 Mon Sep 17 00:00:00 2001 From: vyudu Date: Sun, 1 Dec 2024 16:18:18 -0500 Subject: [PATCH 0409/1337] up --- src/systems/diffeqs/abstractodesystem.jl | 64 +++++++++--------------- 1 file changed, 23 insertions(+), 41 deletions(-) diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index 7a8bf56562..bb4cbbb6c3 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -469,6 +469,17 @@ function DiffEqBase.ODEFunction{iip, specialize}(sys::AbstractODESystem, initializeprobpmap = initializeprobpmap) end +""" +```julia +SciMLBase.BVPFunction{iip}(sys::AbstractODESystem, u0map, tspan, + parammap = DiffEqBase.NullParameters(); + version = nothing, tgrad = false, + jac = true, sparse = true, + simplify = false, + kwargs...) where {iip} +``` +""" + """ ```julia SciMLBase.BVProblem{iip}(sys::AbstractODESystem, u0map, tspan, @@ -481,7 +492,7 @@ SciMLBase.BVProblem{iip}(sys::AbstractODESystem, u0map, tspan, Create a `BVProblem` from the [`ODESystem`](@ref). The arguments `dvs` and `ps` are used to set the order of the dependent variable and parameter vectors, -respectively. +respectively. `u0` should be either the initial condition, a vector of values `u(t_i)` for collocation methods, or a function returning one or the other. """ function SciMLBase.BVProblem(sys::AbstractODESystem, args...; kwargs...) BVProblem{true}(sys, args...; kwargs...) @@ -502,12 +513,13 @@ function SciMLBase.BVProblem{false}(sys::AbstractODESystem, args...; kwargs...) BVProblem{false, SciMLBase.FullSpecialize}(sys, args...; kwargs...) end +# figure out what's going on when we try to set `sparse`? + function SciMLBase.BVProblem{iip, specialize}(sys::AbstractODESystem, u0map = [], tspan = get_tspan(sys), parammap = DiffEqBase.NullParameters(); version = nothing, tgrad = false, jac = true, sparse = true, - sparsity = true, callback = nothing, check_length = true, warn_initialize_determined = true, @@ -521,57 +533,27 @@ function SciMLBase.BVProblem{iip, specialize}(sys::AbstractODESystem, u0map = [] f, u0, p = process_SciMLProblem(ODEFunction{iip, specialize}, sys, u0map, parammap; t = tspan !== nothing ? tspan[1] : tspan, - check_length, warn_initialize_determined, eval_expression, eval_module, jac, sparse, sparsity, kwargs...) - - # if jac - # jac_gen = generate_jacobian(sys, dvs, ps; - # simplify = simplify, sparse = sparse, - # expression = Val{true}, - # expression_module = eval_module, - # checkbounds = checkbounds, kwargs...) - # jac_oop, jac_iip = eval_or_rgf.(jac_gen; eval_expression, eval_module) - - # _jac(u, p, t) = jac_oop(u, p, t) - # _jac(J, u, p, t) = jac_iip(J, u, p, t) - # _jac(u, p::Tuple{Vararg{Number}}, t) = jac_oop(u, p, t) - # _jac(J, u, p::Tuple{Vararg{Number}}, t) = jac_iip(J, u, p, t) - # _jac(u, p::Tuple, t) = jac_oop(u, p..., t) - # _jac(J, u, p::Tuple, t) = jac_iip(J, u, p..., t) - # _jac(u, p::MTKParameters, t) = jac_oop(u, p..., t) - # _jac(J, u, p::MTKParameters, t) = jac_iip(J, u, p..., t) - # else - # _jac = nothing - # end - - # jac_prototype = if sparse - # uElType = u0 === nothing ? Float64 : eltype(u0) - # if jac - # similar(calculate_jacobian(sys, sparse = sparse), uElType) - # else - # similar(jacobian_sparsity(sys), uElType) - # end - # else - # nothing - # end - - # f.jac = _jac - # f.jac_prototype = jac_prototype - # f.sparsity = jac ? jacobian_sparsity(sys) : nothing + check_length, warn_initialize_determined, eval_expression, eval_module, jac, kwargs...) cbs = process_events(sys; callback, eval_expression, eval_module, kwargs...) - kwargs = filter_kwargs(kwargs) kwargs1 = (;) if cbs !== nothing kwargs1 = merge(kwargs1, (callback = cbs,)) end + + # Construct initial conditions + _u0 = prepare_initial_state(u0) + __u0 = if _u0 isa Function + _u0(t_i) + end # Define the boundary conditions bc = if iip - (residual, u, p, t) -> (residual = u[1] - u0) + (residual, u, p, t) -> (residual = u[1] - __u0) else - (u, p, t) -> (u[1] - u0) + (u, p, t) -> (u[1] - __u0) end return BVProblem{iip}(f, bc, u0, tspan, p; kwargs1..., kwargs...) From 4affeac4b340a06c770373b498ec6b0e94c25a06 Mon Sep 17 00:00:00 2001 From: vyudu Date: Sun, 1 Dec 2024 17:35:05 -0500 Subject: [PATCH 0410/1337] up --- src/systems/diffeqs/abstractodesystem.jl | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index 263a75d153..33bddece30 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -545,9 +545,7 @@ function SciMLBase.BVProblem{iip, specialize}(sys::AbstractODESystem, u0map = [] # Construct initial conditions _u0 = prepare_initial_state(u0) - __u0 = if _u0 isa Function - _u0(t_i) - end + __u0 = _u0 isa Function ? _u0(tspan[1]) : _u0 # Define the boundary conditions bc = if iip From a3429ea2b7d9e67898023eac1599e71c3d1b7bec Mon Sep 17 00:00:00 2001 From: vyudu Date: Sun, 1 Dec 2024 17:42:39 -0500 Subject: [PATCH 0411/1337] up --- src/systems/diffeqs/abstractodesystem.jl | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index 33bddece30..84f40a01f7 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -513,8 +513,6 @@ function SciMLBase.BVProblem{false}(sys::AbstractODESystem, args...; kwargs...) BVProblem{false, SciMLBase.FullSpecialize}(sys, args...; kwargs...) end -# figure out what's going on when we try to set `sparse`? - function SciMLBase.BVProblem{iip, specialize}(sys::AbstractODESystem, u0map = [], tspan = get_tspan(sys), parammap = DiffEqBase.NullParameters(); @@ -544,14 +542,13 @@ function SciMLBase.BVProblem{iip, specialize}(sys::AbstractODESystem, u0map = [] end # Construct initial conditions - _u0 = prepare_initial_state(u0) - __u0 = _u0 isa Function ? _u0(tspan[1]) : _u0 + _u0 = u0 isa Function ? u0(tspan[1]) : u0 # Define the boundary conditions bc = if iip - (residual, u, p, t) -> (residual = u[1] - __u0) + (residual, u, p, t) -> (residual = u[1] - _u0) else - (u, p, t) -> (u[1] - __u0) + (u, p, t) -> (u[1] - _u0) end return BVProblem{iip}(f, bc, u0, tspan, p; kwargs1..., kwargs...) From f751fbb51c49e0b35ee84a12c54ec2de919660a5 Mon Sep 17 00:00:00 2001 From: vyudu Date: Sun, 1 Dec 2024 21:22:08 -0500 Subject: [PATCH 0412/1337] up --- src/systems/diffeqs/abstractodesystem.jl | 18 ++------ test/bvproblem.jl | 56 ++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 15 deletions(-) create mode 100644 test/bvproblem.jl diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index 84f40a01f7..112419dea8 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -469,17 +469,6 @@ function DiffEqBase.ODEFunction{iip, specialize}(sys::AbstractODESystem, initializeprobpmap = initializeprobpmap) end -""" -```julia -SciMLBase.BVPFunction{iip}(sys::AbstractODESystem, u0map, tspan, - parammap = DiffEqBase.NullParameters(); - version = nothing, tgrad = false, - jac = true, sparse = true, - simplify = false, - kwargs...) where {iip} -``` -""" - """ ```julia SciMLBase.BVProblem{iip}(sys::AbstractODESystem, u0map, tspan, @@ -492,7 +481,7 @@ SciMLBase.BVProblem{iip}(sys::AbstractODESystem, u0map, tspan, Create a `BVProblem` from the [`ODESystem`](@ref). The arguments `dvs` and `ps` are used to set the order of the dependent variable and parameter vectors, -respectively. `u0` should be either the initial condition, a vector of values `u(t_i)` for collocation methods, or a function returning one or the other. +respectively. `u0map` should be used to specify the initial condition, or be a function returning an initial condition. """ function SciMLBase.BVProblem(sys::AbstractODESystem, args...; kwargs...) BVProblem{true}(sys, args...; kwargs...) @@ -517,7 +506,6 @@ function SciMLBase.BVProblem{iip, specialize}(sys::AbstractODESystem, u0map = [] tspan = get_tspan(sys), parammap = DiffEqBase.NullParameters(); version = nothing, tgrad = false, - jac = true, sparse = true, callback = nothing, check_length = true, warn_initialize_determined = true, @@ -531,7 +519,7 @@ function SciMLBase.BVProblem{iip, specialize}(sys::AbstractODESystem, u0map = [] f, u0, p = process_SciMLProblem(ODEFunction{iip, specialize}, sys, u0map, parammap; t = tspan !== nothing ? tspan[1] : tspan, - check_length, warn_initialize_determined, eval_expression, eval_module, jac, kwargs...) + check_length, warn_initialize_determined, eval_expression, eval_module, kwargs...) cbs = process_events(sys; callback, eval_expression, eval_module, kwargs...) kwargs = filter_kwargs(kwargs) @@ -546,7 +534,7 @@ function SciMLBase.BVProblem{iip, specialize}(sys::AbstractODESystem, u0map = [] # Define the boundary conditions bc = if iip - (residual, u, p, t) -> (residual = u[1] - _u0) + (residual, u, p, t) -> residual .= u[1] - _u0 else (u, p, t) -> (u[1] - _u0) end diff --git a/test/bvproblem.jl b/test/bvproblem.jl new file mode 100644 index 0000000000..90d41a96ad --- /dev/null +++ b/test/bvproblem.jl @@ -0,0 +1,56 @@ +using BoundaryValueDiffEq, OrdinaryDiffEq +using ModelingToolkit +using ModelingToolkit: t_nounits as t, D_nounits as D + +@parameters σ = 10. ρ = 28 β = 8/3 +@variables x(t) = 1 y(t) = 0 z(t) = 0 + +eqs = [D(x) ~ σ*(y-x), + D(y) ~ x*(ρ-z)-y, + D(z) ~ x*y - β*z] + +u0map = [:x => 1., :y => 0., :z => 0.] +parammap = [:ρ => 28., :β => 8/3, :σ => 10.] +tspan = (0., 10.) + +@mtkbuild lorenz = ODESystem(eqs, t) + +bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(lorenz, u0map, tspan, parammap) +sol = solve(bvp, MIRK4(), dt = 0.1); + +bvp2 = SciMLBase.BVProblem{false, SciMLBase.AutoSpecialize}(lorenz, u0map, tspan, parammap) +sol2 = solve(bvp, MIRK4(), dt = 0.1); + +op = ODEProblem(lorenz, u0map, tspan, parammap) +osol = solve(op) + +@test sol.u[end] ≈ osol.u[end] +@test sol2.u[end] ≈ osol.u[end] +@test sol.u[1] == [1., 0., 0.] +@test sol2.u[1] == [1., 0., 0.] + +### Testing on pendulum + +@parameters g = 9.81 L = 1. +@variables θ(t) = π/2 + +eqs = [D(D(θ)) ~ -(g / L) * sin(θ)] + +@mtkbuild pend = ODESystem(eqs, t) + +u0map = [θ => π/2, D(θ) => π/2] +parammap = [:L => 2.] +tspan = (0., 10.) + +bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(pend, u0map, tspan, parammap) +sol = solve(bvp, MIRK4(), dt = 0.05); + +bvp2 = SciMLBase.BVProblem{false, SciMLBase.AutoSpecialize}(pend, u0map, tspan, parammap) +sol2 = solve(bvp2, MIRK4(), dt = 0.05); + +osol = solve(pend) + +@test sol.u[end] ≈ osol.u[end] +@test sol.u[1] == [π/2, π/2] +@test sol2.u[end] ≈ osol.u[end] +@test sol2.u[1] == [π/2, π/2] From b063e6b7cc12747b6ffb7a474d5bbe06165867d5 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 2 Dec 2024 13:18:11 +0530 Subject: [PATCH 0413/1337] refactor: separate out `resid_prototype` calculation --- src/systems/nonlinear/nonlinearsystem.jl | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/systems/nonlinear/nonlinearsystem.jl b/src/systems/nonlinear/nonlinearsystem.jl index 94b44e0a6f..1289388197 100644 --- a/src/systems/nonlinear/nonlinearsystem.jl +++ b/src/systems/nonlinear/nonlinearsystem.jl @@ -283,6 +283,16 @@ function hessian_sparsity(sys::NonlinearSystem) unknowns(sys)) for eq in equations(sys)] end +function calculate_resid_prototype(N, u0, p) + u0ElType = u0 === nothing ? Float64 : eltype(u0) + if SciMLStructures.isscimlstructure(p) + u0ElType = promote_type( + eltype(SciMLStructures.canonicalize(SciMLStructures.Tunable(), p)[1]), + u0ElType) + end + return zeros(u0ElType, N) +end + """ ```julia SciMLBase.NonlinearFunction{iip}(sys::NonlinearSystem, dvs = unknowns(sys), @@ -337,13 +347,7 @@ function SciMLBase.NonlinearFunction{iip}(sys::NonlinearSystem, dvs = unknowns(s if length(dvs) == length(equations(sys)) resid_prototype = nothing else - u0ElType = u0 === nothing ? Float64 : eltype(u0) - if SciMLStructures.isscimlstructure(p) - u0ElType = promote_type( - eltype(SciMLStructures.canonicalize(SciMLStructures.Tunable(), p)[1]), - u0ElType) - end - resid_prototype = zeros(u0ElType, length(equations(sys))) + resid_prototype = calculate_resid_prototype(length(equations(sys)), u0, p) end NonlinearFunction{iip}(f, From c578da182393e8ba8efac181b796e9838d093680 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 2 Dec 2024 13:18:33 +0530 Subject: [PATCH 0414/1337] fix: recalculate `resid_prototype` in `remake_initialization_data` --- src/systems/nonlinear/initializesystem.jl | 11 ++++++++++- test/initializationsystem.jl | 17 +++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/src/systems/nonlinear/initializesystem.jl b/src/systems/nonlinear/initializesystem.jl index 4abb345822..726d171bd0 100644 --- a/src/systems/nonlinear/initializesystem.jl +++ b/src/systems/nonlinear/initializesystem.jl @@ -260,7 +260,16 @@ function SciMLBase.remake_initialization_data(sys::ODESystem, odefn, u0, t0, p, newp = remake_buffer( oldinitprob.f.sys, parameter_values(oldinitprob), pidxs, pvals) end - initprob = remake(oldinitprob; u0 = newu0, p = newp) + if oldinitprob.f.resid_prototype === nothing + newf = oldinitprob.f + else + newf = NonlinearFunction{ + SciMLBase.isinplace(oldinitprob.f), SciMLBase.specialization(oldinitprob.f)}( + oldinitprob.f; + resid_prototype = calculate_resid_prototype( + length(oldinitprob.f.resid_prototype), newu0, newp)) + end + initprob = remake(oldinitprob; f = newf, u0 = newu0, p = newp) return SciMLBase.OverrideInitData(initprob, odefn.update_initializeprob!, odefn.initializeprobmap, odefn.initializeprobpmap) end diff --git a/test/initializationsystem.jl b/test/initializationsystem.jl index 0b0dc42c1e..f3015f7db0 100644 --- a/test/initializationsystem.jl +++ b/test/initializationsystem.jl @@ -1032,3 +1032,20 @@ end @test prob3.f.initialization_data !== nothing @test init(prob3)[x] ≈ 0.5 end + +@testset "Issue#3246: type promotion with parameter dependent initialization_eqs" begin + @variables x(t)=1 y(t)=1 + @parameters a = 1 + @named sys = ODESystem([D(x) ~ 0, D(y) ~ x + a], t; initialization_eqs = [y ~ a]) + + ssys = structural_simplify(sys) + prob = ODEProblem(ssys, [], (0, 1), []) + + @test SciMLBase.successful_retcode(solve(prob)) + + seta = setsym_oop(prob, [a]) + (newu0, newp) = seta(prob, ForwardDiff.Dual{ForwardDiff.Tag{:tag, Float64}}.([1.0], 1)) + newprob = remake(prob, u0 = newu0, p = newp) + + @test SciMLBase.successful_retcode(solve(newprob)) +end From 44a60c5c171f312f7ee0afe7bc3acad62ad0714a Mon Sep 17 00:00:00 2001 From: ArnoStrouwen Date: Tue, 3 Dec 2024 01:09:00 +0100 Subject: [PATCH 0415/1337] update higher order documentation to modern MTK --- docs/src/examples/higher_order.md | 55 ++++++++++++++++--------------- 1 file changed, 28 insertions(+), 27 deletions(-) diff --git a/docs/src/examples/higher_order.md b/docs/src/examples/higher_order.md index 7dafe758dc..fac707525f 100644 --- a/docs/src/examples/higher_order.md +++ b/docs/src/examples/higher_order.md @@ -3,7 +3,7 @@ ModelingToolkit has a system for transformations of mathematical systems. These transformations allow for symbolically changing the representation of the model to problems that are easier to -numerically solve. One simple to demonstrate transformation is the +numerically solve. One simple to demonstrate transformation, is `structural_simplify`, which does a lot of tricks, one being the transformation that turns an Nth order ODE into N coupled 1st order ODEs. @@ -15,16 +15,28 @@ We utilize the derivative operator twice here to define the second order: using ModelingToolkit, OrdinaryDiffEq using ModelingToolkit: t_nounits as t, D_nounits as D -@parameters σ ρ β -@variables x(t) y(t) z(t) - -eqs = [D(D(x)) ~ σ * (y - x), - D(y) ~ x * (ρ - z) - y, - D(z) ~ x * y - β * z] - -@named sys = ODESystem(eqs, t) +@mtkmodel SECOND_ORDER begin + @parameters begin + σ = 28.0 + ρ = 10.0 + β = 8 / 3 + end + @variables begin + x(t) = 1.0 + y(t) = 0.0 + z(t) = 0.0 + end + @equations begin + D(D(x)) ~ σ * (y - x) + D(y) ~ x * (ρ - z) - y + D(z) ~ x * y - β * z + end +end +@mtkbuild sys = SECOND_ORDER() ``` +The second order ODE has been automatically transformed to two first order ODEs. + Note that we could've used an alternative syntax for 2nd order, i.e. `D = Differential(t)^2` and then `D(x)` would be the second derivative, and this syntax extends to `N`-th order. Also, we can use `*` or `∘` to compose @@ -33,28 +45,17 @@ and this syntax extends to `N`-th order. Also, we can use `*` or `∘` to compos Now let's transform this into the `ODESystem` of first order components. We do this by calling `structural_simplify`: -```@example orderlowering -sys = structural_simplify(sys) -``` - Now we can directly numerically solve the lowered system. Note that, following the original problem, the solution requires knowing the -initial condition for `x'`, and thus we include that in our input -specification: +initial condition for both `x` and `D(x)`. +The former already got assigned a default value in the `@mtkmodel`, +but we still have to provide a value for the latter. ```@example orderlowering -u0 = [D(x) => 2.0, - x => 1.0, - y => 0.0, - z => 0.0] - -p = [σ => 28.0, - ρ => 10.0, - β => 8 / 3] - +u0 = [D(sys.x) => 2.0] tspan = (0.0, 100.0) -prob = ODEProblem(sys, u0, tspan, p, jac = true) +prob = ODEProblem(sys, u0, tspan, [], jac = true) sol = solve(prob, Tsit5()) -using Plots; -plot(sol, idxs = (x, y)); +using Plots +plot(sol, idxs = (sys.x, sys.y)) ``` From a9fdfd6a3a114c4df28e4781c6b667990c01bafc Mon Sep 17 00:00:00 2001 From: vyudu Date: Tue, 3 Dec 2024 05:04:45 -0500 Subject: [PATCH 0416/1337] up --- src/systems/diffeqs/abstractodesystem.jl | 6 ++-- test/bvproblem.jl | 44 ++++++++++++------------ 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index 112419dea8..b795bb981d 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -529,12 +529,12 @@ function SciMLBase.BVProblem{iip, specialize}(sys::AbstractODESystem, u0map = [] kwargs1 = merge(kwargs1, (callback = cbs,)) end - # Construct initial conditions + # Construct initial conditions. _u0 = u0 isa Function ? u0(tspan[1]) : u0 - # Define the boundary conditions + # Define the boundary conditions. bc = if iip - (residual, u, p, t) -> residual .= u[1] - _u0 + (residual, u, p, t) -> (residual .= u[1] - _u0) else (u, p, t) -> (u[1] - _u0) end diff --git a/test/bvproblem.jl b/test/bvproblem.jl index 90d41a96ad..4865c867b3 100644 --- a/test/bvproblem.jl +++ b/test/bvproblem.jl @@ -2,32 +2,31 @@ using BoundaryValueDiffEq, OrdinaryDiffEq using ModelingToolkit using ModelingToolkit: t_nounits as t, D_nounits as D -@parameters σ = 10. ρ = 28 β = 8/3 -@variables x(t) = 1 y(t) = 0 z(t) = 0 +@parameters α = 7.5 β = 4. γ = 8. δ = 5. +@variables x(t) = 1. y(t) = 2. -eqs = [D(x) ~ σ*(y-x), - D(y) ~ x*(ρ-z)-y, - D(z) ~ x*y - β*z] +eqs = [D(x) ~ α*x - β*x*y, + D(y) ~ -γ*y + δ*x*y] -u0map = [:x => 1., :y => 0., :z => 0.] -parammap = [:ρ => 28., :β => 8/3, :σ => 10.] +u0map = [:x => 1., :y => 2.] +parammap = [:α => 7.5, :β => 4, :γ => 8., :δ => 5.] tspan = (0., 10.) -@mtkbuild lorenz = ODESystem(eqs, t) +@mtkbuild lotkavolterra = ODESystem(eqs, t) -bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(lorenz, u0map, tspan, parammap) -sol = solve(bvp, MIRK4(), dt = 0.1); +bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(lotkavolterra, u0map, tspan, parammap) +sol = solve(bvp, MIRK4(), dt = 0.01); -bvp2 = SciMLBase.BVProblem{false, SciMLBase.AutoSpecialize}(lorenz, u0map, tspan, parammap) -sol2 = solve(bvp, MIRK4(), dt = 0.1); +bvp2 = SciMLBase.BVProblem{false, SciMLBase.AutoSpecialize}(lotkavolterra, u0map, tspan, parammap) +sol2 = solve(bvp, MIRK4(), dt = 0.01); -op = ODEProblem(lorenz, u0map, tspan, parammap) -osol = solve(op) +op = ODEProblem(lotkavolterra, u0map, tspan, parammap) +osol = solve(op, Vern9()) -@test sol.u[end] ≈ osol.u[end] -@test sol2.u[end] ≈ osol.u[end] -@test sol.u[1] == [1., 0., 0.] -@test sol2.u[1] == [1., 0., 0.] +@test isapprox(sol.u[end],osol.u[end]; atol = 0.001) +@test isapprox(sol2.u[end],osol.u[end]; atol = 0.001) +@test sol.u[1] == [1., 2.] +@test sol2.u[1] == [1., 2.] ### Testing on pendulum @@ -39,16 +38,17 @@ eqs = [D(D(θ)) ~ -(g / L) * sin(θ)] @mtkbuild pend = ODESystem(eqs, t) u0map = [θ => π/2, D(θ) => π/2] -parammap = [:L => 2.] +parammap = [:L => 2., :g => 9.81] tspan = (0., 10.) bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(pend, u0map, tspan, parammap) -sol = solve(bvp, MIRK4(), dt = 0.05); +sol = solve(bvp, MIRK4(), dt = 0.01); bvp2 = SciMLBase.BVProblem{false, SciMLBase.AutoSpecialize}(pend, u0map, tspan, parammap) -sol2 = solve(bvp2, MIRK4(), dt = 0.05); +sol2 = solve(bvp2, MIRK4(), dt = 0.01); -osol = solve(pend) +op = ODEProblem(pend, u0map, tspan, parammap) +osol = solve(op, Vern9()) @test sol.u[end] ≈ osol.u[end] @test sol.u[1] == [π/2, π/2] From a9f210691c1f38ef0f575eb89a939763b81c9c1b Mon Sep 17 00:00:00 2001 From: vyudu Date: Tue, 3 Dec 2024 19:11:35 -0500 Subject: [PATCH 0417/1337] up --- src/systems/diffeqs/abstractodesystem.jl | 4 ++-- test/bvproblem.jl | 8 ++++---- test/runtests.jl | 1 + 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index b795bb981d..8dac19f296 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -534,7 +534,7 @@ function SciMLBase.BVProblem{iip, specialize}(sys::AbstractODESystem, u0map = [] # Define the boundary conditions. bc = if iip - (residual, u, p, t) -> (residual .= u[1] - _u0) + (residual, u, p, t) -> (residual .= u[1] .- _u0) else (u, p, t) -> (u[1] - _u0) end @@ -542,7 +542,7 @@ function SciMLBase.BVProblem{iip, specialize}(sys::AbstractODESystem, u0map = [] return BVProblem{iip}(f, bc, u0, tspan, p; kwargs1..., kwargs...) end -get_callback(prob::BVProblem) = prob.kwargs[:callback] +get_callback(prob::BVProblem) = error("BVP solvers do not support callbacks.") """ ```julia diff --git a/test/bvproblem.jl b/test/bvproblem.jl index 4865c867b3..7235638cf0 100644 --- a/test/bvproblem.jl +++ b/test/bvproblem.jl @@ -23,8 +23,8 @@ sol2 = solve(bvp, MIRK4(), dt = 0.01); op = ODEProblem(lotkavolterra, u0map, tspan, parammap) osol = solve(op, Vern9()) -@test isapprox(sol.u[end],osol.u[end]; atol = 0.001) -@test isapprox(sol2.u[end],osol.u[end]; atol = 0.001) +@test isapprox(sol.u[end],osol.u[end]; atol = 0.01) +@test isapprox(sol2.u[end],osol.u[end]; atol = 0.01) @test sol.u[1] == [1., 2.] @test sol2.u[1] == [1., 2.] @@ -50,7 +50,7 @@ sol2 = solve(bvp2, MIRK4(), dt = 0.01); op = ODEProblem(pend, u0map, tspan, parammap) osol = solve(op, Vern9()) -@test sol.u[end] ≈ osol.u[end] +@test isapprox(sol.u[end], osol.u[end]; atol = 0.01) @test sol.u[1] == [π/2, π/2] -@test sol2.u[end] ≈ osol.u[end] +@test isapprox(sol2.u[end], osol.u[end]; atol = 0.01) @test sol2.u[1] == [π/2, π/2] diff --git a/test/runtests.jl b/test/runtests.jl index 44846eed57..eaa87e6407 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -80,6 +80,7 @@ end @safetestset "NonlinearSystem Test" include("nonlinearsystem.jl") @safetestset "PDE Construction Test" include("pde.jl") @safetestset "JumpSystem Test" include("jumpsystem.jl") + @safetestset "BVProblem Test" include("bvproblem.jl") @safetestset "print_tree" include("print_tree.jl") @safetestset "Constraints Test" include("constraints.jl") end From c11e76a6bdeee93ffbfacd144356026ee6589eb9 Mon Sep 17 00:00:00 2001 From: ArnoStrouwen Date: Wed, 4 Dec 2024 01:32:46 +0100 Subject: [PATCH 0418/1337] add Lagrangian explanation to DAE reduction tutorial. --- docs/src/examples/modelingtoolkitize_index_reduction.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/src/examples/modelingtoolkitize_index_reduction.md b/docs/src/examples/modelingtoolkitize_index_reduction.md index 415d5b85ff..8686fd60d4 100644 --- a/docs/src/examples/modelingtoolkitize_index_reduction.md +++ b/docs/src/examples/modelingtoolkitize_index_reduction.md @@ -51,6 +51,14 @@ In this tutorial, we will look at the pendulum system: \end{aligned} ``` +These equations can be derived using the [Lagrangian equation of the first kind.](https://en.wikipedia.org/wiki/Lagrangian_mechanics#Lagrangian) +Specifically, for a pendulum with unit mass and length $L$, which thus has +kinetic energy $\frac{1}{2}(v_x^2 + v_y^2)$, +potential energy $gy$, +and holonomic constraint $x^2 + y^2 - L^2 = 0$. +The Lagrange multiplier related to this constraint is equal to half of $T$, +and represents the tension in the rope of the pendulum. + As a good DifferentialEquations.jl user, one would follow [the mass matrix DAE tutorial](https://docs.sciml.ai/DiffEqDocs/stable/tutorials/dae_example/#Mass-Matrix-Differential-Algebraic-Equations-(DAEs)) to arrive at code for simulating the model: From 56a9bb33b582aff924d8b1a30c140f2f3382ca40 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 2 Dec 2024 17:18:07 +0530 Subject: [PATCH 0419/1337] fix: retain system data on `structural_simplify` of `SDESystem` --- src/systems/systems.jl | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/systems/systems.jl b/src/systems/systems.jl index a54206d1dd..862718968d 100644 --- a/src/systems/systems.jl +++ b/src/systems/systems.jl @@ -154,8 +154,9 @@ function __structural_simplify(sys::AbstractSystem, io = nothing; simplify = fal end noise_eqs = StructuralTransformations.tearing_substitute_expr(ode_sys, noise_eqs) - return SDESystem(full_equations(ode_sys), noise_eqs, + return SDESystem(Vector{Equation}(full_equations(ode_sys)), noise_eqs, get_iv(ode_sys), unknowns(ode_sys), parameters(ode_sys); - name = nameof(ode_sys), is_scalar_noise, observed = observed(ode_sys)) + name = nameof(ode_sys), is_scalar_noise, observed = observed(ode_sys), defaults = defaults(sys), + parameter_dependencies = parameter_dependencies(sys)) end end From 3b63f826b4b69838cff4e12d0a6146c32e614f40 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 4 Dec 2024 13:18:01 +0530 Subject: [PATCH 0420/1337] test: test observed equations are retained after simplifying `SDESystem` --- test/dde.jl | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/dde.jl b/test/dde.jl index 2030a90d06..c7561e6c24 100644 --- a/test/dde.jl +++ b/test/dde.jl @@ -76,12 +76,13 @@ prob = SDDEProblem(hayes_modelf, hayes_modelg, [1.0], h, tspan, pmul; constant_lags = (pmul[1],)); sol = solve(prob, RKMil(), seed = 100) -@variables x(..) +@variables x(..) delx(t) @parameters a=-4.0 b=-2.0 c=10.0 α=-1.3 β=-1.2 γ=1.1 @brownian η τ = 1.0 -eqs = [D(x(t)) ~ a * x(t) + b * x(t - τ) + c + (α * x(t) + γ) * η] +eqs = [D(x(t)) ~ a * x(t) + b * x(t - τ) + c + (α * x(t) + γ) * η, delx ~ x(t - τ)] @mtkbuild sys = System(eqs, t) +@test ModelingToolkit.has_observed_with_lhs(sys, delx) @test ModelingToolkit.is_dde(sys) @test !is_markovian(sys) @test equations(sys) == [D(x(t)) ~ a * x(t) + b * x(t - τ) + c] From 9c1bed9f22aeb0c1e490871509cc5e3361e5b1f7 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 4 Dec 2024 14:17:55 +0530 Subject: [PATCH 0421/1337] refactor: format --- docs/src/examples/modelingtoolkitize_index_reduction.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/examples/modelingtoolkitize_index_reduction.md b/docs/src/examples/modelingtoolkitize_index_reduction.md index 8686fd60d4..b19ea46701 100644 --- a/docs/src/examples/modelingtoolkitize_index_reduction.md +++ b/docs/src/examples/modelingtoolkitize_index_reduction.md @@ -56,7 +56,7 @@ Specifically, for a pendulum with unit mass and length $L$, which thus has kinetic energy $\frac{1}{2}(v_x^2 + v_y^2)$, potential energy $gy$, and holonomic constraint $x^2 + y^2 - L^2 = 0$. -The Lagrange multiplier related to this constraint is equal to half of $T$, +The Lagrange multiplier related to this constraint is equal to half of $T$, and represents the tension in the rope of the pendulum. As a good DifferentialEquations.jl user, one would follow From 565f02ada5ec6ee30bdf49f104067e2a44a52c51 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 4 Dec 2024 15:43:33 +0530 Subject: [PATCH 0422/1337] build: bump minor version --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 1621669c2c..b1f5701d13 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "ModelingToolkit" uuid = "961ee093-0014-501f-94e3-6117800e7a78" authors = ["Yingbo Ma ", "Chris Rackauckas and contributors"] -version = "9.54.0" +version = "9.55.0" [deps] AbstractTrees = "1520ce14-60c1-5f80-bbc7-55ef81b5835c" From 17359ce72a9c690ce53b809c041888038cd3f468 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 15 Nov 2024 18:49:53 +0530 Subject: [PATCH 0423/1337] feat: initial implementation of `SCCNonlinearProblem` codegen --- .../bipartite_tearing/modia_tearing.jl | 14 +- src/systems/abstractsystem.jl | 190 ++++++------------ src/systems/nonlinear/nonlinearsystem.jl | 111 ++++++++++ src/systems/parameter_buffer.jl | 35 +++- src/utils.jl | 47 +++++ 5 files changed, 250 insertions(+), 147 deletions(-) diff --git a/src/structural_transformation/bipartite_tearing/modia_tearing.jl b/src/structural_transformation/bipartite_tearing/modia_tearing.jl index cef2f5f6d7..5da873afdf 100644 --- a/src/structural_transformation/bipartite_tearing/modia_tearing.jl +++ b/src/structural_transformation/bipartite_tearing/modia_tearing.jl @@ -62,6 +62,15 @@ function tear_graph_block_modia!(var_eq_matching, ict, solvable_graph, eqs, vars return nothing end +function build_var_eq_matching(structure::SystemStructure, ::Type{U} = Unassigned; + varfilter::F2 = v -> true, eqfilter::F3 = eq -> true) where {U, F2, F3} + @unpack graph, solvable_graph = structure + var_eq_matching = maximal_matching(graph, eqfilter, varfilter, U) + matching_len = max(length(var_eq_matching), + maximum(x -> x isa Int ? x : 0, var_eq_matching, init = 0)) + return complete(var_eq_matching, matching_len), matching_len +end + function tear_graph_modia(structure::SystemStructure, isder::F = nothing, ::Type{U} = Unassigned; varfilter::F2 = v -> true, @@ -78,10 +87,7 @@ function tear_graph_modia(structure::SystemStructure, isder::F = nothing, # find them here [TODO: It would be good to have an explicit example of this.] @unpack graph, solvable_graph = structure - var_eq_matching = maximal_matching(graph, eqfilter, varfilter, U) - matching_len = max(length(var_eq_matching), - maximum(x -> x isa Int ? x : 0, var_eq_matching, init = 0)) - var_eq_matching = complete(var_eq_matching, matching_len) + var_eq_matching, matching_len = build_var_eq_matching(structure, U; varfilter, eqfilter) full_var_eq_matching = copy(var_eq_matching) var_sccs = find_var_sccs(graph, var_eq_matching) vargraph = DiCMOBiGraph{true}(graph, 0, Matching(matching_len)) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index 3a9bbd33e1..60ed2fa1ce 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -162,11 +162,12 @@ object. """ function generate_custom_function(sys::AbstractSystem, exprs, dvs = unknowns(sys), ps = parameters(sys); wrap_code = nothing, postprocess_fbody = nothing, states = nothing, - expression = Val{true}, eval_expression = false, eval_module = @__MODULE__, kwargs...) + expression = Val{true}, eval_expression = false, eval_module = @__MODULE__, + cachesyms::Tuple = (), kwargs...) if !iscomplete(sys) error("A completed system is required. Call `complete` or `structural_simplify` on the system.") end - p = reorder_parameters(sys, unwrap.(ps)) + p = (reorder_parameters(sys, unwrap.(ps))..., cachesyms...) isscalar = !(exprs isa AbstractArray) if wrap_code === nothing wrap_code = isscalar ? identity : (identity, identity) @@ -187,7 +188,7 @@ function generate_custom_function(sys::AbstractSystem, exprs, dvs = unknowns(sys postprocess_fbody, states, wrap_code = wrap_code .∘ wrap_mtkparameters(sys, isscalar) .∘ - wrap_array_vars(sys, exprs; dvs) .∘ + wrap_array_vars(sys, exprs; dvs, cachesyms) .∘ wrap_parameter_dependencies(sys, isscalar), expression = Val{true} ) @@ -199,7 +200,7 @@ function generate_custom_function(sys::AbstractSystem, exprs, dvs = unknowns(sys postprocess_fbody, states, wrap_code = wrap_code .∘ wrap_mtkparameters(sys, isscalar) .∘ - wrap_array_vars(sys, exprs; dvs) .∘ + wrap_array_vars(sys, exprs; dvs, cachesyms) .∘ wrap_parameter_dependencies(sys, isscalar), expression = Val{true} ) @@ -231,116 +232,51 @@ end function wrap_array_vars( sys::AbstractSystem, exprs; dvs = unknowns(sys), ps = parameters(sys), - inputs = nothing, history = false) + inputs = nothing, history = false, cachesyms::Tuple = ()) isscalar = !(exprs isa AbstractArray) - array_vars = Dict{Any, AbstractArray{Int}}() - if dvs !== nothing - for (j, x) in enumerate(dvs) - if iscall(x) && operation(x) == getindex - arg = arguments(x)[1] - inds = get!(() -> Int[], array_vars, arg) - push!(inds, j) - end - end - for (k, inds) in array_vars - if inds == (inds′ = inds[1]:inds[end]) - array_vars[k] = inds′ - end - end + var_to_arridxs = Dict() - uind = 1 - else + if dvs === nothing uind = 0 - end - # values are (indexes, index of buffer, size of parameter) - array_parameters = Dict{Any, Tuple{AbstractArray{Int}, Int, Tuple{Vararg{Int}}}}() - # If for some reason different elements of an array parameter are in different buffers - other_array_parameters = Dict{Any, Any}() - - hasinputs = inputs !== nothing - input_vars = Dict{Any, AbstractArray{Int}}() - if hasinputs - for (j, x) in enumerate(inputs) - if iscall(x) && operation(x) == getindex - arg = arguments(x)[1] - inds = get!(() -> Int[], input_vars, arg) - push!(inds, j) - end - end - for (k, inds) in input_vars - if inds == (inds′ = inds[1]:inds[end]) - input_vars[k] = inds′ - end - end - end - if has_index_cache(sys) - ic = get_index_cache(sys) else - ic = nothing - end - if ps isa Tuple && eltype(ps) <: AbstractArray - ps = Iterators.flatten(ps) - end - for p in ps - p = unwrap(p) - if iscall(p) && operation(p) == getindex - p = arguments(p)[1] - end - symtype(p) <: AbstractArray && Symbolics.shape(p) != Symbolics.Unknown() || continue - scal = collect(p) - # all scalarized variables are in `ps` - any(isequal(p), ps) || all(x -> any(isequal(x), ps), scal) || continue - (haskey(array_parameters, p) || haskey(other_array_parameters, p)) && continue - - idx = parameter_index(sys, p) - idx isa Int && continue - if idx isa ParameterIndex - if idx.portion != SciMLStructures.Tunable() + uind = 1 + for (i, x) in enumerate(dvs) + iscall(x) && operation(x) == getindex || continue + arg = arguments(x)[1] + inds = get!(() -> [], var_to_arridxs, arg) + push!(inds, (uind, i)) + end + end + p_start = uind + 1 + (inputs !== nothing) + history + input_ind = inputs === nothing ? -1 : (p_start - 1) + rps = (reorder_parameters(sys, ps)..., cachesyms...) + for sym in reduce(vcat, rps; init = []) + iscall(sym) && operation(sym) == getindex || continue + arg = arguments(sym)[1] + if inputs !== nothing + idx = findfirst(isequal(sym), inputs) + if idx !== nothing + inds = get!(() -> [], var_to_arridxs, arg) + push!(inds, (input_ind, idx)) continue end - array_parameters[p] = (vec(idx.idx), 1, size(idx.idx)) - else - # idx === nothing - idxs = map(Base.Fix1(parameter_index, sys), scal) - if first(idxs) isa ParameterIndex - buffer_idxs = map(Base.Fix1(iterated_buffer_index, ic), idxs) - if allequal(buffer_idxs) - buffer_idx = first(buffer_idxs) - if first(idxs).portion == SciMLStructures.Tunable() - idxs = map(x -> x.idx, idxs) - else - idxs = map(x -> x.idx[end], idxs) - end - else - other_array_parameters[p] = scal - continue - end - else - buffer_idx = 1 - end - - sz = size(idxs) - if vec(idxs) == idxs[begin]:idxs[end] - idxs = idxs[begin]:idxs[end] - elseif vec(idxs) == idxs[begin]:-1:idxs[end] - idxs = idxs[begin]:-1:idxs[end] - end - idxs = vec(idxs) - array_parameters[p] = (idxs, buffer_idx, sz) end + bufferidx = findfirst(buf -> any(isequal(sym), buf), rps) + idxinbuffer = findfirst(isequal(sym), rps[bufferidx]) + inds = get!(() -> [], var_to_arridxs, arg) + push!(inds, (p_start + bufferidx - 1, idxinbuffer)) end - inputind = if history - uind + 2 - else - uind + 1 - end - params_offset = if history && hasinputs - uind + 2 - elseif history || hasinputs - uind + 1 - else - uind + viewsyms = Dict() + splitsyms = Dict() + for (arrsym, idxs) in var_to_arridxs + length(idxs) == length(arrsym) || continue + # allequal(first, idxs) is a 1.11 feature + if allequal(Iterators.map(first, idxs)) + viewsyms[arrsym] = (first(first(idxs)), reshape(last.(idxs), size(arrsym))) + else + splitsyms[arrsym] = reshape(idxs, size(arrsym)) + end end if isscalar function (expr) @@ -349,15 +285,11 @@ function wrap_array_vars( [], Let( vcat( - [k ← :(view($(expr.args[uind].name), $v)) for (k, v) in array_vars], - [k ← :(view($(expr.args[inputind].name), $v)) - for (k, v) in input_vars], - [k ← :(reshape( - view($(expr.args[params_offset + buffer_idx].name), $idxs), - $sz)) - for (k, (idxs, buffer_idx, sz)) in array_parameters], - [k ← Code.MakeArray(v, symtype(k)) - for (k, v) in other_array_parameters] + [sym ← :(view($(expr.args[i].name), $idxs)) + for (sym, (i, idxs)) in viewsyms], + [sym ← + MakeArray([expr.args[bufi].elems[vali] for (bufi, vali) in idxs], + expr.args[idxs[1][1]]) for (sym, idxs) in splitsyms] ), expr.body, false @@ -371,15 +303,11 @@ function wrap_array_vars( [], Let( vcat( - [k ← :(view($(expr.args[uind].name), $v)) for (k, v) in array_vars], - [k ← :(view($(expr.args[inputind].name), $v)) - for (k, v) in input_vars], - [k ← :(reshape( - view($(expr.args[params_offset + buffer_idx].name), $idxs), - $sz)) - for (k, (idxs, buffer_idx, sz)) in array_parameters], - [k ← Code.MakeArray(v, symtype(k)) - for (k, v) in other_array_parameters] + [sym ← :(view($(expr.args[i].name), $idxs)) + for (sym, (i, idxs)) in viewsyms], + [sym ← + MakeArray([expr.args[bufi].elems[vali] for (bufi, vali) in idxs], + expr.args[idxs[1][1]]) for (sym, idxs) in splitsyms] ), expr.body, false @@ -392,17 +320,11 @@ function wrap_array_vars( [], Let( vcat( - [k ← :(view($(expr.args[uind + 1].name), $v)) - for (k, v) in array_vars], - [k ← :(view($(expr.args[inputind + 1].name), $v)) - for (k, v) in input_vars], - [k ← :(reshape( - view($(expr.args[params_offset + buffer_idx + 1].name), - $idxs), - $sz)) - for (k, (idxs, buffer_idx, sz)) in array_parameters], - [k ← Code.MakeArray(v, symtype(k)) - for (k, v) in other_array_parameters] + [sym ← :(view($(expr.args[i + 1].name), $idxs)) + for (sym, (i, idxs)) in viewsyms], + [sym ← MakeArray( + [expr.args[bufi + 1].elems[vali] for (bufi, vali) in idxs], + expr.args[idxs[1][1] + 1]) for (sym, idxs) in splitsyms] ), expr.body, false diff --git a/src/systems/nonlinear/nonlinearsystem.jl b/src/systems/nonlinear/nonlinearsystem.jl index 1289388197..a7028b3a1e 100644 --- a/src/systems/nonlinear/nonlinearsystem.jl +++ b/src/systems/nonlinear/nonlinearsystem.jl @@ -535,6 +535,117 @@ function DiffEqBase.NonlinearLeastSquaresProblem{iip}(sys::NonlinearSystem, u0ma NonlinearLeastSquaresProblem{iip}(f, u0, p; filter_kwargs(kwargs)...) end +struct CacheWriter{F} + fn::F +end + +function (cw::CacheWriter)(p, sols) + cw.fn(p.caches[1], sols, p...) +end + +function CacheWriter(sys::AbstractSystem, exprs, solsyms; + eval_expression = false, eval_module = @__MODULE__) + ps = parameters(sys) + rps = reorder_parameters(sys, ps) + fn = Func( + [:out, DestructuredArgs(DestructuredArgs.(solsyms)), + DestructuredArgs.(rps)...], + [], + SetArray(true, :out, exprs) + ) |> wrap_parameter_dependencies(sys, false)[2] |> + wrap_array_vars(sys, exprs; dvs = nothing)[2] |> toexpr + return CacheWriter(eval_or_rgf(fn; eval_expression, eval_module)) +end + +struct SCCNonlinearFunction{iip} end + +function SCCNonlinearFunction{iip}( + sys::NonlinearSystem, vscc, escc, cachesyms; eval_expression = false, + eval_module = @__MODULE__, kwargs...) where {iip} + dvs = unknowns(sys) + ps = parameters(sys) + rps = reorder_parameters(sys, ps) + eqs = equations(sys) + obs = observed(sys) + + _dvs = dvs[vscc] + _eqs = eqs[escc] + obsidxs = observed_equations_used_by(sys, _eqs) + _obs = obs[obsidxs] + + cmap, cs = get_cmap(sys) + assignments = [eq.lhs ← eq.rhs for eq in cmap] + rhss = [eq.rhs - eq.lhs for eq in _eqs] + wrap_code = wrap_assignments(false, assignments) .∘ + (wrap_array_vars(sys, rhss; dvs = _dvs, cachesyms)) .∘ + wrap_parameter_dependencies(sys, false) + @show _dvs + f_gen = build_function( + rhss, _dvs, ps..., cachesyms...; wrap_code, expression = Val{true}) + f_oop, f_iip = eval_or_rgf.(f_gen; eval_expression, eval_module) + + f(u, p) = f_oop(u, p) + f(u, p::MTKParameters) = f_oop(u, p...) + f(resid, u, p) = f_iip(resid, u, p) + f(resid, u, p::MTKParameters) = f_iip(resid, u, p...) + + return NonlinearFunction{iip}(f) +end + +function SciMLBase.SCCNonlinearProblem(sys::NonlinearSystem, args...; kwargs...) + SCCNonlinearProblem{true}(sys, args...; kwargs...) +end + +function SciMLBase.SCCNonlinearProblem{iip}(sys::NonlinearSystem, u0map, + parammap = SciMLBase.NullParameters(); eval_expression = false, eval_module = @__MODULE__, kwargs...) where {iip} + if !iscomplete(sys) || get_tearing_state(sys) === nothing + error("A simplified `NonlinearSystem` is required. Call `structural_simplify` on the system before creating an `SCCNonlinearProblem`.") + end + + if !is_split(sys) + error("The system has been simplified with `split = false`. `SCCNonlinearProblem` is not compatible with this system. Pass `split = true` to `structural_simplify` to use `SCCNonlinearProblem`.") + end + + ts = get_tearing_state(sys) + var_eq_matching, var_sccs = StructuralTransformations.algebraic_variables_scc(ts) + condensed_graph = StructuralTransformations.MatchedCondensationGraph( + StructuralTransformations.DiCMOBiGraph{true}(ts.structure.graph, var_eq_matching), + var_sccs) + toporder = topological_sort_by_dfs(condensed_graph) + var_sccs = var_sccs[toporder] + eq_sccs = map(Base.Fix1(getindex, var_eq_matching), var_sccs) + + dvs = unknowns(sys) + ps = parameters(sys) + eqs = equations(sys) + obs = observed(sys) + + _, u0, p = process_SciMLProblem( + EmptySciMLFunction, sys, u0map, parammap; eval_expression, eval_module, kwargs...) + p = rebuild_with_caches(p, BufferTemplate(eltype(u0), length(u0))) + + subprobs = [] + explicitfuns = [] + for (i, (escc, vscc)) in enumerate(zip(eq_sccs, var_sccs)) + oldvars = dvs[reduce(vcat, view(var_sccs, 1:(i - 1)); init = Int[])] + if isempty(oldvars) + push!(explicitfuns, (_...) -> nothing) + else + solsyms = getindex.((dvs,), view(var_sccs, 1:(i - 1))) + push!(explicitfuns, + CacheWriter(sys, oldvars, solsyms; eval_expression, eval_module)) + end + prob = NonlinearProblem( + SCCNonlinearFunction{iip}( + sys, vscc, escc, (oldvars,); eval_expression, eval_module, kwargs...), + u0[vscc], + p) + push!(subprobs, prob) + end + + return SCCNonlinearProblem(subprobs, explicitfuns) +end + """ $(TYPEDSIGNATURES) diff --git a/src/systems/parameter_buffer.jl b/src/systems/parameter_buffer.jl index 72af9f594d..7d64054acc 100644 --- a/src/systems/parameter_buffer.jl +++ b/src/systems/parameter_buffer.jl @@ -3,11 +3,12 @@ symconvert(::Type{T}, x) where {T} = convert(T, x) symconvert(::Type{Real}, x::Integer) = convert(Float64, x) symconvert(::Type{V}, x) where {V <: AbstractArray} = convert(V, symconvert.(eltype(V), x)) -struct MTKParameters{T, D, C, N} +struct MTKParameters{T, D, C, N, H} tunable::T discrete::D constant::C nonnumeric::N + caches::H end """ @@ -181,11 +182,18 @@ function MTKParameters( mtkps = MTKParameters{ typeof(tunable_buffer), typeof(disc_buffer), typeof(const_buffer), - typeof(nonnumeric_buffer)}(tunable_buffer, disc_buffer, const_buffer, - nonnumeric_buffer) + typeof(nonnumeric_buffer), typeof(())}(tunable_buffer, + disc_buffer, const_buffer, nonnumeric_buffer, ()) return mtkps end +function rebuild_with_caches(p::MTKParameters, cache_templates::BufferTemplate...) + buffers = map(cache_templates) do template + Vector{template.type}(undef, template.length) + end + @set p.caches = buffers +end + function narrow_buffer_type(buffer::AbstractArray) type = Union{} for x in buffer @@ -297,7 +305,8 @@ end for (Portion, field, recurse) in [(SciMLStructures.Discrete, :discrete, 1) (SciMLStructures.Constants, :constant, 1) - (Nonnumeric, :nonnumeric, 1)] + (Nonnumeric, :nonnumeric, 1) + (SciMLStructures.Caches, :caches, 1)] @eval function SciMLStructures.canonicalize(::$Portion, p::MTKParameters) as_vector = buffer_to_arraypartition(p.$field) repack = let p = p @@ -324,11 +333,13 @@ function Base.copy(p::MTKParameters) discrete = Tuple(eltype(buf) <: Real ? copy(buf) : copy.(buf) for buf in p.discrete) constant = Tuple(eltype(buf) <: Real ? copy(buf) : copy.(buf) for buf in p.constant) nonnumeric = copy.(p.nonnumeric) + caches = copy.(p.caches) return MTKParameters( tunable, discrete, constant, - nonnumeric + nonnumeric, + caches ) end @@ -640,7 +651,7 @@ end # getindex indexes the vectors, setindex! linearly indexes values # it's inconsistent, but we need it to be this way @generated function Base.getindex( - ps::MTKParameters{T, D, C, N}, idx::Int) where {T, D, C, N} + ps::MTKParameters{T, D, C, N, H}, idx::Int) where {T, D, C, N, H} paths = [] if !(T <: SizedVector{0, Float64}) push!(paths, :(ps.tunable)) @@ -654,6 +665,9 @@ end for i in 1:fieldcount(N) push!(paths, :(ps.nonnumeric[$i])) end + for i in 1:fieldcount(H) + push!(paths, :(ps.caches[$i])) + end expr = Expr(:if, :(idx == 1), :(return $(paths[1]))) curexpr = expr for i in 2:length(paths) @@ -663,12 +677,12 @@ end return Expr(:block, expr, :(throw(BoundsError(ps, idx)))) end -@generated function Base.length(ps::MTKParameters{T, D, C, N}) where {T, D, C, N} +@generated function Base.length(ps::MTKParameters{T, D, C, N, H}) where {T, D, C, N, H} len = 0 if !(T <: SizedVector{0, Float64}) len += 1 end - len += fieldcount(D) + fieldcount(C) + fieldcount(N) + len += fieldcount(D) + fieldcount(C) + fieldcount(N) + fieldcount(H) return len end @@ -691,7 +705,10 @@ end function Base.:(==)(a::MTKParameters, b::MTKParameters) return a.tunable == b.tunable && a.discrete == b.discrete && - a.constant == b.constant && a.nonnumeric == b.nonnumeric + a.constant == b.constant && a.nonnumeric == b.nonnumeric && + all(Iterators.map(a.caches, b.caches) do acache, bcache + eltype(acache) == eltype(bcache) && length(acache) == length(bcache) + end) end # to support linearize/linearization_function diff --git a/src/utils.jl b/src/utils.jl index 416efd8f2c..7be247429c 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -1007,3 +1007,50 @@ function is_variable_floatingpoint(sym) return T == Real || T <: AbstractFloat || T <: AbstractArray{Real} || T <: AbstractArray{<:AbstractFloat} end + +function observed_dependency_graph(eqs::Vector{Equation}) + for eq in eqs + if symbolic_type(eq.lhs) == NotSymbolic() + error("All equations must be observed equations of the form `var ~ expr`. Got $eq") + end + end + + idxmap = Dict(eq.lhs => i for (i, eq) in enumerate(eqs)) + g = SimpleDiGraph(length(eqs)) + + syms = Set() + for (i, eq) in enumerate(eqs) + vars!(syms, eq) + for sym in syms + idx = get(idxmap, sym, nothing) + idx === nothing && continue + add_edge!(g, i, idx) + end + end + + return g +end + +function observed_equations_used_by(sys::AbstractSystem, exprs) + obs = observed(sys) + + obsvars = getproperty.(obs, :lhs) + graph = observed_dependency_graph(obs) + + syms = vars(exprs) + + obsidxs = BitSet() + for sym in syms + idx = findfirst(isequal(sym), obsvars) + idx === nothing && continue + parents = dfs_parents(graph, idx) + for i in eachindex(parents) + parents[i] == 0 && continue + push!(obsidxs, i) + end + end + + obsidxs = collect(obsidxs) + sort!(obsidxs) + return obsidxs +end From 7d3b3f41dc7cd9cd746b2cdef7ca24896ba2e6ed Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 18 Nov 2024 15:35:49 +0530 Subject: [PATCH 0424/1337] refactor: no need to re-sort SCCs --- src/systems/nonlinear/nonlinearsystem.jl | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/systems/nonlinear/nonlinearsystem.jl b/src/systems/nonlinear/nonlinearsystem.jl index a7028b3a1e..68758de169 100644 --- a/src/systems/nonlinear/nonlinearsystem.jl +++ b/src/systems/nonlinear/nonlinearsystem.jl @@ -579,9 +579,8 @@ function SCCNonlinearFunction{iip}( wrap_code = wrap_assignments(false, assignments) .∘ (wrap_array_vars(sys, rhss; dvs = _dvs, cachesyms)) .∘ wrap_parameter_dependencies(sys, false) - @show _dvs f_gen = build_function( - rhss, _dvs, ps..., cachesyms...; wrap_code, expression = Val{true}) + rhss, _dvs, rps..., cachesyms...; wrap_code, expression = Val{true}) f_oop, f_iip = eval_or_rgf.(f_gen; eval_expression, eval_module) f(u, p) = f_oop(u, p) @@ -608,11 +607,9 @@ function SciMLBase.SCCNonlinearProblem{iip}(sys::NonlinearSystem, u0map, ts = get_tearing_state(sys) var_eq_matching, var_sccs = StructuralTransformations.algebraic_variables_scc(ts) - condensed_graph = StructuralTransformations.MatchedCondensationGraph( - StructuralTransformations.DiCMOBiGraph{true}(ts.structure.graph, var_eq_matching), - var_sccs) - toporder = topological_sort_by_dfs(condensed_graph) - var_sccs = var_sccs[toporder] + # The system is simplified, so SCCs are already in sorted order. We just need to get them and sort + # according to index in unknowns(sys) + sort!(var_sccs) eq_sccs = map(Base.Fix1(getindex, var_eq_matching), var_sccs) dvs = unknowns(sys) From 3e4a648488e9bd1a1fc276798a5b1274d8463eac Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 18 Nov 2024 19:27:58 +0530 Subject: [PATCH 0425/1337] fix: minor bug fix --- src/systems/nonlinear/nonlinearsystem.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/systems/nonlinear/nonlinearsystem.jl b/src/systems/nonlinear/nonlinearsystem.jl index 68758de169..1742e83371 100644 --- a/src/systems/nonlinear/nonlinearsystem.jl +++ b/src/systems/nonlinear/nonlinearsystem.jl @@ -553,7 +553,7 @@ function CacheWriter(sys::AbstractSystem, exprs, solsyms; [], SetArray(true, :out, exprs) ) |> wrap_parameter_dependencies(sys, false)[2] |> - wrap_array_vars(sys, exprs; dvs = nothing)[2] |> toexpr + wrap_array_vars(sys, exprs; dvs = nothing, inputs = [])[2] |> toexpr return CacheWriter(eval_or_rgf(fn; eval_expression, eval_module)) end From b99ab7d12c897333d28c4fe3bd01c34103069065 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 18 Nov 2024 21:51:43 +0530 Subject: [PATCH 0426/1337] fix: fix observed equations not being generated --- src/systems/nonlinear/nonlinearsystem.jl | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/systems/nonlinear/nonlinearsystem.jl b/src/systems/nonlinear/nonlinearsystem.jl index 1742e83371..9e6208ed10 100644 --- a/src/systems/nonlinear/nonlinearsystem.jl +++ b/src/systems/nonlinear/nonlinearsystem.jl @@ -572,13 +572,15 @@ function SCCNonlinearFunction{iip}( _eqs = eqs[escc] obsidxs = observed_equations_used_by(sys, _eqs) _obs = obs[obsidxs] + obs_assignments = [eq.lhs ← eq.rhs for eq in _obs] cmap, cs = get_cmap(sys) - assignments = [eq.lhs ← eq.rhs for eq in cmap] + cmap_assignments = [eq.lhs ← eq.rhs for eq in cmap] rhss = [eq.rhs - eq.lhs for eq in _eqs] - wrap_code = wrap_assignments(false, assignments) .∘ + wrap_code = wrap_assignments(false, cmap_assignments) .∘ (wrap_array_vars(sys, rhss; dvs = _dvs, cachesyms)) .∘ - wrap_parameter_dependencies(sys, false) + wrap_parameter_dependencies(sys, false) .∘ + wrap_assignments(false, obs_assignments) f_gen = build_function( rhss, _dvs, rps..., cachesyms...; wrap_code, expression = Val{true}) f_oop, f_iip = eval_or_rgf.(f_gen; eval_expression, eval_module) From 04c0cf883fe5714d32d3b04ecdc69c503318292b Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 18 Nov 2024 21:52:02 +0530 Subject: [PATCH 0427/1337] test: add tests for `SCCNonlinearProblem` codegen --- test/runtests.jl | 1 + test/scc_nonlinear_problem.jl | 145 ++++++++++++++++++++++++++++++++++ 2 files changed, 146 insertions(+) create mode 100644 test/scc_nonlinear_problem.jl diff --git a/test/runtests.jl b/test/runtests.jl index 44846eed57..677f40c717 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -78,6 +78,7 @@ end @safetestset "SDESystem Test" include("sdesystem.jl") @safetestset "DDESystem Test" include("dde.jl") @safetestset "NonlinearSystem Test" include("nonlinearsystem.jl") + @safetestset "SCCNonlinearProblem Test" include("scc_nonlinear_problem.jl") @safetestset "PDE Construction Test" include("pde.jl") @safetestset "JumpSystem Test" include("jumpsystem.jl") @safetestset "print_tree" include("print_tree.jl") diff --git a/test/scc_nonlinear_problem.jl b/test/scc_nonlinear_problem.jl new file mode 100644 index 0000000000..c4368bf0b9 --- /dev/null +++ b/test/scc_nonlinear_problem.jl @@ -0,0 +1,145 @@ +using ModelingToolkit +using NonlinearSolve, SCCNonlinearSolve +using OrdinaryDiffEq +using SciMLBase, Symbolics +using LinearAlgebra, Test + +@testset "Trivial case" begin + function f!(du, u, p) + du[1] = cos(u[2]) - u[1] + du[2] = sin(u[1] + u[2]) + u[2] + du[3] = 2u[4] + u[3] + 1.0 + du[4] = u[5]^2 + u[4] + du[5] = u[3]^2 + u[5] + du[6] = u[1] + u[2] + u[3] + u[4] + u[5] + 2.0u[6] + 2.5u[7] + 1.5u[8] + du[7] = u[1] + u[2] + u[3] + 2.0u[4] + u[5] + 4.0u[6] - 1.5u[7] + 1.5u[8] + du[8] = u[1] + 2.0u[2] + 3.0u[3] + 5.0u[4] + 6.0u[5] + u[6] - u[7] - u[8] + end + @variables u[1:8] [irreducible = true] + eqs = Any[0 for _ in 1:8] + f!(eqs, u, nothing) + eqs = 0 .~ eqs + @named model = NonlinearSystem(eqs) + @test_throws ["simplified", "required"] SCCNonlinearProblem(model, []) + _model = structural_simplify(model; split = false) + @test_throws ["not compatible"] SCCNonlinearProblem(_model, []) + model = structural_simplify(model) + prob = NonlinearProblem(model, [u => zeros(8)]) + sccprob = SCCNonlinearProblem(model, [u => zeros(8)]) + sol1 = solve(prob, NewtonRaphson()) + sol2 = solve(sccprob, NewtonRaphson()) + @test SciMLBase.successful_retcode(sol1) + @test SciMLBase.successful_retcode(sol2) + @test sol1.u ≈ sol2.u +end + +@testset "With parameters" begin + function f!(du, u, (p1, p2), t) + x = (*)(p1[4], u[1]) + y = (*)(p1[4], (+)(0.1016, (*)(-1, u[1]))) + z1 = ifelse((<)(p2[1], 0), + (*)((*)(457896.07999999996, p1[2]), sqrt((*)(1.1686468413521012e-5, p1[3]))), + 0) + z2 = ifelse((>)(p2[1], 0), + (*)((*)((*)(0.58, p1[2]), sqrt((*)(1 // 86100, p1[3]))), u[4]), + 0) + z3 = ifelse((>)(p2[1], 0), + (*)((*)(457896.07999999996, p1[2]), sqrt((*)(1.1686468413521012e-5, p1[3]))), + 0) + z4 = ifelse((<)(p2[1], 0), + (*)((*)((*)(0.58, p1[2]), sqrt((*)(1 // 86100, p1[3]))), u[5]), + 0) + du[1] = p2[1] + du[2] = (+)(z1, (*)(-1, z2)) + du[3] = (+)(z3, (*)(-1, z4)) + du[4] = (+)((*)(-1, u[2]), (*)((*)(1 // 86100, y), u[4])) + du[5] = (+)((*)(-1, u[3]), (*)((*)(1 // 86100, x), u[5])) + end + p = ( + [0.04864391799335977, 7.853981633974484e-5, 1.4034843205574914, + 0.018241469247509915, 300237.05, 9.226186337232914], + [0.0508]) + u0 = [0.0, 0.0, 0.0, 789476.0, 101325.0] + tspan = (0.0, 1.0) + mass_matrix = [1.0 0.0 0.0 0.0 0.0; 0.0 1.0 0.0 0.0 0.0; 0.0 0.0 1.0 0.0 0.0; + 0.0 0.0 0.0 0.0 0.0; 0.0 0.0 0.0 0.0 0.0] + dt = 1e-3 + function nlf(u1, (u0, p)) + resid = Any[0 for _ in u0] + f!(resid, u1, p, 0.0) + return mass_matrix * (u1 - u0) - dt * resid + end + + prob = NonlinearProblem(nlf, u0, (u0, p)) + @test_throws Exception solve(prob, SimpleNewtonRaphson(), abstol = 1e-9) + sol = solve(prob, TrustRegion(); abstol = 1e-9) + + @variables u[1:5] [irreducible = true] + @parameters p1[1:6] p2 + eqs = 0 .~ collect(nlf(u, (u0, (p1, p2)))) + @mtkbuild sys = NonlinearSystem(eqs, [u], [p1, p2]) + sccprob = SCCNonlinearProblem(sys, [u => u0], [p1 => p[1], p2 => p[2][]]) + sccsol = solve(sccprob, SimpleNewtonRaphson(); abstol = 1e-9) + @test SciMLBase.successful_retcode(sccsol) + @test norm(sccsol.resid) < norm(sol.resid) +end + +@testset "Transistor amplifier" begin + C = [k * 1e-6 for k in 1:5] + Ub = 6 + UF = 0.026 + α = 0.99 + β = 1e-6 + R0 = 1000 + R = 9000 + Ue(t) = 0.1 * sin(200 * π * t) + + function transamp(out, du, u, p, t) + g(x) = 1e-6 * (exp(x / 0.026) - 1) + y1, y2, y3, y4, y5, y6, y7, y8 = u + out[1] = -Ue(t) / R0 + y1 / R0 + C[1] * du[1] - C[1] * du[2] + out[2] = -Ub / R + y2 * 2 / R - (α - 1) * g(y2 - y3) - C[1] * du[1] + C[1] * du[2] + out[3] = -g(y2 - y3) + y3 / R + C[2] * du[3] + out[4] = -Ub / R + y4 / R + α * g(y2 - y3) + C[3] * du[4] - C[3] * du[5] + out[5] = -Ub / R + y5 * 2 / R - (α - 1) * g(y5 - y6) - C[3] * du[4] + C[3] * du[5] + out[6] = -g(y5 - y6) + y6 / R + C[4] * du[6] + out[7] = -Ub / R + y7 / R + α * g(y5 - y6) + C[5] * du[7] - C[5] * du[8] + out[8] = y8 / R - C[5] * du[7] + C[5] * du[8] + end + + u0 = [0, Ub / 2, Ub / 2, Ub, Ub / 2, Ub / 2, Ub, 0] + du0 = [ + 51.338775, + 51.338775, + -Ub / (2 * (C[2] * R)), + -24.9757667, + -24.9757667, + -Ub / (2 * (C[4] * R)), + -10.00564453, + -10.00564453 + ] + daeprob = DAEProblem(transamp, du0, u0, (0.0, 0.1)) + daesol = solve(daeprob, DImplicitEuler()) + + t0 = daesol.t[5] + t1 = daesol.t[6] + u0 = daesol.u[5] + u1 = daesol.u[6] + dt = t1 - t0 + + @variables y(t)[1:8] + eqs = Any[0 for _ in 1:8] + transamp(eqs, collect(D(y)), y, nothing, t) + eqs = 0 .~ eqs + subrules = Dict(Symbolics.unwrap(D(y[i])) => ((y[i] - u0[i]) / dt) for i in 1:8) + eqs = substitute.(eqs, (subrules,)) + @mtkbuild sys = NonlinearSystem(eqs) + prob = NonlinearProblem(sys, [y => u0], [t => t0]) + sol = solve(prob, NewtonRaphson(); abstol = 1e-12) + + sccprob = SCCNonlinearProblem(sys, [y => u0], [t => t0]) + sccsol = solve(sccprob, NewtonRaphson(); abstol = 1e-12) + + @test sol.u≈sccsol.u atol=1e-10 +end + From 5639bd11c390974a37ead01cca46d4959680dee8 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 19 Nov 2024 14:37:05 +0530 Subject: [PATCH 0428/1337] feat: pre-compute observed equations of previous SCCs --- src/systems/nonlinear/nonlinearsystem.jl | 53 +++++++++++++++--------- 1 file changed, 34 insertions(+), 19 deletions(-) diff --git a/src/systems/nonlinear/nonlinearsystem.jl b/src/systems/nonlinear/nonlinearsystem.jl index 9e6208ed10..b608fd642a 100644 --- a/src/systems/nonlinear/nonlinearsystem.jl +++ b/src/systems/nonlinear/nonlinearsystem.jl @@ -560,18 +560,11 @@ end struct SCCNonlinearFunction{iip} end function SCCNonlinearFunction{iip}( - sys::NonlinearSystem, vscc, escc, cachesyms; eval_expression = false, + sys::NonlinearSystem, _eqs, _dvs, _obs, cachesyms; eval_expression = false, eval_module = @__MODULE__, kwargs...) where {iip} - dvs = unknowns(sys) ps = parameters(sys) rps = reorder_parameters(sys, ps) - eqs = equations(sys) - obs = observed(sys) - _dvs = dvs[vscc] - _eqs = eqs[escc] - obsidxs = observed_equations_used_by(sys, _eqs) - _obs = obs[obsidxs] obs_assignments = [eq.lhs ← eq.rhs for eq in _obs] cmap, cs = get_cmap(sys) @@ -621,24 +614,46 @@ function SciMLBase.SCCNonlinearProblem{iip}(sys::NonlinearSystem, u0map, _, u0, p = process_SciMLProblem( EmptySciMLFunction, sys, u0map, parammap; eval_expression, eval_module, kwargs...) - p = rebuild_with_caches(p, BufferTemplate(eltype(u0), length(u0))) - subprobs = [] explicitfuns = [] + nlfuns = [] + prevobsidxs = Int[] + cachevars = [] + cacheexprs = [] for (i, (escc, vscc)) in enumerate(zip(eq_sccs, var_sccs)) - oldvars = dvs[reduce(vcat, view(var_sccs, 1:(i - 1)); init = Int[])] - if isempty(oldvars) - push!(explicitfuns, (_...) -> nothing) + # subset unknowns and equations + _dvs = dvs[vscc] + _eqs = eqs[escc] + # get observed equations required by this SCC + obsidxs = observed_equations_used_by(sys, _eqs) + # the ones used by previous SCCs can be precomputed into the cache + setdiff!(obsidxs, prevobsidxs) + _obs = obs[obsidxs] + + if isempty(cachevars) + push!(explicitfuns, Returns(nothing)) else solsyms = getindex.((dvs,), view(var_sccs, 1:(i - 1))) push!(explicitfuns, - CacheWriter(sys, oldvars, solsyms; eval_expression, eval_module)) + CacheWriter(sys, cacheexprs, solsyms; eval_expression, eval_module)) + end + f = SCCNonlinearFunction{iip}( + sys, _eqs, _dvs, _obs, (cachevars,); eval_expression, eval_module, kwargs...) + push!(nlfuns, f) + append!(cachevars, _dvs) + append!(cacheexprs, _dvs) + for i in obsidxs + push!(cachevars, obs[i].lhs) + push!(cacheexprs, obs[i].rhs) end - prob = NonlinearProblem( - SCCNonlinearFunction{iip}( - sys, vscc, escc, (oldvars,); eval_expression, eval_module, kwargs...), - u0[vscc], - p) + append!(prevobsidxs, obsidxs) + end + + p = rebuild_with_caches(p, BufferTemplate(eltype(u0), length(cachevars))) + + subprobs = [] + for (f, vscc) in zip(nlfuns, var_sccs) + prob = NonlinearProblem(f, u0[vscc], p) push!(subprobs, prob) end From 26269d974ebddd971e918b840a0d2d7fdf4f39b2 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 19 Nov 2024 14:37:22 +0530 Subject: [PATCH 0429/1337] feat: subset system and pass to SCC problems --- src/systems/index_cache.jl | 25 ++++++++++++++++++++++++ src/systems/nonlinear/nonlinearsystem.jl | 8 +++++++- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/src/systems/index_cache.jl b/src/systems/index_cache.jl index ab0dd08764..aca6a0547d 100644 --- a/src/systems/index_cache.jl +++ b/src/systems/index_cache.jl @@ -594,3 +594,28 @@ function reorder_dimension_by_tunables( reorder_dimension_by_tunables!(buffer, sys, arr, syms; dim) return buffer end + +function subset_unknowns_observed( + ic::IndexCache, sys::AbstractSystem, newunknowns, newobsvars) + unknown_idx = copy(ic.unknown_idx) + empty!(unknown_idx) + for (i, sym) in enumerate(newunknowns) + ttsym = default_toterm(sym) + rsym = renamespace(sys, sym) + rttsym = renamespace(sys, ttsym) + unknown_idx[sym] = unknown_idx[ttsym] = unknown_idx[rsym] = unknown_idx[rttsym] = i + end + observed_syms_to_timeseries = copy(ic.observed_syms_to_timeseries) + empty!(observed_syms_to_timeseries) + for sym in newobsvars + ttsym = default_toterm(sym) + rsym = renamespace(sys, sym) + rttsym = renamespace(sys, ttsym) + for s in (sym, ttsym, rsym, rttsym) + observed_syms_to_timeseries[s] = ic.observed_syms_to_timeseries[sym] + end + end + ic = @set ic.unknown_idx = unknown_idx + @set! ic.observed_syms_to_timeseries = observed_syms_to_timeseries + return ic +end diff --git a/src/systems/nonlinear/nonlinearsystem.jl b/src/systems/nonlinear/nonlinearsystem.jl index b608fd642a..40e883222d 100644 --- a/src/systems/nonlinear/nonlinearsystem.jl +++ b/src/systems/nonlinear/nonlinearsystem.jl @@ -583,7 +583,13 @@ function SCCNonlinearFunction{iip}( f(resid, u, p) = f_iip(resid, u, p) f(resid, u, p::MTKParameters) = f_iip(resid, u, p...) - return NonlinearFunction{iip}(f) + subsys = NonlinearSystem(_eqs, _dvs, ps; observed = _obs, parameter_dependencies = parameter_dependencies(sys), name = nameof(sys)) + if get_index_cache(sys) !== nothing + @set! subsys.index_cache = subset_unknowns_observed(get_index_cache(sys), sys, _dvs, getproperty.(_obs, (:lhs,))) + @set! subsys.complete = true + end + + return NonlinearFunction{iip}(f; sys = subsys) end function SciMLBase.SCCNonlinearProblem(sys::NonlinearSystem, args...; kwargs...) From beb7070234a6cb24e9601c1fb7bedc89de847922 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 20 Nov 2024 18:12:24 +0530 Subject: [PATCH 0430/1337] refactor: improve `observed_dependency_graph` and add docstrings --- src/utils.jl | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/src/utils.jl b/src/utils.jl index 7be247429c..d223c835e2 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -1008,29 +1008,27 @@ function is_variable_floatingpoint(sym) T <: AbstractArray{<:AbstractFloat} end +""" + $(TYPEDSIGNATURES) + +Return the `DiCMOBiGraph` denoting the dependencies between observed equations `eqs`. +""" function observed_dependency_graph(eqs::Vector{Equation}) for eq in eqs if symbolic_type(eq.lhs) == NotSymbolic() error("All equations must be observed equations of the form `var ~ expr`. Got $eq") end end - - idxmap = Dict(eq.lhs => i for (i, eq) in enumerate(eqs)) - g = SimpleDiGraph(length(eqs)) - - syms = Set() - for (i, eq) in enumerate(eqs) - vars!(syms, eq) - for sym in syms - idx = get(idxmap, sym, nothing) - idx === nothing && continue - add_edge!(g, i, idx) - end - end - - return g + graph, assigns = observed2graph(eqs, getproperty.(eqs, (:lhs,))) + matching = complete(Matching(Vector{Union{Unassigned, Int}}(assigns))) + return DiCMOBiGraph{false}(graph, matching) end +""" + $(TYPEDSIGNATURES) + +Return the indexes of observed equations of `sys` used by expression `exprs`. +""" function observed_equations_used_by(sys::AbstractSystem, exprs) obs = observed(sys) From 27d7f706dfb56eb995e1d7c686fbb16ce14c9d95 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 20 Nov 2024 18:14:15 +0530 Subject: [PATCH 0431/1337] feat: cache subexpressions dependent only on previous SCCs --- src/systems/nonlinear/nonlinearsystem.jl | 39 +++++++++-- src/utils.jl | 89 ++++++++++++++++++++++++ test/scc_nonlinear_problem.jl | 17 +++++ 3 files changed, 140 insertions(+), 5 deletions(-) diff --git a/src/systems/nonlinear/nonlinearsystem.jl b/src/systems/nonlinear/nonlinearsystem.jl index 40e883222d..84cfe58189 100644 --- a/src/systems/nonlinear/nonlinearsystem.jl +++ b/src/systems/nonlinear/nonlinearsystem.jl @@ -583,9 +583,11 @@ function SCCNonlinearFunction{iip}( f(resid, u, p) = f_iip(resid, u, p) f(resid, u, p::MTKParameters) = f_iip(resid, u, p...) - subsys = NonlinearSystem(_eqs, _dvs, ps; observed = _obs, parameter_dependencies = parameter_dependencies(sys), name = nameof(sys)) + subsys = NonlinearSystem(_eqs, _dvs, ps; observed = _obs, + parameter_dependencies = parameter_dependencies(sys), name = nameof(sys)) if get_index_cache(sys) !== nothing - @set! subsys.index_cache = subset_unknowns_observed(get_index_cache(sys), sys, _dvs, getproperty.(_obs, (:lhs,))) + @set! subsys.index_cache = subset_unknowns_observed( + get_index_cache(sys), sys, _dvs, getproperty.(_obs, (:lhs,))) @set! subsys.complete = true end @@ -624,8 +626,7 @@ function SciMLBase.SCCNonlinearProblem{iip}(sys::NonlinearSystem, u0map, explicitfuns = [] nlfuns = [] prevobsidxs = Int[] - cachevars = [] - cacheexprs = [] + cachesize = 0 for (i, (escc, vscc)) in enumerate(zip(eq_sccs, var_sccs)) # subset unknowns and equations _dvs = dvs[vscc] @@ -636,6 +637,32 @@ function SciMLBase.SCCNonlinearProblem{iip}(sys::NonlinearSystem, u0map, setdiff!(obsidxs, prevobsidxs) _obs = obs[obsidxs] + # get all subexpressions in the RHS which we can precompute in the cache + banned_vars = Set{Any}(vcat(_dvs, getproperty.(_obs, (:lhs,)))) + for var in banned_vars + iscall(var) || continue + operation(var) === getindex || continue + push!(banned_vars, arguments(var)[1]) + end + state = Dict() + for i in eachindex(_obs) + _obs[i] = _obs[i].lhs ~ subexpressions_not_involving_vars!( + _obs[i].rhs, banned_vars, state) + end + for i in eachindex(_eqs) + _eqs[i] = _eqs[i].lhs ~ subexpressions_not_involving_vars!( + _eqs[i].rhs, banned_vars, state) + end + + # cached variables and their corresponding expressions + cachevars = Any[obs[i].lhs for i in prevobsidxs] + cacheexprs = Any[obs[i].rhs for i in prevobsidxs] + for (k, v) in state + push!(cachevars, unwrap(v)) + push!(cacheexprs, unwrap(k)) + end + cachesize = max(cachesize, length(cachevars)) + if isempty(cachevars) push!(explicitfuns, Returns(nothing)) else @@ -655,7 +682,9 @@ function SciMLBase.SCCNonlinearProblem{iip}(sys::NonlinearSystem, u0map, append!(prevobsidxs, obsidxs) end - p = rebuild_with_caches(p, BufferTemplate(eltype(u0), length(cachevars))) + if cachesize != 0 + p = rebuild_with_caches(p, BufferTemplate(eltype(u0), cachesize)) + end subprobs = [] for (f, vscc) in zip(nlfuns, var_sccs) diff --git a/src/utils.jl b/src/utils.jl index d223c835e2..dd7113cbe6 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -1001,6 +1001,11 @@ end diff2term_with_unit(x, t) = _with_unit(diff2term, x, t) lower_varname_with_unit(var, iv, order) = _with_unit(lower_varname, var, iv, iv, order) +""" + $(TYPEDSIGNATURES) + +Check if `sym` represents a symbolic floating point number or array of such numbers. +""" function is_variable_floatingpoint(sym) sym = unwrap(sym) T = symtype(sym) @@ -1052,3 +1057,87 @@ function observed_equations_used_by(sys::AbstractSystem, exprs) sort!(obsidxs) return obsidxs end + +""" + $(TYPEDSIGNATURES) + +Given an expression `expr`, return a dictionary mapping subexpressions of `expr` that do +not involve variables in `vars` to anonymous symbolic variables. Also return the modified +`expr` with the substitutions indicated by the dictionary. If `expr` is a function +of only `vars`, then all of the returned subexpressions can be precomputed. + +Note that this will only process subexpressions floating point value. Additionally, +array variables must be passed in both scalarized and non-scalarized forms in `vars`. +""" +function subexpressions_not_involving_vars(expr, vars) + expr = unwrap(expr) + vars = map(unwrap, vars) + state = Dict() + newexpr = subexpressions_not_involving_vars!(expr, vars, state) + return state, newexpr +end + +""" + $(TYPEDSIGNATURES) + +Mutating version of `subexpressions_not_involving_vars` which writes to `state`. Only +returns the modified `expr`. +""" +function subexpressions_not_involving_vars!(expr, vars, state::Dict{Any, Any}) + expr = unwrap(expr) + symbolic_type(expr) == NotSymbolic() && return expr + iscall(expr) || return expr + is_variable_floatingpoint(expr) || return expr + symtype(expr) <: Union{Real, AbstractArray{<:Real}} || return expr + Symbolics.shape(expr) == Symbolics.Unknown() && return expr + haskey(state, expr) && return state[expr] + vs = ModelingToolkit.vars(expr) + intersect!(vs, vars) + if isempty(vs) + sym = gensym(:subexpr) + stype = symtype(expr) + var = similar_variable(expr, sym) + state[expr] = var + return var + end + op = operation(expr) + args = arguments(expr) + if (op == (+) || op == (*)) && symbolic_type(expr) !== ArraySymbolic() + indep_args = [] + dep_args = [] + for arg in args + _vs = ModelingToolkit.vars(arg) + intersect!(_vs, vars) + if !isempty(_vs) + push!(dep_args, subexpressions_not_involving_vars!(arg, vars, state)) + else + push!(indep_args, arg) + end + end + indep_term = reduce(op, indep_args; init = Int(op == (*))) + indep_term = subexpressions_not_involving_vars!(indep_term, vars, state) + dep_term = reduce(op, dep_args; init = Int(op == (*))) + return op(indep_term, dep_term) + end + newargs = map(args) do arg + symbolic_type(arg) != NotSymbolic() || is_array_of_symbolics(arg) || return arg + subexpressions_not_involving_vars!(arg, vars, state) + end + return maketerm(typeof(expr), op, newargs, metadata(expr)) +end + +""" + $(TYPEDSIGNATURES) + +Create an anonymous symbolic variable of the same shape, size and symtype as `var`, with +name `gensym(name)`. Does not support unsized array symbolics. +""" +function similar_variable(var::BasicSymbolic, name = :anon) + name = gensym(name) + stype = symtype(var) + sym = Symbolics.variable(name; T = stype) + if size(var) !== () + sym = setmetadata(sym, Symbolics.ArrayShapeCtx, map(Base.OneTo, size(var))) + end + return sym +end diff --git a/test/scc_nonlinear_problem.jl b/test/scc_nonlinear_problem.jl index c4368bf0b9..0c4bfe61d0 100644 --- a/test/scc_nonlinear_problem.jl +++ b/test/scc_nonlinear_problem.jl @@ -143,3 +143,20 @@ end @test sol.u≈sccsol.u atol=1e-10 end +@testset "Expression caching" begin + @variables x[1:4] = rand(4) + val = Ref(0) + function func(x, y) + val[] += 1 + x + y + end + @register_symbolic func(x, y) + @mtkbuild sys = NonlinearSystem([0 ~ x[1]^3 + x[2]^3 - 5 + 0 ~ sin(x[1] - x[2]) - 0.5 + 0 ~ func(x[1], x[2]) * exp(x[3]) - x[4]^3 - 5 + 0 ~ func(x[1], x[2]) * exp(x[4]) - x[3]^3 - 4]) + sccprob = SCCNonlinearProblem(sys, []) + sccsol = solve(sccprob, NewtonRaphson()) + @test SciMLBase.successful_retcode(sccsol) + @test val[] == 1 +end From 691747aa1053a6d18eb0ce8fdc80e063eaa3eaed Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 26 Nov 2024 12:16:31 +0530 Subject: [PATCH 0432/1337] feat: use SCCNonlinearProblem for initialization --- Project.toml | 4 +++- src/ModelingToolkit.jl | 1 + src/systems/diffeqs/abstractodesystem.jl | 13 ++++++++----- src/systems/nonlinear/nonlinearsystem.jl | 2 +- src/systems/problem_utils.jl | 6 ++++-- 5 files changed, 17 insertions(+), 9 deletions(-) diff --git a/Project.toml b/Project.toml index b1f5701d13..b520fa3e0c 100644 --- a/Project.toml +++ b/Project.toml @@ -44,6 +44,7 @@ PrecompileTools = "aea7be01-6a6a-4083-8856-8a6e6704d82a" RecursiveArrayTools = "731186ca-8d62-57ce-b412-fbd966d074cd" Reexport = "189a3867-3050-52da-a836-e630ba90ab69" RuntimeGeneratedFunctions = "7e49a35a-f44a-4d26-94aa-eba1b4ca6b47" +SCCNonlinearSolve = "9dfe8606-65a1-4bb3-9748-cb89d1561431" SciMLBase = "0bca4576-84f4-4d90-8ffe-ffa030f20462" SciMLStructures = "53ae85a6-f571-4167-b2af-e1d143709226" Serialization = "9e88b42a-f829-5b0c-bbe9-9e923198166b" @@ -126,7 +127,8 @@ REPL = "1" RecursiveArrayTools = "3.26" Reexport = "0.2, 1" RuntimeGeneratedFunctions = "0.5.9" -SciMLBase = "2.64" +SCCNonlinearSolve = "1.0.0" +SciMLBase = "2.65" SciMLStructures = "1.0" Serialization = "1" Setfield = "0.7, 0.8, 1" diff --git a/src/ModelingToolkit.jl b/src/ModelingToolkit.jl index de8e69c41f..59be358349 100644 --- a/src/ModelingToolkit.jl +++ b/src/ModelingToolkit.jl @@ -50,6 +50,7 @@ using Distributed import JuliaFormatter using MLStyle using NonlinearSolve +import SCCNonlinearSolve using Reexport using RecursiveArrayTools import Graphs: SimpleDiGraph, add_edge!, incidence_matrix diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index af96c4fcfe..ac8870fcaa 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -1301,6 +1301,7 @@ function InitializationProblem{iip, specialize}(sys::AbstractODESystem, initialization_eqs = [], fully_determined = nothing, check_units = true, + use_scc = true, kwargs...) where {iip, specialize} if !iscomplete(sys) error("A completed system is required. Call `complete` or `structural_simplify` on the system before creating an `ODEProblem`") @@ -1318,8 +1319,8 @@ function InitializationProblem{iip, specialize}(sys::AbstractODESystem, end ts = get_tearing_state(isys) - if warn_initialize_determined && - (unassigned_vars = StructuralTransformations.singular_check(ts); !isempty(unassigned_vars)) + unassigned_vars = StructuralTransformations.singular_check(ts) + if warn_initialize_determined && !isempty(unassigned_vars) errmsg = """ The initialization system is structurally singular. Guess values may \ significantly affect the initial values of the ODE. The problematic variables \ @@ -1381,9 +1382,11 @@ function InitializationProblem{iip, specialize}(sys::AbstractODESystem, end for (k, v) in u0map) end - if neqs == nunknown - NonlinearProblem(isys, u0map, parammap; kwargs...) + + TProb = if neqs == nunknown && isempty(unassigned_vars) + use_scc && neqs > 0 && is_split(isys) ? SCCNonlinearProblem : NonlinearProblem else - NonlinearLeastSquaresProblem(isys, u0map, parammap; kwargs...) + NonlinearLeastSquaresProblem end + TProb(isys, u0map, parammap; kwargs...) end diff --git a/src/systems/nonlinear/nonlinearsystem.jl b/src/systems/nonlinear/nonlinearsystem.jl index 84cfe58189..4299eba62d 100644 --- a/src/systems/nonlinear/nonlinearsystem.jl +++ b/src/systems/nonlinear/nonlinearsystem.jl @@ -692,7 +692,7 @@ function SciMLBase.SCCNonlinearProblem{iip}(sys::NonlinearSystem, u0map, push!(subprobs, prob) end - return SCCNonlinearProblem(subprobs, explicitfuns) + return SCCNonlinearProblem(subprobs, explicitfuns, sys, p) end """ diff --git a/src/systems/problem_utils.jl b/src/systems/problem_utils.jl index b837eef98e..ebc5a5bd75 100644 --- a/src/systems/problem_utils.jl +++ b/src/systems/problem_utils.jl @@ -541,6 +541,8 @@ Keyword arguments: Only applicable if `warn_cyclic_dependency == true`. - `substitution_limit`: The number times to substitute initial conditions into each other to attempt to arrive at a numeric value. +- `use_scc`: Whether to use `SCCNonlinearProblem` for initialization if the system is fully + determined. All other keyword arguments are passed as-is to `constructor`. """ @@ -554,7 +556,7 @@ function process_SciMLProblem( symbolic_u0 = false, warn_cyclic_dependency = false, circular_dependency_max_cycle_length = length(all_symbols(sys)), circular_dependency_max_cycles = 10, - substitution_limit = 100, kwargs...) + substitution_limit = 100, use_scc = true, kwargs...) dvs = unknowns(sys) ps = parameters(sys) iv = has_iv(sys) ? get_iv(sys) : nothing @@ -607,7 +609,7 @@ function process_SciMLProblem( sys, t, u0map, pmap; guesses, warn_initialize_determined, initialization_eqs, eval_expression, eval_module, fully_determined, warn_cyclic_dependency, check_units = check_initialization_units, - circular_dependency_max_cycle_length, circular_dependency_max_cycles) + circular_dependency_max_cycle_length, circular_dependency_max_cycles, use_scc) initializeprobmap = getu(initializeprob, unknowns(sys)) punknowns = [p From b5fc3ed1e19b9a0d38ba22c67afdf9dcc43751c1 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 26 Nov 2024 16:57:58 +0530 Subject: [PATCH 0433/1337] refactor: better handle inputs in `wrap_array_vars` --- src/systems/abstractsystem.jl | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index 60ed2fa1ce..e44f250a7f 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -247,20 +247,15 @@ function wrap_array_vars( push!(inds, (uind, i)) end end - p_start = uind + 1 + (inputs !== nothing) + history - input_ind = inputs === nothing ? -1 : (p_start - 1) + p_start = uind + 1 + history rps = (reorder_parameters(sys, ps)..., cachesyms...) + if inputs !== nothing + rps = (inputs, rps...) + end for sym in reduce(vcat, rps; init = []) iscall(sym) && operation(sym) == getindex || continue arg = arguments(sym)[1] - if inputs !== nothing - idx = findfirst(isequal(sym), inputs) - if idx !== nothing - inds = get!(() -> [], var_to_arridxs, arg) - push!(inds, (input_ind, idx)) - continue - end - end + bufferidx = findfirst(buf -> any(isequal(sym), buf), rps) idxinbuffer = findfirst(isequal(sym), rps[bufferidx]) inds = get!(() -> [], var_to_arridxs, arg) From e79dc902fc3377a8ff5adb5623d1bb26a969ecbe Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 26 Nov 2024 16:58:22 +0530 Subject: [PATCH 0434/1337] fix: properly sort SCCs --- src/systems/nonlinear/nonlinearsystem.jl | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/systems/nonlinear/nonlinearsystem.jl b/src/systems/nonlinear/nonlinearsystem.jl index 4299eba62d..bb37dadb8c 100644 --- a/src/systems/nonlinear/nonlinearsystem.jl +++ b/src/systems/nonlinear/nonlinearsystem.jl @@ -610,9 +610,17 @@ function SciMLBase.SCCNonlinearProblem{iip}(sys::NonlinearSystem, u0map, ts = get_tearing_state(sys) var_eq_matching, var_sccs = StructuralTransformations.algebraic_variables_scc(ts) - # The system is simplified, so SCCs are already in sorted order. We just need to get them and sort - # according to index in unknowns(sys) - sort!(var_sccs) + + if length(var_sccs) == 1 + return NonlinearProblem{iip}(sys, u0map, parammap; eval_expression, eval_module, kwargs...) + end + + condensed_graph = MatchedCondensationGraph( + DiCMOBiGraph{true}(complete(ts.structure.graph), + complete(var_eq_matching)), + var_sccs) + toporder = topological_sort_by_dfs(condensed_graph) + var_sccs = var_sccs[toporder] eq_sccs = map(Base.Fix1(getindex, var_eq_matching), var_sccs) dvs = unknowns(sys) From c1e1523cae45dc2e5a1ecb6d8aa8a914cb36aa3b Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 26 Nov 2024 16:58:38 +0530 Subject: [PATCH 0435/1337] feat: better handle observed variables, constants in SCCNonlinearProblem --- src/systems/nonlinear/nonlinearsystem.jl | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/systems/nonlinear/nonlinearsystem.jl b/src/systems/nonlinear/nonlinearsystem.jl index bb37dadb8c..636c1202f7 100644 --- a/src/systems/nonlinear/nonlinearsystem.jl +++ b/src/systems/nonlinear/nonlinearsystem.jl @@ -543,17 +543,22 @@ function (cw::CacheWriter)(p, sols) cw.fn(p.caches[1], sols, p...) end -function CacheWriter(sys::AbstractSystem, exprs, solsyms; +function CacheWriter(sys::AbstractSystem, exprs, solsyms, obseqs::Vector{Equation}; eval_expression = false, eval_module = @__MODULE__) ps = parameters(sys) rps = reorder_parameters(sys, ps) + obs_assigns = [eq.lhs ← eq.rhs for eq in obseqs] + cmap, cs = get_cmap(sys) + cmap_assigns = [eq.lhs ← eq.rhs for eq in cmap] fn = Func( [:out, DestructuredArgs(DestructuredArgs.(solsyms)), DestructuredArgs.(rps)...], [], SetArray(true, :out, exprs) - ) |> wrap_parameter_dependencies(sys, false)[2] |> - wrap_array_vars(sys, exprs; dvs = nothing, inputs = [])[2] |> toexpr + ) |> wrap_assignments(false, obs_assigns)[2] |> + wrap_parameter_dependencies(sys, false)[2] |> + wrap_array_vars(sys, exprs; dvs = nothing, inputs = [])[2] |> + wrap_assignments(false, cmap_assigns)[2] |> toexpr return CacheWriter(eval_or_rgf(fn; eval_expression, eval_module)) end @@ -612,7 +617,8 @@ function SciMLBase.SCCNonlinearProblem{iip}(sys::NonlinearSystem, u0map, var_eq_matching, var_sccs = StructuralTransformations.algebraic_variables_scc(ts) if length(var_sccs) == 1 - return NonlinearProblem{iip}(sys, u0map, parammap; eval_expression, eval_module, kwargs...) + return NonlinearProblem{iip}( + sys, u0map, parammap; eval_expression, eval_module, kwargs...) end condensed_graph = MatchedCondensationGraph( @@ -664,7 +670,7 @@ function SciMLBase.SCCNonlinearProblem{iip}(sys::NonlinearSystem, u0map, # cached variables and their corresponding expressions cachevars = Any[obs[i].lhs for i in prevobsidxs] - cacheexprs = Any[obs[i].rhs for i in prevobsidxs] + cacheexprs = Any[obs[i].lhs for i in prevobsidxs] for (k, v) in state push!(cachevars, unwrap(v)) push!(cacheexprs, unwrap(k)) @@ -676,7 +682,8 @@ function SciMLBase.SCCNonlinearProblem{iip}(sys::NonlinearSystem, u0map, else solsyms = getindex.((dvs,), view(var_sccs, 1:(i - 1))) push!(explicitfuns, - CacheWriter(sys, cacheexprs, solsyms; eval_expression, eval_module)) + CacheWriter(sys, cacheexprs, solsyms, obs[prevobsidxs]; + eval_expression, eval_module)) end f = SCCNonlinearFunction{iip}( sys, _eqs, _dvs, _obs, (cachevars,); eval_expression, eval_module, kwargs...) From 205d76a2f6b64820f59c17a2fdf8e2253f914cf4 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 26 Nov 2024 16:59:17 +0530 Subject: [PATCH 0436/1337] test: fix tests --- test/initializationsystem.jl | 2 +- test/mtkparameters.jl | 4 ++-- test/scc_nonlinear_problem.jl | 3 ++- test/split_parameters.jl | 2 +- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/test/initializationsystem.jl b/test/initializationsystem.jl index f3015f7db0..9c06dd2030 100644 --- a/test/initializationsystem.jl +++ b/test/initializationsystem.jl @@ -28,7 +28,7 @@ sol = solve(initprob) initprob = ModelingToolkit.InitializationProblem(pend, 0.0, [x => 1, y => 0], [g => 1]; guesses = ModelingToolkit.missing_variable_defaults(pend)) -@test initprob isa NonlinearProblem +@test initprob isa NonlinearLeastSquaresProblem sol = solve(initprob) @test SciMLBase.successful_retcode(sol) @test sol.u == [0.0, 0.0, 0.0, 0.0] diff --git a/test/mtkparameters.jl b/test/mtkparameters.jl index 603426aaf7..ce524fdb76 100644 --- a/test/mtkparameters.jl +++ b/test/mtkparameters.jl @@ -299,7 +299,7 @@ end # Parameter timeseries ps = MTKParameters(([1.0, 1.0],), (BlockedArray(zeros(4), [2, 2]),), - (), ()) + (), (), ()) ps2 = SciMLStructures.replace(Discrete(), ps, ones(4)) @test typeof(ps2.discrete) == typeof(ps.discrete) with_updated_parameter_timeseries_values( @@ -316,7 +316,7 @@ with_updated_parameter_timeseries_values( ps = MTKParameters( (), (BlockedArray([1.0, 2.0, 3.0, 4.0, 5.0, 6.0], [3, 3]), BlockedArray(falses(1), [1, 0])), - (), ()) + (), (), ()) @test SciMLBase.get_saveable_values(sys, ps, 1).x isa Tuple{Vector{Float64}, Vector{Bool}} tsidx1 = 1 tsidx2 = 2 diff --git a/test/scc_nonlinear_problem.jl b/test/scc_nonlinear_problem.jl index 0c4bfe61d0..fdf1646343 100644 --- a/test/scc_nonlinear_problem.jl +++ b/test/scc_nonlinear_problem.jl @@ -3,6 +3,7 @@ using NonlinearSolve, SCCNonlinearSolve using OrdinaryDiffEq using SciMLBase, Symbolics using LinearAlgebra, Test +using ModelingToolkit: t_nounits as t, D_nounits as D @testset "Trivial case" begin function f!(du, u, p) @@ -30,7 +31,7 @@ using LinearAlgebra, Test sol2 = solve(sccprob, NewtonRaphson()) @test SciMLBase.successful_retcode(sol1) @test SciMLBase.successful_retcode(sol2) - @test sol1.u ≈ sol2.u + @test sol1[u] ≈ sol2[u] end @testset "With parameters" begin diff --git a/test/split_parameters.jl b/test/split_parameters.jl index 22c90edf7a..2f8667faa8 100644 --- a/test/split_parameters.jl +++ b/test/split_parameters.jl @@ -206,7 +206,7 @@ S = get_sensitivity(closed_loop, :u) BlockedArray([[1 2; 3 4], [2 4; 6 8]], [1, 1])), # (BlockedArray([[true, false], [false, true]]), BlockedArray([[[1 2; 3 4]], [[2 4; 6 8]]])), ([5, 6],), - (["hi", "bye"], [:lie, :die])) + (["hi", "bye"], [:lie, :die]), ()) @test ps[ParameterIndex(Tunable(), 1)] == 1.0 @test ps[ParameterIndex(Tunable(), 2:4)] == collect(2.0:4.0) @test ps[ParameterIndex(Tunable(), reshape(4:7, 2, 2))] == reshape(4.0:7.0, 2, 2) From ed9cdf380488fdbd1e43753fb689f76e9474437e Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 26 Nov 2024 23:54:58 +0530 Subject: [PATCH 0437/1337] fix: reorder system in `SCCNonlinearProblem` --- src/systems/nonlinear/nonlinearsystem.jl | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/systems/nonlinear/nonlinearsystem.jl b/src/systems/nonlinear/nonlinearsystem.jl index 636c1202f7..24680fead4 100644 --- a/src/systems/nonlinear/nonlinearsystem.jl +++ b/src/systems/nonlinear/nonlinearsystem.jl @@ -707,6 +707,11 @@ function SciMLBase.SCCNonlinearProblem{iip}(sys::NonlinearSystem, u0map, push!(subprobs, prob) end + new_dvs = dvs[reduce(vcat, var_sccs)] + new_eqs = eqs[reduce(vcat, eq_sccs)] + @set! sys.unknowns = new_dvs + @set! sys.eqs = new_eqs + sys = complete(sys) return SCCNonlinearProblem(subprobs, explicitfuns, sys, p) end From a3819300c07a9e8c1a70ccff93b71574a832bc75 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Sat, 30 Nov 2024 23:28:06 +0530 Subject: [PATCH 0438/1337] build: bump OrdinaryDiffEqCore, OrdinaryDiffEqNonlinearSolve compats --- Project.toml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Project.toml b/Project.toml index b520fa3e0c..7ac6f940be 100644 --- a/Project.toml +++ b/Project.toml @@ -121,7 +121,8 @@ NonlinearSolve = "3.14, 4" OffsetArrays = "1" OrderedCollections = "1" OrdinaryDiffEq = "6.82.0" -OrdinaryDiffEqCore = "1.7.0" +OrdinaryDiffEqCore = "1.13.0" +OrdinaryDiffEqNonlinearSolve = "1.3.0" PrecompileTools = "1" REPL = "1" RecursiveArrayTools = "3.26" @@ -162,6 +163,7 @@ OptimizationMOI = "fd9f6733-72f4-499f-8506-86b2bdd0dea1" OptimizationOptimJL = "36348300-93cb-4f02-beb5-3c3902f8871e" OrdinaryDiffEq = "1dea7af3-3e70-54e6-95c3-0bf5283fa5ed" OrdinaryDiffEqCore = "bbf590c4-e513-4bbe-9b18-05decba2e5d8" +OrdinaryDiffEqNonlinearSolve = "127b3ac7-2247-4354-8eb6-78cf4e7c58e8" Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" REPL = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" @@ -176,4 +178,4 @@ Sundials = "c3572dad-4567-51f8-b174-8c6c989267f4" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [targets] -test = ["AmplNLWriter", "BenchmarkTools", "ControlSystemsBase", "DataInterpolations", "DelayDiffEq", "NonlinearSolve", "ForwardDiff", "Ipopt", "Ipopt_jll", "ModelingToolkitStandardLibrary", "Optimization", "OptimizationOptimJL", "OptimizationMOI", "OrdinaryDiffEq", "OrdinaryDiffEqCore", "REPL", "Random", "ReferenceTests", "SafeTestsets", "StableRNGs", "Statistics", "SteadyStateDiffEq", "Test", "StochasticDiffEq", "Sundials", "StochasticDelayDiffEq", "Pkg", "JET"] +test = ["AmplNLWriter", "BenchmarkTools", "ControlSystemsBase", "DataInterpolations", "DelayDiffEq", "NonlinearSolve", "ForwardDiff", "Ipopt", "Ipopt_jll", "ModelingToolkitStandardLibrary", "Optimization", "OptimizationOptimJL", "OptimizationMOI", "OrdinaryDiffEq", "OrdinaryDiffEqCore", "REPL", "Random", "ReferenceTests", "SafeTestsets", "StableRNGs", "Statistics", "SteadyStateDiffEq", "Test", "StochasticDiffEq", "Sundials", "StochasticDelayDiffEq", "Pkg", "JET", "OrdinaryDiffEqNonlinearSolve"] From 52eba502e7e85670095ee4dafe7b131370c5d14f Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 4 Dec 2024 14:14:27 +0530 Subject: [PATCH 0439/1337] refactor: update to new `SCCNonlinearProblem` constructor --- Project.toml | 2 +- src/systems/nonlinear/nonlinearsystem.jl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Project.toml b/Project.toml index 7ac6f940be..d81b3d4ef9 100644 --- a/Project.toml +++ b/Project.toml @@ -129,7 +129,7 @@ RecursiveArrayTools = "3.26" Reexport = "0.2, 1" RuntimeGeneratedFunctions = "0.5.9" SCCNonlinearSolve = "1.0.0" -SciMLBase = "2.65" +SciMLBase = "2.66" SciMLStructures = "1.0" Serialization = "1" Setfield = "0.7, 0.8, 1" diff --git a/src/systems/nonlinear/nonlinearsystem.jl b/src/systems/nonlinear/nonlinearsystem.jl index 24680fead4..7826e06f76 100644 --- a/src/systems/nonlinear/nonlinearsystem.jl +++ b/src/systems/nonlinear/nonlinearsystem.jl @@ -712,7 +712,7 @@ function SciMLBase.SCCNonlinearProblem{iip}(sys::NonlinearSystem, u0map, @set! sys.unknowns = new_dvs @set! sys.eqs = new_eqs sys = complete(sys) - return SCCNonlinearProblem(subprobs, explicitfuns, sys, p) + return SCCNonlinearProblem(subprobs, explicitfuns, p, true; sys) end """ From 24d39af42431ac0fff321bb785e7ff6dac864120 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 4 Dec 2024 14:14:41 +0530 Subject: [PATCH 0440/1337] test: fix test to new SciMLBase error message --- test/symbolic_events.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/symbolic_events.jl b/test/symbolic_events.jl index 61690acdce..9d3aac4074 100644 --- a/test/symbolic_events.jl +++ b/test/symbolic_events.jl @@ -1063,7 +1063,7 @@ end 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 "CheckInit specified but initialization" solve(prob, Rodas5()) + @test_throws "DAE initialization failed" solve(prob, Rodas5()) cb = [x ~ 0.0] => [y ~ 1] @mtkbuild pend = ODESystem(eqs, t; continuous_events = [cb]) From 1320fc0bd6bd11e819405512bb48ab6737094fa8 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 5 Dec 2024 13:06:04 +0530 Subject: [PATCH 0441/1337] refactor: separate out operating point and initializeprob construction --- src/systems/problem_utils.jl | 154 +++++++++++++++++++++-------------- 1 file changed, 94 insertions(+), 60 deletions(-) diff --git a/src/systems/problem_utils.jl b/src/systems/problem_utils.jl index ebc5a5bd75..6b75fb2c6d 100644 --- a/src/systems/problem_utils.jl +++ b/src/systems/problem_utils.jl @@ -489,6 +489,89 @@ function EmptySciMLFunction(args...; kwargs...) return EmptySciMLFunction{typeof(args), typeof(kwargs)}(args, kwargs) end +""" + $(TYPEDSIGNATURES) + +Construct the operating point of the system from the user-provided `u0map` and `pmap`, system +defaults `defs`, constant equations `cmap` (from `get_cmap(sys)`), unknowns `dvs` and +parameters `ps`. Return the operating point as a dictionary, the list of unknowns for which +no values can be determined, and the list of parameters for which no values can be determined. +""" +function build_operating_point( + u0map::AbstractDict, pmap::AbstractDict, defs::AbstractDict, cmap, dvs, ps) + op = add_toterms(u0map) + missing_unknowns = add_fallbacks!(op, dvs, defs) + for (k, v) in defs + haskey(op, k) && continue + op[k] = v + end + merge!(op, pmap) + missing_pars = add_fallbacks!(op, ps, defs) + for eq in cmap + op[eq.lhs] = eq.rhs + end + return op, missing_unknowns, missing_pars +end + +""" + $(TYPEDSIGNATURES) + +Build and return the initialization problem and associated data as a `NamedTuple` to be passed +to the `SciMLFunction` constructor. Requires the system `sys`, operating point `op`, +user-provided `u0map` and `pmap`, initial time `t`, system defaults `defs`, user-provided +`guesses`, and list of unknowns which don't have a value in `op`. The keyword `implicit_dae` +denotes whether the `SciMLProblem` being constructed is in implicit DAE form (`DAEProblem`). +All other keyword arguments are forwarded to `InitializationProblem`. +""" +function maybe_build_initialization_problem( + sys::AbstractSystem, op::AbstractDict, u0map, pmap, t, defs, + guesses, missing_unknowns; implicit_dae = false, kwargs...) + guesses = merge(ModelingToolkit.guesses(sys), todict(guesses)) + has_observed_u0s = any( + k -> has_observed_with_lhs(sys, k) || has_parameter_dependency_with_lhs(sys, k), + keys(op)) + solvablepars = [p + for p in parameters(sys) + if is_parameter_solvable(p, pmap, defs, guesses)] + has_dependent_unknowns = any(unknowns(sys)) do sym + val = get(op, sym, nothing) + val === nothing && return false + return symbolic_type(val) != NotSymbolic() || is_array_of_symbolics(val) + end + if (((implicit_dae || has_observed_u0s || !isempty(missing_unknowns) || + !isempty(solvablepars) || has_dependent_unknowns) && + get_tearing_state(sys) !== nothing) || + !isempty(initialization_equations(sys))) && t !== nothing + initializeprob = ModelingToolkit.InitializationProblem( + sys, t, u0map, pmap; guesses, kwargs...) + initializeprobmap = getu(initializeprob, unknowns(sys)) + + punknowns = [p + for p in all_variable_symbols(initializeprob) + if is_parameter(sys, p)] + getpunknowns = getu(initializeprob, punknowns) + setpunknowns = setp(sys, punknowns) + initializeprobpmap = GetUpdatedMTKParameters(getpunknowns, setpunknowns) + + reqd_syms = parameter_symbols(initializeprob) + update_initializeprob! = UpdateInitializeprob( + getu(sys, reqd_syms), setu(initializeprob, reqd_syms)) + for p in punknowns + p = unwrap(p) + stype = symtype(p) + op[p] = get_temporary_value(p) + end + + for v in missing_unknowns + op[v] = zero_var(v) + end + empty!(missing_unknowns) + return (; + initializeprob, initializeprobmap, initializeprobpmap, update_initializeprob!) + end + return (;) +end + """ $(TYPEDSIGNATURES) @@ -576,67 +659,18 @@ function process_SciMLProblem( cmap, cs = get_cmap(sys) kwargs = NamedTuple(kwargs) - op = add_toterms(u0map) - missing_unknowns = add_fallbacks!(op, dvs, defs) - for (k, v) in defs - haskey(op, k) && continue - op[k] = v - end - merge!(op, pmap) - missing_pars = add_fallbacks!(op, ps, defs) - for eq in cmap - op[eq.lhs] = eq.rhs - end - if sys isa ODESystem - guesses = merge(ModelingToolkit.guesses(sys), todict(guesses)) - has_observed_u0s = any( - k -> has_observed_with_lhs(sys, k) || has_parameter_dependency_with_lhs(sys, k), - keys(op)) - solvablepars = [p - for p in parameters(sys) - if is_parameter_solvable(p, pmap, defs, guesses)] - has_dependent_unknowns = any(unknowns(sys)) do sym - val = get(op, sym, nothing) - val === nothing && return false - return symbolic_type(val) != NotSymbolic() || is_array_of_symbolics(val) - end - if build_initializeprob && - (((implicit_dae || has_observed_u0s || !isempty(missing_unknowns) || - !isempty(solvablepars) || has_dependent_unknowns) && - get_tearing_state(sys) !== nothing) || - !isempty(initialization_equations(sys))) && t !== nothing - initializeprob = ModelingToolkit.InitializationProblem( - sys, t, u0map, pmap; guesses, warn_initialize_determined, - initialization_eqs, eval_expression, eval_module, fully_determined, - warn_cyclic_dependency, check_units = check_initialization_units, - circular_dependency_max_cycle_length, circular_dependency_max_cycles, use_scc) - initializeprobmap = getu(initializeprob, unknowns(sys)) - - punknowns = [p - for p in all_variable_symbols(initializeprob) - if is_parameter(sys, p)] - getpunknowns = getu(initializeprob, punknowns) - setpunknowns = setp(sys, punknowns) - initializeprobpmap = GetUpdatedMTKParameters(getpunknowns, setpunknowns) - - reqd_syms = parameter_symbols(initializeprob) - update_initializeprob! = UpdateInitializeprob( - getu(sys, reqd_syms), setu(initializeprob, reqd_syms)) - for p in punknowns - p = unwrap(p) - stype = symtype(p) - op[p] = get_temporary_value(p) - delete!(missing_pars, p) - end + op, missing_unknowns, missing_pars = build_operating_point( + u0map, pmap, defs, cmap, dvs, ps) - for v in missing_unknowns - op[v] = zero_var(v) - end - empty!(missing_unknowns) - kwargs = merge(kwargs, - (; initializeprob, initializeprobmap, - initializeprobpmap, update_initializeprob!)) - end + if sys isa ODESystem && build_initializeprob + kws = maybe_build_initialization_problem( + sys, op, u0map, pmap, t, defs, guesses, missing_unknowns; + implicit_dae, warn_initialize_determined, initialization_eqs, + eval_expression, eval_module, fully_determined, + warn_cyclic_dependency, check_units = check_initialization_units, + circular_dependency_max_cycle_length, circular_dependency_max_cycles, use_scc) + + kwargs = merge(kwargs, kws) end if t !== nothing && !(constructor <: Union{DDEFunction, SDDEFunction}) From 35b407c728ba9a8590f1567140822cba9c57e924 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 5 Dec 2024 03:12:26 +0530 Subject: [PATCH 0442/1337] fix: fix `remake_initialization_data` on problems with no initprob --- src/systems/nonlinear/initializesystem.jl | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/systems/nonlinear/initializesystem.jl b/src/systems/nonlinear/initializesystem.jl index 726d171bd0..4544f0fa96 100644 --- a/src/systems/nonlinear/initializesystem.jl +++ b/src/systems/nonlinear/initializesystem.jl @@ -281,6 +281,8 @@ function SciMLBase.remake_initialization_data(sys::ODESystem, odefn, u0, t0, p, symbols_to_symbolics!(sys, pmap) guesses = Dict() defs = defaults(sys) + cmap, cs = get_cmap(sys) + if SciMLBase.has_initializeprob(odefn) oldsys = odefn.initializeprob.f.sys meta = get_metadata(oldsys) @@ -324,8 +326,9 @@ function SciMLBase.remake_initialization_data(sys::ODESystem, odefn, u0, t0, p, end filter_missing_values!(u0map) filter_missing_values!(pmap) - f, _ = process_SciMLProblem(EmptySciMLFunction, sys, u0map, pmap; guesses, t = t0) - kws = f.kwargs + + op, missing_unknowns, missing_pars = build_operating_point(u0map, pmap, defs, cmap, dvs, ps) + kws = maybe_build_initialization_problem(sys, op, u0map, pmap, t0, defs, guesses, missing_unknowns; use_scc = true) initprob = get(kws, :initializeprob, nothing) if initprob === nothing return nothing From 99435326bd009f38bdd9545801dfcfd2ac1e28b6 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 5 Dec 2024 13:11:17 +0530 Subject: [PATCH 0443/1337] refactor: add better warnings when SCC initialization cannot be used --- src/systems/diffeqs/abstractodesystem.jl | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index ac8870fcaa..8b1476e582 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -1342,11 +1342,17 @@ function InitializationProblem{iip, specialize}(sys::AbstractODESystem, neqs = length(equations(isys)) nunknown = length(unknowns(isys)) + if use_scc + scc_message = "`SCCNonlinearProblem` can only be used for initialization of fully determined systems and hence will not be used here. " + else + scc_message = "" + end + if warn_initialize_determined && neqs > nunknown - @warn "Initialization system is overdetermined. $neqs equations for $nunknown unknowns. Initialization will default to using least squares. To suppress this warning pass warn_initialize_determined = false. To make this warning into an error, pass fully_determined = true" + @warn "Initialization system is overdetermined. $neqs equations for $nunknown unknowns. Initialization will default to using least squares. $(scc_message)To suppress this warning pass warn_initialize_determined = false. To make this warning into an error, pass fully_determined = true" end if warn_initialize_determined && neqs < nunknown - @warn "Initialization system is underdetermined. $neqs equations for $nunknown unknowns. Initialization will default to using least squares. To suppress this warning pass warn_initialize_determined = false. To make this warning into an error, pass fully_determined = true" + @warn "Initialization system is underdetermined. $neqs equations for $nunknown unknowns. Initialization will default to using least squares. $(scc_message)To suppress this warning pass warn_initialize_determined = false. To make this warning into an error, pass fully_determined = true" end parammap = parammap isa DiffEqBase.NullParameters || isempty(parammap) ? @@ -1384,7 +1390,16 @@ function InitializationProblem{iip, specialize}(sys::AbstractODESystem, end TProb = if neqs == nunknown && isempty(unassigned_vars) - use_scc && neqs > 0 && is_split(isys) ? SCCNonlinearProblem : NonlinearProblem + if use_scc && neqs > 0 + if is_split(isys) + SCCNonlinearProblem + else + @warn "`SCCNonlinearProblem` can only be used with `split = true` systems. Simplify your `ODESystem` with `split = true` or pass `use_scc = false` to disable this warning" + NonlinearProblem + end + else + NonlinearProblem + end else NonlinearLeastSquaresProblem end From 0537715df6c5a0ed03c5d47b26a0768b42b2b184 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 5 Dec 2024 13:22:29 +0530 Subject: [PATCH 0444/1337] refactor: propagate `use_scc` to `remake_initialization_data` --- src/systems/diffeqs/abstractodesystem.jl | 6 ++++-- src/systems/nonlinear/initializesystem.jl | 14 ++++++++++---- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index 8b1476e582..f4e29346ff 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -1311,11 +1311,13 @@ function InitializationProblem{iip, specialize}(sys::AbstractODESystem, elseif isempty(u0map) && get_initializesystem(sys) === nothing isys = structural_simplify( generate_initializesystem( - sys; initialization_eqs, check_units, pmap = parammap, guesses); fully_determined) + sys; initialization_eqs, check_units, pmap = parammap, + guesses, extra_metadata = (; use_scc)); fully_determined) else isys = structural_simplify( generate_initializesystem( - sys; u0map, initialization_eqs, check_units, pmap = parammap, guesses); fully_determined) + sys; u0map, initialization_eqs, check_units, + pmap = parammap, guesses, extra_metadata = (; use_scc)); fully_determined) end ts = get_tearing_state(isys) diff --git a/src/systems/nonlinear/initializesystem.jl b/src/systems/nonlinear/initializesystem.jl index 4544f0fa96..2344727920 100644 --- a/src/systems/nonlinear/initializesystem.jl +++ b/src/systems/nonlinear/initializesystem.jl @@ -11,7 +11,7 @@ function generate_initializesystem(sys::ODESystem; default_dd_guess = 0.0, algebraic_only = false, check_units = true, check_defguess = false, - name = nameof(sys), kwargs...) + name = nameof(sys), extra_metadata = (;), kwargs...) trueobs, eqs = unhack_observed(observed(sys), equations(sys)) vars = unique([unknowns(sys); getfield.(trueobs, :lhs)]) vars_set = Set(vars) # for efficient in-lookup @@ -179,7 +179,8 @@ function generate_initializesystem(sys::ODESystem; for k in keys(defs) defs[k] = substitute(defs[k], paramsubs) end - meta = InitializationSystemMetadata(anydict(u0map), anydict(pmap), additional_guesses) + meta = InitializationSystemMetadata( + anydict(u0map), anydict(pmap), additional_guesses, extra_metadata) return NonlinearSystem(eqs_ics, vars, pars; @@ -195,6 +196,7 @@ struct InitializationSystemMetadata u0map::Dict{Any, Any} pmap::Dict{Any, Any} additional_guesses::Dict{Any, Any} + extra_metadata::NamedTuple end function is_parameter_solvable(p, pmap, defs, guesses) @@ -282,6 +284,7 @@ function SciMLBase.remake_initialization_data(sys::ODESystem, odefn, u0, t0, p, guesses = Dict() defs = defaults(sys) cmap, cs = get_cmap(sys) + use_scc = true if SciMLBase.has_initializeprob(odefn) oldsys = odefn.initializeprob.f.sys @@ -290,6 +293,7 @@ function SciMLBase.remake_initialization_data(sys::ODESystem, odefn, u0, t0, p, u0map = merge(meta.u0map, u0map) pmap = merge(meta.pmap, pmap) merge!(guesses, meta.additional_guesses) + use_scc = get(meta.extra_metadata, :use_scc, true) end else # there is no initializeprob, so the original problem construction @@ -327,8 +331,10 @@ function SciMLBase.remake_initialization_data(sys::ODESystem, odefn, u0, t0, p, filter_missing_values!(u0map) filter_missing_values!(pmap) - op, missing_unknowns, missing_pars = build_operating_point(u0map, pmap, defs, cmap, dvs, ps) - kws = maybe_build_initialization_problem(sys, op, u0map, pmap, t0, defs, guesses, missing_unknowns; use_scc = true) + op, missing_unknowns, missing_pars = build_operating_point( + u0map, pmap, defs, cmap, dvs, ps) + kws = maybe_build_initialization_problem( + sys, op, u0map, pmap, t0, defs, guesses, missing_unknowns; use_scc) initprob = get(kws, :initializeprob, nothing) if initprob === nothing return nothing From ab5747f208a9cf5c2924fa251c739053cccf859d Mon Sep 17 00:00:00 2001 From: Christopher Rackauckas Date: Thu, 5 Dec 2024 07:30:41 -0700 Subject: [PATCH 0445/1337] Update Project.toml --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index d81b3d4ef9..fe3a9e88e5 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "ModelingToolkit" uuid = "961ee093-0014-501f-94e3-6117800e7a78" authors = ["Yingbo Ma ", "Chris Rackauckas and contributors"] -version = "9.55.0" +version = "9.56.0" [deps] AbstractTrees = "1520ce14-60c1-5f80-bbc7-55ef81b5835c" From 82198b7f49cb707b31b06af6d49dd0a7ecd33160 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Sat, 30 Nov 2024 14:09:11 +0530 Subject: [PATCH 0446/1337] refactor: move HomtopyContinuation-related code to its own file --- src/ModelingToolkit.jl | 1 + .../nonlinear/homotopy_continuation.jl | 68 ++++++++++++++++++ src/systems/nonlinear/nonlinearsystem.jl | 69 ------------------- 3 files changed, 69 insertions(+), 69 deletions(-) create mode 100644 src/systems/nonlinear/homotopy_continuation.jl diff --git a/src/ModelingToolkit.jl b/src/ModelingToolkit.jl index 59be358349..de9a06d774 100644 --- a/src/ModelingToolkit.jl +++ b/src/ModelingToolkit.jl @@ -150,6 +150,7 @@ include("systems/callbacks.jl") include("systems/problem_utils.jl") include("systems/nonlinear/nonlinearsystem.jl") +include("systems/nonlinear/homotopy_continuation.jl") include("systems/diffeqs/odesystem.jl") include("systems/diffeqs/sdesystem.jl") include("systems/diffeqs/abstractodesystem.jl") diff --git a/src/systems/nonlinear/homotopy_continuation.jl b/src/systems/nonlinear/homotopy_continuation.jl new file mode 100644 index 0000000000..e41a3f0ce7 --- /dev/null +++ b/src/systems/nonlinear/homotopy_continuation.jl @@ -0,0 +1,68 @@ +""" +$(TYPEDEF) + +A type of Nonlinear problem which specializes on polynomial systems and uses +HomotopyContinuation.jl to solve the system. Requires importing HomotopyContinuation.jl to +create and solve. +""" +struct HomotopyContinuationProblem{uType, H, D, O, SS, U} <: + SciMLBase.AbstractNonlinearProblem{uType, true} + """ + The initial values of states in the system. If there are multiple real roots of + the system, the one closest to this point is returned. + """ + u0::uType + """ + A subtype of `HomotopyContinuation.AbstractSystem` to solve. Also contains the + parameter object. + """ + homotopy_continuation_system::H + """ + A function with signature `(u, p) -> resid`. In case of rational functions, this + is used to rule out roots of the system which would cause the denominator to be + zero. + """ + denominator::D + """ + The `NonlinearSystem` used to create this problem. Used for symbolic indexing. + """ + sys::NonlinearSystem + """ + A function which generates and returns observed expressions for the given system. + """ + obsfn::O + """ + The HomotopyContinuation.jl solver and start system, obtained through + `HomotopyContinuation.solver_startsystems`. + """ + solver_and_starts::SS + """ + A function which takes a solution of the transformed system, and returns a vector + of solutions for the original system. This is utilized when converting systems + to polynomials. + """ + unpack_solution::U +end + +function HomotopyContinuationProblem(::AbstractSystem, _u0, _p; kwargs...) + error("HomotopyContinuation.jl is required to create and solve `HomotopyContinuationProblem`s. Please run `Pkg.add(\"HomotopyContinuation\")` to continue.") +end + +SymbolicIndexingInterface.symbolic_container(p::HomotopyContinuationProblem) = p.sys +SymbolicIndexingInterface.state_values(p::HomotopyContinuationProblem) = p.u0 +function SymbolicIndexingInterface.set_state!(p::HomotopyContinuationProblem, args...) + set_state!(p.u0, args...) +end +function SymbolicIndexingInterface.parameter_values(p::HomotopyContinuationProblem) + parameter_values(p.homotopy_continuation_system) +end +function SymbolicIndexingInterface.set_parameter!(p::HomotopyContinuationProblem, args...) + set_parameter!(parameter_values(p), args...) +end +function SymbolicIndexingInterface.observed(p::HomotopyContinuationProblem, sym) + if p.obsfn !== nothing + return p.obsfn(sym) + else + return SymbolicIndexingInterface.observed(p.sys, sym) + end +end diff --git a/src/systems/nonlinear/nonlinearsystem.jl b/src/systems/nonlinear/nonlinearsystem.jl index 7826e06f76..ff4a7d0eb2 100644 --- a/src/systems/nonlinear/nonlinearsystem.jl +++ b/src/systems/nonlinear/nonlinearsystem.jl @@ -866,72 +866,3 @@ function Base.:(==)(sys1::NonlinearSystem, sys2::NonlinearSystem) _eq_unordered(get_ps(sys1), get_ps(sys2)) && all(s1 == s2 for (s1, s2) in zip(get_systems(sys1), get_systems(sys2))) end - -""" -$(TYPEDEF) - -A type of Nonlinear problem which specializes on polynomial systems and uses -HomotopyContinuation.jl to solve the system. Requires importing HomotopyContinuation.jl to -create and solve. -""" -struct HomotopyContinuationProblem{uType, H, D, O, SS, U} <: - SciMLBase.AbstractNonlinearProblem{uType, true} - """ - The initial values of states in the system. If there are multiple real roots of - the system, the one closest to this point is returned. - """ - u0::uType - """ - A subtype of `HomotopyContinuation.AbstractSystem` to solve. Also contains the - parameter object. - """ - homotopy_continuation_system::H - """ - A function with signature `(u, p) -> resid`. In case of rational functions, this - is used to rule out roots of the system which would cause the denominator to be - zero. - """ - denominator::D - """ - The `NonlinearSystem` used to create this problem. Used for symbolic indexing. - """ - sys::NonlinearSystem - """ - A function which generates and returns observed expressions for the given system. - """ - obsfn::O - """ - The HomotopyContinuation.jl solver and start system, obtained through - `HomotopyContinuation.solver_startsystems`. - """ - solver_and_starts::SS - """ - A function which takes a solution of the transformed system, and returns a vector - of solutions for the original system. This is utilized when converting systems - to polynomials. - """ - unpack_solution::U -end - -function HomotopyContinuationProblem(::AbstractSystem, _u0, _p; kwargs...) - error("HomotopyContinuation.jl is required to create and solve `HomotopyContinuationProblem`s. Please run `Pkg.add(\"HomotopyContinuation\")` to continue.") -end - -SymbolicIndexingInterface.symbolic_container(p::HomotopyContinuationProblem) = p.sys -SymbolicIndexingInterface.state_values(p::HomotopyContinuationProblem) = p.u0 -function SymbolicIndexingInterface.set_state!(p::HomotopyContinuationProblem, args...) - set_state!(p.u0, args...) -end -function SymbolicIndexingInterface.parameter_values(p::HomotopyContinuationProblem) - parameter_values(p.homotopy_continuation_system) -end -function SymbolicIndexingInterface.set_parameter!(p::HomotopyContinuationProblem, args...) - set_parameter!(parameter_values(p), args...) -end -function SymbolicIndexingInterface.observed(p::HomotopyContinuationProblem, sym) - if p.obsfn !== nothing - return p.obsfn(sym) - else - return SymbolicIndexingInterface.observed(p.sys, sym) - end -end From 2401aa22fc2ef382589efb57e75bede4a8ed14a3 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Sat, 30 Nov 2024 16:08:38 +0530 Subject: [PATCH 0447/1337] refactor: move HomotopyContinuation internals to MTK --- ext/MTKHomotopyContinuationExt.jl | 355 +------------- .../nonlinear/homotopy_continuation.jl | 437 ++++++++++++++++++ 2 files changed, 458 insertions(+), 334 deletions(-) diff --git a/ext/MTKHomotopyContinuationExt.jl b/ext/MTKHomotopyContinuationExt.jl index fa5d1bfcd4..586d608156 100644 --- a/ext/MTKHomotopyContinuationExt.jl +++ b/ext/MTKHomotopyContinuationExt.jl @@ -11,217 +11,6 @@ using ModelingToolkit: iscomplete, parameters, has_index_cache, get_index_cache, const MTK = ModelingToolkit -function contains_variable(x, wrt) - any(y -> occursin(y, x), wrt) -end - -""" -Possible reasons why a term is not polynomial -""" -MTK.EnumX.@enumx NonPolynomialReason begin - NonIntegerExponent - ExponentContainsUnknowns - BaseNotPolynomial - UnrecognizedOperation -end - -function display_reason(reason::NonPolynomialReason.T, sym) - if reason == NonPolynomialReason.NonIntegerExponent - pow = arguments(sym)[2] - "In $sym: Exponent $pow is not an integer" - elseif reason == NonPolynomialReason.ExponentContainsUnknowns - pow = arguments(sym)[2] - "In $sym: Exponent $pow contains unknowns of the system" - elseif reason == NonPolynomialReason.BaseNotPolynomial - base = arguments(sym)[1] - "In $sym: Base $base is not a polynomial in the unknowns" - elseif reason == NonPolynomialReason.UnrecognizedOperation - op = operation(sym) - """ - In $sym: Operation $op is not recognized. Allowed polynomial operations are \ - `*, /, +, -, ^`. - """ - else - error("This should never happen. Please open an issue in ModelingToolkit.jl.") - end -end - -mutable struct PolynomialData - non_polynomial_terms::Vector{BasicSymbolic} - reasons::Vector{NonPolynomialReason.T} - has_parametric_exponent::Bool -end - -PolynomialData() = PolynomialData(BasicSymbolic[], NonPolynomialReason.T[], false) - -abstract type PolynomialTransformationError <: Exception end - -struct MultivarTerm <: PolynomialTransformationError - term::Any - vars::Any -end - -function Base.showerror(io::IO, err::MultivarTerm) - println(io, - "Cannot convert system to polynomial: Found term $(err.term) which is a function of multiple unknowns $(err.vars).") -end - -struct MultipleTermsOfSameVar <: PolynomialTransformationError - terms::Any - var::Any -end - -function Base.showerror(io::IO, err::MultipleTermsOfSameVar) - println(io, - "Cannot convert system to polynomial: Found multiple non-polynomial terms $(err.terms) involving the same unknown $(err.var).") -end - -struct SymbolicSolveFailure <: PolynomialTransformationError - term::Any - var::Any -end - -function Base.showerror(io::IO, err::SymbolicSolveFailure) - println(io, - "Cannot convert system to polynomial: Unable to symbolically solve $(err.term) for $(err.var).") -end - -struct NemoNotLoaded <: PolynomialTransformationError end - -function Base.showerror(io::IO, err::NemoNotLoaded) - println(io, - "ModelingToolkit may be able to solve this system as a polynomial system if `Nemo` is loaded. Run `import Nemo` and try again.") -end - -struct VariablesAsPolyAndNonPoly <: PolynomialTransformationError - vars::Any -end - -function Base.showerror(io::IO, err::VariablesAsPolyAndNonPoly) - println(io, - "Cannot convert convert system to polynomial: Variables $(err.vars) occur in both polynomial and non-polynomial terms in the system.") -end - -struct NotPolynomialError <: Exception - transformation_err::Union{PolynomialTransformationError, Nothing} - eq::Vector{Equation} - data::Vector{PolynomialData} -end - -function Base.showerror(io::IO, err::NotPolynomialError) - if err.transformation_err !== nothing - Base.showerror(io, err.transformation_err) - end - for (eq, data) in zip(err.eq, err.data) - if isempty(data.non_polynomial_terms) - continue - end - println(io, - "Equation $(eq) is not a polynomial in the unknowns for the following reasons:") - for (term, reason) in zip(data.non_polynomial_terms, data.reasons) - println(io, display_reason(reason, term)) - end - end -end - -function is_polynomial!(data, y, wrt) - process_polynomial!(data, y, wrt) - isempty(data.reasons) -end - -""" -$(TYPEDSIGNATURES) - -Return information about the polynmial `x` with respect to variables in `wrt`, -writing said information to `data`. -""" -function process_polynomial!(data::PolynomialData, x, wrt) - x = unwrap(x) - symbolic_type(x) == NotSymbolic() && return true - iscall(x) || return true - contains_variable(x, wrt) || return true - any(isequal(x), wrt) && return true - - if operation(x) in (*, +, -, /) - # `map` because `all` will early exit, but we want to search - # through everything to get all the non-polynomial terms - return all(map(y -> is_polynomial!(data, y, wrt), arguments(x))) - end - if operation(x) == (^) - b, p = arguments(x) - is_pow_integer = symtype(p) <: Integer - if !is_pow_integer - push!(data.non_polynomial_terms, x) - push!(data.reasons, NonPolynomialReason.NonIntegerExponent) - end - if symbolic_type(p) != NotSymbolic() - data.has_parametric_exponent = true - end - - exponent_has_unknowns = contains_variable(p, wrt) - if exponent_has_unknowns - push!(data.non_polynomial_terms, x) - push!(data.reasons, NonPolynomialReason.ExponentContainsUnknowns) - end - base_polynomial = is_polynomial!(data, b, wrt) - return base_polynomial && !exponent_has_unknowns && is_pow_integer - end - push!(data.non_polynomial_terms, x) - push!(data.reasons, NonPolynomialReason.UnrecognizedOperation) - return false -end - -""" -$(TYPEDSIGNATURES) - -Given a `x`, a polynomial in variables in `wrt` which may contain rational functions, -express `x` as a single rational function with polynomial `num` and denominator `den`. -Return `(num, den)`. -""" -function handle_rational_polynomials(x, wrt) - x = unwrap(x) - symbolic_type(x) == NotSymbolic() && return x, 1 - iscall(x) || return x, 1 - contains_variable(x, wrt) || return x, 1 - any(isequal(x), wrt) && return x, 1 - - # simplify_fractions cancels out some common factors - # and expands (a / b)^c to a^c / b^c, so we only need - # to handle these cases - x = simplify_fractions(x) - op = operation(x) - args = arguments(x) - - if op == / - # numerator and denominator are trivial - num, den = args - # but also search for rational functions in numerator - n, d = handle_rational_polynomials(num, wrt) - num, den = n, den * d - elseif op == + - num = 0 - den = 1 - - # we don't need to do common denominator - # because we don't care about cases where denominator - # is zero. The expression is zero when all the numerators - # are zero. - for arg in args - n, d = handle_rational_polynomials(arg, wrt) - num += n - den *= d - end - else - return x, 1 - end - # if the denominator isn't a polynomial in `wrt`, better to not include it - # to reduce the size of the gcd polynomial - if !contains_variable(den, wrt) - return num / den, 1 - end - return num, den -end - """ $(TYPEDSIGNATURES) @@ -289,12 +78,6 @@ end SymbolicIndexingInterface.parameter_values(s::MTKHomotopySystem) = s.p -struct PolynomialTransformationData - new_var::BasicSymbolic - term::BasicSymbolic - inv_term::Vector -end - """ $(TYPEDSIGNATURES) @@ -312,128 +95,31 @@ Keyword arguments: All other keyword arguments are forwarded to `HomotopyContinuation.solver_startsystems`. """ function MTK.HomotopyContinuationProblem( - sys::NonlinearSystem, u0map, parammap = nothing; eval_expression = false, - eval_module = ModelingToolkit, warn_parametric_exponent = true, kwargs...) + sys::NonlinearSystem, u0map, parammap = nothing; kwargs...) if !iscomplete(sys) error("A completed `NonlinearSystem` is required. Call `complete` or `structural_simplify` on the system before creating a `HomotopyContinuationProblem`") end - - dvs = unknowns(sys) - # we need to consider `full_equations` because observed also should be - # polynomials (if used in equations) and we don't know if observed is used - # in denominator. - # This is not the most efficient, and would be improved significantly with - # CSE/hashconsing. - eqs = full_equations(sys) - - polydata = map(eqs) do eq - data = PolynomialData() - process_polynomial!(data, eq.lhs, dvs) - process_polynomial!(data, eq.rhs, dvs) - data - end - - has_parametric_exponents = any(d -> d.has_parametric_exponent, polydata) - - all_non_poly_terms = mapreduce(d -> d.non_polynomial_terms, vcat, polydata) - unique!(all_non_poly_terms) - - var_to_nonpoly = Dict{BasicSymbolic, PolynomialTransformationData}() - - is_poly = true - transformation_err = nothing - for t in all_non_poly_terms - # if the term involves multiple unknowns, we can't invert it - dvs_in_term = map(x -> occursin(x, t), dvs) - if count(dvs_in_term) > 1 - transformation_err = MultivarTerm(t, dvs[dvs_in_term]) - is_poly = false - break - end - # we already have a substitution solving for `var` - var = dvs[findfirst(dvs_in_term)] - if haskey(var_to_nonpoly, var) && !isequal(var_to_nonpoly[var].term, t) - transformation_err = MultipleTermsOfSameVar([t, var_to_nonpoly[var].term], var) - is_poly = false - break - end - # we want to solve `term - new_var` for `var` - new_var = gensym(Symbol(var)) - new_var = unwrap(only(@variables $new_var)) - invterm = Symbolics.ia_solve( - t - new_var, var; complex_roots = false, periodic_roots = false, warns = false) - # if we can't invert it, quit - if invterm === nothing || isempty(invterm) - transformation_err = SymbolicSolveFailure(t, var) - is_poly = false - break - end - # `ia_solve` returns lazy terms i.e. `asin(1.0)` instead of `pi/2` - # this just evaluates the constant expressions - invterm = Symbolics.substitute.(invterm, (Dict(),)) - # RootsOf implies Symbolics couldn't solve the inner polynomial because - # `Nemo` wasn't loaded. - if any(x -> MTK.iscall(x) && MTK.operation(x) == Symbolics.RootsOf, invterm) - transformation_err = NemoNotLoaded() - is_poly = false - break - end - var_to_nonpoly[var] = PolynomialTransformationData(new_var, t, invterm) + transformation = MTK.PolynomialTransformation(sys) + if transformation isa MTK.NotPolynomialError + throw(transformation) end - - if !is_poly - throw(NotPolynomialError(transformation_err, eqs, polydata)) - end - - subrules = Dict() - combinations = Vector[] - new_dvs = [] - for x in dvs - if haskey(var_to_nonpoly, x) - _data = var_to_nonpoly[x] - subrules[_data.term] = _data.new_var - push!(combinations, _data.inv_term) - push!(new_dvs, _data.new_var) - else - push!(combinations, [x]) - push!(new_dvs, x) - end - end - all_solutions = collect.(collect(Iterators.product(combinations...))) - - denoms = [] - eqs2 = map(eqs) do eq - t = eq.rhs - eq.lhs - t = Symbolics.fixpoint_sub(t, subrules; maxiters = length(dvs)) - # the substituted variable occurs outside the substituted term - poly_and_nonpoly = map(dvs) do x - haskey(var_to_nonpoly, x) && occursin(x, t) - end - if any(poly_and_nonpoly) - throw(NotPolynomialError( - VariablesAsPolyAndNonPoly(dvs[poly_and_nonpoly]), eqs, polydata)) - end - - num, den = handle_rational_polynomials(t, new_dvs) - # make factors different elements, otherwise the nonzero factors artificially - # inflate the error of the zero factor. - if iscall(den) && operation(den) == * - for arg in arguments(den) - # ignore constant factors - symbolic_type(arg) == NotSymbolic() && continue - push!(denoms, abs(arg)) - end - elseif symbolic_type(den) != NotSymbolic() - push!(denoms, abs(den)) - end - return 0 ~ num + result = MTK.transform_system(sys, transformation) + if result isa MTK.NotPolynomialError + throw(result) end + MTK.HomotopyContinuationProblem(sys, transformation, result, u0map, parammap; kwargs...) +end - sys2 = MTK.@set sys.eqs = eqs2 - MTK.@set! sys2.unknowns = new_dvs - # remove observed equations to avoid adding them in codegen - MTK.@set! sys2.observed = Equation[] - MTK.@set! sys2.substitutions = nothing +function MTK.HomotopyContinuationProblem( + sys::MTK.NonlinearSystem, transformation::MTK.PolynomialTransformation, + result::MTK.PolynomialTransformationResult, u0map, + parammap = nothing; eval_expression = false, + eval_module = ModelingToolkit, warn_parametric_exponent = true, kwargs...) + sys2 = result.sys + denoms = result.denominators + polydata = transformation.polydata + new_dvs = transformation.new_dvs + all_solutions = transformation.all_solutions _, u0, p = MTK.process_SciMLProblem( MTK.EmptySciMLFunction, sys, u0map, parammap; eval_expression, eval_module) @@ -443,10 +129,11 @@ function MTK.HomotopyContinuationProblem( unpack_solution = MTK.build_explicit_observed_function(sys2, all_solutions) hvars = symbolics_to_hc.(new_dvs) - mtkhsys = MTKHomotopySystem(nlfn.f, p, nlfn.jac, hvars, length(eqs)) + mtkhsys = MTKHomotopySystem(nlfn.f, p, nlfn.jac, hvars, length(new_dvs)) obsfn = MTK.ObservedFunctionCache(sys; eval_expression, eval_module) + has_parametric_exponents = any(d -> d.has_parametric_exponent, polydata) if has_parametric_exponents if warn_parametric_exponent @warn """ diff --git a/src/systems/nonlinear/homotopy_continuation.jl b/src/systems/nonlinear/homotopy_continuation.jl index e41a3f0ce7..044f44f70f 100644 --- a/src/systems/nonlinear/homotopy_continuation.jl +++ b/src/systems/nonlinear/homotopy_continuation.jl @@ -66,3 +66,440 @@ function SymbolicIndexingInterface.observed(p::HomotopyContinuationProblem, sym) return SymbolicIndexingInterface.observed(p.sys, sym) end end + +function contains_variable(x, wrt) + any(y -> occursin(y, x), wrt) +end + +""" +Possible reasons why a term is not polynomial +""" +EnumX.@enumx NonPolynomialReason begin + NonIntegerExponent + ExponentContainsUnknowns + BaseNotPolynomial + UnrecognizedOperation +end + +function display_reason(reason::NonPolynomialReason.T, sym) + if reason == NonPolynomialReason.NonIntegerExponent + pow = arguments(sym)[2] + "In $sym: Exponent $pow is not an integer" + elseif reason == NonPolynomialReason.ExponentContainsUnknowns + pow = arguments(sym)[2] + "In $sym: Exponent $pow contains unknowns of the system" + elseif reason == NonPolynomialReason.BaseNotPolynomial + base = arguments(sym)[1] + "In $sym: Base $base is not a polynomial in the unknowns" + elseif reason == NonPolynomialReason.UnrecognizedOperation + op = operation(sym) + """ + In $sym: Operation $op is not recognized. Allowed polynomial operations are \ + `*, /, +, -, ^`. + """ + else + error("This should never happen. Please open an issue in ModelingToolkit.jl.") + end +end + +""" + $(TYPEDEF) + +Information about an expression about its polynomial nature. +""" +mutable struct PolynomialData + """ + A list of all non-polynomial terms in the expression. + """ + non_polynomial_terms::Vector{BasicSymbolic} + """ + Corresponding to `non_polynomial_terms`, a list of reasons why they are + not polynomial. + """ + reasons::Vector{NonPolynomialReason.T} + """ + Whether the polynomial contains parametric exponents of unknowns. + """ + has_parametric_exponent::Bool +end + +PolynomialData() = PolynomialData(BasicSymbolic[], NonPolynomialReason.T[], false) + +abstract type PolynomialTransformationError <: Exception end + +struct MultivarTerm <: PolynomialTransformationError + term::Any + vars::Any +end + +function Base.showerror(io::IO, err::MultivarTerm) + println(io, + "Cannot convert system to polynomial: Found term $(err.term) which is a function of multiple unknowns $(err.vars).") +end + +struct MultipleTermsOfSameVar <: PolynomialTransformationError + terms::Any + var::Any +end + +function Base.showerror(io::IO, err::MultipleTermsOfSameVar) + println(io, + "Cannot convert system to polynomial: Found multiple non-polynomial terms $(err.terms) involving the same unknown $(err.var).") +end + +struct SymbolicSolveFailure <: PolynomialTransformationError + term::Any + var::Any +end + +function Base.showerror(io::IO, err::SymbolicSolveFailure) + println(io, + "Cannot convert system to polynomial: Unable to symbolically solve $(err.term) for $(err.var).") +end + +struct NemoNotLoaded <: PolynomialTransformationError end + +function Base.showerror(io::IO, err::NemoNotLoaded) + println(io, + "ModelingToolkit may be able to solve this system as a polynomial system if `Nemo` is loaded. Run `import Nemo` and try again.") +end + +struct VariablesAsPolyAndNonPoly <: PolynomialTransformationError + vars::Any +end + +function Base.showerror(io::IO, err::VariablesAsPolyAndNonPoly) + println(io, + "Cannot convert convert system to polynomial: Variables $(err.vars) occur in both polynomial and non-polynomial terms in the system.") +end + +struct NotPolynomialError <: Exception + transformation_err::Union{PolynomialTransformationError, Nothing} + eq::Vector{Equation} + data::Vector{PolynomialData} +end + +function Base.showerror(io::IO, err::NotPolynomialError) + if err.transformation_err !== nothing + Base.showerror(io, err.transformation_err) + end + for (eq, data) in zip(err.eq, err.data) + if isempty(data.non_polynomial_terms) + continue + end + println(io, + "Equation $(eq) is not a polynomial in the unknowns for the following reasons:") + for (term, reason) in zip(data.non_polynomial_terms, data.reasons) + println(io, display_reason(reason, term)) + end + end +end + +function is_polynomial!(data, y, wrt) + process_polynomial!(data, y, wrt) + isempty(data.reasons) +end + +""" +$(TYPEDSIGNATURES) + +Return information about the polynmial `x` with respect to variables in `wrt`, +writing said information to `data`. +""" +function process_polynomial!(data::PolynomialData, x, wrt) + x = unwrap(x) + symbolic_type(x) == NotSymbolic() && return true + iscall(x) || return true + contains_variable(x, wrt) || return true + any(isequal(x), wrt) && return true + + if operation(x) in (*, +, -, /) + # `map` because `all` will early exit, but we want to search + # through everything to get all the non-polynomial terms + return all(map(y -> is_polynomial!(data, y, wrt), arguments(x))) + end + if operation(x) == (^) + b, p = arguments(x) + is_pow_integer = symtype(p) <: Integer + if !is_pow_integer + push!(data.non_polynomial_terms, x) + push!(data.reasons, NonPolynomialReason.NonIntegerExponent) + end + if symbolic_type(p) != NotSymbolic() + data.has_parametric_exponent = true + end + + exponent_has_unknowns = contains_variable(p, wrt) + if exponent_has_unknowns + push!(data.non_polynomial_terms, x) + push!(data.reasons, NonPolynomialReason.ExponentContainsUnknowns) + end + base_polynomial = is_polynomial!(data, b, wrt) + return base_polynomial && !exponent_has_unknowns && is_pow_integer + end + push!(data.non_polynomial_terms, x) + push!(data.reasons, NonPolynomialReason.UnrecognizedOperation) + return false +end + +""" + $(TYPEDEF) + +Information about how an unknown in the system is substituted for a non-polynomial +expression to turn the system into a polynomial. Used in `PolynomialTransformation`. +""" +struct PolynomialTransformationData + """ + The new variable to use as an unknown of the transformed system. + """ + new_var::BasicSymbolic + """ + The non-polynomial expression being substituted. + """ + term::BasicSymbolic + """ + A vector of expressions corresponding to the solutions of + the non-polynomial expression `term` in terms of the new unknown `new_var`, + used to backsolve for the original unknown of the system. + """ + inv_term::Vector{BasicSymbolic} +end + +""" + $(TYPEDEF) + +Information representing how to transform a `NonlinearSystem` into a polynomial +system. +""" +struct PolynomialTransformation + """ + Substitutions mapping non-polynomial terms to temporary unknowns. The system + is a polynomial in the new unknowns. Currently, each non-polynomial term is a + function of a single unknown of the original system. + """ + substitution_rules::Dict{BasicSymbolic, BasicSymbolic} + """ + A vector of expressions involving unknowns of the transformed system, mapping + back to solutions of the original system. + """ + all_solutions::Vector{Vector{BasicSymbolic}} + """ + The new unknowns of the transformed system. + """ + new_dvs::Vector{BasicSymbolic} + """ + The polynomial data for each equation. + """ + polydata::Vector{PolynomialData} +end + +function PolynomialTransformation(sys::NonlinearSystem) + # we need to consider `full_equations` because observed also should be + # polynomials (if used in equations) and we don't know if observed is used + # in denominator. + # This is not the most efficient, and would be improved significantly with + # CSE/hashconsing. + eqs = full_equations(sys) + dvs = unknowns(sys) + + # Collect polynomial information about all equations + polydata = map(eqs) do eq + data = PolynomialData() + process_polynomial!(data, eq.lhs, dvs) + process_polynomial!(data, eq.rhs, dvs) + data + end + + # Get all unique non-polynomial terms + # NOTE: + # Is there a better way to check for uniqueness? `simplify` is relatively slow + # (maybe use the threaded version?) and `expand` can blow up expression size. + # Could metatheory help? + all_non_poly_terms = mapreduce(d -> d.non_polynomial_terms, vcat, polydata) + unique!(all_non_poly_terms) + + # each variable can only be replaced by one non-polynomial expression involving + # that variable. Keep track of this mapping. + var_to_nonpoly = Dict{BasicSymbolic, PolynomialTransformationData}() + + is_poly = true + transformation_err = nothing + for t in all_non_poly_terms + # if the term involves multiple unknowns, we can't invert it + dvs_in_term = map(x -> occursin(x, t), dvs) + if count(dvs_in_term) > 1 + transformation_err = MultivarTerm(t, dvs[dvs_in_term]) + is_poly = false + break + end + # we already have a substitution solving for `var` + var = dvs[findfirst(dvs_in_term)] + if haskey(var_to_nonpoly, var) && !isequal(var_to_nonpoly[var].term, t) + transformation_err = MultipleTermsOfSameVar([t, var_to_nonpoly[var].term], var) + is_poly = false + break + end + # we want to solve `term - new_var` for `var` + new_var = gensym(Symbol(var)) + new_var = unwrap(only(@variables $new_var)) + invterm = Symbolics.ia_solve( + t - new_var, var; complex_roots = false, periodic_roots = false, warns = false) + # if we can't invert it, quit + if invterm === nothing || isempty(invterm) + transformation_err = SymbolicSolveFailure(t, var) + is_poly = false + break + end + # `ia_solve` returns lazy terms i.e. `asin(1.0)` instead of `pi/2` + # this just evaluates the constant expressions + invterm = Symbolics.substitute.(invterm, (Dict(),)) + # RootsOf implies Symbolics couldn't solve the inner polynomial because + # `Nemo` wasn't loaded. + if any(x -> iscall(x) && operation(x) == Symbolics.RootsOf, invterm) + transformation_err = NemoNotLoaded() + is_poly = false + break + end + var_to_nonpoly[var] = PolynomialTransformationData(new_var, t, invterm) + end + + # return the error instead of throwing it, so the user can choose what to do + # without having to catch the exception + if !is_poly + return NotPolynomialError(transformation_err, eqs, polydata) + end + + subrules = Dict{BasicSymbolic, BasicSymbolic}() + # corresponding to each unknown in `dvs`, the list of its possible solutions + # in terms of the new unknown. + combinations = Vector{BasicSymbolic}[] + new_dvs = BasicSymbolic[] + for x in dvs + if haskey(var_to_nonpoly, x) + _data = var_to_nonpoly[x] + # map term to new unknown + subrules[_data.term] = _data.new_var + push!(combinations, _data.inv_term) + push!(new_dvs, _data.new_var) + else + push!(combinations, BasicSymbolic[x]) + push!(new_dvs, x) + end + end + all_solutions = vec(collect.(collect(Iterators.product(combinations...)))) + return PolynomialTransformation(subrules, all_solutions, new_dvs, polydata) +end + +""" + $(TYPEDEF) + +A struct containing the result of transforming a system into a polynomial system +using the appropriate `PolynomialTransformation`. Also contains the denominators +in the equations, to rule out invalid roots. +""" +struct PolynomialTransformationResult + sys::NonlinearSystem + denominators::Vector{BasicSymbolic} +end + +""" + $(TYPEDSIGNATURES) + +Transform the system `sys` with `transformation` and return a +`PolynomialTransformationResult`, or a `NotPolynomialError` if the system cannot +be transformed. +""" +function transform_system(sys::NonlinearSystem, transformation::PolynomialTransformation) + subrules = transformation.substitution_rules + dvs = unknowns(sys) + eqs = full_equations(sys) + polydata = transformation.polydata + new_dvs = transformation.new_dvs + all_solutions = transformation.all_solutions + + eqs2 = Equation[] + denoms = BasicSymbolic[] + for eq in eqs + t = eq.rhs - eq.lhs + t = Symbolics.fixpoint_sub(t, subrules; maxiters = length(dvs)) + # the substituted variable occurs outside the substituted term + poly_and_nonpoly = map(dvs) do x + all(!isequal(x), new_dvs) && occursin(x, t) + end + if any(poly_and_nonpoly) + return NotPolynomialError( + VariablesAsPolyAndNonPoly(dvs[poly_and_nonpoly]), eqs, polydata) + end + num, den = handle_rational_polynomials(t, new_dvs) + # make factors different elements, otherwise the nonzero factors artificially + # inflate the error of the zero factor. + if iscall(den) && operation(den) == * + for arg in arguments(den) + # ignore constant factors + symbolic_type(arg) == NotSymbolic() && continue + push!(denoms, abs(arg)) + end + elseif symbolic_type(den) != NotSymbolic() + push!(denoms, abs(den)) + end + push!(eqs2, 0 ~ num) + end + + sys2 = @set sys.eqs = eqs2 + @set! sys2.unknowns = new_dvs + # remove observed equations to avoid adding them in codegen + @set! sys2.observed = Equation[] + @set! sys2.substitutions = nothing + return PolynomialTransformationResult(sys2, denoms) +end + +""" +$(TYPEDSIGNATURES) + +Given a `x`, a polynomial in variables in `wrt` which may contain rational functions, +express `x` as a single rational function with polynomial `num` and denominator `den`. +Return `(num, den)`. +""" +function handle_rational_polynomials(x, wrt) + x = unwrap(x) + symbolic_type(x) == NotSymbolic() && return x, 1 + iscall(x) || return x, 1 + contains_variable(x, wrt) || return x, 1 + any(isequal(x), wrt) && return x, 1 + + # simplify_fractions cancels out some common factors + # and expands (a / b)^c to a^c / b^c, so we only need + # to handle these cases + x = simplify_fractions(x) + op = operation(x) + args = arguments(x) + + if op == / + # numerator and denominator are trivial + num, den = args + # but also search for rational functions in numerator + n, d = handle_rational_polynomials(num, wrt) + num, den = n, den * d + elseif op == + + num = 0 + den = 1 + + # we don't need to do common denominator + # because we don't care about cases where denominator + # is zero. The expression is zero when all the numerators + # are zero. + for arg in args + n, d = handle_rational_polynomials(arg, wrt) + num += n + den *= d + end + else + return x, 1 + end + # if the denominator isn't a polynomial in `wrt`, better to not include it + # to reduce the size of the gcd polynomial + if !contains_variable(den, wrt) + return num / den, 1 + end + return num, den +end From 669bc84e41897d8c3a0a270adf941829be1cdd10 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Sat, 30 Nov 2024 19:53:48 +0530 Subject: [PATCH 0448/1337] feat: add `safe_HomotopyContinuationProblem` --- ext/MTKHomotopyContinuationExt.jl | 10 +++++++-- .../nonlinear/homotopy_continuation.jl | 21 +++++++++++++++++++ test/extensions/homotopy_continuation.jl | 20 ++++++++++++++++++ 3 files changed, 49 insertions(+), 2 deletions(-) diff --git a/ext/MTKHomotopyContinuationExt.jl b/ext/MTKHomotopyContinuationExt.jl index 586d608156..c4a090d9a8 100644 --- a/ext/MTKHomotopyContinuationExt.jl +++ b/ext/MTKHomotopyContinuationExt.jl @@ -96,16 +96,22 @@ All other keyword arguments are forwarded to `HomotopyContinuation.solver_starts """ function MTK.HomotopyContinuationProblem( sys::NonlinearSystem, u0map, parammap = nothing; kwargs...) + prob = MTK._safe_HomotopyContinuationProblem(sys, u0map, parammap; kwargs...) + prob isa MTK.HomotopyContinuationProblem || throw(prob) + return prob +end + +function MTK._safe_HomotopyContinuationProblem(sys, u0map, parammap = nothing; kwargs...) if !iscomplete(sys) error("A completed `NonlinearSystem` is required. Call `complete` or `structural_simplify` on the system before creating a `HomotopyContinuationProblem`") end transformation = MTK.PolynomialTransformation(sys) if transformation isa MTK.NotPolynomialError - throw(transformation) + return transformation end result = MTK.transform_system(sys, transformation) if result isa MTK.NotPolynomialError - throw(result) + return result end MTK.HomotopyContinuationProblem(sys, transformation, result, u0map, parammap; kwargs...) end diff --git a/src/systems/nonlinear/homotopy_continuation.jl b/src/systems/nonlinear/homotopy_continuation.jl index 044f44f70f..ed69db1aee 100644 --- a/src/systems/nonlinear/homotopy_continuation.jl +++ b/src/systems/nonlinear/homotopy_continuation.jl @@ -48,6 +48,27 @@ function HomotopyContinuationProblem(::AbstractSystem, _u0, _p; kwargs...) error("HomotopyContinuation.jl is required to create and solve `HomotopyContinuationProblem`s. Please run `Pkg.add(\"HomotopyContinuation\")` to continue.") end +""" + $(TYPEDSIGNATURES) + +Utility function for `safe_HomotopyContinuationProblem`, implemented in the extension. +""" +function _safe_HomotopyContinuationProblem end + +""" + $(TYPEDSIGNATURES) + +Return a `HomotopyContinuationProblem` if the extension is loaded and the system is +polynomial. If the extension is not loaded, return `nothing`. If the system is not +polynomial, return the appropriate `NotPolynomialError`. +""" +function safe_HomotopyContinuationProblem(sys::NonlinearSystem, args...; kwargs...) + if Base.get_extension(ModelingToolkit, :MTKHomotopyContinuationExt) === nothing + return nothing + end + return _safe_HomotopyContinuationProblem(sys, args...; kwargs...) +end + SymbolicIndexingInterface.symbolic_container(p::HomotopyContinuationProblem) = p.sys SymbolicIndexingInterface.state_values(p::HomotopyContinuationProblem) = p.u0 function SymbolicIndexingInterface.set_state!(p::HomotopyContinuationProblem, args...) diff --git a/test/extensions/homotopy_continuation.jl b/test/extensions/homotopy_continuation.jl index 81c252c84a..3bdfa06a5d 100644 --- a/test/extensions/homotopy_continuation.jl +++ b/test/extensions/homotopy_continuation.jl @@ -1,6 +1,19 @@ using ModelingToolkit, NonlinearSolve, SymbolicIndexingInterface +import ModelingToolkit as MTK using LinearAlgebra using Test + +@testset "Safe HCProblem" begin + @variables x y z + eqs = [0 ~ x^2 + y^2 + 2x * y + 0 ~ x^2 + 4x + 4 + 0 ~ y * z + 4x^2] + @mtkbuild sys = NonlinearSystem(eqs) + prob = MTK.safe_HomotopyContinuationProblem(sys, [x => 1.0, y => 1.0, z => 1.0], []) + @test prob === nothing +end + + import HomotopyContinuation @testset "No parameters" begin @@ -78,30 +91,37 @@ end @test_throws ["Cannot convert", "Unable", "symbolically solve", "Exponent", "not an integer", "not a polynomial"] HomotopyContinuationProblem( sys, []) + @test MTK.safe_HomotopyContinuationProblem(sys, []) isa MTK.NotPolynomialError @mtkbuild sys = NonlinearSystem([x^x - x ~ 0]) @test_throws ["Cannot convert", "Unable", "symbolically solve", "Exponent", "unknowns", "not a polynomial"] HomotopyContinuationProblem( sys, []) + @test MTK.safe_HomotopyContinuationProblem(sys, []) isa MTK.NotPolynomialError @mtkbuild sys = NonlinearSystem([((x^2) / sin(x))^2 + x ~ 0]) @test_throws ["Cannot convert", "both polynomial", "non-polynomial", "recognized", "sin", "not a polynomial"] HomotopyContinuationProblem( sys, []) + @test MTK.safe_HomotopyContinuationProblem(sys, []) isa MTK.NotPolynomialError @variables y = 2.0 @mtkbuild sys = NonlinearSystem([x^2 + y^2 + 2 ~ 0, y ~ sin(x)]) @test_throws ["Cannot convert", "recognized", "sin", "not a polynomial"] HomotopyContinuationProblem( sys, []) + @test MTK.safe_HomotopyContinuationProblem(sys, []) isa MTK.NotPolynomialError @mtkbuild sys = NonlinearSystem([x^2 + y^2 - 2 ~ 0, sin(x + y) ~ 0]) @test_throws ["Cannot convert", "function of multiple unknowns"] HomotopyContinuationProblem( sys, []) + @test MTK.safe_HomotopyContinuationProblem(sys, []) isa MTK.NotPolynomialError @mtkbuild sys = NonlinearSystem([sin(x)^2 + 1 ~ 0, cos(y) - cos(x) - 1 ~ 0]) @test_throws ["Cannot convert", "multiple non-polynomial terms", "same unknown"] HomotopyContinuationProblem( sys, []) + @test MTK.safe_HomotopyContinuationProblem(sys, []) isa MTK.NotPolynomialError @mtkbuild sys = NonlinearSystem([sin(x^2)^2 + sin(x^2) - 1 ~ 0]) @test_throws ["import Nemo"] HomotopyContinuationProblem(sys, []) + @test MTK.safe_HomotopyContinuationProblem(sys, []) isa MTK.NotPolynomialError end import Nemo From 1ef12170e0206694ac627639b28589871e2128a4 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Sat, 30 Nov 2024 19:58:00 +0530 Subject: [PATCH 0449/1337] feat: use `HomotopyContinuationProblem` in `NonlinearProblem` if possible --- src/systems/nonlinear/nonlinearsystem.jl | 6 +++++- test/extensions/homotopy_continuation.jl | 18 ++++++++++++++++-- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/src/systems/nonlinear/nonlinearsystem.jl b/src/systems/nonlinear/nonlinearsystem.jl index ff4a7d0eb2..3cb68853aa 100644 --- a/src/systems/nonlinear/nonlinearsystem.jl +++ b/src/systems/nonlinear/nonlinearsystem.jl @@ -496,10 +496,14 @@ end function DiffEqBase.NonlinearProblem{iip}(sys::NonlinearSystem, u0map, parammap = DiffEqBase.NullParameters(); - check_length = true, kwargs...) where {iip} + check_length = true, use_homotopy_continuation = true, kwargs...) where {iip} if !iscomplete(sys) error("A completed `NonlinearSystem` is required. Call `complete` or `structural_simplify` on the system before creating a `NonlinearProblem`") end + prob = safe_HomotopyContinuationProblem(sys, u0map, parammap; check_length, kwargs...) + if prob isa HomotopyContinuationProblem + return prob + end f, u0, p = process_SciMLProblem(NonlinearFunction{iip}, sys, u0map, parammap; check_length, kwargs...) pt = something(get_metadata(sys), StandardNonlinearProblem()) diff --git a/test/extensions/homotopy_continuation.jl b/test/extensions/homotopy_continuation.jl index 3bdfa06a5d..e57cd0b2f7 100644 --- a/test/extensions/homotopy_continuation.jl +++ b/test/extensions/homotopy_continuation.jl @@ -13,7 +13,6 @@ using Test @test prob === nothing end - import HomotopyContinuation @testset "No parameters" begin @@ -22,12 +21,19 @@ import HomotopyContinuation 0 ~ x^2 + 4x + 4 0 ~ y * z + 4x^2] @mtkbuild sys = NonlinearSystem(eqs) - prob = HomotopyContinuationProblem(sys, [x => 1.0, y => 1.0, z => 1.0], []) + u0 = [x => 1.0, y => 1.0, z => 1.0] + prob = HomotopyContinuationProblem(sys, u0) @test prob[x] == prob[y] == prob[z] == 1.0 @test prob[x + y] == 2.0 sol = solve(prob; threading = false) @test SciMLBase.successful_retcode(sol) @test norm(sol.resid)≈0.0 atol=1e-10 + + prob2 = NonlinearProblem(sys, u0) + @test prob2 isa HomotopyContinuationProblem + sol = solve(prob2; threading = false) + @test SciMLBase.successful_retcode(sol) + @test norm(sol.resid)≈0.0 atol=1e-10 end struct Wrapper @@ -92,36 +98,44 @@ end "Exponent", "not an integer", "not a polynomial"] HomotopyContinuationProblem( sys, []) @test MTK.safe_HomotopyContinuationProblem(sys, []) isa MTK.NotPolynomialError + @test NonlinearProblem(sys, []) isa NonlinearProblem + @mtkbuild sys = NonlinearSystem([x^x - x ~ 0]) @test_throws ["Cannot convert", "Unable", "symbolically solve", "Exponent", "unknowns", "not a polynomial"] HomotopyContinuationProblem( sys, []) @test MTK.safe_HomotopyContinuationProblem(sys, []) isa MTK.NotPolynomialError + @test NonlinearProblem(sys, []) isa NonlinearProblem @mtkbuild sys = NonlinearSystem([((x^2) / sin(x))^2 + x ~ 0]) @test_throws ["Cannot convert", "both polynomial", "non-polynomial", "recognized", "sin", "not a polynomial"] HomotopyContinuationProblem( sys, []) @test MTK.safe_HomotopyContinuationProblem(sys, []) isa MTK.NotPolynomialError + @test NonlinearProblem(sys, []) isa NonlinearProblem @variables y = 2.0 @mtkbuild sys = NonlinearSystem([x^2 + y^2 + 2 ~ 0, y ~ sin(x)]) @test_throws ["Cannot convert", "recognized", "sin", "not a polynomial"] HomotopyContinuationProblem( sys, []) @test MTK.safe_HomotopyContinuationProblem(sys, []) isa MTK.NotPolynomialError + @test NonlinearProblem(sys, []) isa NonlinearProblem @mtkbuild sys = NonlinearSystem([x^2 + y^2 - 2 ~ 0, sin(x + y) ~ 0]) @test_throws ["Cannot convert", "function of multiple unknowns"] HomotopyContinuationProblem( sys, []) @test MTK.safe_HomotopyContinuationProblem(sys, []) isa MTK.NotPolynomialError + @test NonlinearProblem(sys, []) isa NonlinearProblem @mtkbuild sys = NonlinearSystem([sin(x)^2 + 1 ~ 0, cos(y) - cos(x) - 1 ~ 0]) @test_throws ["Cannot convert", "multiple non-polynomial terms", "same unknown"] HomotopyContinuationProblem( sys, []) @test MTK.safe_HomotopyContinuationProblem(sys, []) isa MTK.NotPolynomialError + @test NonlinearProblem(sys, []) isa NonlinearProblem @mtkbuild sys = NonlinearSystem([sin(x^2)^2 + sin(x^2) - 1 ~ 0]) @test_throws ["import Nemo"] HomotopyContinuationProblem(sys, []) @test MTK.safe_HomotopyContinuationProblem(sys, []) isa MTK.NotPolynomialError + @test NonlinearProblem(sys, []) isa NonlinearProblem end import Nemo From aa0e08ad700d5c9f52c0a6fd258ee11c3a5add3e Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Sat, 30 Nov 2024 23:16:17 +0530 Subject: [PATCH 0450/1337] docs: add docstrings to `NonPolynomialReason` enum variants --- src/systems/nonlinear/homotopy_continuation.jl | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/systems/nonlinear/homotopy_continuation.jl b/src/systems/nonlinear/homotopy_continuation.jl index ed69db1aee..03aeed1edf 100644 --- a/src/systems/nonlinear/homotopy_continuation.jl +++ b/src/systems/nonlinear/homotopy_continuation.jl @@ -96,9 +96,21 @@ end Possible reasons why a term is not polynomial """ EnumX.@enumx NonPolynomialReason begin + """ + Exponent of an expression involving unknowns is not an integer. + """ NonIntegerExponent + """ + Exponent is an expression containing unknowns. + """ ExponentContainsUnknowns + """ + The base of an exponent is not a polynomial in the unknowns. + """ BaseNotPolynomial + """ + An expression involves a non-polynomial operation involving unknowns. + """ UnrecognizedOperation end From b9b396ddb80537b71d9c06dca20df22e44eb6bc3 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Sun, 8 Dec 2024 10:56:17 +0530 Subject: [PATCH 0451/1337] test: add logs to debug tests --- test/extensions/homotopy_continuation.jl | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/extensions/homotopy_continuation.jl b/test/extensions/homotopy_continuation.jl index e57cd0b2f7..f9724692e7 100644 --- a/test/extensions/homotopy_continuation.jl +++ b/test/extensions/homotopy_continuation.jl @@ -160,6 +160,9 @@ end @test prob[x] ≈ 0.25 @test prob[y] ≈ 0.125 sol = solve(prob; threading = false) + # can't replicate the solve failure locally, so CI logs might help + @show sol.u HomotopyContinuation.real_solutions(sol.original) + @test SciMLBase.successful_retcode(sol) @test sol[a]≈0.5 atol=1e-6 @test sol[b]≈0.25 atol=1e-6 end From f428df4bc0dbc298a0f15280bb980679d7f895ef Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 9 Dec 2024 12:41:19 +0530 Subject: [PATCH 0452/1337] fixup! test: add logs to debug tests --- test/extensions/homotopy_continuation.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/extensions/homotopy_continuation.jl b/test/extensions/homotopy_continuation.jl index f9724692e7..554f9e1e1d 100644 --- a/test/extensions/homotopy_continuation.jl +++ b/test/extensions/homotopy_continuation.jl @@ -161,7 +161,7 @@ end @test prob[y] ≈ 0.125 sol = solve(prob; threading = false) # can't replicate the solve failure locally, so CI logs might help - @show sol.u HomotopyContinuation.real_solutions(sol.original) + @show sol.u sol.original.path_results @test SciMLBase.successful_retcode(sol) @test sol[a]≈0.5 atol=1e-6 @test sol[b]≈0.25 atol=1e-6 From 16d3f5c7a57bffd698a6c69da3ea89e0089b2d54 Mon Sep 17 00:00:00 2001 From: Benjamin Chung Date: Mon, 9 Dec 2024 02:52:15 -0800 Subject: [PATCH 0453/1337] Refactor ImperativeAffect into its own file --- src/ModelingToolkit.jl | 1 + src/systems/callbacks.jl | 221 +------------------------------ src/systems/diffeqs/odesystem.jl | 2 - src/systems/imperative_affect.jl | 220 ++++++++++++++++++++++++++++++ 4 files changed, 224 insertions(+), 220 deletions(-) create mode 100644 src/systems/imperative_affect.jl diff --git a/src/ModelingToolkit.jl b/src/ModelingToolkit.jl index de8e69c41f..ccdaa15e99 100644 --- a/src/ModelingToolkit.jl +++ b/src/ModelingToolkit.jl @@ -145,6 +145,7 @@ include("systems/parameter_buffer.jl") include("systems/abstractsystem.jl") include("systems/model_parsing.jl") include("systems/connectors.jl") +include("systems/imperative_affect.jl") include("systems/callbacks.jl") include("systems/problem_utils.jl") diff --git a/src/systems/callbacks.jl b/src/systems/callbacks.jl index 0cb7f16c9f..4533534029 100644 --- a/src/systems/callbacks.jl +++ b/src/systems/callbacks.jl @@ -73,111 +73,6 @@ function namespace_affect(affect::FunctionalAffect, s) context(affect)) end -""" - ImperativeAffect(f::Function; modified::NamedTuple, observed::NamedTuple, ctx) - -`ImperativeAffect` is a helper for writing affect functions that will compute observed values and -ensure that modified values are correctly written back into the system. The affect function `f` needs to have -the signature - -``` - f(modified::NamedTuple, observed::NamedTuple, ctx, integrator)::NamedTuple -``` - -The function `f` will be called with `observed` and `modified` `NamedTuple`s that are derived from their respective `NamedTuple` definitions. -Each declaration`NamedTuple` should map an expression to a symbol; for example if we pass `observed=(; x = a + b)` this will alias the result of executing `a+b` in the system as `x` -so the value of `a + b` will be accessible as `observed.x` in `f`. `modified` currently restricts symbolic expressions to only bare variables, so only tuples of the form -`(; x = y)` or `(; x)` (which aliases `x` as itself) are allowed. - -The argument NamedTuples (for instance `(;x=y)`) will be populated with the declared values on function entry; if we require `(;x=y)` in `observed` and `y=2`, for example, -then the NamedTuple `(;x=2)` will be passed as `observed` to the affect function `f`. - -The NamedTuple returned from `f` includes the values to be written back to the system after `f` returns. For example, if we want to update the value of `x` to be the result of `x + y` we could write - - ImperativeAffect(observed=(; x_plus_y = x + y), modified=(; x)) do m, o - @set! m.x = o.x_plus_y - end - -Where we use Setfield to copy the tuple `m` with a new value for `x`, then return the modified value of `m`. All values updated by the tuple must have names originally declared in -`modified`; a runtime error will be produced if a value is written that does not appear in `modified`. The user can dynamically decide not to write a value back by not including it -in the returned tuple, in which case the associated field will not be updated. -""" -@kwdef struct ImperativeAffect - f::Any - obs::Vector - obs_syms::Vector{Symbol} - modified::Vector - mod_syms::Vector{Symbol} - ctx::Any - skip_checks::Bool -end - -function ImperativeAffect(f::Function; - observed::NamedTuple = NamedTuple{()}(()), - modified::NamedTuple = NamedTuple{()}(()), - ctx = nothing, - skip_checks = false) - ImperativeAffect(f, - collect(values(observed)), collect(keys(observed)), - collect(values(modified)), collect(keys(modified)), - ctx, skip_checks) -end -function ImperativeAffect(f::Function, modified::NamedTuple; - observed::NamedTuple = NamedTuple{()}(()), ctx = nothing, skip_checks = false) - ImperativeAffect( - f, observed = observed, modified = modified, ctx = ctx, skip_checks = skip_checks) -end -function ImperativeAffect( - f::Function, modified::NamedTuple, observed::NamedTuple; ctx = nothing, skip_checks = false) - ImperativeAffect( - f, observed = observed, modified = modified, ctx = ctx, skip_checks = skip_checks) -end -function ImperativeAffect( - f::Function, modified::NamedTuple, observed::NamedTuple, ctx; skip_checks = false) - ImperativeAffect( - f, observed = observed, modified = modified, ctx = ctx, skip_checks = skip_checks) -end - -function Base.show(io::IO, mfa::ImperativeAffect) - obs_vals = join(map((ob, nm) -> "$ob => $nm", mfa.obs, mfa.obs_syms), ", ") - mod_vals = join(map((md, nm) -> "$md => $nm", mfa.modified, mfa.mod_syms), ", ") - affect = mfa.f - print(io, - "ImperativeAffect(observed: [$obs_vals], modified: [$mod_vals], affect:$affect)") -end -func(f::ImperativeAffect) = f.f -context(a::ImperativeAffect) = a.ctx -observed(a::ImperativeAffect) = a.obs -observed_syms(a::ImperativeAffect) = a.obs_syms -discretes(a::ImperativeAffect) = filter(ModelingToolkit.isparameter, a.modified) -modified(a::ImperativeAffect) = a.modified -modified_syms(a::ImperativeAffect) = a.mod_syms - -function Base.:(==)(a1::ImperativeAffect, a2::ImperativeAffect) - isequal(a1.f, a2.f) && isequal(a1.obs, a2.obs) && isequal(a1.modified, a2.modified) && - isequal(a1.obs_syms, a2.obs_syms) && isequal(a1.mod_syms, a2.mod_syms) && - isequal(a1.ctx, a2.ctx) -end - -function Base.hash(a::ImperativeAffect, s::UInt) - s = hash(a.f, s) - s = hash(a.obs, s) - s = hash(a.obs_syms, s) - s = hash(a.modified, s) - s = hash(a.mod_syms, s) - hash(a.ctx, s) -end - -function namespace_affect(affect::ImperativeAffect, s) - ImperativeAffect(func(affect), - namespace_expr.(observed(affect), (s,)), - observed_syms(affect), - renamespace.((s,), modified(affect)), - modified_syms(affect), - context(affect), - affect.skip_checks) -end - function has_functional_affect(cb) (affects(cb) isa FunctionalAffect || affects(cb) isa ImperativeAffect) end @@ -203,13 +98,13 @@ sharp discontinuity between integrator steps (which in this example would not no guaranteed to be triggered. Once detected the integrator will "wind back" through a root-finding process to identify the point when the condition became active; the method used -is specified by `rootfind` from [`SciMLBase.RootfindOpt`](@ref). If we denote the time when the condition becomes active at tc, +is specified by `rootfind` from [`SciMLBase.RootfindOpt`](@ref). If we denote the time when the condition becomes active as `tc``, the value in the integrator after windback will be: * `u[tc-epsilon], p[tc-epsilon], tc` if `LeftRootFind` is used, * `u[tc+epsilon], p[tc+epsilon], tc` if `RightRootFind` is used, * or `u[t], p[t], t` if `NoRootFind` is used. For example, if we want to detect when an unknown variable `x` satisfies `x > 0` using the condition `x ~ 0` on a positive edge (that is, `D(x) > 0`), -then left root finding will get us `x=-epsilon`, right root finding `x=epsilon` and no root finding whatever the next step of the integrator was after +then left root finding will get us `x=-epsilon`, right root finding `x=epsilon` and no root finding will produce whatever the next step of the integrator was after it passed through 0. Multiple callbacks in the same system with different `rootfind` operations will be grouped @@ -405,7 +300,6 @@ 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(af::ImperativeAffect, s) = namespace_affect(af, s) namespace_affects(::Nothing, s) = nothing function namespace_callback(cb::SymbolicContinuousCallback, s)::SymbolicContinuousCallback @@ -480,7 +374,6 @@ scalarize_affects(affects) = scalarize(affects) scalarize_affects(affects::Tuple) = FunctionalAffect(affects...) scalarize_affects(affects::NamedTuple) = FunctionalAffect(; affects...) scalarize_affects(affects::FunctionalAffect) = affects -scalarize_affects(affects::ImperativeAffect) = affects SymbolicDiscreteCallback(p::Pair) = SymbolicDiscreteCallback(p[1], p[2]) SymbolicDiscreteCallback(cb::SymbolicDiscreteCallback) = cb # passthrough @@ -1099,117 +992,9 @@ function check_assignable(sys, sym) end end -function compile_user_affect(affect::ImperativeAffect, cb, sys, dvs, ps; kwargs...) - #= - Implementation sketch: - generate observed function (oop), should save to a component array under obs_syms - do the same stuff as the normal FA for pars_syms - call the affect method - unpack and apply the resulting values - =# - function check_dups(syms, exprs) # = (syms_dedup, exprs_dedup) - seen = Set{Symbol}() - syms_dedup = [] - exprs_dedup = [] - for (sym, exp) in Iterators.zip(syms, exprs) - if !in(sym, seen) - push!(syms_dedup, sym) - push!(exprs_dedup, exp) - push!(seen, sym) - elseif !affect.skip_checks - @warn "Expression $(expr) is aliased as $sym, which has already been used. The first definition will be used." - end - end - return (syms_dedup, exprs_dedup) - end - - obs_exprs = observed(affect) - if !affect.skip_checks - for oexpr in obs_exprs - invalid_vars = invalid_variables(sys, oexpr) - if length(invalid_vars) > 0 - error("Observed equation $(oexpr) in affect refers to missing variable(s) $(invalid_vars); the variables may not have been added (e.g. if a component is missing).") - end - end - end - obs_syms = observed_syms(affect) - obs_syms, obs_exprs = check_dups(obs_syms, obs_exprs) - - mod_exprs = modified(affect) - if !affect.skip_checks - for mexpr in mod_exprs - if !check_assignable(sys, mexpr) - @warn ("Expression $mexpr cannot be assigned to; currently only unknowns and parameters may be updated by an affect.") - end - invalid_vars = unassignable_variables(sys, mexpr) - if length(invalid_vars) > 0 - error("Modified equation $(mexpr) in affect refers to missing variable(s) $(invalid_vars); the variables may not have been added (e.g. if a component is missing) or they may have been reduced away.") - end - end - end - mod_syms = modified_syms(affect) - mod_syms, mod_exprs = check_dups(mod_syms, mod_exprs) - - overlapping_syms = intersect(mod_syms, obs_syms) - if length(overlapping_syms) > 0 && !affect.skip_checks - @warn "The symbols $overlapping_syms 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." - end - - # sanity checks done! now build the data and update function for observed values - mkzero(sz) = - if sz === () - 0.0 - else - zeros(sz) - end - obs_fun = build_explicit_observed_function( - sys, Symbolics.scalarize.(obs_exprs); - array_type = Tuple) - obs_sym_tuple = (obs_syms...,) - - # okay so now to generate the stuff to assign it back into the system - mod_pairs = mod_exprs .=> mod_syms - mod_names = (mod_syms...,) - mod_og_val_fun = build_explicit_observed_function( - sys, Symbolics.scalarize.(first.(mod_pairs)); - array_type = Tuple) - - 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) - # 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) - - # update the observed values - obs_component_array = NamedTuple{obs_sym_tuple}(obs_fun( - integ.u, integ.p, integ.t)) - - # let the user do their thing - upd_vals = user_affect(upd_component_array, obs_component_array, ctx, integ) - - # 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 - -function compile_affect( - affect::Union{FunctionalAffect, ImperativeAffect}, cb, sys, dvs, ps; kwargs...) +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 diff --git a/src/systems/diffeqs/odesystem.jl b/src/systems/diffeqs/odesystem.jl index 4040b7a646..2b0bd8c8d7 100644 --- a/src/systems/diffeqs/odesystem.jl +++ b/src/systems/diffeqs/odesystem.jl @@ -629,8 +629,6 @@ function build_explicit_observed_function(sys, ts; oop_mtkp_wrapper = mtkparams_wrapper end - output_expr = isscalar ? ts[1] : - (array_type <: Vector ? MakeArray(ts, output_type) : MakeTuple(ts)) # Need to keep old method of building the function since it uses `output_type`, # which can't be provided to `build_function` return_value = if isscalar diff --git a/src/systems/imperative_affect.jl b/src/systems/imperative_affect.jl new file mode 100644 index 0000000000..f20ad941da --- /dev/null +++ b/src/systems/imperative_affect.jl @@ -0,0 +1,220 @@ + +""" + ImperativeAffect(f::Function; modified::NamedTuple, observed::NamedTuple, ctx) + +`ImperativeAffect` is a helper for writing affect functions that will compute observed values and +ensure that modified values are correctly written back into the system. The affect function `f` needs to have +the signature + +``` + f(modified::NamedTuple, observed::NamedTuple, ctx, integrator)::NamedTuple +``` + +The function `f` will be called with `observed` and `modified` `NamedTuple`s that are derived from their respective `NamedTuple` definitions. +Each declaration`NamedTuple` should map an expression to a symbol; for example if we pass `observed=(; x = a + b)` this will alias the result of executing `a+b` in the system as `x` +so the value of `a + b` will be accessible as `observed.x` in `f`. `modified` currently restricts symbolic expressions to only bare variables, so only tuples of the form +`(; x = y)` or `(; x)` (which aliases `x` as itself) are allowed. + +The argument NamedTuples (for instance `(;x=y)`) will be populated with the declared values on function entry; if we require `(;x=y)` in `observed` and `y=2`, for example, +then the NamedTuple `(;x=2)` will be passed as `observed` to the affect function `f`. + +The NamedTuple returned from `f` includes the values to be written back to the system after `f` returns. For example, if we want to update the value of `x` to be the result of `x + y` we could write + + ImperativeAffect(observed=(; x_plus_y = x + y), modified=(; x)) do m, o + @set! m.x = o.x_plus_y + end + +Where we use Setfield to copy the tuple `m` with a new value for `x`, then return the modified value of `m`. All values updated by the tuple must have names originally declared in +`modified`; a runtime error will be produced if a value is written that does not appear in `modified`. The user can dynamically decide not to write a value back by not including it +in the returned tuple, in which case the associated field will not be updated. +""" +@kwdef struct ImperativeAffect + f::Any + obs::Vector + obs_syms::Vector{Symbol} + modified::Vector + mod_syms::Vector{Symbol} + ctx::Any + skip_checks::Bool +end + +function ImperativeAffect(f::Function; + observed::NamedTuple = NamedTuple{()}(()), + modified::NamedTuple = NamedTuple{()}(()), + ctx = nothing, + skip_checks = false) + ImperativeAffect(f, + collect(values(observed)), collect(keys(observed)), + collect(values(modified)), collect(keys(modified)), + ctx, skip_checks) +end +function ImperativeAffect(f::Function, modified::NamedTuple; + observed::NamedTuple = NamedTuple{()}(()), ctx = nothing, skip_checks = false) + ImperativeAffect( + f, observed = observed, modified = modified, ctx = ctx, skip_checks = skip_checks) +end +function ImperativeAffect( + f::Function, modified::NamedTuple, observed::NamedTuple; ctx = nothing, skip_checks = false) + ImperativeAffect( + f, observed = observed, modified = modified, ctx = ctx, skip_checks = skip_checks) +end +function ImperativeAffect( + f::Function, modified::NamedTuple, observed::NamedTuple, ctx; skip_checks = false) + ImperativeAffect( + f, observed = observed, modified = modified, ctx = ctx, skip_checks = skip_checks) +end + +function Base.show(io::IO, mfa::ImperativeAffect) + obs_vals = join(map((ob, nm) -> "$ob => $nm", mfa.obs, mfa.obs_syms), ", ") + mod_vals = join(map((md, nm) -> "$md => $nm", mfa.modified, mfa.mod_syms), ", ") + affect = mfa.f + print(io, + "ImperativeAffect(observed: [$obs_vals], modified: [$mod_vals], affect:$affect)") +end +func(f::ImperativeAffect) = f.f +context(a::ImperativeAffect) = a.ctx +observed(a::ImperativeAffect) = a.obs +observed_syms(a::ImperativeAffect) = a.obs_syms +discretes(a::ImperativeAffect) = filter(ModelingToolkit.isparameter, a.modified) +modified(a::ImperativeAffect) = a.modified +modified_syms(a::ImperativeAffect) = a.mod_syms + +function Base.:(==)(a1::ImperativeAffect, a2::ImperativeAffect) + isequal(a1.f, a2.f) && isequal(a1.obs, a2.obs) && isequal(a1.modified, a2.modified) && + isequal(a1.obs_syms, a2.obs_syms) && isequal(a1.mod_syms, a2.mod_syms) && + isequal(a1.ctx, a2.ctx) +end + +function Base.hash(a::ImperativeAffect, s::UInt) + s = hash(a.f, s) + s = hash(a.obs, s) + s = hash(a.obs_syms, s) + s = hash(a.modified, s) + s = hash(a.mod_syms, s) + 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,)), + observed_syms(affect), + renamespace.((s,), modified(affect)), + modified_syms(affect), + context(affect), + affect.skip_checks) +end + +function compile_affect(affect::ImperativeAffect, cb, sys, dvs, ps; kwargs...) + compile_user_affect(affect, cb, sys, dvs, ps; kwargs...) +end + +function compile_user_affect(affect::ImperativeAffect, cb, sys, dvs, ps; kwargs...) + #= + Implementation sketch: + generate observed function (oop), should save to a component array under obs_syms + do the same stuff as the normal FA for pars_syms + call the affect method + unpack and apply the resulting values + =# + function check_dups(syms, exprs) # = (syms_dedup, exprs_dedup) + seen = Set{Symbol}() + syms_dedup = [] + exprs_dedup = [] + for (sym, exp) in Iterators.zip(syms, exprs) + if !in(sym, seen) + push!(syms_dedup, sym) + push!(exprs_dedup, exp) + push!(seen, sym) + elseif !affect.skip_checks + @warn "Expression $(expr) is aliased as $sym, which has already been used. The first definition will be used." + end + end + return (syms_dedup, exprs_dedup) + end + + obs_exprs = observed(affect) + if !affect.skip_checks + for oexpr in obs_exprs + invalid_vars = invalid_variables(sys, oexpr) + if length(invalid_vars) > 0 + error("Observed equation $(oexpr) in affect refers to missing variable(s) $(invalid_vars); the variables may not have been added (e.g. if a component is missing).") + end + end + end + obs_syms = observed_syms(affect) + obs_syms, obs_exprs = check_dups(obs_syms, obs_exprs) + + mod_exprs = modified(affect) + if !affect.skip_checks + for mexpr in mod_exprs + if !check_assignable(sys, mexpr) + @warn ("Expression $mexpr cannot be assigned to; currently only unknowns and parameters may be updated by an affect.") + end + invalid_vars = unassignable_variables(sys, mexpr) + if length(invalid_vars) > 0 + error("Modified equation $(mexpr) in affect refers to missing variable(s) $(invalid_vars); the variables may not have been added (e.g. if a component is missing) or they may have been reduced away.") + end + end + end + mod_syms = modified_syms(affect) + mod_syms, mod_exprs = check_dups(mod_syms, mod_exprs) + + overlapping_syms = intersect(mod_syms, obs_syms) + if length(overlapping_syms) > 0 && !affect.skip_checks + @warn "The symbols $overlapping_syms 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." + end + + # sanity checks done! now build the data and update function for observed values + mkzero(sz) = + if sz === () + 0.0 + else + zeros(sz) + end + obs_fun = build_explicit_observed_function( + sys, Symbolics.scalarize.(obs_exprs); + array_type = Tuple) + obs_sym_tuple = (obs_syms...,) + + # okay so now to generate the stuff to assign it back into the system + mod_pairs = mod_exprs .=> mod_syms + mod_names = (mod_syms...,) + mod_og_val_fun = build_explicit_observed_function( + sys, Symbolics.scalarize.(first.(mod_pairs)); + array_type = Tuple) + + 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) + # 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) + + # update the observed values + obs_component_array = NamedTuple{obs_sym_tuple}(obs_fun( + integ.u, integ.p, integ.t)) + + # let the user do their thing + upd_vals = user_affect(upd_component_array, obs_component_array, ctx, integ) + + # 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 + + +scalarize_affects(affects::ImperativeAffect) = affects From 1c78dee591ac5623a240084005545dbca04e2941 Mon Sep 17 00:00:00 2001 From: Benjamin Chung Date: Mon, 9 Dec 2024 03:16:24 -0800 Subject: [PATCH 0454/1337] Fix tests & update API usage --- src/systems/callbacks.jl | 2 +- src/systems/imperative_affect.jl | 4 ++-- test/symbolic_events.jl | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/systems/callbacks.jl b/src/systems/callbacks.jl index 4533534029..57db5e097c 100644 --- a/src/systems/callbacks.jl +++ b/src/systems/callbacks.jl @@ -818,7 +818,7 @@ function generate_vector_rootfinding_callback( let save_idxs = save_idxs custom_init = fn.initialize (i) -> begin - isnothing(custom_init) && custom_init(i) + !isnothing(custom_init) && custom_init(i) for idx in save_idxs SciMLBase.save_discretes!(i, idx) end diff --git a/src/systems/imperative_affect.jl b/src/systems/imperative_affect.jl index f20ad941da..19f7f7590d 100644 --- a/src/systems/imperative_affect.jl +++ b/src/systems/imperative_affect.jl @@ -175,7 +175,7 @@ function compile_user_affect(affect::ImperativeAffect, cb, sys, dvs, ps; kwargs. end obs_fun = build_explicit_observed_function( sys, Symbolics.scalarize.(obs_exprs); - array_type = Tuple) + mkarray = (es,_) -> MakeTuple(es)) obs_sym_tuple = (obs_syms...,) # okay so now to generate the stuff to assign it back into the system @@ -183,7 +183,7 @@ function compile_user_affect(affect::ImperativeAffect, cb, sys, dvs, ps; kwargs. mod_names = (mod_syms...,) mod_og_val_fun = build_explicit_observed_function( sys, Symbolics.scalarize.(first.(mod_pairs)); - array_type = Tuple) + mkarray = (es,_) -> MakeTuple(es)) upd_funs = NamedTuple{mod_names}((setu.((sys,), first.(mod_pairs))...,)) diff --git a/test/symbolic_events.jl b/test/symbolic_events.jl index 717d2438ac..858a30f4fd 100644 --- a/test/symbolic_events.jl +++ b/test/symbolic_events.jl @@ -936,7 +936,7 @@ end @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()) + 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 @@ -1079,8 +1079,8 @@ end @test sort(canonicalize(Discrete(), prob.p)[1]) == [0.0, 1.0, 2.0] sol = solve(prob, Tsit5()) - @test sol[a] == [-1.0] - @test sol[b] == [5.0, 5.0] + @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 @@ -1248,7 +1248,7 @@ end ss = structural_simplify(sys) prob = ODEProblem(ss, [theta => 0.0], (0.0, pi)) sol = solve(prob, Tsit5(); dtmax = 0.01) - @test getp(sol, cnt)(sol) == 197 # 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 + @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 From aa556d612a12f2cc297b7af7282a959aa8674775 Mon Sep 17 00:00:00 2001 From: Benjamin Chung Date: Mon, 9 Dec 2024 03:59:31 -0800 Subject: [PATCH 0455/1337] Adjust quadrature test forward a little to avoid numerical issues --- test/symbolic_events.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/symbolic_events.jl b/test/symbolic_events.jl index 858a30f4fd..1138b7c96f 100644 --- a/test/symbolic_events.jl +++ b/test/symbolic_events.jl @@ -1246,7 +1246,7 @@ end @named sys = ODESystem( eqs, t, [theta, omega], params; continuous_events = [qAevt, qBevt]) ss = structural_simplify(sys) - prob = ODEProblem(ss, [theta => 0.0], (0.0, pi)) + 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 From c169b9e97629c3031bc9f0e82f3c0899e6e17f71 Mon Sep 17 00:00:00 2001 From: Benjamin Chung Date: Mon, 9 Dec 2024 04:01:00 -0800 Subject: [PATCH 0456/1337] Formatter --- src/systems/imperative_affect.jl | 6 ++---- test/symbolic_events.jl | 6 +++--- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/systems/imperative_affect.jl b/src/systems/imperative_affect.jl index 19f7f7590d..2f489913ad 100644 --- a/src/systems/imperative_affect.jl +++ b/src/systems/imperative_affect.jl @@ -94,7 +94,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), @@ -175,7 +174,7 @@ function compile_user_affect(affect::ImperativeAffect, cb, sys, dvs, ps; kwargs. end obs_fun = build_explicit_observed_function( sys, Symbolics.scalarize.(obs_exprs); - mkarray = (es,_) -> MakeTuple(es)) + mkarray = (es, _) -> MakeTuple(es)) obs_sym_tuple = (obs_syms...,) # okay so now to generate the stuff to assign it back into the system @@ -183,7 +182,7 @@ function compile_user_affect(affect::ImperativeAffect, cb, sys, dvs, ps; kwargs. mod_names = (mod_syms...,) mod_og_val_fun = build_explicit_observed_function( sys, Symbolics.scalarize.(first.(mod_pairs)); - mkarray = (es,_) -> MakeTuple(es)) + mkarray = (es, _) -> MakeTuple(es)) upd_funs = NamedTuple{mod_names}((setu.((sys,), first.(mod_pairs))...,)) @@ -216,5 +215,4 @@ function compile_user_affect(affect::ImperativeAffect, cb, sys, dvs, ps; kwargs. end end - scalarize_affects(affects::ImperativeAffect) = affects diff --git a/test/symbolic_events.jl b/test/symbolic_events.jl index 1138b7c96f..ccf0a17a40 100644 --- a/test/symbolic_events.jl +++ b/test/symbolic_events.jl @@ -936,7 +936,7 @@ end @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) + 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 @@ -1079,8 +1079,8 @@ end @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[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 From c8a207bb10803ae3248c61245fdf6d7f05e0366e Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 10 Dec 2024 16:45:01 +0530 Subject: [PATCH 0457/1337] build: bump version --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index fe3a9e88e5..534b0bd32b 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "ModelingToolkit" uuid = "961ee093-0014-501f-94e3-6117800e7a78" authors = ["Yingbo Ma ", "Chris Rackauckas and contributors"] -version = "9.56.0" +version = "9.57.0" [deps] AbstractTrees = "1520ce14-60c1-5f80-bbc7-55ef81b5835c" From b24e5256cd9d3e15b25e9b9468ee2996af6febdc Mon Sep 17 00:00:00 2001 From: Benjamin Chung Date: Tue, 10 Dec 2024 17:23:24 -0800 Subject: [PATCH 0458/1337] Update src/systems/callbacks.jl Co-authored-by: Aayush Sabharwal --- 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 57db5e097c..eaf31a9c5f 100644 --- a/src/systems/callbacks.jl +++ b/src/systems/callbacks.jl @@ -98,7 +98,7 @@ sharp discontinuity between integrator steps (which in this example would not no guaranteed to be triggered. Once detected the integrator will "wind back" through a root-finding process to identify the point when the condition became active; the method used -is specified by `rootfind` from [`SciMLBase.RootfindOpt`](@ref). If we denote the time when the condition becomes active as `tc``, +is specified by `rootfind` from [`SciMLBase.RootfindOpt`](@ref). If we denote the time when the condition becomes active as `tc`, the value in the integrator after windback will be: * `u[tc-epsilon], p[tc-epsilon], tc` if `LeftRootFind` is used, * `u[tc+epsilon], p[tc+epsilon], tc` if `RightRootFind` is used, From e5b5f61f8dcd4d61c5ae450a82c869ac51462dd8 Mon Sep 17 00:00:00 2001 From: Christopher Rackauckas Date: Wed, 11 Dec 2024 01:43:34 -0100 Subject: [PATCH 0459/1337] Update Project.toml --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 534b0bd32b..0b56e53805 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "ModelingToolkit" uuid = "961ee093-0014-501f-94e3-6117800e7a78" authors = ["Yingbo Ma ", "Chris Rackauckas and contributors"] -version = "9.57.0" +version = "9.58.0" [deps] AbstractTrees = "1520ce14-60c1-5f80-bbc7-55ef81b5835c" From 6d6383abf73d16b6c3ecb86bfed3500d70d25257 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 11 Dec 2024 17:11:17 +0530 Subject: [PATCH 0460/1337] fix: allow interpolating names in `@brownian` --- src/variables.jl | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/variables.jl b/src/variables.jl index d11c6c1834..3b0d64e7ea 100644 --- a/src/variables.jl +++ b/src/variables.jl @@ -489,7 +489,9 @@ $(SIGNATURES) Define one or more Brownian variables. """ macro brownian(xs...) - all(x -> x isa Symbol || Meta.isexpr(x, :call) && x.args[1] == :$, xs) || + all( + x -> x isa Symbol || Meta.isexpr(x, :call) && x.args[1] == :$ || Meta.isexpr(x, :$), + xs) || error("@brownian only takes scalar expressions!") Symbolics._parse_vars(:brownian, Real, From a2acbe59eebeec40b6ddb4355636aedb8ec5134f Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 11 Dec 2024 17:25:26 +0530 Subject: [PATCH 0461/1337] feat: add ability to convert `SDESystem` to equivalent `ODESystem` --- src/systems/diffeqs/sdesystem.jl | 41 ++++++++++++++++++++++++++ test/sdesystem.jl | 49 ++++++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+) diff --git a/src/systems/diffeqs/sdesystem.jl b/src/systems/diffeqs/sdesystem.jl index 011ba6e216..ac47f4c45c 100644 --- a/src/systems/diffeqs/sdesystem.jl +++ b/src/systems/diffeqs/sdesystem.jl @@ -263,6 +263,47 @@ function Base.:(==)(sys1::SDESystem, sys2::SDESystem) all(s1 == s2 for (s1, s2) in zip(get_systems(sys1), get_systems(sys2))) end +""" + function ODESystem(sys::SDESystem) + +Convert an `SDESystem` to the equivalent `ODESystem` using `@brownian` variables instead +of noise equations. The returned system will not be `iscomplete` and will not have an +index cache, regardless of `iscomplete(sys)`. +""" +function ODESystem(sys::SDESystem) + neqs = get_noiseeqs(sys) + eqs = equations(sys) + is_scalar_noise = get_is_scalar_noise(sys) + nbrownian = if is_scalar_noise + length(neqs) + else + size(neqs, 2) + end + brownvars = map(1:nbrownian) do i + name = gensym(Symbol(:brown_, i)) + only(@brownian $name) + end + if is_scalar_noise + brownterms = reduce(+, neqs .* brownvars; init = 0) + neweqs = map(eqs) do eq + eq.lhs ~ eq.rhs + brownterms + end + else + if neqs isa AbstractVector + neqs = reshape(neqs, (length(neqs), 1)) + end + brownterms = neqs * brownvars + neweqs = map(eqs, brownterms) do eq, brown + eq.lhs ~ eq.rhs + brown + end + end + newsys = ODESystem(neweqs, get_iv(sys), unknowns(sys), parameters(sys); + parameter_dependencies = parameter_dependencies(sys), defaults = defaults(sys), + continuous_events = continuous_events(sys), discrete_events = discrete_events(sys), + name = nameof(sys), description = description(sys), metadata = get_metadata(sys)) + @set newsys.parent = sys +end + function __num_isdiag_noise(mat) for i in axes(mat, 1) nnz = 0 diff --git a/test/sdesystem.jl b/test/sdesystem.jl index 749aca86a7..036c94b868 100644 --- a/test/sdesystem.jl +++ b/test/sdesystem.jl @@ -809,3 +809,52 @@ end prob = SDEProblem(sys, [x => 1.0, y => 1.0], (0.0, 1.0)) @test prob[z] ≈ 2.0 end + +@testset "SDESystem to ODESystem" begin + @variables x(t) y(t) z(t) + @testset "Scalar noise" begin + @named sys = SDESystem([D(x) ~ x, D(y) ~ y, z ~ x + y], [x, y, 3], + t, [x, y, z], [], is_scalar_noise = true) + odesys = ODESystem(sys) + @test odesys isa ODESystem + vs = ModelingToolkit.vars(equations(odesys)) + nbrownian = count( + v -> ModelingToolkit.getvariabletype(v) == ModelingToolkit.BROWNIAN, vs) + @test nbrownian == 3 + for eq in equations(odesys) + ModelingToolkit.isdiffeq(eq) || continue + @test length(arguments(eq.rhs)) == 4 + end + end + + @testset "Non-scalar vector noise" begin + @named sys = SDESystem([D(x) ~ x, D(y) ~ y, z ~ x + y], [x, y, 0], + t, [x, y, z], [], is_scalar_noise = false) + odesys = ODESystem(sys) + @test odesys isa ODESystem + vs = ModelingToolkit.vars(equations(odesys)) + nbrownian = count( + v -> ModelingToolkit.getvariabletype(v) == ModelingToolkit.BROWNIAN, vs) + @test nbrownian == 1 + for eq in equations(odesys) + ModelingToolkit.isdiffeq(eq) || continue + @test length(arguments(eq.rhs)) == 2 + end + end + + @testset "Matrix noise" begin + noiseeqs = [x+y y+z z+x + 2y 2z 2x + z+1 x+1 y+1] + @named sys = SDESystem([D(x) ~ x, D(y) ~ y, D(z) ~ z], noiseeqs, t, [x, y, z], []) + odesys = ODESystem(sys) + @test odesys isa ODESystem + vs = ModelingToolkit.vars(equations(odesys)) + nbrownian = count( + v -> ModelingToolkit.getvariabletype(v) == ModelingToolkit.BROWNIAN, vs) + @test nbrownian == 3 + for eq in equations(odesys) + @test length(arguments(eq.rhs)) == 4 + end + end +end From ea9b6bd02f683f7c75ce858187e92293f4c4e72a Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 11 Dec 2024 17:25:38 +0530 Subject: [PATCH 0462/1337] feat: enable `structural_simplify(::SDESystem)` --- src/systems/systems.jl | 4 ++++ test/sdesystem.jl | 10 ++++++++++ 2 files changed, 14 insertions(+) diff --git a/src/systems/systems.jl b/src/systems/systems.jl index 862718968d..97d22cf4cf 100644 --- a/src/systems/systems.jl +++ b/src/systems/systems.jl @@ -72,6 +72,10 @@ function __structural_simplify(sys::JumpSystem, args...; kwargs...) return sys end +function __structural_simplify(sys::SDESystem, args...; kwargs...) + return __structural_simplify(ODESystem(sys), args...; kwargs...) +end + function __structural_simplify(sys::AbstractSystem, io = nothing; simplify = false, kwargs...) sys = expand_connections(sys) diff --git a/test/sdesystem.jl b/test/sdesystem.jl index 036c94b868..8069581dcc 100644 --- a/test/sdesystem.jl +++ b/test/sdesystem.jl @@ -858,3 +858,13 @@ end end end end + +@testset "`structural_simplify(::SDESystem)`" begin + @variables x(t) y(t) + @mtkbuild sys = SDESystem( + [D(x) ~ x, y ~ 2x], [x, 0], t, [x, y], []; is_scalar_noise = true) + @test sys isa SDESystem + @test length(equations(sys)) == 1 + @test length(ModelingToolkit.get_noiseeqs(sys)) == 1 + @test length(observed(sys)) == 1 +end From 22d70938e2ac432c29c4787aa57c687bf45b76f3 Mon Sep 17 00:00:00 2001 From: CompatHelper Julia Date: Thu, 12 Dec 2024 00:25:48 +0000 Subject: [PATCH 0463/1337] CompatHelper: add new compat entry for Setfield at version 1 for package docs, (keep existing compat) --- docs/Project.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/Project.toml b/docs/Project.toml index 24f00b4db2..a358455503 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -38,6 +38,7 @@ OptimizationOptimJL = "0.1, 0.4" OrdinaryDiffEq = "6.31" Plots = "1.36" SciMLStructures = "1.1" +Setfield = "1" StochasticDiffEq = "6" SymbolicIndexingInterface = "0.3.1" SymbolicUtils = "3" From 552b0393f683fb16935c90779296347454cae9c3 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 11 Dec 2024 12:14:27 +0530 Subject: [PATCH 0464/1337] fix: respect `use_homotopy_continuation` in `NonlinearProblem` and default it to `false` --- src/systems/nonlinear/nonlinearsystem.jl | 10 ++++++---- test/extensions/homotopy_continuation.jl | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/systems/nonlinear/nonlinearsystem.jl b/src/systems/nonlinear/nonlinearsystem.jl index 3cb68853aa..2c788688bd 100644 --- a/src/systems/nonlinear/nonlinearsystem.jl +++ b/src/systems/nonlinear/nonlinearsystem.jl @@ -496,13 +496,15 @@ end function DiffEqBase.NonlinearProblem{iip}(sys::NonlinearSystem, u0map, parammap = DiffEqBase.NullParameters(); - check_length = true, use_homotopy_continuation = true, kwargs...) where {iip} + check_length = true, use_homotopy_continuation = false, kwargs...) where {iip} if !iscomplete(sys) error("A completed `NonlinearSystem` is required. Call `complete` or `structural_simplify` on the system before creating a `NonlinearProblem`") end - prob = safe_HomotopyContinuationProblem(sys, u0map, parammap; check_length, kwargs...) - if prob isa HomotopyContinuationProblem - return prob + if use_homotopy_continuation + prob = safe_HomotopyContinuationProblem(sys, u0map, parammap; check_length, kwargs...) + if prob isa HomotopyContinuationProblem + return prob + end end f, u0, p = process_SciMLProblem(NonlinearFunction{iip}, sys, u0map, parammap; check_length, kwargs...) diff --git a/test/extensions/homotopy_continuation.jl b/test/extensions/homotopy_continuation.jl index 554f9e1e1d..3f4a3fbc71 100644 --- a/test/extensions/homotopy_continuation.jl +++ b/test/extensions/homotopy_continuation.jl @@ -29,7 +29,7 @@ import HomotopyContinuation @test SciMLBase.successful_retcode(sol) @test norm(sol.resid)≈0.0 atol=1e-10 - prob2 = NonlinearProblem(sys, u0) + prob2 = NonlinearProblem(sys, u0; use_homotopy_continuation = true) @test prob2 isa HomotopyContinuationProblem sol = solve(prob2; threading = false) @test SciMLBase.successful_retcode(sol) From e75d06fb2f680ccda550f6ad37cc875497119736 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 11 Dec 2024 14:35:16 +0530 Subject: [PATCH 0465/1337] fix: properly handle rational functions in HomotopyContinuation --- ext/MTKHomotopyContinuationExt.jl | 5 +- .../nonlinear/homotopy_continuation.jl | 63 +++++++++++++------ src/systems/nonlinear/nonlinearsystem.jl | 3 +- test/extensions/homotopy_continuation.jl | 31 ++++++++- 4 files changed, 79 insertions(+), 23 deletions(-) diff --git a/ext/MTKHomotopyContinuationExt.jl b/ext/MTKHomotopyContinuationExt.jl index c4a090d9a8..8f17c05b18 100644 --- a/ext/MTKHomotopyContinuationExt.jl +++ b/ext/MTKHomotopyContinuationExt.jl @@ -101,7 +101,8 @@ function MTK.HomotopyContinuationProblem( return prob end -function MTK._safe_HomotopyContinuationProblem(sys, u0map, parammap = nothing; kwargs...) +function MTK._safe_HomotopyContinuationProblem(sys, u0map, parammap = nothing; + fraction_cancel_fn = SymbolicUtils.simplify_fractions, kwargs...) if !iscomplete(sys) error("A completed `NonlinearSystem` is required. Call `complete` or `structural_simplify` on the system before creating a `HomotopyContinuationProblem`") end @@ -109,7 +110,7 @@ function MTK._safe_HomotopyContinuationProblem(sys, u0map, parammap = nothing; k if transformation isa MTK.NotPolynomialError return transformation end - result = MTK.transform_system(sys, transformation) + result = MTK.transform_system(sys, transformation; fraction_cancel_fn) if result isa MTK.NotPolynomialError return result end diff --git a/src/systems/nonlinear/homotopy_continuation.jl b/src/systems/nonlinear/homotopy_continuation.jl index 03aeed1edf..00c6c7f1b0 100644 --- a/src/systems/nonlinear/homotopy_continuation.jl +++ b/src/systems/nonlinear/homotopy_continuation.jl @@ -442,7 +442,8 @@ Transform the system `sys` with `transformation` and return a `PolynomialTransformationResult`, or a `NotPolynomialError` if the system cannot be transformed. """ -function transform_system(sys::NonlinearSystem, transformation::PolynomialTransformation) +function transform_system(sys::NonlinearSystem, transformation::PolynomialTransformation; + fraction_cancel_fn = simplify_fractions) subrules = transformation.substitution_rules dvs = unknowns(sys) eqs = full_equations(sys) @@ -463,7 +464,7 @@ function transform_system(sys::NonlinearSystem, transformation::PolynomialTransf return NotPolynomialError( VariablesAsPolyAndNonPoly(dvs[poly_and_nonpoly]), eqs, polydata) end - num, den = handle_rational_polynomials(t, new_dvs) + num, den = handle_rational_polynomials(t, new_dvs; fraction_cancel_fn) # make factors different elements, otherwise the nonzero factors artificially # inflate the error of the zero factor. if iscall(den) && operation(den) == * @@ -492,43 +493,67 @@ $(TYPEDSIGNATURES) Given a `x`, a polynomial in variables in `wrt` which may contain rational functions, express `x` as a single rational function with polynomial `num` and denominator `den`. Return `(num, den)`. + +Keyword arguments: +- `fraction_cancel_fn`: A function which takes a fraction (`operation(expr) == /`) and returns + a simplified symbolic quantity with common factors in the numerator and denominator are + cancelled. Defaults to `SymbolicUtils.simplify_fractions`, but can be changed to + `nothing` to improve performance on large polynomials at the cost of avoiding non-trivial + cancellation. """ -function handle_rational_polynomials(x, wrt) +function handle_rational_polynomials(x, wrt; fraction_cancel_fn = simplify_fractions) x = unwrap(x) symbolic_type(x) == NotSymbolic() && return x, 1 iscall(x) || return x, 1 contains_variable(x, wrt) || return x, 1 any(isequal(x), wrt) && return x, 1 - # simplify_fractions cancels out some common factors - # and expands (a / b)^c to a^c / b^c, so we only need - # to handle these cases - x = simplify_fractions(x) op = operation(x) args = arguments(x) if op == / # numerator and denominator are trivial num, den = args - # but also search for rational functions in numerator - n, d = handle_rational_polynomials(num, wrt) - num, den = n, den * d - elseif op == + + n1, d1 = handle_rational_polynomials(num, wrt; fraction_cancel_fn) + n2, d2 = handle_rational_polynomials(den, wrt; fraction_cancel_fn) + num, den = n1 * d2, d1 * n2 + elseif (op == +) || (op == -) num = 0 den = 1 - - # we don't need to do common denominator - # because we don't care about cases where denominator - # is zero. The expression is zero when all the numerators - # are zero. + if op == - + args[2] = -args[2] + end + for arg in args + n, d = handle_rational_polynomials(arg, wrt; fraction_cancel_fn) + num = num * d + n * den + den *= d + end + elseif op == ^ + base, pow = args + num, den = handle_rational_polynomials(base, wrt; fraction_cancel_fn) + num ^= pow + den ^= pow + elseif op == * + num = 1 + den = 1 for arg in args - n, d = handle_rational_polynomials(arg, wrt) - num += n + n, d = handle_rational_polynomials(arg, wrt; fraction_cancel_fn) + num *= n den *= d end else - return x, 1 + error("Unhandled operation in `handle_rational_polynomials`. This should never happen. Please open an issue in ModelingToolkit.jl with an MWE.") + end + + if fraction_cancel_fn !== nothing + expr = fraction_cancel_fn(num / den) + if iscall(expr) && operation(expr) == / + num, den = arguments(expr) + else + num, den = expr, 1 + end end + # if the denominator isn't a polynomial in `wrt`, better to not include it # to reduce the size of the gcd polynomial if !contains_variable(den, wrt) diff --git a/src/systems/nonlinear/nonlinearsystem.jl b/src/systems/nonlinear/nonlinearsystem.jl index 2c788688bd..2751f4f5cd 100644 --- a/src/systems/nonlinear/nonlinearsystem.jl +++ b/src/systems/nonlinear/nonlinearsystem.jl @@ -501,7 +501,8 @@ function DiffEqBase.NonlinearProblem{iip}(sys::NonlinearSystem, u0map, error("A completed `NonlinearSystem` is required. Call `complete` or `structural_simplify` on the system before creating a `NonlinearProblem`") end if use_homotopy_continuation - prob = safe_HomotopyContinuationProblem(sys, u0map, parammap; check_length, kwargs...) + prob = safe_HomotopyContinuationProblem( + sys, u0map, parammap; check_length, kwargs...) if prob isa HomotopyContinuationProblem return prob end diff --git a/test/extensions/homotopy_continuation.jl b/test/extensions/homotopy_continuation.jl index 3f4a3fbc71..9e15ea857e 100644 --- a/test/extensions/homotopy_continuation.jl +++ b/test/extensions/homotopy_continuation.jl @@ -1,4 +1,5 @@ using ModelingToolkit, NonlinearSolve, SymbolicIndexingInterface +using SymbolicUtils import ModelingToolkit as MTK using LinearAlgebra using Test @@ -34,6 +35,8 @@ import HomotopyContinuation sol = solve(prob2; threading = false) @test SciMLBase.successful_retcode(sol) @test norm(sol.resid)≈0.0 atol=1e-10 + + @test NonlinearProblem(sys, u0; use_homotopy_continuation = false) isa NonlinearProblem end struct Wrapper @@ -217,7 +220,17 @@ end @mtkbuild sys = NonlinearSystem([x^2 + y^2 - 2x - 2 ~ 0, y ~ (x - 1) / (x - 2)]) prob = HomotopyContinuationProblem(sys, []) @test any(prob.denominator([2.0], parameter_values(prob)) .≈ 0.0) - @test_nowarn solve(prob; threading = false) + @test SciMLBase.successful_retcode(solve(prob; threading = false)) + end + + @testset "Rational function forced to common denominators" begin + @variables x = 1 + @mtkbuild sys = NonlinearSystem([0 ~ 1 / (1 + x) - x]) + prob = HomotopyContinuationProblem(sys, []) + @test any(prob.denominator([-1.0], parameter_values(prob)) .≈ 0.0) + sol = solve(prob; threading = false) + @test SciMLBase.successful_retcode(sol) + @test 1 / (1 + sol.u[1]) - sol.u[1]≈0.0 atol=1e-10 end end @@ -229,3 +242,19 @@ end @test sol[x] ≈ √2.0 @test sol[y] ≈ sin(√2.0) end + +@testset "`fraction_cancel_fn`" begin + @variables x = 1 + @named sys = NonlinearSystem([0 ~ ((x^2 - 5x + 6) / (x - 2) - 1) * (x^2 - 7x + 12) / + (x - 4)^3]) + sys = complete(sys) + + @testset "`simplify_fractions`" begin + prob = HomotopyContinuationProblem(sys, []) + @test prob.denominator([0.0], parameter_values(prob)) ≈ [4.0] + end + @testset "`nothing`" begin + prob = HomotopyContinuationProblem(sys, []; fraction_cancel_fn = nothing) + @test sort(prob.denominator([0.0], parameter_values(prob))) ≈ [2.0, 4.0^3] + end +end From 41600b14e7ddea22cf17e529f005eb85aa98ce31 Mon Sep 17 00:00:00 2001 From: Benjamin Chung Date: Tue, 12 Nov 2024 23:53:53 -0800 Subject: [PATCH 0466/1337] Add a simple mechanism to add passes to structural simplify --- src/systems/systems.jl | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/systems/systems.jl b/src/systems/systems.jl index 97d22cf4cf..29cc96d28f 100644 --- a/src/systems/systems.jl +++ b/src/systems/systems.jl @@ -26,7 +26,7 @@ topological sort of the observed equations in `sys`. + `fully_determined=true` controls whether or not an error will be thrown if the number of equations don't match the number of inputs, outputs, and equations. """ function structural_simplify( - sys::AbstractSystem, io = nothing; simplify = false, split = true, + sys::AbstractSystem, io = nothing; additional_passes = [], simplify = false, split = true, allow_symbolic = false, allow_parameter = true, conservative = false, fully_determined = true, kwargs...) isscheduled(sys) && throw(RepeatedStructuralSimplificationError()) @@ -49,6 +49,9 @@ function structural_simplify( if newsys isa ODESystem || has_parent(newsys) @set! newsys.parent = complete(sys; split, flatten = false) end + for pass in additional_passes + newsys = pass(newsys) + end newsys = complete(newsys; split) if has_defaults(newsys) && (defs = get_defaults(newsys)) !== nothing ks = collect(keys(defs)) # take copy to avoid mutating defs while iterating. From c0abc56decc7691b25254af716f20cc9979f73cf Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 12 Dec 2024 14:59:55 +0530 Subject: [PATCH 0467/1337] fix: run additional passes before setting the parent of the system --- src/systems/systems.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/systems/systems.jl b/src/systems/systems.jl index 29cc96d28f..47acd81a82 100644 --- a/src/systems/systems.jl +++ b/src/systems/systems.jl @@ -46,12 +46,12 @@ function structural_simplify( not yet supported. """) end + for pass in additional_passes + newsys = pass(newsys) + end if newsys isa ODESystem || has_parent(newsys) @set! newsys.parent = complete(sys; split, flatten = false) end - for pass in additional_passes - newsys = pass(newsys) - end newsys = complete(newsys; split) if has_defaults(newsys) && (defs = get_defaults(newsys)) !== nothing ks = collect(keys(defs)) # take copy to avoid mutating defs while iterating. From daf93edfde0a60ec6fb460f83e977c248cfeb849 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 12 Dec 2024 15:00:08 +0530 Subject: [PATCH 0468/1337] test: test additional passes mechanism --- test/structural_transformation/utils.jl | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/test/structural_transformation/utils.jl b/test/structural_transformation/utils.jl index 2704559f72..863e091aad 100644 --- a/test/structural_transformation/utils.jl +++ b/test/structural_transformation/utils.jl @@ -152,3 +152,12 @@ end end end end + +@testset "additional passes" begin + @variables x(t) y(t) + @named sys = ODESystem([D(x) ~ x, y ~ x + t], t) + value = Ref(0) + pass(sys; kwargs...) = (value[] += 1; return sys) + structural_simplify(sys; additional_passes = [pass]) + @test value[] == 1 +end From 18fdd5f79702627e30ce0acc971677a5cd1d303f Mon Sep 17 00:00:00 2001 From: vyudu Date: Fri, 13 Dec 2024 10:04:59 +0900 Subject: [PATCH 0469/1337] up --- Project.toml | 6 ++++-- test/bvproblem.jl | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Project.toml b/Project.toml index 093c632c42..19063a0342 100644 --- a/Project.toml +++ b/Project.toml @@ -72,14 +72,15 @@ MTKBifurcationKitExt = "BifurcationKit" MTKChainRulesCoreExt = "ChainRulesCore" MTKDeepDiffsExt = "DeepDiffs" MTKHomotopyContinuationExt = "HomotopyContinuation" -MTKLabelledArraysExt = "LabelledArrays" MTKInfiniteOptExt = "InfiniteOpt" +MTKLabelledArraysExt = "LabelledArrays" [compat] AbstractTrees = "0.3, 0.4" ArrayInterface = "6, 7" BifurcationKit = "0.4" BlockArrays = "1.1" +BoundaryValueDiffEq = "5.12.0" ChainRulesCore = "1" Combinatorics = "1" CommonSolve = "0.2.4" @@ -145,6 +146,7 @@ julia = "1.9" [extras] AmplNLWriter = "7c4d4715-977e-5154-bfe0-e096adeac482" BenchmarkTools = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf" +BoundaryValueDiffEq = "764a87c0-6b3e-53db-9096-fe964310641d" ControlSystemsBase = "aaaaaaaa-a6ca-5380-bf3e-84a91bcd477e" DataInterpolations = "82cc6244-b520-54b8-b5a6-8a565e85f1d0" DeepDiffs = "ab62b9b5-e342-54a8-a765-a90f495de1a6" @@ -174,4 +176,4 @@ Sundials = "c3572dad-4567-51f8-b174-8c6c989267f4" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [targets] -test = ["AmplNLWriter", "BenchmarkTools", "ControlSystemsBase", "DataInterpolations", "DelayDiffEq", "NonlinearSolve", "ForwardDiff", "Ipopt", "Ipopt_jll", "ModelingToolkitStandardLibrary", "Optimization", "OptimizationOptimJL", "OptimizationMOI", "OrdinaryDiffEq", "OrdinaryDiffEqCore", "REPL", "Random", "ReferenceTests", "SafeTestsets", "StableRNGs", "Statistics", "SteadyStateDiffEq", "Test", "StochasticDiffEq", "Sundials", "StochasticDelayDiffEq", "Pkg", "JET"] +test = ["AmplNLWriter", "BenchmarkTools", "BoundaryValueDiffEq", "ControlSystemsBase", "DataInterpolations", "DelayDiffEq", "NonlinearSolve", "ForwardDiff", "Ipopt", "Ipopt_jll", "ModelingToolkitStandardLibrary", "Optimization", "OptimizationOptimJL", "OptimizationMOI", "OrdinaryDiffEq", "OrdinaryDiffEqCore", "REPL", "Random", "ReferenceTests", "SafeTestsets", "StableRNGs", "Statistics", "SteadyStateDiffEq", "Test", "StochasticDiffEq", "Sundials", "StochasticDelayDiffEq", "Pkg", "JET"] diff --git a/test/bvproblem.jl b/test/bvproblem.jl index 7235638cf0..7787bd9c3e 100644 --- a/test/bvproblem.jl +++ b/test/bvproblem.jl @@ -44,7 +44,7 @@ tspan = (0., 10.) bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(pend, u0map, tspan, parammap) sol = solve(bvp, MIRK4(), dt = 0.01); -bvp2 = SciMLBase.BVProblem{false, SciMLBase.AutoSpecialize}(pend, u0map, tspan, parammap) +bvp2 = SciMLBase.BVProblem{false, SciMLBase.FullSpecialize}(pend, u0map, tspan, parammap) sol2 = solve(bvp2, MIRK4(), dt = 0.01); op = ODEProblem(pend, u0map, tspan, parammap) From 61b79a849cc643982aa036158d7b5fd5b7e11c7b Mon Sep 17 00:00:00 2001 From: Fredrik Bagge Carlson Date: Mon, 16 Dec 2024 10:41:04 +0100 Subject: [PATCH 0470/1337] add option to include disturbance arguments in `generate_control_function` --- src/inputoutput.jl | 21 +++++++++++++------- test/input_output_handling.jl | 37 +++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 7 deletions(-) diff --git a/src/inputoutput.jl b/src/inputoutput.jl index 4a99ec11f5..89db016795 100644 --- a/src/inputoutput.jl +++ b/src/inputoutput.jl @@ -160,7 +160,7 @@ has_var(ex, x) = x ∈ Set(get_variables(ex)) # Build control function """ - (f_oop, f_ip), x_sym, p, io_sys = generate_control_function( + (f_oop, f_ip), x_sym, p_sym, io_sys = generate_control_function( sys::AbstractODESystem, inputs = unbound_inputs(sys), disturbance_inputs = nothing; @@ -177,8 +177,7 @@ f_ip : (xout,x,u,p,t) -> nothing The return values also include the chosen state-realization (the remaining unknowns) `x_sym` and parameters, in the order they appear as arguments to `f`. -If `disturbance_inputs` is an array of variables, the generated dynamics function will preserve any state and dynamics associated with disturbance inputs, but the disturbance inputs themselves will not be included as inputs to the generated function. The use case for this is to generate dynamics for state observers that estimate the influence of unmeasured disturbances, and thus require unknown variables for the disturbance model, but without disturbance inputs since the disturbances are not available for measurement. -See [`add_input_disturbance`](@ref) for a higher-level interface to this functionality. +If `disturbance_inputs` is an array of variables, the generated dynamics function will preserve any state and dynamics associated with disturbance inputs, but the disturbance inputs themselves will (by default) not be included as inputs to the generated function. The use case for this is to generate dynamics for state observers that estimate the influence of unmeasured disturbances, and thus require unknown variables for the disturbance model, but without disturbance inputs since the disturbances are not available for measurement. To add an input argument corresponding to the disturbance inputs, either include the disturbance inputs among the control inputs, or set `disturbance_argument=true`, in which case an additional input argument `w` is added to the generated function `(x,u,p,t,w)->rhs`. !!! note "Un-simplified system" This function expects `sys` to be un-simplified, i.e., `structural_simplify` or `@mtkbuild` should not be called on the system before passing it into this function. `generate_control_function` calls a special version of `structural_simplify` internally. @@ -196,6 +195,7 @@ f[1](x, inputs, p, t) """ function generate_control_function(sys::AbstractODESystem, inputs = unbound_inputs(sys), disturbance_inputs = disturbances(sys); + disturbance_argument = false, implicit_dae = false, simplify = false, eval_expression = false, @@ -219,10 +219,11 @@ function generate_control_function(sys::AbstractODESystem, inputs = unbound_inpu # ps = [ps; disturbance_inputs] end inputs = map(x -> time_varying_as_func(value(x), sys), inputs) + disturbance_inputs = map(x -> time_varying_as_func(value(x), sys), disturbance_inputs) eqs = [eq for eq in full_equations(sys)] eqs = map(subs_constants, eqs) - if disturbance_inputs !== nothing + if disturbance_inputs !== nothing && !disturbance_argument # Set all disturbance *inputs* to zero (we just want to keep the disturbance state) subs = Dict(disturbance_inputs .=> 0) eqs = [eq.lhs ~ substitute(eq.rhs, subs) for eq in eqs] @@ -239,16 +240,22 @@ function generate_control_function(sys::AbstractODESystem, inputs = unbound_inpu t = get_iv(sys) # pre = has_difference ? (ex -> ex) : get_postprocess_fbody(sys) - - args = (u, inputs, p..., t) + if disturbance_argument + args = (u, inputs, p..., t, disturbance_inputs) + else + args = (u, inputs, p..., t) + end if implicit_dae ddvs = map(Differential(get_iv(sys)), dvs) args = (ddvs, args...) end process = get_postprocess_fbody(sys) + wrapped_arrays_vars = disturbance_argument ? + wrap_array_vars(sys, rhss; dvs, ps, inputs, disturbance_inputs) : + wrap_array_vars(sys, rhss; dvs, ps, inputs) f = build_function(rhss, args...; postprocess_fbody = process, expression = Val{true}, wrap_code = wrap_mtkparameters(sys, false, 3) .∘ - wrap_array_vars(sys, rhss; dvs, ps, inputs) .∘ + wrapped_arrays_vars .∘ wrap_parameter_dependencies(sys, false), kwargs...) f = eval_or_rgf.(f; eval_expression, eval_module) diff --git a/test/input_output_handling.jl b/test/input_output_handling.jl index 9550a87f31..25434e3dfb 100644 --- a/test/input_output_handling.jl +++ b/test/input_output_handling.jl @@ -170,6 +170,43 @@ x = [rand()] u = [rand()] @test f[1](x, u, p, 1) == -x + u +# With disturbance inputs +@variables x(t)=0 u(t)=0 [input = true] d(t)=0 +eqs = [ + D(x) ~ -x + u + d^2 +] + +@named sys = ODESystem(eqs, t) +f, dvs, ps, io_sys = ModelingToolkit.generate_control_function( + sys, [u], [d], simplify = true) + +@test isequal(dvs[], x) +@test isempty(ps) + +p = nothing +x = [rand()] +u = [rand()] +@test f[1](x, u, p, 1) == -x + u + +# With added d argument +@variables x(t)=0 u(t)=0 [input = true] d(t)=0 +eqs = [ + D(x) ~ -x + u + d^2 +] + +@named sys = ODESystem(eqs, t) +f, dvs, ps, io_sys = ModelingToolkit.generate_control_function( + sys, [u], [d], simplify = true, disturbance_argument = true) + +@test isequal(dvs[], x) +@test isempty(ps) + +p = nothing +x = [rand()] +u = [rand()] +d = [rand()] +@test f[1](x, u, p, 1, d) == -x + u + [d[]^2] + # more complicated system @variables u(t) [input = true] From b7a4cdf71bbca02bb4f4a6f62a6c0d65964188d1 Mon Sep 17 00:00:00 2001 From: Fredrik Bagge Carlson Date: Mon, 16 Dec 2024 11:25:51 +0100 Subject: [PATCH 0471/1337] Apply suggestions from code review Co-authored-by: Aayush Sabharwal --- src/inputoutput.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/inputoutput.jl b/src/inputoutput.jl index 89db016795..f1ea0bc749 100644 --- a/src/inputoutput.jl +++ b/src/inputoutput.jl @@ -219,7 +219,7 @@ function generate_control_function(sys::AbstractODESystem, inputs = unbound_inpu # ps = [ps; disturbance_inputs] end inputs = map(x -> time_varying_as_func(value(x), sys), inputs) - disturbance_inputs = map(x -> time_varying_as_func(value(x), sys), disturbance_inputs) + disturbance_inputs = unwrap.(disturbance_inputs) eqs = [eq for eq in full_equations(sys)] eqs = map(subs_constants, eqs) @@ -251,7 +251,7 @@ function generate_control_function(sys::AbstractODESystem, inputs = unbound_inpu end process = get_postprocess_fbody(sys) wrapped_arrays_vars = disturbance_argument ? - wrap_array_vars(sys, rhss; dvs, ps, inputs, disturbance_inputs) : + wrap_array_vars(sys, rhss; dvs, ps, inputs, cachesyms = (disturbance_inputs,)) : wrap_array_vars(sys, rhss; dvs, ps, inputs) f = build_function(rhss, args...; postprocess_fbody = process, expression = Val{true}, wrap_code = wrap_mtkparameters(sys, false, 3) .∘ From d75009ba5fde932da03095edb5b811d3b03d38a6 Mon Sep 17 00:00:00 2001 From: Fredrik Bagge Carlson Date: Mon, 16 Dec 2024 12:08:12 +0100 Subject: [PATCH 0472/1337] Apply suggestions from code review Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- src/inputoutput.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/inputoutput.jl b/src/inputoutput.jl index f1ea0bc749..1be1fbb8dd 100644 --- a/src/inputoutput.jl +++ b/src/inputoutput.jl @@ -251,7 +251,8 @@ function generate_control_function(sys::AbstractODESystem, inputs = unbound_inpu end process = get_postprocess_fbody(sys) wrapped_arrays_vars = disturbance_argument ? - wrap_array_vars(sys, rhss; dvs, ps, inputs, cachesyms = (disturbance_inputs,)) : + wrap_array_vars( + sys, rhss; dvs, ps, inputs, cachesyms = (disturbance_inputs,)) : wrap_array_vars(sys, rhss; dvs, ps, inputs) f = build_function(rhss, args...; postprocess_fbody = process, expression = Val{true}, wrap_code = wrap_mtkparameters(sys, false, 3) .∘ From a3789ae17a9f67df87b98179483e044c33e1fe44 Mon Sep 17 00:00:00 2001 From: Fredrik Bagge Carlson Date: Mon, 16 Dec 2024 14:20:27 +0100 Subject: [PATCH 0473/1337] test with split true and false --- test/input_output_handling.jl | 113 ++++++++++++++++++---------------- 1 file changed, 59 insertions(+), 54 deletions(-) diff --git a/test/input_output_handling.jl b/test/input_output_handling.jl index 25434e3dfb..de6fc92b5c 100644 --- a/test/input_output_handling.jl +++ b/test/input_output_handling.jl @@ -153,61 +153,66 @@ if VERSION >= v"1.8" # :opaque_closure not supported before end ## Code generation with unbound inputs +@testset "generate_control_function with disturbance inputs" begin + for split in [true, false] + simplify = true + + @variables x(t)=0 u(t)=0 [input = true] + eqs = [ + D(x) ~ -x + u + ] + + @named sys = ODESystem(eqs, t) + f, dvs, ps, io_sys = ModelingToolkit.generate_control_function(sys; simplify, split) + + @test isequal(dvs[], x) + @test isempty(ps) + + p = nothing + x = [rand()] + u = [rand()] + @test f[1](x, u, p, 1) == -x + u + + # With disturbance inputs + @variables x(t)=0 u(t)=0 [input = true] d(t)=0 + eqs = [ + D(x) ~ -x + u + d^2 + ] + + @named sys = ODESystem(eqs, t) + f, dvs, ps, io_sys = ModelingToolkit.generate_control_function( + sys, [u], [d]; simplify, split) + + @test isequal(dvs[], x) + @test isempty(ps) + + p = nothing + x = [rand()] + u = [rand()] + @test f[1](x, u, p, 1) == -x + u + + ## With added d argument + @variables x(t)=0 u(t)=0 [input = true] d(t)=0 + eqs = [ + D(x) ~ -x + u + d^2 + ] + + @named sys = ODESystem(eqs, t) + f, dvs, ps, io_sys = ModelingToolkit.generate_control_function( + sys, [u], [d]; simplify, split, disturbance_argument = true) + + @test isequal(dvs[], x) + @test isempty(ps) + + p = nothing + x = [rand()] + u = [rand()] + d = [rand()] + @test f[1](x, u, p, t, d) == -x + u + [d[]^2] + end +end -@variables x(t)=0 u(t)=0 [input = true] -eqs = [ - D(x) ~ -x + u -] - -@named sys = ODESystem(eqs, t) -f, dvs, ps, io_sys = ModelingToolkit.generate_control_function(sys, simplify = true) - -@test isequal(dvs[], x) -@test isempty(ps) - -p = nothing -x = [rand()] -u = [rand()] -@test f[1](x, u, p, 1) == -x + u - -# With disturbance inputs -@variables x(t)=0 u(t)=0 [input = true] d(t)=0 -eqs = [ - D(x) ~ -x + u + d^2 -] - -@named sys = ODESystem(eqs, t) -f, dvs, ps, io_sys = ModelingToolkit.generate_control_function( - sys, [u], [d], simplify = true) - -@test isequal(dvs[], x) -@test isempty(ps) - -p = nothing -x = [rand()] -u = [rand()] -@test f[1](x, u, p, 1) == -x + u - -# With added d argument -@variables x(t)=0 u(t)=0 [input = true] d(t)=0 -eqs = [ - D(x) ~ -x + u + d^2 -] - -@named sys = ODESystem(eqs, t) -f, dvs, ps, io_sys = ModelingToolkit.generate_control_function( - sys, [u], [d], simplify = true, disturbance_argument = true) - -@test isequal(dvs[], x) -@test isempty(ps) - -p = nothing -x = [rand()] -u = [rand()] -d = [rand()] -@test f[1](x, u, p, 1, d) == -x + u + [d[]^2] - -# more complicated system +## more complicated system @variables u(t) [input = true] From cb6ca4ce4cfd8547117317d23e5b32558d995a68 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 16 Dec 2024 19:25:25 +0530 Subject: [PATCH 0474/1337] feat: add support for `extra_args` in `wrap_array_vars` --- src/systems/abstractsystem.jl | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index e44f250a7f..6a20ee17dc 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -230,9 +230,33 @@ function wrap_parameter_dependencies(sys::AbstractSystem, isscalar) wrap_assignments(isscalar, [eq.lhs ← eq.rhs for eq in parameter_dependencies(sys)]) end +""" + $(TYPEDSIGNATURES) + +Add the necessary assignment statements to allow use of unscalarized array variables +in the generated code. `expr` is the expression returned by the function. `dvs` and +`ps` are the unknowns and parameters of the system `sys` to use in the generated code. +`inputs` can be specified as an array of symbolics if the generated function has inputs. +If `history == true`, the generated function accepts a history function. `cachesyms` are +extra variables (arrays of variables) stored in the cache array(s) of the parameter +object. `extra_args` are extra arguments appended to the end of the argument list. + +The function is assumed to have the signature `f(du, u, h, x, p, cache_syms..., t, extra_args...)` +Where: +- `du` is the optional buffer to write to for in-place functions. +- `u` is the list of unknowns. This argument is not present if `dvs === nothing`. +- `h` is the optional history function, present if `history == true`. +- `x` is the array of inputs, present only if `inputs !== nothing`. Values are assumed + to be in the order of variables passed to `inputs`. +- `p` is the parameter object. +- `cache_syms` are the cache variables. These are part of the splatted parameter object. +- `t` is time, present only if the system is time dependent. +- `extra_args` are the extra arguments passed to the function, present only if + `extra_args` is non-empty. +""" function wrap_array_vars( sys::AbstractSystem, exprs; dvs = unknowns(sys), ps = parameters(sys), - inputs = nothing, history = false, cachesyms::Tuple = ()) + inputs = nothing, history = false, cachesyms::Tuple = (), extra_args::Tuple = ()) isscalar = !(exprs isa AbstractArray) var_to_arridxs = Dict() @@ -252,6 +276,10 @@ function wrap_array_vars( if inputs !== nothing rps = (inputs, rps...) end + if has_iv(sys) + rps = (rps..., get_iv(sys)) + end + rps = (rps..., extra_args...) for sym in reduce(vcat, rps; init = []) iscall(sym) && operation(sym) == getindex || continue arg = arguments(sym)[1] From 266630dacfa864b79fae6e2eb534b6c4be99001d Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 16 Dec 2024 19:44:06 +0530 Subject: [PATCH 0475/1337] feat: handle additional arguments in `wrap_mtkparameters` --- src/systems/abstractsystem.jl | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index 6a20ee17dc..168260ae69 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -360,7 +360,7 @@ end const MTKPARAMETERS_ARG = Sym{Vector{Vector}}(:___mtkparameters___) """ - wrap_mtkparameters(sys::AbstractSystem, isscalar::Bool, p_start = 2) + wrap_mtkparameters(sys::AbstractSystem, isscalar::Bool, p_start = 2, offset = Int(is_time_dependent(sys))) Return function(s) to be passed to the `wrap_code` keyword of `build_function` which allow the compiled function to be called as `f(u, p, t)` where `p isa MTKParameters` @@ -370,12 +370,14 @@ the first parameter vector in the out-of-place version of the function. For exam if a history function (DDEs) was passed before `p`, then the function before wrapping would have the signature `f(u, h, p..., t)` and hence `p_start` would need to be `3`. +`offset` is the number of arguments at the end of the argument list to ignore. Defaults +to 1 if the system is time-dependent (to ignore `t`) and 0 otherwise. + The returned function is `identity` if the system does not have an `IndexCache`. """ -function wrap_mtkparameters(sys::AbstractSystem, isscalar::Bool, p_start = 2) +function wrap_mtkparameters(sys::AbstractSystem, isscalar::Bool, p_start = 2, + offset = Int(is_time_dependent(sys))) if has_index_cache(sys) && get_index_cache(sys) !== nothing - offset = Int(is_time_dependent(sys)) - if isscalar function (expr) param_args = expr.args[p_start:(end - offset)] From a79f4c2918b309cf4653f5cb110812cddaa214db Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 16 Dec 2024 19:25:42 +0530 Subject: [PATCH 0476/1337] fix: use `extra_args` in `generate_control_function` --- src/inputoutput.jl | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/inputoutput.jl b/src/inputoutput.jl index 1be1fbb8dd..6bdcac6dd4 100644 --- a/src/inputoutput.jl +++ b/src/inputoutput.jl @@ -252,10 +252,11 @@ function generate_control_function(sys::AbstractODESystem, inputs = unbound_inpu process = get_postprocess_fbody(sys) wrapped_arrays_vars = disturbance_argument ? wrap_array_vars( - sys, rhss; dvs, ps, inputs, cachesyms = (disturbance_inputs,)) : + sys, rhss; dvs, ps, inputs, extra_args = (disturbance_inputs,)) : wrap_array_vars(sys, rhss; dvs, ps, inputs) f = build_function(rhss, args...; postprocess_fbody = process, - expression = Val{true}, wrap_code = wrap_mtkparameters(sys, false, 3) .∘ + expression = Val{true}, wrap_code = wrap_mtkparameters( + sys, false, 3, Int(disturbance_argument) + 1) .∘ wrapped_arrays_vars .∘ wrap_parameter_dependencies(sys, false), kwargs...) From 9d65a3345ade530654a64ace2069ff24e8c67875 Mon Sep 17 00:00:00 2001 From: vyudu Date: Mon, 16 Dec 2024 23:43:35 +0800 Subject: [PATCH 0477/1337] fixing create_array --- Project.toml | 3 ++ src/systems/diffeqs/abstractodesystem.jl | 10 ++++- test/bvproblem.jl | 53 +++++++++++++++--------- 3 files changed, 45 insertions(+), 21 deletions(-) diff --git a/Project.toml b/Project.toml index 19063a0342..ab854b80bd 100644 --- a/Project.toml +++ b/Project.toml @@ -7,6 +7,9 @@ version = "9.54.0" AbstractTrees = "1520ce14-60c1-5f80-bbc7-55ef81b5835c" ArrayInterface = "4fba245c-0d91-5ea0-9b3e-6abc04ee57a9" BlockArrays = "8e7c35d0-a365-5155-bbbb-fb81a777f24e" +BoundaryValueDiffEq = "764a87c0-6b3e-53db-9096-fe964310641d" +BoundaryValueDiffEqCore = "56b672f2-a5fe-4263-ab2d-da677488eb3a" +BoundaryValueDiffEqMIRK = "1a22d4ce-7765-49ea-b6f2-13c8438986a6" Combinatorics = "861a8166-3701-5b0c-9a16-15d98fcdc6aa" CommonSolve = "38540f10-b2f7-11e9-35d8-d573e4eb0ff2" Compat = "34da2185-b29b-5c13-b0c7-acf172513d20" diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index 8dac19f296..3a502d3183 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -539,11 +539,19 @@ function SciMLBase.BVProblem{iip, specialize}(sys::AbstractODESystem, u0map = [] (u, p, t) -> (u[1] - _u0) end - return BVProblem{iip}(f, bc, u0, tspan, p; kwargs1..., kwargs...) + return BVProblem{iip}(f, bc, _u0, tspan, p; kwargs1..., kwargs...) end get_callback(prob::BVProblem) = error("BVP solvers do not support callbacks.") +@inline function create_array(::Type{Base.ReinterpretArray}, ::Nothing, ::Val{1}, ::Val{dims}, elems...) where dims + [elems...] +end + +@inline function create_array(::Type{Base.ReinterpretArray}, T, ::Val{1}, ::Val{dims}, elems...) where dims + T[elems...] +end + """ ```julia DiffEqBase.DAEFunction{iip}(sys::AbstractODESystem, dvs = unknowns(sys), diff --git a/test/bvproblem.jl b/test/bvproblem.jl index 7787bd9c3e..7cdb7e1837 100644 --- a/test/bvproblem.jl +++ b/test/bvproblem.jl @@ -2,6 +2,8 @@ using BoundaryValueDiffEq, OrdinaryDiffEq using ModelingToolkit using ModelingToolkit: t_nounits as t, D_nounits as D +solvers = [MIRK4, RadauIIa5, LobattoIIIa3] + @parameters α = 7.5 β = 4. γ = 8. δ = 5. @variables x(t) = 1. y(t) = 2. @@ -13,20 +15,26 @@ parammap = [:α => 7.5, :β => 4, :γ => 8., :δ => 5.] tspan = (0., 10.) @mtkbuild lotkavolterra = ODESystem(eqs, t) +op = ODEProblem(lotkavolterra, u0map, tspan, parammap) +osol = solve(op, Vern9()) -bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(lotkavolterra, u0map, tspan, parammap) -sol = solve(bvp, MIRK4(), dt = 0.01); +bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(lotkavolterra, u0map, tspan, parammap; eval_expression = true) -bvp2 = SciMLBase.BVProblem{false, SciMLBase.AutoSpecialize}(lotkavolterra, u0map, tspan, parammap) -sol2 = solve(bvp, MIRK4(), dt = 0.01); +for solver in solvers + println("$solver") + sol = solve(bvp, solver(), dt = 0.01) + @test isapprox(sol.u[end], osol.u[end]; atol = 0.01) + @test sol.u[1] == [1., 2.] +end -op = ODEProblem(lotkavolterra, u0map, tspan, parammap) -osol = solve(op, Vern9()) +# Test out of place +bvp2 = SciMLBase.BVProblem{false, SciMLBase.AutoSpecialize}(lotkavolterra, u0map, tspan, parammap; eval_expression = true) -@test isapprox(sol.u[end],osol.u[end]; atol = 0.01) -@test isapprox(sol2.u[end],osol.u[end]; atol = 0.01) -@test sol.u[1] == [1., 2.] -@test sol2.u[1] == [1., 2.] +for solver in solvers + sol = solve(bvp2, solver(), dt = 0.01) + @test isapprox(sol.u[end],osol.u[end]; atol = 0.01) + @test sol.u[1] == [1., 2.] +end ### Testing on pendulum @@ -38,19 +46,24 @@ eqs = [D(D(θ)) ~ -(g / L) * sin(θ)] @mtkbuild pend = ODESystem(eqs, t) u0map = [θ => π/2, D(θ) => π/2] -parammap = [:L => 2., :g => 9.81] +parammap = [:L => 1., :g => 9.81] tspan = (0., 10.) +op = ODEProblem(pend, u0map, tspan, parammap) +osol = solve(op, Vern9()) + bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(pend, u0map, tspan, parammap) -sol = solve(bvp, MIRK4(), dt = 0.01); +for solver in solvers + sol = solve(bvp2, solver(), dt = 0.01) + @test isapprox(sol.u[end],osol.u[end]; atol = 0.01) + @test sol.u[1] == [π/2, π/2] +end +# Test out-of-place bvp2 = SciMLBase.BVProblem{false, SciMLBase.FullSpecialize}(pend, u0map, tspan, parammap) -sol2 = solve(bvp2, MIRK4(), dt = 0.01); - -op = ODEProblem(pend, u0map, tspan, parammap) -osol = solve(op, Vern9()) -@test isapprox(sol.u[end], osol.u[end]; atol = 0.01) -@test sol.u[1] == [π/2, π/2] -@test isapprox(sol2.u[end], osol.u[end]; atol = 0.01) -@test sol2.u[1] == [π/2, π/2] +for solver in solvers + sol = solve(bvp2, solver(), dt = 0.01) + @test isapprox(sol.u[end],osol.u[end]; atol = 0.01) + @test sol.u[1] == [π/2, π/2] +end From 999ec308d58e32c51941743a6ba5a8f096c56ac2 Mon Sep 17 00:00:00 2001 From: vyudu Date: Mon, 16 Dec 2024 23:44:24 +0800 Subject: [PATCH 0478/1337] revert Project.toml --- Project.toml | 3 --- 1 file changed, 3 deletions(-) diff --git a/Project.toml b/Project.toml index ab854b80bd..19063a0342 100644 --- a/Project.toml +++ b/Project.toml @@ -7,9 +7,6 @@ version = "9.54.0" AbstractTrees = "1520ce14-60c1-5f80-bbc7-55ef81b5835c" ArrayInterface = "4fba245c-0d91-5ea0-9b3e-6abc04ee57a9" BlockArrays = "8e7c35d0-a365-5155-bbbb-fb81a777f24e" -BoundaryValueDiffEq = "764a87c0-6b3e-53db-9096-fe964310641d" -BoundaryValueDiffEqCore = "56b672f2-a5fe-4263-ab2d-da677488eb3a" -BoundaryValueDiffEqMIRK = "1a22d4ce-7765-49ea-b6f2-13c8438986a6" Combinatorics = "861a8166-3701-5b0c-9a16-15d98fcdc6aa" CommonSolve = "38540f10-b2f7-11e9-35d8-d573e4eb0ff2" Compat = "34da2185-b29b-5c13-b0c7-acf172513d20" From 9226ad687ff06cbc1dbbdcaa16ba3df85ef32d4e Mon Sep 17 00:00:00 2001 From: vyudu Date: Mon, 16 Dec 2024 23:56:04 +0800 Subject: [PATCH 0479/1337] Up --- test/bvproblem.jl | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/test/bvproblem.jl b/test/bvproblem.jl index 7cdb7e1837..86e3722eec 100644 --- a/test/bvproblem.jl +++ b/test/bvproblem.jl @@ -21,7 +21,6 @@ osol = solve(op, Vern9()) bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(lotkavolterra, u0map, tspan, parammap; eval_expression = true) for solver in solvers - println("$solver") sol = solve(bvp, solver(), dt = 0.01) @test isapprox(sol.u[end], osol.u[end]; atol = 0.01) @test sol.u[1] == [1., 2.] @@ -47,15 +46,15 @@ eqs = [D(D(θ)) ~ -(g / L) * sin(θ)] u0map = [θ => π/2, D(θ) => π/2] parammap = [:L => 1., :g => 9.81] -tspan = (0., 10.) +tspan = (0., 6.) op = ODEProblem(pend, u0map, tspan, parammap) osol = solve(op, Vern9()) bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(pend, u0map, tspan, parammap) for solver in solvers - sol = solve(bvp2, solver(), dt = 0.01) - @test isapprox(sol.u[end],osol.u[end]; atol = 0.01) + sol = solve(bvp, solver(), dt = 0.01) + @test isapprox(sol.u[end], osol.u[end]; atol = 0.01) @test sol.u[1] == [π/2, π/2] end From 67d8164c591b74a9b985ffbcaf1ef248fb0efaaa Mon Sep 17 00:00:00 2001 From: vyudu Date: Tue, 17 Dec 2024 00:09:52 +0800 Subject: [PATCH 0480/1337] formatting --- src/systems/diffeqs/abstractodesystem.jl | 11 ++++--- test/bvproblem.jl | 42 +++++++++++++----------- 2 files changed, 28 insertions(+), 25 deletions(-) diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index 87ee6e823d..06c83073bf 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -512,7 +512,6 @@ function SciMLBase.BVProblem{iip, specialize}(sys::AbstractODESystem, u0map = [] eval_expression = false, eval_module = @__MODULE__, kwargs...) where {iip, specialize} - if !iscomplete(sys) error("A completed system is required. Call `complete` or `structural_simplify` on the system before creating an `BVProblem`") end @@ -528,12 +527,12 @@ function SciMLBase.BVProblem{iip, specialize}(sys::AbstractODESystem, u0map = [] if cbs !== nothing kwargs1 = merge(kwargs1, (callback = cbs,)) end - + # Construct initial conditions. _u0 = u0 isa Function ? u0(tspan[1]) : u0 # Define the boundary conditions. - bc = if iip + bc = if iip (residual, u, p, t) -> (residual .= u[1] .- _u0) else (u, p, t) -> (u[1] - _u0) @@ -544,11 +543,13 @@ end get_callback(prob::BVProblem) = error("BVP solvers do not support callbacks.") -@inline function create_array(::Type{Base.ReinterpretArray}, ::Nothing, ::Val{1}, ::Val{dims}, elems...) where dims +@inline function create_array(::Type{Base.ReinterpretArray}, ::Nothing, + ::Val{1}, ::Val{dims}, elems...) where {dims} [elems...] end -@inline function create_array(::Type{Base.ReinterpretArray}, T, ::Val{1}, ::Val{dims}, elems...) where dims +@inline function create_array( + ::Type{Base.ReinterpretArray}, T, ::Val{1}, ::Val{dims}, elems...) where {dims} T[elems...] end diff --git a/test/bvproblem.jl b/test/bvproblem.jl index 86e3722eec..1072874917 100644 --- a/test/bvproblem.jl +++ b/test/bvproblem.jl @@ -4,49 +4,51 @@ using ModelingToolkit: t_nounits as t, D_nounits as D solvers = [MIRK4, RadauIIa5, LobattoIIIa3] -@parameters α = 7.5 β = 4. γ = 8. δ = 5. -@variables x(t) = 1. y(t) = 2. +@parameters α=7.5 β=4.0 γ=8.0 δ=5.0 +@variables x(t)=1.0 y(t)=2.0 -eqs = [D(x) ~ α*x - β*x*y, - D(y) ~ -γ*y + δ*x*y] +eqs = [D(x) ~ α * x - β * x * y, + D(y) ~ -γ * y + δ * x * y] -u0map = [:x => 1., :y => 2.] -parammap = [:α => 7.5, :β => 4, :γ => 8., :δ => 5.] -tspan = (0., 10.) +u0map = [:x => 1.0, :y => 2.0] +parammap = [:α => 7.5, :β => 4, :γ => 8.0, :δ => 5.0] +tspan = (0.0, 10.0) @mtkbuild lotkavolterra = ODESystem(eqs, t) op = ODEProblem(lotkavolterra, u0map, tspan, parammap) osol = solve(op, Vern9()) -bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(lotkavolterra, u0map, tspan, parammap; eval_expression = true) +bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}( + lotkavolterra, u0map, tspan, parammap; eval_expression = true) for solver in solvers sol = solve(bvp, solver(), dt = 0.01) @test isapprox(sol.u[end], osol.u[end]; atol = 0.01) - @test sol.u[1] == [1., 2.] + @test sol.u[1] == [1.0, 2.0] end # Test out of place -bvp2 = SciMLBase.BVProblem{false, SciMLBase.AutoSpecialize}(lotkavolterra, u0map, tspan, parammap; eval_expression = true) +bvp2 = SciMLBase.BVProblem{false, SciMLBase.AutoSpecialize}( + lotkavolterra, u0map, tspan, parammap; eval_expression = true) for solver in solvers sol = solve(bvp2, solver(), dt = 0.01) - @test isapprox(sol.u[end],osol.u[end]; atol = 0.01) - @test sol.u[1] == [1., 2.] + @test isapprox(sol.u[end], osol.u[end]; atol = 0.01) + @test sol.u[1] == [1.0, 2.0] end ### Testing on pendulum -@parameters g = 9.81 L = 1. -@variables θ(t) = π/2 +@parameters g=9.81 L=1.0 +@variables θ(t) = π / 2 eqs = [D(D(θ)) ~ -(g / L) * sin(θ)] @mtkbuild pend = ODESystem(eqs, t) -u0map = [θ => π/2, D(θ) => π/2] -parammap = [:L => 1., :g => 9.81] -tspan = (0., 6.) +u0map = [θ => π / 2, D(θ) => π / 2] +parammap = [:L => 1.0, :g => 9.81] +tspan = (0.0, 6.0) op = ODEProblem(pend, u0map, tspan, parammap) osol = solve(op, Vern9()) @@ -55,7 +57,7 @@ bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(pend, u0map, tspan, pa for solver in solvers sol = solve(bvp, solver(), dt = 0.01) @test isapprox(sol.u[end], osol.u[end]; atol = 0.01) - @test sol.u[1] == [π/2, π/2] + @test sol.u[1] == [π / 2, π / 2] end # Test out-of-place @@ -63,6 +65,6 @@ bvp2 = SciMLBase.BVProblem{false, SciMLBase.FullSpecialize}(pend, u0map, tspan, for solver in solvers sol = solve(bvp2, solver(), dt = 0.01) - @test isapprox(sol.u[end],osol.u[end]; atol = 0.01) - @test sol.u[1] == [π/2, π/2] + @test isapprox(sol.u[end], osol.u[end]; atol = 0.01) + @test sol.u[1] == [π / 2, π / 2] end From 25988f3bc6b6d66b008b6a40341f75e4547e574a Mon Sep 17 00:00:00 2001 From: vyudu Date: Tue, 17 Dec 2024 10:52:37 +0800 Subject: [PATCH 0481/1337] up --- src/systems/diffeqs/abstractodesystem.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index 06c83073bf..c84f5ff5be 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -543,13 +543,13 @@ end get_callback(prob::BVProblem) = error("BVP solvers do not support callbacks.") -@inline function create_array(::Type{Base.ReinterpretArray}, ::Nothing, +@inline function SciML.Code.create_array(::Type{<:Base.ReinterpretArray}, ::Nothing, ::Val{1}, ::Val{dims}, elems...) where {dims} [elems...] end -@inline function create_array( - ::Type{Base.ReinterpretArray}, T, ::Val{1}, ::Val{dims}, elems...) where {dims} +@inline function SciML.Code.create_array( + ::Type{<:Base.ReinterpretArray}, T, ::Val{1}, ::Val{dims}, elems...) where {dims} T[elems...] end From bb28d4fe2a0d753221104186fe42808c7657f5d6 Mon Sep 17 00:00:00 2001 From: vyudu Date: Tue, 17 Dec 2024 10:53:22 +0800 Subject: [PATCH 0482/1337] up --- src/systems/diffeqs/abstractodesystem.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index c84f5ff5be..eac2df16aa 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -543,12 +543,12 @@ end get_callback(prob::BVProblem) = error("BVP solvers do not support callbacks.") -@inline function SciML.Code.create_array(::Type{<:Base.ReinterpretArray}, ::Nothing, +@inline function SciMLBase.Code.create_array(::Type{<:Base.ReinterpretArray}, ::Nothing, ::Val{1}, ::Val{dims}, elems...) where {dims} [elems...] end -@inline function SciML.Code.create_array( +@inline function SciMLBase.Code.create_array( ::Type{<:Base.ReinterpretArray}, T, ::Val{1}, ::Val{dims}, elems...) where {dims} T[elems...] end From b2bf7c05532bdf68d93e566d07c5e7444473dd7a Mon Sep 17 00:00:00 2001 From: vyudu Date: Tue, 17 Dec 2024 11:00:30 +0800 Subject: [PATCH 0483/1337] fix --- src/systems/diffeqs/abstractodesystem.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index eac2df16aa..3d143d49fb 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -543,12 +543,12 @@ end get_callback(prob::BVProblem) = error("BVP solvers do not support callbacks.") -@inline function SciMLBase.Code.create_array(::Type{<:Base.ReinterpretArray}, ::Nothing, +@inline function SymbolicUtils.Code.create_array(::Type{<:Base.ReinterpretArray}, ::Nothing, ::Val{1}, ::Val{dims}, elems...) where {dims} [elems...] end -@inline function SciMLBase.Code.create_array( +@inline function SymbolicUtils.Code.create_array( ::Type{<:Base.ReinterpretArray}, T, ::Val{1}, ::Val{dims}, elems...) where {dims} T[elems...] end From 21a2a33aa6d505da57c30c8e8326b40201106aae Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 13 Dec 2024 18:21:18 +0530 Subject: [PATCH 0484/1337] feat: allow passing pre-computed `vars` to `observed_equations_used_by` --- src/utils.jl | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/utils.jl b/src/utils.jl index dd7113cbe6..e9ddad3a07 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -1033,19 +1033,24 @@ end $(TYPEDSIGNATURES) Return the indexes of observed equations of `sys` used by expression `exprs`. + +Keyword arguments: +- `involved_vars`: A collection of the variables involved in `exprs`. This is the set of + variables which will be explored to find dependencies on observed equations. Typically, + providing this keyword is not necessary and is only useful to avoid repeatedly calling + `vars(exprs)` """ -function observed_equations_used_by(sys::AbstractSystem, exprs) +function observed_equations_used_by(sys::AbstractSystem, exprs; involved_vars = vars(exprs)) obs = observed(sys) obsvars = getproperty.(obs, :lhs) graph = observed_dependency_graph(obs) - syms = vars(exprs) - obsidxs = BitSet() - for sym in syms + for sym in involved_vars idx = findfirst(isequal(sym), obsvars) idx === nothing && continue + idx in obsidxs && continue parents = dfs_parents(graph, idx) for i in eachindex(parents) parents[i] == 0 && continue From 7deb72ba09091a1324c8efb75dc8b55a5c06906e Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 13 Dec 2024 18:21:33 +0530 Subject: [PATCH 0485/1337] feat: implement `IfLifting` structural simplification pass Co-authored-by: Benjamin Chung --- src/ModelingToolkit.jl | 1 + src/systems/if_lifting.jl | 511 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 512 insertions(+) create mode 100644 src/systems/if_lifting.jl diff --git a/src/ModelingToolkit.jl b/src/ModelingToolkit.jl index 20b2ada8fa..10aba4d8a9 100644 --- a/src/ModelingToolkit.jl +++ b/src/ModelingToolkit.jl @@ -181,6 +181,7 @@ include("discretedomain.jl") include("systems/systemstructure.jl") include("systems/clock_inference.jl") include("systems/systems.jl") +include("systems/if_lifting.jl") include("debugging.jl") include("systems/alias_elimination.jl") diff --git a/src/systems/if_lifting.jl b/src/systems/if_lifting.jl new file mode 100644 index 0000000000..53e1ca4957 --- /dev/null +++ b/src/systems/if_lifting.jl @@ -0,0 +1,511 @@ +""" + struct CondRewriter + +Callable struct used to transform symbolic conditions into conditions involving discrete +variables. +""" +struct CondRewriter + """ + The independent variable which the discrete variables depend on. + """ + iv::BasicSymbolic + """ + A mapping from a discrete variables to a `NamedTuple` containing the condition + determining whether the discrete variable needs to be evaluated and the symbolic + expression the discrete variable represents. The expression is used as a rootfinding + function, and zero-crossings trigger re-evaluation of the condition (if `dependency` + is `true`). `expression < 0` is evaluated on an up-crossing and `expression <= 0` is + evaluated on a down-crossing to get the updated value of the condition variable. + """ + conditions::Dict{Any, @NamedTuple{dependency, expression}} +end + +function CondRewriter(iv) + return CondRewriter(iv, Dict()) +end + +""" + $(TYPEDSIGNATURES) + +Given a symbolic condition `expr` and the condition `dep` it depends on, update the +mapping in `cw` and generate a new discrete variable if necessary. +""" +function new_cond_sym(cw::CondRewriter, expr, dep) + if !iscall(expr) || operation(expr) != Base.:(<) || !iszero(arguments(expr)[2]) + throw(ArgumentError("`expr` passed to `new_cond_sym` must be of the form `f(args...) < 0`. Got $expr.")) + end + # check if the same expression exists in the mapping + existing_var = findfirst(p -> isequal(p.expression, expr), cw.conditions) + if existing_var !== nothing + # cache hit + (existing_dep, _) = cw.conditions[existing_var] + # update the dependency condition + cw.conditions[existing_var] = (dependency = (dep | existing_dep), expression = expr) + return existing_var + end + # generate a new condition variable + cvar = gensym("cond") + st = symtype(expr) + iv = cw.iv + cv = unwrap(first(@parameters $(cvar)(iv)::st = true)) # TODO: real init + cw.conditions[cv] = (dependency = dep, expression = expr) + return cv +end + +""" +Utility function for boolean implication. +""" +implies(a, b) = !a & b + +""" + $(TYPEDSIGNATURES) + +Recursively rewrite conditions into discrete variables. `expr` is the condition to rewrite, +`dep` is a boolean expression/value which determines when the `expr` is to be evaluated. For +example, if `expr = expr1 | expr2` and `dep = dep1`, then `expr` should only be evaluated if +`dep1` evaluates to `true`. Recursively, `expr1` should only be evaluated if `dep1` is `true`, +and `expr2` should only be evaluated if `dep & !expr1`. + +Returns a 3-tuple of the substituted expression, a condition describing when `expr` evaluates +to `true`, and a condition describing when `expr` evaluates to `false`. + +This expects that all expressions with discontinuities or with discontinuous derivatives have +been rewritten into the form of `ifelse(rootfunc(args...) < 0, left(args...), right(args...))`. +The transformation is performed via `discontinuities_to_ifelse` using `Symbolics.rootfunction` +and family. +""" +function (cw::CondRewriter)(expr, dep) + # single variable, trivial case + if issym(expr) || iscall(expr) && issym(operation(expr)) + return (expr, expr, !expr) + # literal boolean or integer + elseif expr isa Bool + return (expr, expr, !expr) + elseif expr isa Int + return (expr, true, true) + # other singleton symbolic variables + elseif !iscall(expr) + @warn "Automatic conversion of if statments to events requires use of a limited conditional grammar; see the documentation. Skipping due to $expr" + return (expr, true, true) # error case => conservative assumption is that both true and false have to be evaluated + elseif operation(expr) == Base.:(|) # OR of two conditions + a, b = arguments(expr) + (rw_conda, truea, falsea) = cw(a, dep) + # only evaluate second if first is false + (rw_condb, trueb, falseb) = cw(b, dep & falsea) + return (rw_conda | rw_condb, truea | trueb, falsea & falseb) + + elseif operation(expr) == Base.:(&) # AND of two conditions + a, b = arguments(expr) + (rw_conda, truea, falsea) = cw(a, dep) + # only evaluate second if first is true + (rw_condb, trueb, falseb) = cw(b, dep & truea) + return (rw_conda & rw_condb, truea & trueb, falsea | falseb) + elseif operation(expr) == ifelse + c, a, b = arguments(expr) + (rw_cond, ctrue, cfalse) = cw(c, dep) + # only evaluate if condition is true + (rw_conda, truea, falsea) = cw(a, dep & ctrue) + # only evaluate if condition is false + (rw_condb, trueb, falseb) = cw(b, dep & cfalse) + # expression is true if condition is true and THEN branch is true, or condition is false + # and ELSE branch is true + # similarly for expression being false + return (ifelse(rw_cond, rw_conda, rw_condb), + implies(ctrue, truea) | implies(cfalse, trueb), + implies(ctrue, falsea) | implies(cfalse, falseb)) + elseif operation(expr) == Base.:(!) # NOT of expression + (a,) = arguments(expr) + (rw, ctrue, cfalse) = cw(a, dep) + return (!rw, cfalse, ctrue) + elseif operation(expr) == Base.:(<) + if !isequal(arguments(expr)[2], 0) + throw(ArgumentError("Expected comparison to be written as `f(args...) < 0`. Found $expr.")) + end + + # if the comparison does not include time-dependent variables, + # don't create a callback for it + + # Calling `expression_is_time_dependent` is `O(d)` where `d` is the depth of the + # expression tree. We only call this in this here to avoid turning this into + # an `O(d^2)` time complexity recursion, which would happen if it were called + # at the beginning of the function. Now, it only happens near the leaves of + # the recursive tree. + if !expression_is_time_dependent(expr, cw.iv) + return (expr, expr, !expr) + end + cv = new_cond_sym(cw, expr, dep) + return (cv, cv, !cv) + elseif operation(expr) == (==) + # we don't touch equality since it's a point discontinuity. It's basically always + # false for continuous variables. In case it's an equality between discrete + # quantities, we don't need to transform it. + return (expr, expr, !expr) + elseif !expression_is_time_dependent(expr, cw.iv) + return (expr, expr, !expr) + end + error(""" + Unsupported expression form in decision variable computation $expr. If the expression + involves a registered function, declare the discontinuity using + `Symbolics.@register_discontinuity`. If this is not meant to be transformed via + `IfLifting`, wrap the parent expression in `ModelingToolkit.no_if_lift`. + """) +end + +""" + $(TYPEDSIGNATURES) + +Acts as the identity function, and prevents transformation of conditional expressions inside it. Useful +if specific `ifelse` or other functions with discontinuous derivatives shouldn't be transformed into +callbacks. +""" +no_if_lift(s) = s +@register_symbolic no_if_lift(s) + +""" + $(TYPEDEF) + +A utility struct to search through an expression specifically for `ifelse` terms, and find +all variables used in the condition of such terms. The variables are stored in a field of +the struct. +""" +struct VarsUsedInCondition + """ + Stores variables used in conditions of `ifelse` statements in the expression. + """ + vars::Set{Any} +end + +VarsUsedInCondition() = VarsUsedInCondition(Set()) + +function (v::VarsUsedInCondition)(expr) + expr = Symbolics.unwrap(expr) + if symbolic_type(expr) == NotSymbolic() + is_array_of_symbolics(expr) || return + foreach(v, expr) + return + end + iscall(expr) || return + op = operation(expr) + + # do not search inside no_if_lift to avoid discovering + # redundant variables + op == no_if_lift && return + + args = arguments(expr) + if op == ifelse + cond, branch_a, branch_b = arguments(expr) + vars!(v.vars, cond) + v(branch_a) + v(branch_b) + end + foreach(v, args) + return +end + +""" + $(TYPEDSIGNATURES) + +Check if `expr` depends on the independent variable `iv`. Return `true` if `iv` is present +in the expression, `Differential(iv)` is in the expression, or a dependent variable such +as `@variables x(iv)` is in the expression. +""" +function expression_is_time_dependent(expr, iv) + any(vars(expr)) do sym + sym = unwrap(sym) + isequal(sym, iv) && return true + iscall(sym) || return false + op = operation(sym) + args = arguments(sym) + op isa Differential && op == Differential(iv) || + issym(op) && length(args) == 1 && expression_is_time_dependent(args[1], iv) + end +end + +""" + $(TYPEDSIGNATURES) + +Given an expression `expr` which is to be evaluated if `dep` evaluates to `true`, transform +the conditions of all all `ifelse` statements in `expr` into functions of new discrete +variables. `cw` is used to store the information relevant to these newly introduced variables. +""" +function rewrite_ifs(cw::CondRewriter, expr, dep) + expr = unwrap(expr) + if symbolic_type(expr) == NotSymbolic() + # non-symbolic expression might still be an array of symbolic expressions + is_array_of_symbolics(expr) || return expr + return map(ex -> rewrite_ifs(cw, ex, dep), expr) + end + + iscall(expr) || return expr + op = operation(expr) + args = arguments(expr) + # do not search into `no_if_lift` + op == no_if_lift && return expr + + # transform `ifelse` + if op == ifelse + cond, iftrue, iffalse = args + + (rw_cond, deptrue, depfalse) = cw(cond, dep) + rw_iftrue = rewrite_ifs(cw, iftrue, deptrue) + rw_iffalse = rewrite_ifs(cw, iffalse, depfalse) + return maketerm( + typeof(expr), ifelse, [unwrap(rw_cond), rw_iftrue, rw_iffalse], metadata(expr)) + end + + # recurse into the rest of the cases + args = map(ex -> rewrite_ifs(cw, ex, dep), args) + return maketerm(typeof(expr), op, args, metadata(expr)) +end + +""" + $(TYPEDSIGNATURES) + +Return a modified `expr` where functions with known discontinuities or discontinuous +derivatives are transformed into `ifelse` statements. Utilizes the discontinuity API +in Symbolics. See [`Symbolics.rootfunction`](@ref), +[`Symbolics.left_continuous_function`](@ref), [`Symbolics.right_continuous_function`](@ref). + +`iv` is the independent variable of the system. Only subexpressions of `expr` which +depend on `iv` are transformed. +""" +function discontinuities_to_ifelse(expr, iv) + expr = unwrap(expr) + if symbolic_type(expr) == NotSymbolic() + # non-symbolic expression might still be an array of symbolic expressions + is_array_of_symbolics(expr) || return expr + return map(ex -> discontinuities_to_ifelse(ex, iv), expr) + end + + iscall(expr) || return expr + op = operation(expr) + args = arguments(expr) + # do not search into `no_if_lift` + op == no_if_lift && return expr + + # Case I: the operation is symbolic. + # We don't actually care if this is a callable parameter or not. + # If it is, we want to search inside and perform if-lifting there. + # If it isn't, either it's `x(t)` in which case this recursion is + # effectively a no-op OR it's `x(f(t))` for DDEs and we want to + # perform if-lifting inside. + # + # Case II: the operation is not symbolic. + # We anyway want to recursively apply the transformation. + # + # Thus, we can do this here regardless of the subsequent checks + args = map(ex -> discontinuities_to_ifelse(ex, iv), args) + + # if the operation is a known discontinuity + if hasmethod(Symbolics.rootfunction, Tuple{typeof(op)}) + rootfn = Symbolics.rootfunction(op) + leftfn = Symbolics.left_continuous_function(op) + rightfn = Symbolics.right_continuous_function(op) + rootexpr = rootfn(args...) < 0 + leftexpr = leftfn(args...) + rightexpr = rightfn(args...) + return maketerm( + typeof(expr), ifelse, [rootexpr, leftexpr, rightexpr], metadata(expr)) + end + + return maketerm(typeof(expr), op, args, metadata(expr)) +end + +""" + $(TYPEDSIGNATURES) + +Generate the symbolic condition for discrete variable `sym`, which represents the condition +of an `ifelse` statement created through [`IfLifting`](@ref). This condition is used to +trigger a callback which updates the value of the condition appropriately. +""" +function generate_condition(cw::CondRewriter, sym) + (dep, expr) = cw.conditions[sym] + + # expr is `f(args...) < 0`, `f(args...)` is the zero-crossing expression + zero_crossing = arguments(expr)[1] + + # if we're meant to evaluate the condition, evaluate it. Otherwise, return `NaN`. + # the solvers don't treat the transition from a number to NaN or back as a zero-crossing, + # so it can be used to effectively disable the affect when the condition is not meant to + # be evaluated. + return ifelse(dep, zero_crossing, NaN) ~ 0 +end + +""" + $(TYPEDSIGNATURES) + +Generate the upcrossing and downcrossing affect functions for discrete variable `sym` involved +in `ifelse` statements that are lifted to callbacks using [`IfLifting`](@ref). `syms` is a +condition variable introduced by `cw`, and is thus a key in `cw.conditions`. `new_cond_vars` +is the list of all such new condition variables, corresponding to the order of vertices in +`new_cond_vars_graph`. `new_cond_vars_graph` is a directed graph where edges denote the +condition variables involved in the dependency expression of the source vertex. +""" +function generate_affects(cw::CondRewriter, sym, new_cond_vars, new_cond_vars_graph) + sym_idx = findfirst(isequal(sym), new_cond_vars) + if sym_idx === nothing + throw(ArgumentError("Expected variable $sym to be a condition variable in $new_cond_vars.")) + end + # use reverse direction of edges because instead of finding the variables it depends + # on, we want the variables that depend on it + parents = bfs_parents(new_cond_vars_graph, sym_idx; dir = :in) + cond_vars_to_update = [new_cond_vars[i] + for i in eachindex(parents) if !iszero(parents[i])] + update_syms = Symbol.(cond_vars_to_update) + modified = NamedTuple{(update_syms...,)}(cond_vars_to_update) + + upcrossing_update_exprs = [arguments(last(cw.conditions[sym]))[1] < 0 + for sym in cond_vars_to_update] + upcrossing = ImperativeAffect( + modified, observed = NamedTuple{(update_syms...,)}(upcrossing_update_exprs), + skip_checks = true) do x, o, c, i + return o + end + downcrossing_update_exprs = [arguments(last(cw.conditions[sym]))[1] <= 0 + for sym in cond_vars_to_update] + downcrossing = ImperativeAffect( + modified, observed = NamedTuple{(update_syms...,)}(downcrossing_update_exprs), + skip_checks = true) do x, o, c, i + return o + end + + return upcrossing, downcrossing +end + +const CONDITION_SIMPLIFIER = Rewriters.Fixpoint(Rewriters.Postwalk(Rewriters.Chain([ + # simple boolean laws + (@rule (!!(~x)) => (~x)) + (@rule ((~x) & true) => (~x)) + (@rule ((~x) & false) => false) + (@rule ((~x) | true) => true) + (@rule ((~x) | false) => (~x)) + (@rule ((~x) & !(~x)) => false) + (@rule ((~x) | !(~x)) => true) + # reversed order of the above, because it matters and `@acrule` refuses to do its job + (@rule (true & (~x)) => (~x)) + (@rule (false & (~x)) => false) + (@rule (true | (~x)) => true) + (@rule (false | (~x)) => (~x)) + (@rule (!(~x) & (~x)) => false) + (@rule (!(~x) | (~x)) => true) + # idempotent + (@rule ((~x) & (~x)) => (~x)) + (@rule ((~x) | (~x)) => (~x)) + # ifelse with determined branches + (@rule ifelse((~x), true, false) => (~x)) + (@rule ifelse((~x), false, true) => !(~x)) + # ifelse with identical branches + (@rule ifelse((~x), (~y), (~y)) => (~y)) + (@rule ifelse((~x), (~y), !(~y)) => ((~x) & + (~y))) + (@rule ifelse((~x), !(~y), (~y)) => ((~x) & + !(~y))) + # ifelse with determined condition + (@rule ifelse(true, (~x), (~y)) => (~x)) + (@rule ifelse(false, (~x), (~y)) => (~y))]))) + +""" +If lifting converts (nested) if statements into a series of continous events + a logically equivalent if statement + parameters. + +Lifting proceeds through the following process: +* rewrite comparisons to be of the form eqn [op] 0; subtract the RHS from the LHS +* replace comparisons with generated parameters; for each comparison eqn [op] 0, generate an event (dependent on op) that sets the parameter +""" +function IfLifting(sys::ODESystem) + cw = CondRewriter(get_iv(sys)) + + eqs = copy(equations(sys)) + obs = copy(observed(sys)) + + # get variables used by `eqs` + syms = vars(eqs) + # get observed equations used by `eqs` + obs_idxs = observed_equations_used_by(sys, eqs; involved_vars = syms) + # and the variables used in those equations + for i in obs_idxs + vars!(syms, obs[i]) + end + + # get all integral variables used in conditions + # this is used when performing the transformation on observed equations + # since they are transformed differently depending on whether they are + # discrete variables involved in a condition or not + condition_vars = Set() + # searcher struct + # we can use the same one since it avoids iterating over duplicates + vars_in_condition! = VarsUsedInCondition() + for i in eachindex(eqs) + eq = eqs[i] + vars_in_condition!(eq.rhs) + # also transform the equation + eqs[i] = eq.lhs ~ rewrite_ifs(cw, discontinuities_to_ifelse(eq.rhs, cw.iv), true) + end + # also search through relevant observed equations + for i in obs_idxs + vars_in_condition!(obs[i].rhs) + end + # add to `condition_vars` after filtering out differential, parameter, independent and + # non-integral variables + for v in vars_in_condition!.vars + v = unwrap(v) + stype = symtype(v) + if isdifferential(v) || isparameter(v) || isequal(v, get_iv(sys)) + continue + end + stype <: Union{Integer, AbstractArray{Integer}} && push!(condition_vars, v) + end + # transform observed equations + for i in obs_idxs + obs[i] = if obs[i].lhs in condition_vars + obs[i].lhs ~ first(cw(discontinuities_to_ifelse(obs[i].rhs, cw.iv), true)) + else + obs[i].lhs ~ rewrite_ifs(cw, discontinuities_to_ifelse(obs[i].rhs, cw.iv), true) + end + end + + # `rewrite_ifs` and calling `cw` generate a lot of redundant code, simplify it + eqs = map(eqs) do eq + eq.lhs ~ CONDITION_SIMPLIFIER(eq.rhs) + end + obs = map(obs) do eq + eq.lhs ~ CONDITION_SIMPLIFIER(eq.rhs) + end + # also simplify dependencies + for (k, v) in cw.conditions + cw.conditions[k] = map(CONDITION_SIMPLIFIER ∘ unwrap, v) + end + + # get directed graph where nodes are the new condition variables and edges from each + # node denote the condition variables used in it's dependency expression + + # so we have an ordering for the vertices + new_cond_vars = collect(keys(cw.conditions)) + # "observed" equations + new_cond_dep_eqs = [v ~ cw.conditions[v] for v in new_cond_vars] + # construct the graph as a `DiCMOBiGraph` + new_cond_vars_graph = observed_dependency_graph(new_cond_dep_eqs) + + new_callbacks = continuous_events(sys) + new_defaults = defaults(sys) + new_ps = Vector{SymbolicParam}(parameters(sys)) + + for var in new_cond_vars + condition = generate_condition(cw, var) + up_affect, down_affect = generate_affects( + cw, var, new_cond_vars, new_cond_vars_graph) + cb = SymbolicContinuousCallback([condition], up_affect; affect_neg = down_affect, + initialize = up_affect, rootfind = SciMLBase.RightRootFind) + + push!(new_callbacks, cb) + new_defaults[var] = getdefault(var) + push!(new_ps, var) + end + + @set! sys.defaults = new_defaults + @set! sys.eqs = eqs + # do not need to topsort because we didn't modify the order + @set! sys.observed = obs + @set! sys.continuous_events = new_callbacks + @set! sys.ps = new_ps + return sys +end From 838ad8034b83bd7e935b64c31e034ecd94bd0fff Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 17 Dec 2024 22:15:26 +0530 Subject: [PATCH 0486/1337] test: add tests for if-lifting --- test/if_lifting.jl | 110 +++++++++++++++++++++++++++++++++++++++++++++ test/runtests.jl | 1 + 2 files changed, 111 insertions(+) create mode 100644 test/if_lifting.jl diff --git a/test/if_lifting.jl b/test/if_lifting.jl new file mode 100644 index 0000000000..d702355506 --- /dev/null +++ b/test/if_lifting.jl @@ -0,0 +1,110 @@ +using ModelingToolkit, OrdinaryDiffEq +using ModelingToolkit: t_nounits as t, D_nounits as D, IfLifting, no_if_lift + +@testset "Simple `abs(x)`" begin + @mtkmodel SimpleAbs begin + @variables begin + x(t) + y(t) + end + @equations begin + D(x) ~ abs(y) + y ~ sin(t) + end + end + @named sys = SimpleAbs() + ss1 = structural_simplify(sys) + @test length(equations(ss1)) == 1 + ss2 = structural_simplify(sys, additional_passes = [IfLifting]) + @test length(equations(ss2)) == 1 + @test length(parameters(ss2)) == 1 + @test operation(only(equations(ss2)).rhs) === ifelse + + discvar = only(parameters(ss2)) + prob2 = ODEProblem(ss2, [x => 0.0], (0.0, 5.0)) + sol2 = solve(prob2, Tsit5()) + @test count(isapprox(pi), sol2.t) == 2 + @test any(isapprox(pi), sol2.discretes[1].t) + @test !sol2[discvar][1] + @test sol2[discvar][end] + + _t = pi + 1.0 + # x(t) = 1 - cos(t) in [0, pi) + # x(t) = 3 + cos(t) in [pi, 2pi) + _trueval = 3 + cos(_t) + @test !isapprox(sol1(_t)[1], _trueval; rtol = 1e-3) + @test isapprox(sol2(_t)[1], _trueval; rtol = 1e-3) +end + +@testset "Big test case" begin + @mtkmodel BigModel begin + @variables begin + x(t) + y(t) + z(t) + c(t)::Bool + w(t) + q(t) + r(t) + end + @parameters begin + p + end + @equations begin + # ifelse, max, min + D(x) ~ ifelse(c, max(x, y), min(x, y)) + # discrete observed + c ~ x <= y + # observed should also get if-lifting + y ~ abs(sin(t)) + # should be ignored + D(z) ~ no_if_lift(ifelse(x < y, x, y)) + # ignore time-independent ifelse + D(w) ~ ifelse(p < 3, 1.0, 2.0) + # all the boolean operators + D(q) ~ ifelse((x < 1) & ((y < 0.5) | ifelse(y > 0.8, c, !c)), 1.0, 2.0) + # don't touch time-independent condition, but modify time-dependent branches + D(r) ~ ifelse(p < 2, abs(x), max(y, 0.9)) + end + end + + @named sys = BigModel() + ss = structural_simplify(sys, additional_passes = [IfLifting]) + + ps = parameters(ss) + @test length(ps) == 9 + eqs = equations(ss) + obs = observed(ss) + + @testset "no_if_lift is untouched" begin + idx = findfirst(eq -> isequal(eq.lhs, D(ss.z)), eqs) + eq = eqs[idx] + @test isequal(eq.rhs, no_if_lift(ifelse(ss.x < ss.y, ss.x, ss.y))) + end + @testset "time-independent ifelse is untouched" begin + idx = findfirst(eq -> isequal(eq.lhs, D(ss.w)), eqs) + eq = eqs[idx] + @test operation(arguments(eq.rhs)[1]) === Base.:< + end + @testset "time-dependent branch of time-independent condition is modified" begin + idx = findfirst(eq -> isequal(eq.lhs, D(ss.r)), eqs) + eq = eqs[idx] + @test operation(eq.rhs) === ifelse + args = arguments(eq.rhs) + @test operation(args[1]) == Base.:< + @test operation(args[2]) === ifelse + condvars = ModelingToolkit.vars(arguments(args[2])[1]) + @test length(condvars) == 1 && any(isequal(only(condvars)), ps) + @test operation(args[3]) === ifelse + condvars = ModelingToolkit.vars(arguments(args[3])[1]) + @test length(condvars) == 1 && any(isequal(only(condvars)), ps) + end + @testset "Observed variables are modified" begin + idx = findfirst(eq -> isequal(eq.lhs, ss.c), obs) + eq = obs[idx] + @test operation(eq.rhs) === Base.:! && any(isequal(only(arguments(eq.rhs))), ps) + idx = findfirst(eq -> isequal(eq.lhs, ss.y), obs) + eq = obs[idx] + @test operation(eq.rhs) === ifelse + end +end diff --git a/test/runtests.jl b/test/runtests.jl index 677f40c717..4a2ec80e6a 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -83,6 +83,7 @@ end @safetestset "JumpSystem Test" include("jumpsystem.jl") @safetestset "print_tree" include("print_tree.jl") @safetestset "Constraints Test" include("constraints.jl") + @safetestset "IfLifting Test" include("if_lifting.jl") end end From e4d5710e74edb0e20b11190f16fcde79d8df97ed Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 18 Dec 2024 11:32:27 +0530 Subject: [PATCH 0487/1337] fix: retain system metadata when calling `flatten` --- src/systems/diffeqs/odesystem.jl | 1 + .../discrete_system/discrete_system.jl | 1 + src/systems/nonlinear/nonlinearsystem.jl | 1 + .../optimization/optimizationsystem.jl | 1 + test/components.jl | 36 +++++++++++++++++++ 5 files changed, 40 insertions(+) diff --git a/src/systems/diffeqs/odesystem.jl b/src/systems/diffeqs/odesystem.jl index 2b0bd8c8d7..34003f40c2 100644 --- a/src/systems/diffeqs/odesystem.jl +++ b/src/systems/diffeqs/odesystem.jl @@ -409,6 +409,7 @@ function flatten(sys::ODESystem, noeqs = false) initialization_eqs = initialization_equations(sys), is_dde = is_dde(sys), tstops = symbolic_tstops(sys), + metadata = get_metadata(sys), checks = false) end end diff --git a/src/systems/discrete_system/discrete_system.jl b/src/systems/discrete_system/discrete_system.jl index 3e220998cb..01fca30235 100644 --- a/src/systems/discrete_system/discrete_system.jl +++ b/src/systems/discrete_system/discrete_system.jl @@ -227,6 +227,7 @@ function flatten(sys::DiscreteSystem, noeqs = false) defaults = defaults(sys), name = nameof(sys), description = description(sys), + metadata = get_metadata(sys), checks = false) end end diff --git a/src/systems/nonlinear/nonlinearsystem.jl b/src/systems/nonlinear/nonlinearsystem.jl index 3cb68853aa..b2abac5184 100644 --- a/src/systems/nonlinear/nonlinearsystem.jl +++ b/src/systems/nonlinear/nonlinearsystem.jl @@ -859,6 +859,7 @@ function flatten(sys::NonlinearSystem, noeqs = false) defaults = defaults(sys), name = nameof(sys), description = description(sys), + metadata = get_metadata(sys), checks = false) end end diff --git a/src/systems/optimization/optimizationsystem.jl b/src/systems/optimization/optimizationsystem.jl index 43e9294dd3..0398c892eb 100644 --- a/src/systems/optimization/optimizationsystem.jl +++ b/src/systems/optimization/optimizationsystem.jl @@ -184,6 +184,7 @@ function flatten(sys::OptimizationSystem) constraints = constraints(sys), defaults = defaults(sys), name = nameof(sys), + metadata = get_metadata(sys), checks = false ) end diff --git a/test/components.jl b/test/components.jl index 298f9fceb9..8ac40f6fbb 100644 --- a/test/components.jl +++ b/test/components.jl @@ -370,3 +370,39 @@ end ss = structural_simplify(cbar) @test isequal(cbar.foo.x, ss.foo.x) end + +@testset "Issue#3275: Metadata retained on `complete`" begin + @variables x(t) y(t) + @testset "ODESystem" begin + @named inner = ODESystem(D(x) ~ x, t) + @named outer = ODESystem(D(y) ~ y, t; systems = [inner], metadata = "test") + @test ModelingToolkit.get_metadata(outer) == "test" + sys = complete(outer) + @test ModelingToolkit.get_metadata(sys) == "test" + end + @testset "NonlinearSystem" begin + @named inner = NonlinearSystem([0 ~ x^2 + 4x + 4], [x], []) + @named outer = NonlinearSystem( + [0 ~ x^3 - y^3], [x, y], []; systems = [inner], metadata = "test") + @test ModelingToolkit.get_metadata(outer) == "test" + sys = complete(outer) + @test ModelingToolkit.get_metadata(sys) == "test" + end + k = ShiftIndex(t) + @testset "DiscreteSystem" begin + @named inner = DiscreteSystem([x(k) ~ x(k - 1) + x(k - 2)], t, [x], []) + @named outer = DiscreteSystem([y(k) ~ y(k - 1) + y(k - 2)], t, [x, y], + []; systems = [inner], metadata = "test") + @test ModelingToolkit.get_metadata(outer) == "test" + sys = complete(outer) + @test ModelingToolkit.get_metadata(sys) == "test" + end + @testset "OptimizationSystem" begin + @named inner = OptimizationSystem(x^2 + y^2 - 3, [x, y], []) + @named outer = OptimizationSystem( + x^3 - y, [x, y], []; systems = [inner], metadata = "test") + @test ModelingToolkit.get_metadata(outer) == "test" + sys = complete(outer) + @test ModelingToolkit.get_metadata(sys) == "test" + end +end From 4792360ad15eff9bc1a77fc5245bfaaec46c96a0 Mon Sep 17 00:00:00 2001 From: Christopher Rackauckas Date: Wed, 18 Dec 2024 14:49:50 -0100 Subject: [PATCH 0488/1337] Update Project.toml --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 0b56e53805..3c6126b7c0 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "ModelingToolkit" uuid = "961ee093-0014-501f-94e3-6117800e7a78" authors = ["Yingbo Ma ", "Chris Rackauckas and contributors"] -version = "9.58.0" +version = "9.59.0" [deps] AbstractTrees = "1520ce14-60c1-5f80-bbc7-55ef81b5835c" From 3751c2a92c282593ef52b67b727c7db635ea677e Mon Sep 17 00:00:00 2001 From: vyudu Date: Fri, 20 Dec 2024 23:58:28 +0900 Subject: [PATCH 0489/1337] up --- src/systems/diffeqs/abstractodesystem.jl | 165 +++++++++++------------ test/bvproblem.jl | 4 +- 2 files changed, 83 insertions(+), 86 deletions(-) diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index 3d143d49fb..d8ff71c324 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -469,90 +469,6 @@ function DiffEqBase.ODEFunction{iip, specialize}(sys::AbstractODESystem, initializeprobpmap = initializeprobpmap) end -""" -```julia -SciMLBase.BVProblem{iip}(sys::AbstractODESystem, u0map, tspan, - parammap = DiffEqBase.NullParameters(); - version = nothing, tgrad = false, - jac = true, sparse = true, - simplify = false, - kwargs...) where {iip} -``` - -Create a `BVProblem` from the [`ODESystem`](@ref). The arguments `dvs` and -`ps` are used to set the order of the dependent variable and parameter vectors, -respectively. `u0map` should be used to specify the initial condition, or be a function returning an initial condition. -""" -function SciMLBase.BVProblem(sys::AbstractODESystem, args...; kwargs...) - BVProblem{true}(sys, args...; kwargs...) -end - -function SciMLBase.BVProblem(sys::AbstractODESystem, - u0map::StaticArray, - args...; - kwargs...) - BVProblem{false, SciMLBase.FullSpecialize}(sys, u0map, args...; kwargs...) -end - -function SciMLBase.BVProblem{true}(sys::AbstractODESystem, args...; kwargs...) - BVProblem{true, SciMLBase.AutoSpecialize}(sys, args...; kwargs...) -end - -function SciMLBase.BVProblem{false}(sys::AbstractODESystem, args...; kwargs...) - BVProblem{false, SciMLBase.FullSpecialize}(sys, args...; kwargs...) -end - -function SciMLBase.BVProblem{iip, specialize}(sys::AbstractODESystem, u0map = [], - tspan = get_tspan(sys), - parammap = DiffEqBase.NullParameters(); - version = nothing, tgrad = false, - callback = nothing, - check_length = true, - warn_initialize_determined = true, - eval_expression = false, - eval_module = @__MODULE__, - kwargs...) where {iip, specialize} - if !iscomplete(sys) - error("A completed system is required. Call `complete` or `structural_simplify` on the system before creating an `BVProblem`") - end - - f, u0, p = process_SciMLProblem(ODEFunction{iip, specialize}, sys, u0map, parammap; - t = tspan !== nothing ? tspan[1] : tspan, - check_length, warn_initialize_determined, eval_expression, eval_module, kwargs...) - - cbs = process_events(sys; callback, eval_expression, eval_module, kwargs...) - kwargs = filter_kwargs(kwargs) - - kwargs1 = (;) - if cbs !== nothing - kwargs1 = merge(kwargs1, (callback = cbs,)) - end - - # Construct initial conditions. - _u0 = u0 isa Function ? u0(tspan[1]) : u0 - - # Define the boundary conditions. - bc = if iip - (residual, u, p, t) -> (residual .= u[1] .- _u0) - else - (u, p, t) -> (u[1] - _u0) - end - - return BVProblem{iip}(f, bc, _u0, tspan, p; kwargs1..., kwargs...) -end - -get_callback(prob::BVProblem) = error("BVP solvers do not support callbacks.") - -@inline function SymbolicUtils.Code.create_array(::Type{<:Base.ReinterpretArray}, ::Nothing, - ::Val{1}, ::Val{dims}, elems...) where {dims} - [elems...] -end - -@inline function SymbolicUtils.Code.create_array( - ::Type{<:Base.ReinterpretArray}, T, ::Val{1}, ::Val{dims}, elems...) where {dims} - T[elems...] -end - """ ```julia DiffEqBase.DAEFunction{iip}(sys::AbstractODESystem, dvs = unknowns(sys), @@ -943,6 +859,87 @@ function DiffEqBase.ODEProblem{iip, specialize}(sys::AbstractODESystem, u0map = end get_callback(prob::ODEProblem) = prob.kwargs[:callback] +""" +```julia +SciMLBase.BVProblem{iip}(sys::AbstractODESystem, u0map, tspan, + parammap = DiffEqBase.NullParameters(); + version = nothing, tgrad = false, + jac = true, sparse = true, + simplify = false, + kwargs...) where {iip} +``` + +Create a `BVProblem` from the [`ODESystem`](@ref). The arguments `dvs` and +`ps` are used to set the order of the dependent variable and parameter vectors, +respectively. `u0map` should be used to specify the initial condition. +""" +function SciMLBase.BVProblem(sys::AbstractODESystem, args...; kwargs...) + BVProblem{true}(sys, args...; kwargs...) +end + +function SciMLBase.BVProblem(sys::AbstractODESystem, + u0map::StaticArray, + args...; + kwargs...) + BVProblem{false, SciMLBase.FullSpecialize}(sys, u0map, args...; kwargs...) +end + +function SciMLBase.BVProblem{true}(sys::AbstractODESystem, args...; kwargs...) + BVProblem{true, SciMLBase.AutoSpecialize}(sys, args...; kwargs...) +end + +function SciMLBase.BVProblem{false}(sys::AbstractODESystem, args...; kwargs...) + BVProblem{false, SciMLBase.FullSpecialize}(sys, args...; kwargs...) +end + +function SciMLBase.BVProblem{iip, specialize}(sys::AbstractODESystem, u0map = [], + tspan = get_tspan(sys), + parammap = DiffEqBase.NullParameters(); + version = nothing, tgrad = false, + callback = nothing, + check_length = true, + warn_initialize_determined = true, + eval_expression = false, + eval_module = @__MODULE__, + kwargs...) where {iip, specialize} + if !iscomplete(sys) + error("A completed system is required. Call `complete` or `structural_simplify` on the system before creating an `BVProblem`") + end + + f, u0, p = process_SciMLProblem(ODEFunction{iip, specialize}, sys, u0map, parammap; + t = tspan !== nothing ? tspan[1] : tspan, + check_length, warn_initialize_determined, eval_expression, eval_module, kwargs...) + + cbs = process_events(sys; callback, eval_expression, eval_module, kwargs...) + kwargs = filter_kwargs(kwargs) + + kwargs1 = (;) + if cbs !== nothing + kwargs1 = merge(kwargs1, (callback = cbs,)) + end + + # Define the boundary conditions. + bc = if iip + (residual, u, p, t) -> (residual .= u[1] .- u0) + else + (u, p, t) -> (u[1] - u0) + end + + return BVProblem{iip}(f, bc, u0, tspan, p; kwargs1..., kwargs...) +end + +get_callback(prob::BVProblem) = error("BVP solvers do not support callbacks.") + +@inline function SymbolicUtils.Code.create_array(::Type{<:Base.ReinterpretArray}, ::Nothing, + ::Val{1}, ::Val{dims}, elems...) where {dims} + [elems...] +end + +@inline function SymbolicUtils.Code.create_array( + ::Type{<:Base.ReinterpretArray}, T, ::Val{1}, ::Val{dims}, elems...) where {dims} + T[elems...] +end + """ ```julia DiffEqBase.DAEProblem{iip}(sys::AbstractODESystem, du0map, u0map, tspan, diff --git a/test/bvproblem.jl b/test/bvproblem.jl index 1072874917..c5a302147d 100644 --- a/test/bvproblem.jl +++ b/test/bvproblem.jl @@ -10,8 +10,8 @@ solvers = [MIRK4, RadauIIa5, LobattoIIIa3] eqs = [D(x) ~ α * x - β * x * y, D(y) ~ -γ * y + δ * x * y] -u0map = [:x => 1.0, :y => 2.0] -parammap = [:α => 7.5, :β => 4, :γ => 8.0, :δ => 5.0] +u0map = [x => 1.0, y => 2.0] +parammap = [α => 7.5, β => 4, γ => 8.0, δ => 5.0] tspan = (0.0, 10.0) @mtkbuild lotkavolterra = ODESystem(eqs, t) From 219aee396ed4d29606fcb3fa6922bb140091a0e6 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Sun, 1 Dec 2024 13:07:15 +0530 Subject: [PATCH 0490/1337] refactor: add guesses to `SDESystem`, `NonlinearSystem`, `JumpSystem` --- src/systems/diffeqs/odesystem.jl | 27 +++------ src/systems/diffeqs/sdesystem.jl | 52 ++++++++++++----- .../discrete_system/discrete_system.jl | 47 +++++++++++---- src/systems/jumps/jumpsystem.jl | 43 ++++++++++---- src/systems/nonlinear/nonlinearsystem.jl | 57 ++++++++++++++----- .../optimization/constraints_system.jl | 4 +- .../optimization/optimizationsystem.jl | 4 +- src/utils.jl | 27 +++++++++ 8 files changed, 187 insertions(+), 74 deletions(-) diff --git a/src/systems/diffeqs/odesystem.jl b/src/systems/diffeqs/odesystem.jl index 34003f40c2..61f16fd926 100644 --- a/src/systems/diffeqs/odesystem.jl +++ b/src/systems/diffeqs/odesystem.jl @@ -256,29 +256,16 @@ function ODESystem(deqs::AbstractVector{<:Equation}, iv, dvs, ps; :ODESystem, force = true) end defaults = Dict{Any, Any}(todict(defaults)) + guesses = Dict{Any, Any}(todict(guesses)) var_to_name = Dict() - process_variables!(var_to_name, defaults, dvs′) - process_variables!(var_to_name, defaults, ps′) - process_variables!(var_to_name, defaults, [eq.lhs for eq in parameter_dependencies]) - process_variables!(var_to_name, defaults, [eq.rhs for eq in parameter_dependencies]) + process_variables!(var_to_name, defaults, guesses, dvs′) + process_variables!(var_to_name, defaults, guesses, ps′) + process_variables!( + var_to_name, defaults, guesses, [eq.lhs for eq in parameter_dependencies]) + process_variables!( + var_to_name, defaults, guesses, [eq.rhs for eq in parameter_dependencies]) defaults = Dict{Any, Any}(value(k) => value(v) for (k, v) in pairs(defaults) if v !== nothing) - - sysdvsguesses = [ModelingToolkit.getguess(st) for st in dvs′] - hasaguess = findall(!isnothing, sysdvsguesses) - var_guesses = dvs′[hasaguess] .=> sysdvsguesses[hasaguess] - sysdvsguesses = isempty(var_guesses) ? Dict() : todict(var_guesses) - syspsguesses = [ModelingToolkit.getguess(st) for st in ps′] - hasaguess = findall(!isnothing, syspsguesses) - ps_guesses = ps′[hasaguess] .=> syspsguesses[hasaguess] - syspsguesses = isempty(ps_guesses) ? Dict() : todict(ps_guesses) - syspdepguesses = [ModelingToolkit.getguess(eq.lhs) for eq in parameter_dependencies] - hasaguess = findall(!isnothing, syspdepguesses) - pdep_guesses = [eq.lhs for eq in parameter_dependencies][hasaguess] .=> - syspdepguesses[hasaguess] - syspdepguesses = isempty(pdep_guesses) ? Dict() : todict(pdep_guesses) - - guesses = merge(sysdvsguesses, syspsguesses, syspdepguesses, todict(guesses)) guesses = Dict{Any, Any}(value(k) => value(v) for (k, v) in pairs(guesses) if v !== nothing) diff --git a/src/systems/diffeqs/sdesystem.jl b/src/systems/diffeqs/sdesystem.jl index ac47f4c45c..d604863024 100644 --- a/src/systems/diffeqs/sdesystem.jl +++ b/src/systems/diffeqs/sdesystem.jl @@ -93,6 +93,19 @@ struct SDESystem <: AbstractODESystem """ defaults::Dict """ + The guesses to use as the initial conditions for the + initialization system. + """ + guesses::Dict + """ + The system for performing the initialization. + """ + initializesystem::Union{Nothing, NonlinearSystem} + """ + Extra equations to be enforced during the initialization sequence. + """ + initialization_eqs::Vector{Equation} + """ Type of the system. """ connector_type::Any @@ -144,9 +157,8 @@ struct SDESystem <: AbstractODESystem isscheduled::Bool function SDESystem(tag, deqs, neqs, iv, dvs, ps, tspan, var_to_name, ctrls, observed, - tgrad, - jac, - ctrl_jac, Wfact, Wfact_t, name, description, systems, defaults, connector_type, + tgrad, jac, ctrl_jac, Wfact, Wfact_t, name, description, systems, defaults, + guesses, initializesystem, initialization_eqs, connector_type, cevents, devents, parameter_dependencies, metadata = nothing, gui_metadata = nothing, complete = false, index_cache = nothing, parent = nothing, is_scalar_noise = false, is_dde = false, @@ -171,9 +183,9 @@ struct SDESystem <: AbstractODESystem check_units(u, deqs, neqs) end new(tag, deqs, neqs, iv, dvs, ps, tspan, var_to_name, ctrls, observed, tgrad, jac, - ctrl_jac, - Wfact, Wfact_t, name, description, systems, - defaults, connector_type, cevents, devents, + ctrl_jac, Wfact, Wfact_t, name, description, systems, + defaults, guesses, initializesystem, initialization_eqs, connector_type, cevents, + devents, parameter_dependencies, metadata, gui_metadata, complete, index_cache, parent, is_scalar_noise, is_dde, isscheduled) end @@ -187,6 +199,9 @@ function SDESystem(deqs::AbstractVector{<:Equation}, neqs::AbstractArray, iv, dv default_u0 = Dict(), default_p = Dict(), defaults = _merge(Dict(default_u0), Dict(default_p)), + guesses = Dict(), + initializesystem = nothing, + initialization_eqs = Equation[], name = nothing, description = "", connector_type = nothing, @@ -207,6 +222,8 @@ function SDESystem(deqs::AbstractVector{<:Equation}, neqs::AbstractArray, iv, dv dvs′ = value.(dvs) ps′ = value.(ps) ctrl′ = value.(controls) + parameter_dependencies, ps′ = process_parameter_dependencies( + parameter_dependencies, ps′) sysnames = nameof.(systems) if length(unique(sysnames)) != length(sysnames) @@ -217,13 +234,21 @@ function SDESystem(deqs::AbstractVector{<:Equation}, neqs::AbstractArray, iv, dv "`default_u0` and `default_p` are deprecated. Use `defaults` instead.", :SDESystem, force = true) end - defaults = todict(defaults) - defaults = Dict(value(k) => value(v) - for (k, v) in pairs(defaults) if value(v) !== nothing) + defaults = Dict{Any, Any}(todict(defaults)) + guesses = Dict{Any, Any}(todict(guesses)) var_to_name = Dict() - process_variables!(var_to_name, defaults, dvs′) - process_variables!(var_to_name, defaults, ps′) + process_variables!(var_to_name, defaults, guesses, dvs′) + process_variables!(var_to_name, defaults, guesses, ps′) + process_variables!( + var_to_name, defaults, guesses, [eq.lhs for eq in parameter_dependencies]) + process_variables!( + var_to_name, defaults, guesses, [eq.rhs for eq in parameter_dependencies]) + defaults = Dict{Any, Any}(value(k) => value(v) + for (k, v) in pairs(defaults) if v !== nothing) + guesses = Dict{Any, Any}(value(k) => value(v) + for (k, v) in pairs(guesses) if v !== nothing) + isempty(observed) || collect_var_to_name!(var_to_name, (eq.lhs for eq in observed)) tgrad = RefValue(EMPTY_TGRAD) @@ -233,14 +258,13 @@ function SDESystem(deqs::AbstractVector{<:Equation}, neqs::AbstractArray, iv, dv Wfact_t = RefValue(EMPTY_JAC) cont_callbacks = SymbolicContinuousCallbacks(continuous_events) disc_callbacks = SymbolicDiscreteCallbacks(discrete_events) - parameter_dependencies, ps′ = process_parameter_dependencies( - parameter_dependencies, ps′) if is_dde === nothing is_dde = _check_if_dde(deqs, iv′, systems) end SDESystem(Threads.atomic_add!(SYSTEM_COUNT, UInt(1)), deqs, neqs, iv′, dvs′, ps′, tspan, var_to_name, ctrl′, observed, tgrad, jac, - ctrl_jac, Wfact, Wfact_t, name, description, systems, defaults, connector_type, + ctrl_jac, Wfact, Wfact_t, name, description, systems, defaults, guesses, + initializesystem, initialization_eqs, connector_type, cont_callbacks, disc_callbacks, parameter_dependencies, metadata, gui_metadata, complete, index_cache, parent, is_scalar_noise, is_dde; checks = checks) end diff --git a/src/systems/discrete_system/discrete_system.jl b/src/systems/discrete_system/discrete_system.jl index 01fca30235..7458237333 100644 --- a/src/systems/discrete_system/discrete_system.jl +++ b/src/systems/discrete_system/discrete_system.jl @@ -55,6 +55,19 @@ struct DiscreteSystem <: AbstractTimeDependentSystem """ defaults::Dict """ + The guesses to use as the initial conditions for the + initialization system. + """ + guesses::Dict + """ + The system for performing the initialization. + """ + initializesystem::Union{Nothing, NonlinearSystem} + """ + Extra equations to be enforced during the initialization sequence. + """ + initialization_eqs::Vector{Equation} + """ Inject assignment statements before the evaluation of the RHS function. """ preface::Any @@ -98,9 +111,8 @@ struct DiscreteSystem <: AbstractTimeDependentSystem isscheduled::Bool function DiscreteSystem(tag, discreteEqs, iv, dvs, ps, tspan, var_to_name, - observed, - name, description, - systems, defaults, preface, connector_type, parameter_dependencies = Equation[], + observed, name, description, systems, defaults, guesses, initializesystem, + initialization_eqs, preface, connector_type, parameter_dependencies = Equation[], metadata = nothing, gui_metadata = nothing, tearing_state = nothing, substitutions = nothing, complete = false, index_cache = nothing, parent = nothing, @@ -116,8 +128,7 @@ struct DiscreteSystem <: AbstractTimeDependentSystem check_units(u, discreteEqs) end new(tag, discreteEqs, iv, dvs, ps, tspan, var_to_name, observed, name, description, - systems, - defaults, + systems, defaults, guesses, initializesystem, initialization_eqs, preface, connector_type, parameter_dependencies, metadata, gui_metadata, tearing_state, substitutions, complete, index_cache, parent, isscheduled) end @@ -135,6 +146,9 @@ function DiscreteSystem(eqs::AbstractVector{<:Equation}, iv, dvs, ps; description = "", default_u0 = Dict(), default_p = Dict(), + guesses = Dict(), + initializesystem = nothing, + initialization_eqs = Equation[], defaults = _merge(Dict(default_u0), Dict(default_p)), preface = nothing, connector_type = nothing, @@ -155,13 +169,21 @@ function DiscreteSystem(eqs::AbstractVector{<:Equation}, iv, dvs, ps; "`default_u0` and `default_p` are deprecated. Use `defaults` instead.", :DiscreteSystem, force = true) end - defaults = todict(defaults) - defaults = Dict(value(k) => value(v) - for (k, v) in pairs(defaults) if value(v) !== nothing) + defaults = Dict{Any, Any}(todict(defaults)) + guesses = Dict{Any, Any}(todict(guesses)) var_to_name = Dict() - process_variables!(var_to_name, defaults, dvs′) - process_variables!(var_to_name, defaults, ps′) + process_variables!(var_to_name, defaults, guesses, dvs′) + process_variables!(var_to_name, defaults, guesses, ps′) + process_variables!( + var_to_name, defaults, guesses, [eq.lhs for eq in parameter_dependencies]) + process_variables!( + var_to_name, defaults, guesses, [eq.rhs for eq in parameter_dependencies]) + defaults = Dict{Any, Any}(value(k) => value(v) + for (k, v) in pairs(defaults) if v !== nothing) + guesses = Dict{Any, Any}(value(k) => value(v) + for (k, v) in pairs(guesses) if v !== nothing) + isempty(observed) || collect_var_to_name!(var_to_name, (eq.lhs for eq in observed)) sysnames = nameof.(systems) @@ -170,7 +192,8 @@ function DiscreteSystem(eqs::AbstractVector{<:Equation}, iv, dvs, ps; end DiscreteSystem(Threads.atomic_add!(SYSTEM_COUNT, UInt(1)), eqs, iv′, dvs′, ps′, tspan, var_to_name, observed, name, description, systems, - defaults, preface, connector_type, parameter_dependencies, metadata, gui_metadata, kwargs...) + defaults, guesses, initializesystem, initialization_eqs, preface, connector_type, + parameter_dependencies, metadata, gui_metadata, kwargs...) end function DiscreteSystem(eqs, iv; kwargs...) @@ -225,6 +248,8 @@ function flatten(sys::DiscreteSystem, noeqs = false) parameters(sys), observed = observed(sys), defaults = defaults(sys), + guesses = guesses(sys), + initialization_eqs = initialization_equations(sys), name = nameof(sys), description = description(sys), metadata = get_metadata(sys), diff --git a/src/systems/jumps/jumpsystem.jl b/src/systems/jumps/jumpsystem.jl index e5e17fb5f9..efc5a9be7d 100644 --- a/src/systems/jumps/jumpsystem.jl +++ b/src/systems/jumps/jumpsystem.jl @@ -84,6 +84,19 @@ struct JumpSystem{U <: ArrayPartition} <: AbstractTimeDependentSystem """ defaults::Dict """ + The guesses to use as the initial conditions for the + initialization system. + """ + guesses::Dict + """ + The system for performing the initialization. + """ + initializesystem::Union{Nothing, NonlinearSystem} + """ + Extra equations to be enforced during the initialization sequence. + """ + initialization_eqs::Vector{Equation} + """ Type of the system. """ connector_type::Any @@ -125,8 +138,9 @@ struct JumpSystem{U <: ArrayPartition} <: AbstractTimeDependentSystem function JumpSystem{U}( tag, ap::U, iv, unknowns, ps, var_to_name, observed, name, description, - systems, defaults, connector_type, cevents, devents, parameter_dependencies, - metadata = nothing, gui_metadata = nothing, + systems, defaults, guesses, initializesystem, initialization_eqs, connector_type, + cevents, devents, + parameter_dependencies, metadata = nothing, gui_metadata = nothing, complete = false, index_cache = nothing, isscheduled = false; checks::Union{Bool, Int} = true) where {U <: ArrayPartition} if checks == true || (checks & CheckComponents) > 0 @@ -139,7 +153,8 @@ struct JumpSystem{U <: ArrayPartition} <: AbstractTimeDependentSystem check_units(u, ap, iv) end new{U}(tag, ap, iv, unknowns, ps, var_to_name, - observed, name, description, systems, defaults, + observed, name, description, systems, defaults, guesses, initializesystem, + initialization_eqs, connector_type, cevents, devents, parameter_dependencies, metadata, gui_metadata, complete, index_cache, isscheduled) end @@ -154,6 +169,9 @@ function JumpSystem(eqs, iv, unknowns, ps; default_u0 = Dict(), default_p = Dict(), defaults = _merge(Dict(default_u0), Dict(default_p)), + guesses = Dict(), + initializesystem = nothing, + initialization_eqs = Equation[], name = nothing, description = "", connector_type = nothing, @@ -179,13 +197,17 @@ function JumpSystem(eqs, iv, unknowns, ps; :JumpSystem, force = true) end defaults = Dict{Any, Any}(todict(defaults)) + guesses = Dict{Any, Any}(todict(guesses)) var_to_name = Dict() - process_variables!(var_to_name, defaults, us′) - process_variables!(var_to_name, defaults, ps′) - process_variables!(var_to_name, defaults, [eq.lhs for eq in parameter_dependencies]) - process_variables!(var_to_name, defaults, [eq.rhs for eq in parameter_dependencies]) + process_variables!(var_to_name, defaults, guesses, us′) + process_variables!(var_to_name, defaults, guesses, ps′) + process_variables!( + var_to_name, defaults, guesses, [eq.lhs for eq in parameter_dependencies]) + process_variables!( + var_to_name, defaults, guesses, [eq.rhs for eq in parameter_dependencies]) #! format: off defaults = Dict{Any, Any}(value(k) => value(v) for (k, v) in pairs(defaults) if value(v) !== nothing) + guesses = Dict{Any, Any}(value(k) => value(v) for (k, v) in pairs(guesses) if v !== nothing) #! format: on isempty(observed) || collect_var_to_name!(var_to_name, (eq.lhs for eq in observed)) @@ -219,8 +241,9 @@ 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, connector_type, cont_callbacks, disc_callbacks, parameter_dependencies, - metadata, gui_metadata, checks = checks) + defaults, guesses, initializesystem, initialization_eqs, connector_type, + cont_callbacks, disc_callbacks, + parameter_dependencies, metadata, gui_metadata, checks = checks) end ##### MTK dispatches for JumpSystems ##### @@ -494,7 +517,7 @@ function DiffEqBase.ODEProblem(sys::JumpSystem, u0map, tspan::Union{Tuple, Nothi if has_equations(sys) osys = ODESystem(equations(sys).x[4], get_iv(sys), unknowns(sys), parameters(sys); observed = observed(sys), name = nameof(sys), description = description(sys), - systems = get_systems(sys), defaults = defaults(sys), + systems = get_systems(sys), defaults = defaults(sys), guesses = guesses(sys), parameter_dependencies = parameter_dependencies(sys), metadata = get_metadata(sys), gui_metadata = get_gui_metadata(sys)) osys = complete(osys) diff --git a/src/systems/nonlinear/nonlinearsystem.jl b/src/systems/nonlinear/nonlinearsystem.jl index b2abac5184..46d39822bf 100644 --- a/src/systems/nonlinear/nonlinearsystem.jl +++ b/src/systems/nonlinear/nonlinearsystem.jl @@ -57,6 +57,19 @@ struct NonlinearSystem <: AbstractTimeIndependentSystem """ defaults::Dict """ + The guesses to use as the initial conditions for the + initialization system. + """ + guesses::Dict + """ + The system for performing the initialization. + """ + initializesystem::Union{Nothing, NonlinearSystem} + """ + Extra equations to be enforced during the initialization sequence. + """ + initialization_eqs::Vector{Equation} + """ Type of the system. """ connector_type::Any @@ -97,9 +110,8 @@ struct NonlinearSystem <: AbstractTimeIndependentSystem function NonlinearSystem( tag, eqs, unknowns, ps, var_to_name, observed, jac, name, description, - systems, - defaults, connector_type, parameter_dependencies = Equation[], metadata = nothing, - gui_metadata = nothing, + systems, defaults, guesses, initializesystem, initialization_eqs, connector_type, + parameter_dependencies = Equation[], metadata = nothing, gui_metadata = nothing, tearing_state = nothing, substitutions = nothing, complete = false, index_cache = nothing, parent = nothing, isscheduled = false; checks::Union{Bool, Int} = true) @@ -107,8 +119,8 @@ struct NonlinearSystem <: AbstractTimeIndependentSystem u = __get_unit_type(unknowns, ps) check_units(u, eqs) end - new(tag, eqs, unknowns, ps, var_to_name, observed, - jac, name, description, systems, defaults, + new(tag, eqs, unknowns, ps, var_to_name, observed, jac, name, description, + systems, defaults, guesses, initializesystem, initialization_eqs, connector_type, parameter_dependencies, metadata, gui_metadata, tearing_state, substitutions, complete, index_cache, parent, isscheduled) end @@ -121,6 +133,9 @@ function NonlinearSystem(eqs, unknowns, ps; default_u0 = Dict(), default_p = Dict(), defaults = _merge(Dict(default_u0), Dict(default_p)), + guesses = Dict(), + initializesystem = nothing, + initialization_eqs = Equation[], systems = NonlinearSystem[], connector_type = nothing, continuous_events = nothing, # this argument is only required for ODESystems, but is added here for the constructor to accept it without error @@ -151,21 +166,32 @@ function NonlinearSystem(eqs, unknowns, ps; eqs = [wrap(eq.lhs) isa Symbolics.Arr ? eq : 0 ~ eq.rhs - eq.lhs for eq in eqs] jac = RefValue{Any}(EMPTY_JAC) - defaults = todict(defaults) - defaults = Dict{Any, Any}(value(k) => value(v) - for (k, v) in pairs(defaults) if value(v) !== nothing) - unknowns, ps = value.(unknowns), value.(ps) + ps′ = value.(ps) + dvs′ = value.(unknowns) + parameter_dependencies, ps′ = process_parameter_dependencies( + parameter_dependencies, ps′) + + defaults = Dict{Any, Any}(todict(defaults)) + guesses = Dict{Any, Any}(todict(guesses)) var_to_name = Dict() - process_variables!(var_to_name, defaults, unknowns) - process_variables!(var_to_name, defaults, ps) + process_variables!(var_to_name, defaults, guesses, dvs′) + process_variables!(var_to_name, defaults, guesses, ps′) + process_variables!( + var_to_name, defaults, guesses, [eq.lhs for eq in parameter_dependencies]) + process_variables!( + var_to_name, defaults, guesses, [eq.rhs for eq in parameter_dependencies]) + defaults = Dict{Any, Any}(value(k) => value(v) + for (k, v) in pairs(defaults) if v !== nothing) + guesses = Dict{Any, Any}(value(k) => value(v) + for (k, v) in pairs(guesses) if v !== nothing) + isempty(observed) || collect_var_to_name!(var_to_name, (eq.lhs for eq in observed)) - parameter_dependencies, ps = process_parameter_dependencies( - parameter_dependencies, ps) NonlinearSystem(Threads.atomic_add!(SYSTEM_COUNT, UInt(1)), - eqs, unknowns, ps, var_to_name, observed, jac, name, description, systems, defaults, - connector_type, parameter_dependencies, metadata, gui_metadata, checks = checks) + eqs, dvs′, ps′, var_to_name, observed, jac, name, description, systems, defaults, + guesses, initializesystem, initialization_eqs, connector_type, parameter_dependencies, + metadata, gui_metadata, checks = checks) end function NonlinearSystem(eqs; kwargs...) @@ -857,6 +883,7 @@ function flatten(sys::NonlinearSystem, noeqs = false) parameters(sys), observed = observed(sys), defaults = defaults(sys), + guesses = guesses(sys), name = nameof(sys), description = description(sys), metadata = get_metadata(sys), diff --git a/src/systems/optimization/constraints_system.jl b/src/systems/optimization/constraints_system.jl index a2756994ac..03225fc900 100644 --- a/src/systems/optimization/constraints_system.jl +++ b/src/systems/optimization/constraints_system.jl @@ -143,8 +143,8 @@ function ConstraintsSystem(constraints, unknowns, ps; for (k, v) in pairs(defaults) if value(v) !== nothing) var_to_name = Dict() - process_variables!(var_to_name, defaults, unknowns′) - process_variables!(var_to_name, defaults, ps′) + process_variables!(var_to_name, defaults, Dict(), unknowns′) + process_variables!(var_to_name, defaults, Dict(), ps′) isempty(observed) || collect_var_to_name!(var_to_name, (eq.lhs for eq in observed)) ConstraintsSystem(Threads.atomic_add!(SYSTEM_COUNT, UInt(1)), diff --git a/src/systems/optimization/optimizationsystem.jl b/src/systems/optimization/optimizationsystem.jl index 0398c892eb..0b20fdef79 100644 --- a/src/systems/optimization/optimizationsystem.jl +++ b/src/systems/optimization/optimizationsystem.jl @@ -132,8 +132,8 @@ function OptimizationSystem(op, unknowns, ps; for (k, v) in pairs(defaults) if value(v) !== nothing) var_to_name = Dict() - process_variables!(var_to_name, defaults, unknowns′) - process_variables!(var_to_name, defaults, ps′) + process_variables!(var_to_name, defaults, Dict(), unknowns′) + process_variables!(var_to_name, defaults, Dict(), ps′) isempty(observed) || collect_var_to_name!(var_to_name, (eq.lhs for eq in observed)) OptimizationSystem(Threads.atomic_add!(SYSTEM_COUNT, UInt(1)), diff --git a/src/utils.jl b/src/utils.jl index e9ddad3a07..c3011c2a79 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -244,6 +244,13 @@ function setdefault(v, val) val === nothing ? v : wrap(setdefaultval(unwrap(v), value(val))) end +function process_variables!(var_to_name, defs, guesses, vars) + collect_defaults!(defs, vars) + collect_guesses!(guesses, vars) + collect_var_to_name!(var_to_name, vars) + return nothing +end + function process_variables!(var_to_name, defs, vars) collect_defaults!(defs, vars) collect_var_to_name!(var_to_name, vars) @@ -261,6 +268,17 @@ function collect_defaults!(defs, vars) return defs end +function collect_guesses!(guesses, vars) + for v in vars + symbolic_type(v) == NotSymbolic() && continue + if haskey(guesses, v) || !hasguess(unwrap(v)) || (def = getguess(v)) === nothing + continue + end + guesses[v] = getguess(v) + end + return guesses +end + function collect_var_to_name!(vars, xs) for x in xs symbolic_type(x) == NotSymbolic() && continue @@ -1146,3 +1164,12 @@ function similar_variable(var::BasicSymbolic, name = :anon) end return sym end + +function guesses_from_metadata!(guesses, vars) + varguesses = [getguess(v) for v in vars] + hasaguess = findall(!isnothing, varguesses) + for i in hasaguess + haskey(guesses, vars[i]) && continue + guesses[vars[i]] = varguesses[i] + end +end From af8cd679571d6c2d078fbebdd9219536b270f335 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Sun, 1 Dec 2024 13:07:33 +0530 Subject: [PATCH 0491/1337] feat: support arbitrary systems in `generate_initializesystem` --- src/systems/nonlinear/initializesystem.jl | 65 +++++++++++++---------- 1 file changed, 38 insertions(+), 27 deletions(-) diff --git a/src/systems/nonlinear/initializesystem.jl b/src/systems/nonlinear/initializesystem.jl index 2344727920..229d462911 100644 --- a/src/systems/nonlinear/initializesystem.jl +++ b/src/systems/nonlinear/initializesystem.jl @@ -3,7 +3,7 @@ $(TYPEDSIGNATURES) Generate `NonlinearSystem` which initializes an ODE problem from specified initial conditions of an `ODESystem`. """ -function generate_initializesystem(sys::ODESystem; +function generate_initializesystem(sys::AbstractSystem; u0map = Dict(), pmap = Dict(), initialization_eqs = [], @@ -12,28 +12,36 @@ function generate_initializesystem(sys::ODESystem; algebraic_only = false, check_units = true, check_defguess = false, name = nameof(sys), extra_metadata = (;), kwargs...) - trueobs, eqs = unhack_observed(observed(sys), equations(sys)) + eqs = equations(sys) + eqs = filter(x -> x isa Equation, eqs) + trueobs, eqs = unhack_observed(observed(sys), eqs) vars = unique([unknowns(sys); getfield.(trueobs, :lhs)]) vars_set = Set(vars) # for efficient in-lookup - idxs_diff = isdiffeq.(eqs) - idxs_alge = .!idxs_diff - - # prepare map for dummy derivative substitution - eqs_diff = eqs[idxs_diff] - D = Differential(get_iv(sys)) - diffmap = merge( - Dict(eq.lhs => eq.rhs for eq in eqs_diff), - Dict(D(eq.lhs) => D(eq.rhs) for eq in trueobs) - ) - - # 1) process dummy derivatives and u0map into initialization system - eqs_ics = eqs[idxs_alge] # start equation list with algebraic equations + eqs_ics = Equation[] defs = copy(defaults(sys)) # copy so we don't modify sys.defaults additional_guesses = anydict(guesses) guesses = merge(get_guesses(sys), additional_guesses) - schedule = getfield(sys, :schedule) - if !isnothing(schedule) + idxs_diff = isdiffeq.(eqs) + + # 1) Use algebraic equations of time-dependent systems as initialization constraints + if has_iv(sys) + idxs_alge = .!idxs_diff + append!(eqs_ics, eqs[idxs_alge]) # start equation list with algebraic equations + + eqs_diff = eqs[idxs_diff] + D = Differential(get_iv(sys)) + diffmap = merge( + Dict(eq.lhs => eq.rhs for eq in eqs_diff), + Dict(D(eq.lhs) => D(eq.rhs) for eq in trueobs) + ) + else + diffmap = Dict() + end + + if has_schedule(sys) && (schedule = get_schedule(sys); !isnothing(schedule)) + # 2) process dummy derivatives and u0map into initialization system + # prepare map for dummy derivative substitution for x in filter(x -> !isnothing(x[1]), schedule.dummy_sub) # set dummy derivatives to default_dd_guess unless specified push!(defs, x[1] => get(guesses, x[1], default_dd_guess)) @@ -61,9 +69,14 @@ function generate_initializesystem(sys::ODESystem; process_u0map_with_dummysubs(y, x) end end + else + # 2) System doesn't have a schedule, so dummy derivatives don't exist/aren't handled (SDESystem) + for (k, v) in u0map + defs[k] = v + end end - # 2) process other variables + # 3) process other variables for var in vars if var ∈ keys(defs) push!(eqs_ics, var ~ defs[var]) @@ -74,7 +87,7 @@ function generate_initializesystem(sys::ODESystem; end end - # 3) process explicitly provided initialization equations + # 4) process explicitly provided initialization equations if !algebraic_only initialization_eqs = [get_initialization_eqs(sys); initialization_eqs] for eq in initialization_eqs @@ -83,7 +96,7 @@ function generate_initializesystem(sys::ODESystem; end end - # 4) process parameters as initialization unknowns + # 5) process parameters as initialization unknowns paramsubs = Dict() if pmap isa SciMLBase.NullParameters pmap = Dict() @@ -138,7 +151,7 @@ function generate_initializesystem(sys::ODESystem; end end - # 5) parameter dependencies become equations, their LHS become unknowns + # 6) parameter dependencies become equations, their LHS become unknowns # non-numeric dependent parameters stay as parameter dependencies new_parameter_deps = Equation[] for eq in parameter_dependencies(sys) @@ -153,7 +166,7 @@ function generate_initializesystem(sys::ODESystem; push!(defs, varp => guessval) end - # 6) handle values provided for dependent parameters similar to values for observed variables + # 7) handle values provided for dependent parameters similar to values for observed variables for (k, v) in merge(defaults(sys), pmap) if is_variable_floatingpoint(k) && has_parameter_dependency_with_lhs(sys, k) push!(eqs_ics, paramsubs[k] ~ v) @@ -161,12 +174,10 @@ function generate_initializesystem(sys::ODESystem; end # parameters do not include ones that became initialization unknowns - pars = vcat( - [get_iv(sys)], # include independent variable as pseudo-parameter - [p for p in parameters(sys) if !haskey(paramsubs, p)] - ) + pars = Vector{SymbolicParam}(filter(p -> !haskey(paramsubs, p), parameters(sys))) + is_time_dependent(sys) && push!(pars, get_iv(sys)) - # 7) use observed equations for guesses of observed variables if not provided + # 8) use observed equations for guesses of observed variables if not provided for eq in trueobs haskey(defs, eq.lhs) && continue any(x -> isequal(default_toterm(x), eq.lhs), keys(defs)) && continue From 39281945c17108c2b4a3434d89a4e57911905927 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 2 Dec 2024 11:32:03 +0530 Subject: [PATCH 0492/1337] refactor: use `initialization_data` in SciMLFunction constructors --- src/systems/diffeqs/abstractodesystem.jl | 28 ++++++++--------------- src/systems/diffeqs/sdesystem.jl | 6 ++--- src/systems/nonlinear/initializesystem.jl | 8 +------ src/systems/nonlinear/nonlinearsystem.jl | 12 ++++++---- 4 files changed, 21 insertions(+), 33 deletions(-) diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index f4e29346ff..7d9369d441 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -359,10 +359,7 @@ function DiffEqBase.ODEFunction{iip, specialize}(sys::AbstractODESystem, sparsity = false, analytic = nothing, split_idxs = nothing, - initializeprob = nothing, - update_initializeprob! = nothing, - initializeprobmap = nothing, - initializeprobpmap = nothing, + initialization_data = nothing, kwargs...) where {iip, specialize} if !iscomplete(sys) error("A completed system is required. Call `complete` or `structural_simplify` on the system before creating an `ODEFunction`") @@ -463,10 +460,7 @@ function DiffEqBase.ODEFunction{iip, specialize}(sys::AbstractODESystem, observed = observedfun, sparsity = sparsity ? jacobian_sparsity(sys) : nothing, analytic = analytic, - initializeprob = initializeprob, - update_initializeprob! = update_initializeprob!, - initializeprobmap = initializeprobmap, - initializeprobpmap = initializeprobpmap) + initialization_data) end """ @@ -496,10 +490,7 @@ function DiffEqBase.DAEFunction{iip}(sys::AbstractODESystem, dvs = unknowns(sys) sparse = false, simplify = false, eval_module = @__MODULE__, checkbounds = false, - initializeprob = nothing, - initializeprobmap = nothing, - initializeprobpmap = nothing, - update_initializeprob! = nothing, + initialization_data = nothing, kwargs...) where {iip} if !iscomplete(sys) error("A completed system is required. Call `complete` or `structural_simplify` on the system before creating a `DAEFunction`") @@ -547,15 +538,12 @@ function DiffEqBase.DAEFunction{iip}(sys::AbstractODESystem, dvs = unknowns(sys) nothing end - DAEFunction{iip}(f, + DAEFunction{iip}(f; sys = sys, jac = _jac === nothing ? nothing : _jac, jac_prototype = jac_prototype, observed = observedfun, - initializeprob = initializeprob, - initializeprobmap = initializeprobmap, - initializeprobpmap = initializeprobpmap, - update_initializeprob! = update_initializeprob!) + initialization_data) end function DiffEqBase.DDEFunction(sys::AbstractODESystem, args...; kwargs...) @@ -567,6 +555,7 @@ function DiffEqBase.DDEFunction{iip}(sys::AbstractODESystem, dvs = unknowns(sys) eval_expression = false, eval_module = @__MODULE__, checkbounds = false, + initialization_data = nothing, kwargs...) where {iip} if !iscomplete(sys) error("A completed system is required. Call `complete` or `structural_simplify` on the system before creating an `DDEFunction`") @@ -579,7 +568,7 @@ function DiffEqBase.DDEFunction{iip}(sys::AbstractODESystem, dvs = unknowns(sys) f(u, h, p, t) = f_oop(u, h, p, t) f(du, u, h, p, t) = f_iip(du, u, h, p, t) - DDEFunction{iip}(f, sys = sys) + DDEFunction{iip}(f; sys = sys, initialization_data) end function DiffEqBase.SDDEFunction(sys::AbstractODESystem, args...; kwargs...) @@ -591,6 +580,7 @@ function DiffEqBase.SDDEFunction{iip}(sys::AbstractODESystem, dvs = unknowns(sys eval_expression = false, eval_module = @__MODULE__, checkbounds = false, + initialization_data = nothing, kwargs...) where {iip} if !iscomplete(sys) error("A completed system is required. Call `complete` or `structural_simplify` on the system before creating an `SDDEFunction`") @@ -609,7 +599,7 @@ function DiffEqBase.SDDEFunction{iip}(sys::AbstractODESystem, dvs = unknowns(sys g(u, h, p, t) = g_oop(u, h, p, t) g(du, u, h, p, t) = g_iip(du, u, h, p, t) - SDDEFunction{iip}(f, g, sys = sys) + SDDEFunction{iip}(f, g; sys = sys, initialization_data) end """ diff --git a/src/systems/diffeqs/sdesystem.jl b/src/systems/diffeqs/sdesystem.jl index d604863024..37e743218a 100644 --- a/src/systems/diffeqs/sdesystem.jl +++ b/src/systems/diffeqs/sdesystem.jl @@ -544,7 +544,7 @@ function DiffEqBase.SDEFunction{iip, specialize}(sys::SDESystem, dvs = unknowns( version = nothing, tgrad = false, sparse = false, jac = false, Wfact = false, eval_expression = false, eval_module = @__MODULE__, - checkbounds = false, + checkbounds = false, initialization_data = nothing, kwargs...) where {iip, specialize} if !iscomplete(sys) error("A completed `SDESystem` is required. Call `complete` or `structural_simplify` on the system before creating an `SDEFunction`") @@ -615,13 +615,13 @@ function DiffEqBase.SDEFunction{iip, specialize}(sys::SDESystem, dvs = unknowns( observedfun = ObservedFunctionCache(sys; eval_expression, eval_module) - SDEFunction{iip, specialize}(f, g, + SDEFunction{iip, specialize}(f, g; sys = sys, jac = _jac === nothing ? nothing : _jac, tgrad = _tgrad === nothing ? nothing : _tgrad, Wfact = _Wfact === nothing ? nothing : _Wfact, Wfact_t = _Wfact_t === nothing ? nothing : _Wfact_t, - mass_matrix = _M, + mass_matrix = _M, initialization_data, observed = observedfun) end diff --git a/src/systems/nonlinear/initializesystem.jl b/src/systems/nonlinear/initializesystem.jl index 229d462911..624ee1bd71 100644 --- a/src/systems/nonlinear/initializesystem.jl +++ b/src/systems/nonlinear/initializesystem.jl @@ -346,13 +346,7 @@ function SciMLBase.remake_initialization_data(sys::ODESystem, odefn, u0, t0, p, u0map, pmap, defs, cmap, dvs, ps) kws = maybe_build_initialization_problem( sys, op, u0map, pmap, t0, defs, guesses, missing_unknowns; use_scc) - initprob = get(kws, :initializeprob, nothing) - if initprob === nothing - return nothing - end - return SciMLBase.OverrideInitData(initprob, get(kws, :update_initializeprob!, nothing), - get(kws, :initializeprobmap, nothing), - get(kws, :initializeprobpmap, nothing)) + return get(kws, :initialization_data, nothing) end """ diff --git a/src/systems/nonlinear/nonlinearsystem.jl b/src/systems/nonlinear/nonlinearsystem.jl index 46d39822bf..8cd8175668 100644 --- a/src/systems/nonlinear/nonlinearsystem.jl +++ b/src/systems/nonlinear/nonlinearsystem.jl @@ -344,6 +344,7 @@ function SciMLBase.NonlinearFunction{iip}(sys::NonlinearSystem, dvs = unknowns(s eval_expression = false, eval_module = @__MODULE__, sparse = false, simplify = false, + initialization_data = nothing, kwargs...) where {iip} if !iscomplete(sys) error("A completed `NonlinearSystem` is required. Call `complete` or `structural_simplify` on the system before creating a `NonlinearFunction`") @@ -376,14 +377,14 @@ function SciMLBase.NonlinearFunction{iip}(sys::NonlinearSystem, dvs = unknowns(s resid_prototype = calculate_resid_prototype(length(equations(sys)), u0, p) end - NonlinearFunction{iip}(f, + NonlinearFunction{iip}(f; sys = sys, jac = _jac === nothing ? nothing : _jac, resid_prototype = resid_prototype, jac_prototype = sparse ? similar(calculate_jacobian(sys, sparse = sparse), Float64) : nothing, - observed = observedfun) + observed = observedfun, initialization_data) end """ @@ -395,7 +396,8 @@ respectively. """ function SciMLBase.IntervalNonlinearFunction( sys::NonlinearSystem, dvs = unknowns(sys), ps = parameters(sys), u0 = nothing; - p = nothing, eval_expression = false, eval_module = @__MODULE__, kwargs...) + p = nothing, eval_expression = false, eval_module = @__MODULE__, + initialization_data = nothing, kwargs...) if !iscomplete(sys) error("A completed `NonlinearSystem` is required. Call `complete` or `structural_simplify` on the system before creating a `IntervalNonlinearFunction`") end @@ -411,7 +413,8 @@ function SciMLBase.IntervalNonlinearFunction( observedfun = ObservedFunctionCache(sys; eval_expression, eval_module) - IntervalNonlinearFunction{false}(f; observed = observedfun, sys = sys) + IntervalNonlinearFunction{false}( + f; observed = observedfun, sys = sys, initialization_data) end """ @@ -884,6 +887,7 @@ function flatten(sys::NonlinearSystem, noeqs = false) observed = observed(sys), defaults = defaults(sys), guesses = guesses(sys), + initialization_eqs = initialization_equations(sys), name = nameof(sys), description = description(sys), metadata = get_metadata(sys), From 4044317b17b3f239f0a4fa6a87f16f368f8f28aa Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 2 Dec 2024 11:33:14 +0530 Subject: [PATCH 0493/1337] fix: don't build initializeprob for initializeprob --- src/systems/diffeqs/abstractodesystem.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index 7d9369d441..b3d4903191 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -1395,5 +1395,5 @@ function InitializationProblem{iip, specialize}(sys::AbstractODESystem, else NonlinearLeastSquaresProblem end - TProb(isys, u0map, parammap; kwargs...) + TProb(isys, u0map, parammap; kwargs..., build_initializeprob = false) end From 180b97801da7d134754c16becc9daebbfb688912 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 2 Dec 2024 11:34:02 +0530 Subject: [PATCH 0494/1337] feat: build initialization system for all system types in `process_SciMLProblem` --- src/systems/problem_utils.jl | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/systems/problem_utils.jl b/src/systems/problem_utils.jl index 6b75fb2c6d..6ce5704daa 100644 --- a/src/systems/problem_utils.jl +++ b/src/systems/problem_utils.jl @@ -540,8 +540,9 @@ function maybe_build_initialization_problem( end if (((implicit_dae || has_observed_u0s || !isempty(missing_unknowns) || !isempty(solvablepars) || has_dependent_unknowns) && - get_tearing_state(sys) !== nothing) || - !isempty(initialization_equations(sys))) && t !== nothing + (!has_tearing_state(sys) || get_tearing_state(sys) !== nothing)) || + !isempty(initialization_equations(sys))) && + (!is_time_dependent(sys) || t !== nothing) initializeprob = ModelingToolkit.InitializationProblem( sys, t, u0map, pmap; guesses, kwargs...) initializeprobmap = getu(initializeprob, unknowns(sys)) @@ -567,7 +568,9 @@ function maybe_build_initialization_problem( end empty!(missing_unknowns) return (; - initializeprob, initializeprobmap, initializeprobpmap, update_initializeprob!) + initialization_data = SciMLBase.OverrideInitData( + initializeprob, update_initializeprob!, initializeprobmap, + initializeprobpmap)) end return (;) end @@ -662,7 +665,7 @@ function process_SciMLProblem( op, missing_unknowns, missing_pars = build_operating_point( u0map, pmap, defs, cmap, dvs, ps) - if sys isa ODESystem && build_initializeprob + if build_initializeprob kws = maybe_build_initialization_problem( sys, op, u0map, pmap, t, defs, guesses, missing_unknowns; implicit_dae, warn_initialize_determined, initialization_eqs, From c9c613f314cddb9ade459920cbb102a3b57b22da Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 2 Dec 2024 17:18:07 +0530 Subject: [PATCH 0495/1337] fix: retain system data on `structural_simplify` of `SDESystem` --- src/systems/systems.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/systems/systems.jl b/src/systems/systems.jl index 47acd81a82..04c50bc766 100644 --- a/src/systems/systems.jl +++ b/src/systems/systems.jl @@ -164,6 +164,7 @@ function __structural_simplify(sys::AbstractSystem, io = nothing; simplify = fal return SDESystem(Vector{Equation}(full_equations(ode_sys)), noise_eqs, 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)) + parameter_dependencies = parameter_dependencies(sys), + guesses = guesses(sys), initialization_eqs = initialization_equations(sys)) end end From 4d5daa3caeaad6ee4ed6f9698194aa467d1c4ea4 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 3 Dec 2024 16:32:11 +0530 Subject: [PATCH 0496/1337] fix: pass `t` to `process_SciMLProblem` in `SDEProblem` --- src/systems/diffeqs/sdesystem.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/systems/diffeqs/sdesystem.jl b/src/systems/diffeqs/sdesystem.jl index 37e743218a..5e0d0e3208 100644 --- a/src/systems/diffeqs/sdesystem.jl +++ b/src/systems/diffeqs/sdesystem.jl @@ -738,7 +738,7 @@ function DiffEqBase.SDEProblem{iip, specialize}( end f, u0, p = process_SciMLProblem( SDEFunction{iip, specialize}, sys, u0map, parammap; check_length, - kwargs...) + t = tspan === nothing ? nothing : tspan[1], kwargs...) cbs = process_events(sys; callback, kwargs...) sparsenoise === nothing && (sparsenoise = get(kwargs, :sparse, false)) From 8d09409a011a4d91dfe6d22d81b255f9a0da753f Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 3 Dec 2024 16:33:03 +0530 Subject: [PATCH 0497/1337] feat: support arbitrary systems in `remake_initialization_data` --- src/systems/nonlinear/initializesystem.jl | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/systems/nonlinear/initializesystem.jl b/src/systems/nonlinear/initializesystem.jl index 624ee1bd71..e32f967f4a 100644 --- a/src/systems/nonlinear/initializesystem.jl +++ b/src/systems/nonlinear/initializesystem.jl @@ -223,16 +223,19 @@ function is_parameter_solvable(p, pmap, defs, guesses) _val1 === nothing && _val2 !== nothing)) && _val3 !== nothing end -function SciMLBase.remake_initialization_data(sys::ODESystem, odefn, u0, t0, p, newu0, newp) +function SciMLBase.remake_initialization_data( + sys::AbstractSystem, odefn, u0, t0, p, newu0, newp) if u0 === missing && p === missing return odefn.initialization_data end if !(eltype(u0) <: Pair) && !(eltype(p) <: Pair) - oldinitprob = odefn.initializeprob + oldinitdata = odefn.initialization_data + oldinitdata === nothing && return nothing + + oldinitprob = oldinitdata.initializeprob oldinitprob === nothing && return nothing if !SciMLBase.has_sys(oldinitprob.f) || !(oldinitprob.f.sys isa NonlinearSystem) - return SciMLBase.OverrideInitData(oldinitprob, odefn.update_initializeprob!, - odefn.initializeprobmap, odefn.initializeprobpmap) + return oldinitdata end pidxs = ParameterIndex[] pvals = [] @@ -254,7 +257,7 @@ function SciMLBase.remake_initialization_data(sys::ODESystem, odefn, u0, t0, p, if p !== missing for sym in parameter_symbols(oldinitprob) push!(pidxs, parameter_index(oldinitprob, sym)) - if isequal(sym, get_iv(sys)) + if is_time_dependent(sys) && isequal(sym, get_iv(sys)) push!(pvals, t0) else push!(pvals, getp(sys, sym)(p)) @@ -283,8 +286,8 @@ function SciMLBase.remake_initialization_data(sys::ODESystem, odefn, u0, t0, p, length(oldinitprob.f.resid_prototype), newu0, newp)) end initprob = remake(oldinitprob; f = newf, u0 = newu0, p = newp) - return SciMLBase.OverrideInitData(initprob, odefn.update_initializeprob!, - odefn.initializeprobmap, odefn.initializeprobpmap) + return SciMLBase.OverrideInitData(initprob, oldinitdata.update_initializeprob!, + oldinitdata.initializeprobmap, oldinitdata.initializeprobpmap) end dvs = unknowns(sys) ps = parameters(sys) @@ -298,7 +301,7 @@ function SciMLBase.remake_initialization_data(sys::ODESystem, odefn, u0, t0, p, use_scc = true if SciMLBase.has_initializeprob(odefn) - oldsys = odefn.initializeprob.f.sys + oldsys = odefn.initialization_data.initializeprob.f.sys meta = get_metadata(oldsys) if meta isa InitializationSystemMetadata u0map = merge(meta.u0map, u0map) @@ -336,7 +339,7 @@ function SciMLBase.remake_initialization_data(sys::ODESystem, odefn, u0, t0, p, pmap[p] = getp(sys, p)(newp) end end - if t0 === nothing + if t0 === nothing && is_time_dependent(sys) t0 = 0.0 end filter_missing_values!(u0map) From def207b92a83baf5363ebaded009c5b95706d645 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 3 Dec 2024 16:33:13 +0530 Subject: [PATCH 0498/1337] fix: fix type promotion bug in `remake_buffer` --- src/systems/parameter_buffer.jl | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/systems/parameter_buffer.jl b/src/systems/parameter_buffer.jl index 7d64054acc..bc4e62a773 100644 --- a/src/systems/parameter_buffer.jl +++ b/src/systems/parameter_buffer.jl @@ -574,6 +574,10 @@ function _remake_buffer(indp, oldbuf::MTKParameters, idxs, vals; validate = true @set! newbuf.tunable = narrow_buffer_type_and_fallback_undefs( oldbuf.tunable, newbuf.tunable) + if eltype(newbuf.tunable) <: Integer + T = promote_type(eltype(newbuf.tunable), Float64) + @set! newbuf.tunable = T.(newbuf.tunable) + end @set! newbuf.discrete = narrow_buffer_type_and_fallback_undefs.( oldbuf.discrete, newbuf.discrete) @set! newbuf.constant = narrow_buffer_type_and_fallback_undefs.( From d971b185ad5d4d9f1714448131b2bb02aff58b2d Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 3 Dec 2024 16:33:51 +0530 Subject: [PATCH 0499/1337] test: test initialization on `SDEProblem`, `DDEProblem`, `SDDEProblem` --- test/initializationsystem.jl | 428 ++++++++++++++++++++--------------- 1 file changed, 247 insertions(+), 181 deletions(-) diff --git a/test/initializationsystem.jl b/test/initializationsystem.jl index 9c06dd2030..045b7d2d2c 100644 --- a/test/initializationsystem.jl +++ b/test/initializationsystem.jl @@ -1,4 +1,5 @@ using ModelingToolkit, OrdinaryDiffEq, NonlinearSolve, Test +using StochasticDiffEq, DelayDiffEq, StochasticDelayDiffEq using ForwardDiff using SymbolicIndexingInterface, SciMLStructures using SciMLStructures: Tunable @@ -583,108 +584,123 @@ sol = solve(oprob_2nd_order_2, Rosenbrock23()) # retcode: Success end @testset "Initialization of parameters" begin - function test_parameter(prob, sym, val, initialval = zero(val)) - @test prob.ps[sym] ≈ initialval - @test init(prob, Tsit5()).ps[sym] ≈ val - @test solve(prob, Tsit5()).ps[sym] ≈ val - end - function test_initializesystem(sys, u0map, pmap, p, equation) - isys = ModelingToolkit.generate_initializesystem( - sys; u0map, pmap, guesses = ModelingToolkit.guesses(sys)) - @test is_variable(isys, p) - @test equation in equations(isys) || (0 ~ -equation.rhs) in equations(isys) - end - @variables x(t) y(t) + @variables _x(..) y(t) @parameters p q - u0map = Dict(x => 1.0, y => 1.0) - pmap = Dict() - pmap[q] = 1.0 - # `missing` default, equation from ODEProblem - @mtkbuild sys = ODESystem( - [D(x) ~ x * q, D(y) ~ y * p], t; defaults = [p => missing], guesses = [p => 1.0]) - pmap[p] = 2q - prob = ODEProblem(sys, u0map, (0.0, 1.0), pmap) - test_parameter(prob, p, 2.0) - prob2 = remake(prob; u0 = u0map, p = pmap) - prob2.ps[p] = 0.0 - test_parameter(prob2, p, 2.0) - # `missing` default, provided guess - @mtkbuild sys = ODESystem( - [D(x) ~ x, p ~ x + y], t; defaults = [p => missing], guesses = [p => 0.0]) - prob = ODEProblem(sys, u0map, (0.0, 1.0)) - test_parameter(prob, p, 2.0) - test_initializesystem(sys, u0map, pmap, p, 0 ~ p - x - y) - prob2 = remake(prob; u0 = u0map) - prob2.ps[p] = 0.0 - test_parameter(prob2, p, 2.0) - - # `missing` to ODEProblem, equation from default - @mtkbuild sys = ODESystem( - [D(x) ~ x * q, D(y) ~ y * p], t; defaults = [p => 2q], guesses = [p => 1.0]) - pmap[p] = missing - prob = ODEProblem(sys, u0map, (0.0, 1.0), pmap) - test_parameter(prob, p, 2.0) - test_initializesystem(sys, u0map, pmap, p, 0 ~ 2q - p) - prob2 = remake(prob; u0 = u0map, p = pmap) - prob2.ps[p] = 0.0 - test_parameter(prob2, p, 2.0) - # `missing` to ODEProblem, provided guess - @mtkbuild sys = ODESystem( - [D(x) ~ x, p ~ x + y], t; guesses = [p => 0.0]) - prob = ODEProblem(sys, u0map, (0.0, 1.0), pmap) - test_parameter(prob, p, 2.0) - test_initializesystem(sys, u0map, pmap, p, 0 ~ x + y - p) - prob2 = remake(prob; u0 = u0map, p = pmap) - prob2.ps[p] = 0.0 - test_parameter(prob2, p, 2.0) - - # No `missing`, default and guess - @mtkbuild sys = ODESystem( - [D(x) ~ x * q, D(y) ~ y * p], t; defaults = [p => 2q], guesses = [p => 0.0]) - delete!(pmap, p) - prob = ODEProblem(sys, u0map, (0.0, 1.0), pmap) - test_parameter(prob, p, 2.0) - test_initializesystem(sys, u0map, pmap, p, 0 ~ 2q - p) - prob2 = remake(prob; u0 = u0map, p = pmap) - prob2.ps[p] = 0.0 - test_parameter(prob2, p, 2.0) - - # Default overridden by ODEProblem, guess provided - @mtkbuild sys = ODESystem( - [D(x) ~ q * x, D(y) ~ y * p], t; defaults = [p => 2q], guesses = [p => 1.0]) - _pmap = merge(pmap, Dict(p => q)) - prob = ODEProblem(sys, u0map, (0.0, 1.0), _pmap) - test_parameter(prob, p, _pmap[q]) - test_initializesystem(sys, u0map, _pmap, p, 0 ~ q - p) - - # ODEProblem dependent value with guess, no `missing` - @mtkbuild sys = ODESystem([D(x) ~ x * q, D(y) ~ y * p], t; guesses = [p => 0.0]) - _pmap = merge(pmap, Dict(p => 3q)) - prob = ODEProblem(sys, u0map, (0.0, 1.0), _pmap) - test_parameter(prob, p, 3pmap[q]) - - # Should not be solved for: - - # Override dependent default with direct value - @mtkbuild sys = ODESystem( - [D(x) ~ q * x, D(y) ~ y * p], t; defaults = [p => 2q], guesses = [p => 1.0]) - _pmap = merge(pmap, Dict(p => 1.0)) - prob = ODEProblem(sys, u0map, (0.0, 1.0), _pmap) - @test prob.ps[p] ≈ 1.0 - @test prob.f.initializeprob === nothing - - # Non-floating point - @parameters r::Int s::Int - @mtkbuild sys = ODESystem( - [D(x) ~ s * x, D(y) ~ y * r], t; defaults = [s => 2r], guesses = [s => 1.0]) - prob = ODEProblem(sys, u0map, (0.0, 1.0), [r => 1]) - @test prob.ps[r] == 1 - @test prob.ps[s] == 2 - @test prob.f.initializeprob === nothing - - @mtkbuild sys = ODESystem([D(x) ~ x, p ~ x + y], t; guesses = [p => 0.0]) - @test_throws ModelingToolkit.MissingParametersError ODEProblem( - sys, [x => 1.0, y => 1.0], (0.0, 1.0)) + @brownian a b + x = _x(t) + + # `System` constructor creates appropriate type with mtkbuild + # `Problem` and `alg` create the problem to test and allow calling `init` with + # the correct solver. + # `rhss` allows adding terms to the end of equations (only 2 equations allowed) to influence + # the system type (brownian vars to turn it into an SDE). + @testset "$Problem" for (Problem, alg, rhss) in [ + (ODEProblem, Tsit5(), zeros(2)), (SDEProblem, ImplicitEM(), [a, b]), + (DDEProblem, MethodOfSteps(Tsit5()), [_x(t - 0.1), 0.0]), + (SDDEProblem, ImplicitEM(), [_x(t - 0.1) + a, b])] + function test_parameter(prob, sym, val, initialval = zero(val)) + @test prob.ps[sym] ≈ initialval + @test init(prob, alg).ps[sym] ≈ val + @test solve(prob, alg).ps[sym] ≈ val + end + function test_initializesystem(sys, u0map, pmap, p, equation) + isys = ModelingToolkit.generate_initializesystem( + sys; u0map, pmap, guesses = ModelingToolkit.guesses(sys)) + @test is_variable(isys, p) + @test equation in equations(isys) || (0 ~ -equation.rhs) in equations(isys) + end + u0map = Dict(x => 1.0, y => 1.0) + pmap = Dict() + pmap[q] = 1.0 + # `missing` default, equation from Problem + @mtkbuild sys = System( + [D(x) ~ x * q + rhss[1], D(y) ~ y * p + rhss[2]], t; defaults = [p => missing], guesses = [p => 1.0]) + pmap[p] = 2q + prob = Problem(sys, u0map, (0.0, 1.0), pmap) + test_parameter(prob, p, 2.0) + prob2 = remake(prob; u0 = u0map, p = pmap) + prob2.ps[p] = 0.0 + test_parameter(prob2, p, 2.0) + # `missing` default, provided guess + @mtkbuild sys = System( + [D(x) ~ x + rhss[1], p ~ x + y + rhss[2]], t; defaults = [p => missing], guesses = [p => 0.0]) + prob = Problem(sys, u0map, (0.0, 1.0)) + test_parameter(prob, p, 2.0) + test_initializesystem(sys, u0map, pmap, p, 0 ~ p - x - y) + prob2 = remake(prob; u0 = u0map) + prob2.ps[p] = 0.0 + test_parameter(prob2, p, 2.0) + + # `missing` to Problem, equation from default + @mtkbuild sys = System( + [D(x) ~ x * q + rhss[1], D(y) ~ y * p + rhss[2]], t; defaults = [p => 2q], guesses = [p => 1.0]) + pmap[p] = missing + prob = Problem(sys, u0map, (0.0, 1.0), pmap) + test_parameter(prob, p, 2.0) + test_initializesystem(sys, u0map, pmap, p, 0 ~ 2q - p) + prob2 = remake(prob; u0 = u0map, p = pmap) + prob2.ps[p] = 0.0 + test_parameter(prob2, p, 2.0) + # `missing` to Problem, provided guess + @mtkbuild sys = System( + [D(x) ~ x + rhss[1], p ~ x + y + rhss[2]], t; guesses = [p => 0.0]) + prob = Problem(sys, u0map, (0.0, 1.0), pmap) + test_parameter(prob, p, 2.0) + test_initializesystem(sys, u0map, pmap, p, 0 ~ x + y - p) + prob2 = remake(prob; u0 = u0map, p = pmap) + prob2.ps[p] = 0.0 + test_parameter(prob2, p, 2.0) + + # No `missing`, default and guess + @mtkbuild sys = System( + [D(x) ~ x * q + rhss[1], D(y) ~ y * p + rhss[2]], t; defaults = [p => 2q], guesses = [p => 0.0]) + delete!(pmap, p) + prob = Problem(sys, u0map, (0.0, 1.0), pmap) + test_parameter(prob, p, 2.0) + test_initializesystem(sys, u0map, pmap, p, 0 ~ 2q - p) + prob2 = remake(prob; u0 = u0map, p = pmap) + prob2.ps[p] = 0.0 + test_parameter(prob2, p, 2.0) + + # Default overridden by Problem, guess provided + @mtkbuild sys = System( + [D(x) ~ q * x + rhss[1], D(y) ~ y * p + rhss[2]], t; defaults = [p => 2q], guesses = [p => 1.0]) + _pmap = merge(pmap, Dict(p => q)) + prob = Problem(sys, u0map, (0.0, 1.0), _pmap) + test_parameter(prob, p, _pmap[q]) + test_initializesystem(sys, u0map, _pmap, p, 0 ~ q - p) + + # Problem dependent value with guess, no `missing` + @mtkbuild sys = System( + [D(x) ~ x * q + rhss[1], D(y) ~ y * p + rhss[2]], t; guesses = [p => 0.0]) + _pmap = merge(pmap, Dict(p => 3q)) + prob = Problem(sys, u0map, (0.0, 1.0), _pmap) + test_parameter(prob, p, 3pmap[q]) + + # Should not be solved for: + + # Override dependent default with direct value + @mtkbuild sys = System( + [D(x) ~ q * x + rhss[1], D(y) ~ y * p + rhss[2]], t; defaults = [p => 2q], guesses = [p => 1.0]) + _pmap = merge(pmap, Dict(p => 1.0)) + prob = Problem(sys, u0map, (0.0, 1.0), _pmap) + @test prob.ps[p] ≈ 1.0 + @test prob.f.initialization_data === nothing + + # Non-floating point + @parameters r::Int s::Int + @mtkbuild sys = System( + [D(x) ~ s * x + rhss[1], D(y) ~ y * r + rhss[2]], t; defaults = [s => 2r], guesses = [s => 1.0]) + prob = Problem(sys, u0map, (0.0, 1.0), [r => 1]) + @test prob.ps[r] == 1 + @test prob.ps[s] == 2 + @test prob.f.initialization_data === nothing + + @mtkbuild sys = System( + [D(x) ~ x + rhss[1], p ~ x + y + rhss[2]], t; guesses = [p => 0.0]) + @test_throws ModelingToolkit.MissingParametersError Problem( + sys, [x => 1.0, y => 1.0], (0.0, 1.0)) + end @testset "Null system" begin @variables x(t) y(t) s(t) @@ -718,103 +734,153 @@ end end @testset "Update initializeprob parameters" begin - @variables x(t) y(t) + @variables _x(..) y(t) @parameters p q - @mtkbuild sys = ODESystem( - [D(x) ~ x, p ~ x + y], t; guesses = [x => 0.0, p => 0.0]) - prob = ODEProblem(sys, [y => 1.0], (0.0, 1.0), [p => 3.0]) - @test prob.f.initializeprob.ps[p] ≈ 3.0 - @test init(prob, Tsit5())[x] ≈ 2.0 - prob.ps[p] = 2.0 - @test prob.f.initializeprob.ps[p] ≈ 3.0 - @test init(prob, Tsit5())[x] ≈ 1.0 - ModelingToolkit.defaults(prob.f.sys)[p] = missing - prob2 = remake(prob; u0 = [y => 1.0], p = [p => 3x]) - @test !is_variable(prob2.f.initializeprob, p) && - !is_parameter(prob2.f.initializeprob, p) - @test init(prob2, Tsit5())[x] ≈ 0.5 - @test_nowarn solve(prob2, Tsit5()) + @brownian a b + x = _x(t) + + @testset "$Problem" for (Problem, alg, rhss) in [ + (ODEProblem, Tsit5(), zeros(2)), (SDEProblem, ImplicitEM(), [a, b]), + (DDEProblem, MethodOfSteps(Tsit5()), [_x(t - 0.1), 0.0]), + (SDDEProblem, ImplicitEM(), [_x(t - 0.1) + a, b])] + @mtkbuild sys = System( + [D(x) ~ x + rhss[1], p ~ x + y + rhss[2]], t; guesses = [x => 0.0, p => 0.0]) + prob = Problem(sys, [y => 1.0], (0.0, 1.0), [p => 3.0]) + @test prob.f.initialization_data.initializeprob.ps[p] ≈ 3.0 + @test init(prob, alg)[x] ≈ 2.0 + prob.ps[p] = 2.0 + @test prob.f.initialization_data.initializeprob.ps[p] ≈ 3.0 + @test init(prob, alg)[x] ≈ 1.0 + ModelingToolkit.defaults(prob.f.sys)[p] = missing + prob2 = remake(prob; u0 = [y => 1.0], p = [p => 3x]) + @test !is_variable(prob2.f.initialization_data.initializeprob, p) && + !is_parameter(prob2.f.initialization_data.initializeprob, p) + @test init(prob2, alg)[x] ≈ 0.5 + @test_nowarn solve(prob2, alg) + end end @testset "Equations for dependent parameters" begin - @variables x(t) + @variables _x(..) @parameters p q=5 r - @mtkbuild sys = ODESystem( - D(x) ~ 2x + r, t; parameter_dependencies = [r ~ p + 2q, q ~ p + 3], - guesses = [p => 1.0]) - prob = ODEProblem(sys, [x => 1.0], (0.0, 1.0), [p => missing]) - @test length(equations(ModelingToolkit.get_parent(prob.f.initializeprob.f.sys))) == 4 - integ = init(prob, Tsit5()) - @test integ.ps[p] ≈ 2 + @brownian a + x = _x(t) + + @testset "$Problem" for (Problem, alg, rhss) in [ + (ODEProblem, Tsit5(), 0), (SDEProblem, ImplicitEM(), a), + (DDEProblem, MethodOfSteps(Tsit5()), _x(t - 0.1)), + (SDDEProblem, ImplicitEM(), _x(t - 0.1) + a)] + @mtkbuild sys = System( + [D(x) ~ 2x + r + rhss], t; parameter_dependencies = [r ~ p + 2q, q ~ p + 3], + guesses = [p => 1.0]) + prob = Problem(sys, [x => 1.0], (0.0, 1.0), [p => missing]) + @test length(equations(ModelingToolkit.get_parent(prob.f.initialization_data.initializeprob.f.sys))) == + 4 + integ = init(prob, alg) + @test integ.ps[p] ≈ 2 + end end @testset "Re-creating initialization problem on remake" begin - @variables x(t) y(t) + @variables _x(..) y(t) @parameters p q - @mtkbuild sys = ODESystem( - [D(x) ~ x, p ~ x + y], t; defaults = [p => missing], guesses = [x => 0.0, p => 0.0]) - prob = ODEProblem(sys, [x => 1.0, y => 1.0], (0.0, 1.0)) - @test init(prob, Tsit5()).ps[p] ≈ 2.0 - # nonsensical value for y just to test that equations work - prob2 = remake(prob; u0 = [x => 1.0, y => 2x + exp(t)]) - @test init(prob2, Tsit5()).ps[p] ≈ 4.0 - # solve for `x` given `p` and `y` - prob3 = remake(prob; u0 = [x => nothing, y => 1.0], p = [p => 2x + exp(t)]) - @test init(prob3, Tsit5())[x] ≈ 0.0 - @test_logs (:warn, r"overdetermined") remake( - prob; u0 = [x => 1.0, y => 2.0], p = [p => 4.0]) - prob4 = remake(prob; u0 = [x => 1.0, y => 2.0], p = [p => 4.0]) - @test solve(prob4, Tsit5()).retcode == ReturnCode.InitialFailure - prob5 = remake(prob) - @test init(prob, Tsit5()).ps[p] ≈ 2.0 + @brownian a b + x = _x(t) + + @testset "$Problem" for (Problem, alg, rhss) in [ + (ODEProblem, Tsit5(), zeros(2)), (SDEProblem, ImplicitEM(), [a, b]), + (DDEProblem, MethodOfSteps(Tsit5()), [_x(t - 0.1), 0.0]), + (SDDEProblem, ImplicitEM(), [_x(t - 0.1) + a, b])] + @mtkbuild sys = System( + [D(x) ~ x + rhss[1], p ~ x + y + rhss[2]], t; defaults = [p => missing], guesses = [ + x => 0.0, p => 0.0]) + prob = Problem(sys, [x => 1.0, y => 1.0], (0.0, 1.0)) + @test init(prob, alg).ps[p] ≈ 2.0 + # nonsensical value for y just to test that equations work + prob2 = remake(prob; u0 = [x => 1.0, y => 2x + exp(t)]) + @test init(prob2, alg).ps[p] ≈ 4.0 + # solve for `x` given `p` and `y` + prob3 = remake(prob; u0 = [x => nothing, y => 1.0], p = [p => 2x + exp(t)]) + @test init(prob3, alg)[x] ≈ 0.0 + @test_logs (:warn, r"overdetermined") remake( + prob; u0 = [x => 1.0, y => 2.0], p = [p => 4.0]) + prob4 = remake(prob; u0 = [x => 1.0, y => 2.0], p = [p => 4.0]) + @test solve(prob4, alg).retcode == ReturnCode.InitialFailure + prob5 = remake(prob) + @test init(prob, alg).ps[p] ≈ 2.0 + end end @testset "`remake` changes initialization problem types" begin - @variables x(t) y(t) z(t) + @variables _x(..) y(t) z(t) @parameters p q - @mtkbuild sys = ODESystem( - [D(x) ~ x * p + y * q, y^2 * q + q^2 * x ~ 0, z * p - p^2 * x * z ~ 0], - t; guesses = [x => 0.0, y => 0.0, z => 0.0, p => 0.0, q => 0.0]) - prob = ODEProblem(sys, [x => 1.0], (0.0, 1.0), [p => 1.0, q => missing]) - @test is_variable(prob.f.initializeprob, q) - ps = prob.p - newps = SciMLStructures.replace(Tunable(), ps, ForwardDiff.Dual.(ps.tunable)) - prob2 = remake(prob; p = newps) - @test eltype(prob2.f.initializeprob.u0) <: ForwardDiff.Dual - @test eltype(prob2.f.initializeprob.p.tunable) <: ForwardDiff.Dual - @test prob2.f.initializeprob.u0 ≈ prob.f.initializeprob.u0 - - prob2 = remake(prob; u0 = ForwardDiff.Dual.(prob.u0)) - @test eltype(prob2.f.initializeprob.u0) <: ForwardDiff.Dual - @test eltype(prob2.f.initializeprob.p.tunable) <: Float64 - @test prob2.f.initializeprob.u0 ≈ prob.f.initializeprob.u0 - - prob2 = remake(prob; u0 = ForwardDiff.Dual.(prob.u0), p = newps) - @test eltype(prob2.f.initializeprob.u0) <: ForwardDiff.Dual - @test eltype(prob2.f.initializeprob.p.tunable) <: ForwardDiff.Dual - @test prob2.f.initializeprob.u0 ≈ prob.f.initializeprob.u0 - - prob2 = remake(prob; u0 = [x => ForwardDiff.Dual(1.0)], - p = [p => ForwardDiff.Dual(1.0), q => missing]) - @test eltype(prob2.f.initializeprob.u0) <: ForwardDiff.Dual - @test eltype(prob2.f.initializeprob.p.tunable) <: ForwardDiff.Dual - @test prob2.f.initializeprob.u0 ≈ prob.f.initializeprob.u0 + @brownian a + x = _x(t) + + @testset "$Problem" for (Problem, alg, rhss) in [ + (ODEProblem, Tsit5(), 0), (SDEProblem, ImplicitEM(), a), + (DDEProblem, MethodOfSteps(Tsit5()), _x(t - 0.1)), + (SDDEProblem, ImplicitEM(), _x(t - 0.1) + a)] + @mtkbuild sys = System( + [D(x) ~ x * p + y * q + rhss, y^2 * q + q^2 * x ~ 0, z * p - p^2 * x * z ~ 0], + t; guesses = [x => 0.0, y => 0.0, z => 0.0, p => 0.0, q => 0.0]) + prob = Problem(sys, [x => 1.0], (0.0, 1.0), [p => 1.0, q => missing]) + @test is_variable(prob.f.initialization_data.initializeprob, q) + ps = prob.p + newps = SciMLStructures.replace(Tunable(), ps, ForwardDiff.Dual.(ps.tunable)) + prob2 = remake(prob; p = newps) + @test eltype(prob2.f.initialization_data.initializeprob.u0) <: ForwardDiff.Dual + @test eltype(prob2.f.initialization_data.initializeprob.p.tunable) <: + ForwardDiff.Dual + @test prob2.f.initialization_data.initializeprob.u0 ≈ + prob.f.initialization_data.initializeprob.u0 + + prob2 = remake(prob; u0 = ForwardDiff.Dual.(prob.u0)) + @test eltype(prob2.f.initialization_data.initializeprob.u0) <: ForwardDiff.Dual + @test eltype(prob2.f.initialization_data.initializeprob.p.tunable) <: Float64 + @test prob2.f.initialization_data.initializeprob.u0 ≈ + prob.f.initialization_data.initializeprob.u0 + + prob2 = remake(prob; u0 = ForwardDiff.Dual.(prob.u0), p = newps) + @test eltype(prob2.f.initialization_data.initializeprob.u0) <: ForwardDiff.Dual + @test eltype(prob2.f.initialization_data.initializeprob.p.tunable) <: + ForwardDiff.Dual + @test prob2.f.initialization_data.initializeprob.u0 ≈ + prob.f.initialization_data.initializeprob.u0 + + prob2 = remake(prob; u0 = [x => ForwardDiff.Dual(1.0)], + p = [p => ForwardDiff.Dual(1.0), q => missing]) + @test eltype(prob2.f.initialization_data.initializeprob.u0) <: ForwardDiff.Dual + @test eltype(prob2.f.initialization_data.initializeprob.p.tunable) <: + ForwardDiff.Dual + @test prob2.f.initialization_data.initializeprob.u0 ≈ + prob.f.initialization_data.initializeprob.u0 + end end @testset "`remake` preserves old u0map and pmap" begin - @variables x(t) y(t) + @variables _x(..) y(t) @parameters p - @mtkbuild sys = ODESystem( - [D(x) ~ x + p * y, y^2 + 4y * p^2 ~ x], t; guesses = [y => 1.0, p => 1.0]) - prob = ODEProblem(sys, [x => 1.0], (0.0, 1.0), [p => 1.0]) - @test is_variable(prob.f.initializeprob, y) - prob2 = @test_nowarn remake(prob; p = [p => 3.0]) # ensure no over/under-determined warning - @test is_variable(prob.f.initializeprob, y) - - prob = ODEProblem(sys, [y => 1.0, x => 2.0], (0.0, 1.0), [p => missing]) - @test is_variable(prob.f.initializeprob, p) - prob2 = @test_nowarn remake(prob; u0 = [y => 0.5]) - @test is_variable(prob.f.initializeprob, p) + @brownian a + x = _x(t) + + @testset "$Problem" for (Problem, alg, rhss) in [ + (ODEProblem, Tsit5(), 0), (SDEProblem, ImplicitEM(), a), + (DDEProblem, MethodOfSteps(Tsit5()), _x(t - 0.1)), + (SDDEProblem, ImplicitEM(), _x(t - 0.1) + a)] + @mtkbuild sys = System( + [D(x) ~ x + p * y + rhss, y^2 + 4y * p^2 ~ x], t; guesses = [ + y => 1.0, p => 1.0]) + prob = Problem(sys, [x => 1.0], (0.0, 1.0), [p => 1.0]) + @test is_variable(prob.f.initialization_data.initializeprob, y) + prob2 = @test_nowarn remake(prob; p = [p => 3.0]) # ensure no over/under-determined warning + @test is_variable(prob.f.initialization_data.initializeprob, y) + + prob = Problem(sys, [y => 1.0, x => 2.0], (0.0, 1.0), [p => missing]) + @test is_variable(prob.f.initialization_data.initializeprob, p) + prob2 = @test_nowarn remake(prob; u0 = [y => 0.5]) + @test is_variable(prob.f.initialization_data.initializeprob, p) + end end struct Multiplier{T} From 39bb59c3ccfcd8efee174829c58b2d546864cea9 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 6 Dec 2024 14:12:35 +0530 Subject: [PATCH 0500/1337] fix: handle integer `u0` in `DDEProblem` --- src/systems/diffeqs/abstractodesystem.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index b3d4903191..40c44b7bae 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -923,7 +923,7 @@ function DiffEqBase.DDEProblem{iip}(sys::AbstractODESystem, u0map = [], h_oop, h_iip = eval_or_rgf.(h_gen; eval_expression, eval_module) h(p, t) = h_oop(p, t) h(p::MTKParameters, t) = h_oop(p..., t) - u0 = h(p, tspan[1]) + u0 = float.(h(p, tspan[1])) if u0 !== nothing u0 = u0_constructor(u0) end From 2f2e62524f42d521e69b692b2801d5ca5e90cfc1 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 6 Dec 2024 14:13:00 +0530 Subject: [PATCH 0501/1337] feat: enable creating `InitializationProblem` for non-`AbstractODESystem`s --- src/systems/diffeqs/abstractodesystem.jl | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index 40c44b7bae..a6607e0c55 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -1247,11 +1247,11 @@ Generates a NonlinearProblem or NonlinearLeastSquaresProblem from an ODESystem which represents the initialization, i.e. the calculation of the consistent initial conditions for the given DAE. """ -function InitializationProblem(sys::AbstractODESystem, args...; kwargs...) +function InitializationProblem(sys::AbstractSystem, args...; kwargs...) InitializationProblem{true}(sys, args...; kwargs...) end -function InitializationProblem(sys::AbstractODESystem, t, +function InitializationProblem(sys::AbstractSystem, t, u0map::StaticArray, args...; kwargs...) @@ -1259,11 +1259,11 @@ function InitializationProblem(sys::AbstractODESystem, t, sys, t, u0map, args...; kwargs...) end -function InitializationProblem{true}(sys::AbstractODESystem, args...; kwargs...) +function InitializationProblem{true}(sys::AbstractSystem, args...; kwargs...) InitializationProblem{true, SciMLBase.AutoSpecialize}(sys, args...; kwargs...) end -function InitializationProblem{false}(sys::AbstractODESystem, args...; kwargs...) +function InitializationProblem{false}(sys::AbstractSystem, args...; kwargs...) InitializationProblem{false, SciMLBase.FullSpecialize}(sys, args...; kwargs...) end @@ -1282,8 +1282,8 @@ function Base.showerror(io::IO, e::IncompleteInitializationError) println(io, e.uninit) end -function InitializationProblem{iip, specialize}(sys::AbstractODESystem, - t::Number, u0map = [], +function InitializationProblem{iip, specialize}(sys::AbstractSystem, + t, u0map = [], parammap = DiffEqBase.NullParameters(); guesses = [], check_length = true, @@ -1347,13 +1347,13 @@ function InitializationProblem{iip, specialize}(sys::AbstractODESystem, @warn "Initialization system is underdetermined. $neqs equations for $nunknown unknowns. Initialization will default to using least squares. $(scc_message)To suppress this warning pass warn_initialize_determined = false. To make this warning into an error, pass fully_determined = true" end - parammap = parammap isa DiffEqBase.NullParameters || isempty(parammap) ? - [get_iv(sys) => t] : - merge(todict(parammap), Dict(get_iv(sys) => t)) - parammap = Dict(k => v for (k, v) in parammap if v !== missing) - if isempty(u0map) - u0map = Dict() + parammap = recursive_unwrap(anydict(parammap)) + if t !== nothing + parammap[get_iv(sys)] = t end + filter!(kvp -> kvp[2] !== missing, parammap) + + u0map = to_varmap(u0map, unknowns(sys)) if isempty(guesses) guesses = Dict() end From 671b93feea590a1a7756fd469f9426cc6105766d Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 6 Dec 2024 14:13:10 +0530 Subject: [PATCH 0502/1337] fix: filter kwargs in `SDEProblem` --- src/systems/diffeqs/sdesystem.jl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/systems/diffeqs/sdesystem.jl b/src/systems/diffeqs/sdesystem.jl index 5e0d0e3208..0d600cfb5f 100644 --- a/src/systems/diffeqs/sdesystem.jl +++ b/src/systems/diffeqs/sdesystem.jl @@ -760,6 +760,8 @@ function DiffEqBase.SDEProblem{iip, specialize}( noise = nothing end + kwargs = filter_kwargs(kwargs) + SDEProblem{iip}(f, u0, tspan, p; callback = cbs, noise, noise_rate_prototype = noise_rate_prototype, kwargs...) end From 9987da0b19e5e2c41aed9af214a215e7dc9b714a Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 6 Dec 2024 14:15:05 +0530 Subject: [PATCH 0503/1337] test: test initialization on `NonlinearProblem` and `NonlinearLeastSquaresProblem` --- test/initializationsystem.jl | 269 ++++++++++++++++++++++++++++------- 1 file changed, 218 insertions(+), 51 deletions(-) diff --git a/test/initializationsystem.jl b/test/initializationsystem.jl index 045b7d2d2c..2745b9d81f 100644 --- a/test/initializationsystem.jl +++ b/test/initializationsystem.jl @@ -583,6 +583,14 @@ sol = solve(oprob_2nd_order_2, Rosenbrock23()) # retcode: Success @test all(sol(1.0, idxs = sys.x) .≈ +exp(1)) && all(sol(1.0, idxs = sys.y) .≈ -exp(1)) end +NonlinearSystemWrapper(eqs, t; kws...) = NonlinearSystem(eqs; kws...) +function NonlinearProblemWrapper(sys, u0, tspan, args...; kwargs...) + NonlinearProblem(sys, u0, args...; kwargs...) +end +function NLLSProblemWrapper(sys, u0, tspan, args...; kwargs...) + NonlinearLeastSquaresProblem(sys, u0, args...; kwargs...) +end + @testset "Initialization of parameters" begin @variables _x(..) y(t) @parameters p q @@ -594,13 +602,37 @@ end # the correct solver. # `rhss` allows adding terms to the end of equations (only 2 equations allowed) to influence # the system type (brownian vars to turn it into an SDE). - @testset "$Problem" for (Problem, alg, rhss) in [ - (ODEProblem, Tsit5(), zeros(2)), (SDEProblem, ImplicitEM(), [a, b]), - (DDEProblem, MethodOfSteps(Tsit5()), [_x(t - 0.1), 0.0]), - (SDDEProblem, ImplicitEM(), [_x(t - 0.1) + a, b])] + @testset "$Problem with $(SciMLBase.parameterless_type(alg))" for (System, Problem, alg, rhss) in [ + (ModelingToolkit.System, ODEProblem, Tsit5(), zeros(2)), + (ModelingToolkit.System, SDEProblem, ImplicitEM(), [a, b]), + (ModelingToolkit.System, DDEProblem, MethodOfSteps(Tsit5()), [_x(t - 0.1), 0.0]), + (ModelingToolkit.System, SDDEProblem, ImplicitEM(), [_x(t - 0.1) + a, b]), + # polyalg cache + (NonlinearSystemWrapper, NonlinearProblemWrapper, + FastShortcutNonlinearPolyalg(), zeros(2)), + # generalized first order cache + (NonlinearSystemWrapper, NonlinearProblemWrapper, NewtonRaphson(), zeros(2)), + # quasi newton cache + (NonlinearSystemWrapper, NonlinearProblemWrapper, Klement(), zeros(2)), + # noinit cache + (NonlinearSystemWrapper, NonlinearProblemWrapper, SimpleNewtonRaphson(), zeros(2)), + # DFSane cache + (NonlinearSystemWrapper, NonlinearProblemWrapper, DFSane(), zeros(2)), + # Least squares + # polyalg cache + (NonlinearSystemWrapper, NLLSProblemWrapper, FastShortcutNLLSPolyalg(), zeros(2)), + # generalized first order cache + (NonlinearSystemWrapper, NLLSProblemWrapper, LevenbergMarquardt(), zeros(2)), + # noinit cache + (NonlinearSystemWrapper, NLLSProblemWrapper, SimpleGaussNewton(), zeros(2)) + ] + is_nlsolve = alg isa SciMLBase.AbstractNonlinearAlgorithm + function test_parameter(prob, sym, val, initialval = zero(val)) @test prob.ps[sym] ≈ initialval - @test init(prob, alg).ps[sym] ≈ val + if !is_nlsolve || prob.u0 !== nothing + @test init(prob, alg).ps[sym] ≈ val + end @test solve(prob, alg).ps[sym] ≈ val end function test_initializesystem(sys, u0map, pmap, p, equation) @@ -609,6 +641,8 @@ end @test is_variable(isys, p) @test equation in equations(isys) || (0 ~ -equation.rhs) in equations(isys) end + D = is_nlsolve ? v -> v^3 : Differential(t) + u0map = Dict(x => 1.0, y => 1.0) pmap = Dict() pmap[q] = 1.0 @@ -669,16 +703,14 @@ end prob = Problem(sys, u0map, (0.0, 1.0), _pmap) test_parameter(prob, p, _pmap[q]) test_initializesystem(sys, u0map, _pmap, p, 0 ~ q - p) - # Problem dependent value with guess, no `missing` @mtkbuild sys = System( - [D(x) ~ x * q + rhss[1], D(y) ~ y * p + rhss[2]], t; guesses = [p => 0.0]) + [D(x) ~ y * q + p + rhss[1], D(y) ~ x * p + q + rhss[2]], t; guesses = [p => 0.0]) _pmap = merge(pmap, Dict(p => 3q)) prob = Problem(sys, u0map, (0.0, 1.0), _pmap) test_parameter(prob, p, 3pmap[q]) # Should not be solved for: - # Override dependent default with direct value @mtkbuild sys = System( [D(x) ~ q * x + rhss[1], D(y) ~ y * p + rhss[2]], t; defaults = [p => 2q], guesses = [p => 1.0]) @@ -700,6 +732,14 @@ end [D(x) ~ x + rhss[1], p ~ x + y + rhss[2]], t; guesses = [p => 0.0]) @test_throws ModelingToolkit.MissingParametersError Problem( sys, [x => 1.0, y => 1.0], (0.0, 1.0)) + + # Unsatisfiable initialization + prob = Problem(sys, [x => 1.0, y => 1.0], (0.0, 1.0), + [p => 2.0]; initialization_eqs = [x^2 + y^2 ~ 3]) + @test prob.f.initialization_data !== nothing + @test solve(prob, alg).retcode == ReturnCode.InitialFailure + cache = init(prob, alg) + @test solve!(cache).retcode == ReturnCode.InitialFailure end @testset "Null system" begin @@ -707,7 +747,9 @@ end @parameters x0 y0 @mtkbuild sys = ODESystem([x ~ x0, y ~ y0, s ~ x + y], t; guesses = [y0 => 0.0]) prob = ODEProblem(sys, [s => 1.0], (0.0, 1.0), [x0 => 0.3, y0 => missing]) - test_parameter(prob, y0, 0.7) + @test prob.ps[y0] ≈ 0.0 + @test init(prob, Tsit5()).ps[y0] ≈ 0.7 + @test solve(prob, Tsit5()).ps[y0] ≈ 0.7 end using ModelingToolkitStandardLibrary.Mechanical.TranslationalModelica: Fixed, Mass, @@ -730,7 +772,9 @@ end systems = [fixed, spring, mass, gravity, constant, damper], guesses = [spring.s_rel0 => 1.0]) prob = ODEProblem(sys, [], (0.0, 1.0), [spring.s_rel0 => missing]) - test_parameter(prob, spring.s_rel0, -3.905) + @test prob.ps[spring.s_rel0] ≈ 0.0 + @test init(prob, Tsit5()).ps[spring.s_rel0] ≈ -3.905 + @test solve(prob, Tsit5()).ps[spring.s_rel0] ≈ -3.905 end @testset "Update initializeprob parameters" begin @@ -739,10 +783,33 @@ end @brownian a b x = _x(t) - @testset "$Problem" for (Problem, alg, rhss) in [ - (ODEProblem, Tsit5(), zeros(2)), (SDEProblem, ImplicitEM(), [a, b]), - (DDEProblem, MethodOfSteps(Tsit5()), [_x(t - 0.1), 0.0]), - (SDDEProblem, ImplicitEM(), [_x(t - 0.1) + a, b])] + @testset "$Problem with $(SciMLBase.parameterless_type(typeof(alg)))" for (System, Problem, alg, rhss) in [ + (ModelingToolkit.System, ODEProblem, Tsit5(), zeros(2)), + (ModelingToolkit.System, SDEProblem, ImplicitEM(), [a, b]), + (ModelingToolkit.System, DDEProblem, MethodOfSteps(Tsit5()), [_x(t - 0.1), 0.0]), + (ModelingToolkit.System, SDDEProblem, ImplicitEM(), [_x(t - 0.1) + a, b]), + # polyalg cache + (NonlinearSystemWrapper, NonlinearProblemWrapper, + FastShortcutNonlinearPolyalg(), zeros(2)), + # generalized first order cache + (NonlinearSystemWrapper, NonlinearProblemWrapper, NewtonRaphson(), zeros(2)), + # quasi newton cache + (NonlinearSystemWrapper, NonlinearProblemWrapper, Klement(), zeros(2)), + # noinit cache + (NonlinearSystemWrapper, NonlinearProblemWrapper, SimpleNewtonRaphson(), zeros(2)), + # DFSane cache + (NonlinearSystemWrapper, NonlinearProblemWrapper, DFSane(), zeros(2)), + # Least squares + # polyalg cache + (NonlinearSystemWrapper, NLLSProblemWrapper, FastShortcutNLLSPolyalg(), zeros(2)), + # generalized first order cache + (NonlinearSystemWrapper, NLLSProblemWrapper, LevenbergMarquardt(), zeros(2)), + # noinit cache + (NonlinearSystemWrapper, NLLSProblemWrapper, SimpleGaussNewton(), zeros(2)) + ] + is_nlsolve = alg isa SciMLBase.AbstractNonlinearAlgorithm + D = is_nlsolve ? v -> v^3 : Differential(t) + @mtkbuild sys = System( [D(x) ~ x + rhss[1], p ~ x + y + rhss[2]], t; guesses = [x => 0.0, p => 0.0]) prob = Problem(sys, [y => 1.0], (0.0, 1.0), [p => 3.0]) @@ -766,10 +833,33 @@ end @brownian a x = _x(t) - @testset "$Problem" for (Problem, alg, rhss) in [ - (ODEProblem, Tsit5(), 0), (SDEProblem, ImplicitEM(), a), - (DDEProblem, MethodOfSteps(Tsit5()), _x(t - 0.1)), - (SDDEProblem, ImplicitEM(), _x(t - 0.1) + a)] + @testset "$Problem with $(SciMLBase.parameterless_type(typeof(alg)))" for (System, Problem, alg, rhss) in [ + (ModelingToolkit.System, ODEProblem, Tsit5(), 0), + (ModelingToolkit.System, SDEProblem, ImplicitEM(), a), + (ModelingToolkit.System, DDEProblem, MethodOfSteps(Tsit5()), _x(t - 0.1)), + (ModelingToolkit.System, SDDEProblem, ImplicitEM(), _x(t - 0.1) + a), + # polyalg cache + (NonlinearSystemWrapper, NonlinearProblemWrapper, + FastShortcutNonlinearPolyalg(), 0), + # generalized first order cache + (NonlinearSystemWrapper, NonlinearProblemWrapper, NewtonRaphson(), 0), + # quasi newton cache + (NonlinearSystemWrapper, NonlinearProblemWrapper, Klement(), 0), + # noinit cache + (NonlinearSystemWrapper, NonlinearProblemWrapper, SimpleNewtonRaphson(), 0), + # DFSane cache + (NonlinearSystemWrapper, NonlinearProblemWrapper, DFSane(), 0), + # Least squares + # polyalg cache + (NonlinearSystemWrapper, NLLSProblemWrapper, FastShortcutNLLSPolyalg(), 0), + # generalized first order cache + (NonlinearSystemWrapper, NLLSProblemWrapper, LevenbergMarquardt(), 0), + # noinit cache + (NonlinearSystemWrapper, NLLSProblemWrapper, SimpleGaussNewton(), 0) + ] + is_nlsolve = alg isa SciMLBase.AbstractNonlinearAlgorithm + D = is_nlsolve ? v -> v^3 : Differential(t) + @mtkbuild sys = System( [D(x) ~ 2x + r + rhss], t; parameter_dependencies = [r ~ p + 2q, q ~ p + 3], guesses = [p => 1.0]) @@ -787,21 +877,44 @@ end @brownian a b x = _x(t) - @testset "$Problem" for (Problem, alg, rhss) in [ - (ODEProblem, Tsit5(), zeros(2)), (SDEProblem, ImplicitEM(), [a, b]), - (DDEProblem, MethodOfSteps(Tsit5()), [_x(t - 0.1), 0.0]), - (SDDEProblem, ImplicitEM(), [_x(t - 0.1) + a, b])] + @testset "$Problem with $(SciMLBase.parameterless_type(typeof(alg)))" for (System, Problem, alg, rhss) in [ + (ModelingToolkit.System, ODEProblem, Tsit5(), zeros(2)), + (ModelingToolkit.System, SDEProblem, ImplicitEM(), [a, b]), + (ModelingToolkit.System, DDEProblem, MethodOfSteps(Tsit5()), [_x(t - 0.1), 0.0]), + (ModelingToolkit.System, SDDEProblem, ImplicitEM(), [_x(t - 0.1) + a, b]), + # polyalg cache + (NonlinearSystemWrapper, NonlinearProblemWrapper, + FastShortcutNonlinearPolyalg(), zeros(2)), + # generalized first order cache + (NonlinearSystemWrapper, NonlinearProblemWrapper, NewtonRaphson(), zeros(2)), + # quasi newton cache + (NonlinearSystemWrapper, NonlinearProblemWrapper, Klement(), zeros(2)), + # noinit cache + (NonlinearSystemWrapper, NonlinearProblemWrapper, SimpleNewtonRaphson(), zeros(2)), + # DFSane cache + (NonlinearSystemWrapper, NonlinearProblemWrapper, DFSane(), zeros(2)), + # Least squares + # polyalg cache + (NonlinearSystemWrapper, NLLSProblemWrapper, FastShortcutNLLSPolyalg(), zeros(2)), + # generalized first order cache + (NonlinearSystemWrapper, NLLSProblemWrapper, LevenbergMarquardt(), zeros(2)), + # noinit cache + (NonlinearSystemWrapper, NLLSProblemWrapper, SimpleGaussNewton(), zeros(2)) + ] + is_nlsolve = alg isa SciMLBase.AbstractNonlinearAlgorithm + D = is_nlsolve ? v -> v^3 : Differential(t) + @mtkbuild sys = System( [D(x) ~ x + rhss[1], p ~ x + y + rhss[2]], t; defaults = [p => missing], guesses = [ x => 0.0, p => 0.0]) prob = Problem(sys, [x => 1.0, y => 1.0], (0.0, 1.0)) @test init(prob, alg).ps[p] ≈ 2.0 # nonsensical value for y just to test that equations work - prob2 = remake(prob; u0 = [x => 1.0, y => 2x + exp(t)]) - @test init(prob2, alg).ps[p] ≈ 4.0 + prob2 = remake(prob; u0 = [x => 1.0, y => 2x + exp(x)]) + @test init(prob2, alg).ps[p] ≈ 3 + exp(1) # solve for `x` given `p` and `y` - prob3 = remake(prob; u0 = [x => nothing, y => 1.0], p = [p => 2x + exp(t)]) - @test init(prob3, alg)[x] ≈ 0.0 + prob3 = remake(prob; u0 = [x => nothing, y => 1.0], p = [p => 2x + exp(y)]) + @test init(prob3, alg)[x] ≈ 1 - exp(1) @test_logs (:warn, r"overdetermined") remake( prob; u0 = [x => 1.0, y => 2.0], p = [p => 4.0]) prob4 = remake(prob; u0 = [x => 1.0, y => 2.0], p = [p => 4.0]) @@ -817,44 +930,73 @@ end @brownian a x = _x(t) - @testset "$Problem" for (Problem, alg, rhss) in [ - (ODEProblem, Tsit5(), 0), (SDEProblem, ImplicitEM(), a), - (DDEProblem, MethodOfSteps(Tsit5()), _x(t - 0.1)), - (SDDEProblem, ImplicitEM(), _x(t - 0.1) + a)] + @testset "$Problem with $(SciMLBase.parameterless_type(typeof(alg)))" for (System, Problem, alg, rhss) in [ + (ModelingToolkit.System, ODEProblem, Tsit5(), 0), + (ModelingToolkit.System, SDEProblem, ImplicitEM(), a), + (ModelingToolkit.System, DDEProblem, MethodOfSteps(Tsit5()), _x(t - 0.1)), + (ModelingToolkit.System, SDDEProblem, ImplicitEM(), _x(t - 0.1) + a), + # polyalg cache + (NonlinearSystemWrapper, NonlinearProblemWrapper, + FastShortcutNonlinearPolyalg(), 0), + # generalized first order cache + (NonlinearSystemWrapper, NonlinearProblemWrapper, NewtonRaphson(), 0), + # quasi newton cache + (NonlinearSystemWrapper, NonlinearProblemWrapper, Klement(), 0), + # noinit cache + (NonlinearSystemWrapper, NonlinearProblemWrapper, SimpleNewtonRaphson(), 0), + # DFSane cache + (NonlinearSystemWrapper, NonlinearProblemWrapper, DFSane(), 0), + # Least squares + # polyalg cache + (NonlinearSystemWrapper, NLLSProblemWrapper, FastShortcutNLLSPolyalg(), 0), + # generalized first order cache + (NonlinearSystemWrapper, NLLSProblemWrapper, LevenbergMarquardt(), 0), + # noinit cache + (NonlinearSystemWrapper, NLLSProblemWrapper, SimpleGaussNewton(), 0) + ] + is_nlsolve = alg isa SciMLBase.AbstractNonlinearAlgorithm + D = is_nlsolve ? v -> v^3 : Differential(t) + alge_eqs = [y^2 * q + q^2 * x ~ 0, z * p - p^2 * x * z ~ 0] + @mtkbuild sys = System( - [D(x) ~ x * p + y * q + rhss, y^2 * q + q^2 * x ~ 0, z * p - p^2 * x * z ~ 0], + [D(x) ~ x * p + y^2 * q + rhss; alge_eqs], t; guesses = [x => 0.0, y => 0.0, z => 0.0, p => 0.0, q => 0.0]) - prob = Problem(sys, [x => 1.0], (0.0, 1.0), [p => 1.0, q => missing]) + prob = Problem(sys, [x => 1.0], (0.0, 1.0), [p => 1.0, q => missing]; + initialization_eqs = is_nlsolve ? alge_eqs : []) @test is_variable(prob.f.initialization_data.initializeprob, q) ps = prob.p newps = SciMLStructures.replace(Tunable(), ps, ForwardDiff.Dual.(ps.tunable)) prob2 = remake(prob; p = newps) - @test eltype(prob2.f.initialization_data.initializeprob.u0) <: ForwardDiff.Dual + @test eltype(state_values(prob2.f.initialization_data.initializeprob)) <: + ForwardDiff.Dual @test eltype(prob2.f.initialization_data.initializeprob.p.tunable) <: ForwardDiff.Dual - @test prob2.f.initialization_data.initializeprob.u0 ≈ - prob.f.initialization_data.initializeprob.u0 + @test state_values(prob2.f.initialization_data.initializeprob) ≈ + state_values(prob.f.initialization_data.initializeprob) prob2 = remake(prob; u0 = ForwardDiff.Dual.(prob.u0)) - @test eltype(prob2.f.initialization_data.initializeprob.u0) <: ForwardDiff.Dual + @test eltype(state_values(prob2.f.initialization_data.initializeprob)) <: + ForwardDiff.Dual @test eltype(prob2.f.initialization_data.initializeprob.p.tunable) <: Float64 - @test prob2.f.initialization_data.initializeprob.u0 ≈ - prob.f.initialization_data.initializeprob.u0 + @test state_values(prob2.f.initialization_data.initializeprob) ≈ + state_values(prob.f.initialization_data.initializeprob) prob2 = remake(prob; u0 = ForwardDiff.Dual.(prob.u0), p = newps) - @test eltype(prob2.f.initialization_data.initializeprob.u0) <: ForwardDiff.Dual + @test eltype(state_values(prob2.f.initialization_data.initializeprob)) <: + ForwardDiff.Dual @test eltype(prob2.f.initialization_data.initializeprob.p.tunable) <: ForwardDiff.Dual - @test prob2.f.initialization_data.initializeprob.u0 ≈ - prob.f.initialization_data.initializeprob.u0 + @test state_values(prob2.f.initialization_data.initializeprob) ≈ + state_values(prob.f.initialization_data.initializeprob) prob2 = remake(prob; u0 = [x => ForwardDiff.Dual(1.0)], p = [p => ForwardDiff.Dual(1.0), q => missing]) - @test eltype(prob2.f.initialization_data.initializeprob.u0) <: ForwardDiff.Dual + @test eltype(state_values(prob2.f.initialization_data.initializeprob)) <: + ForwardDiff.Dual @test eltype(prob2.f.initialization_data.initializeprob.p.tunable) <: ForwardDiff.Dual - @test prob2.f.initialization_data.initializeprob.u0 ≈ - prob.f.initialization_data.initializeprob.u0 + @test state_values(prob2.f.initialization_data.initializeprob) ≈ + state_values(prob.f.initialization_data.initializeprob) end end @@ -864,19 +1006,44 @@ end @brownian a x = _x(t) - @testset "$Problem" for (Problem, alg, rhss) in [ - (ODEProblem, Tsit5(), 0), (SDEProblem, ImplicitEM(), a), - (DDEProblem, MethodOfSteps(Tsit5()), _x(t - 0.1)), - (SDDEProblem, ImplicitEM(), _x(t - 0.1) + a)] + @testset "$Problem with $(SciMLBase.parameterless_type(typeof(alg)))" for (System, Problem, alg, rhss) in [ + (ModelingToolkit.System, ODEProblem, Tsit5(), 0), + (ModelingToolkit.System, SDEProblem, ImplicitEM(), a), + (ModelingToolkit.System, DDEProblem, MethodOfSteps(Tsit5()), _x(t - 0.1)), + (ModelingToolkit.System, SDDEProblem, ImplicitEM(), _x(t - 0.1) + a), + # polyalg cache + (NonlinearSystemWrapper, NonlinearProblemWrapper, + FastShortcutNonlinearPolyalg(), 0), + # generalized first order cache + (NonlinearSystemWrapper, NonlinearProblemWrapper, NewtonRaphson(), 0), + # quasi newton cache + (NonlinearSystemWrapper, NonlinearProblemWrapper, Klement(), 0), + # noinit cache + (NonlinearSystemWrapper, NonlinearProblemWrapper, SimpleNewtonRaphson(), 0), + # DFSane cache + (NonlinearSystemWrapper, NonlinearProblemWrapper, DFSane(), 0), + # Least squares + # polyalg cache + (NonlinearSystemWrapper, NLLSProblemWrapper, FastShortcutNLLSPolyalg(), 0), + # generalized first order cache + (NonlinearSystemWrapper, NLLSProblemWrapper, LevenbergMarquardt(), 0), + # noinit cache + (NonlinearSystemWrapper, NLLSProblemWrapper, SimpleGaussNewton(), 0) + ] + is_nlsolve = alg isa SciMLBase.AbstractNonlinearAlgorithm + D = is_nlsolve ? v -> v^3 : Differential(t) + alge_eqs = [y^2 + 4y * p^2 ~ x^3] @mtkbuild sys = System( - [D(x) ~ x + p * y + rhss, y^2 + 4y * p^2 ~ x], t; guesses = [ + [D(x) ~ x + p * y^2 + rhss; alge_eqs], t; guesses = [ y => 1.0, p => 1.0]) - prob = Problem(sys, [x => 1.0], (0.0, 1.0), [p => 1.0]) + prob = Problem(sys, [x => 1.0], (0.0, 1.0), [p => 1.0]; + initialization_eqs = is_nlsolve ? alge_eqs : []) @test is_variable(prob.f.initialization_data.initializeprob, y) prob2 = @test_nowarn remake(prob; p = [p => 3.0]) # ensure no over/under-determined warning @test is_variable(prob.f.initialization_data.initializeprob, y) - prob = Problem(sys, [y => 1.0, x => 2.0], (0.0, 1.0), [p => missing]) + prob = Problem(sys, [y => 1.0, x => 2.0], (0.0, 1.0), [p => missing]; + initialization_eqs = is_nlsolve ? alge_eqs : []) @test is_variable(prob.f.initialization_data.initializeprob, p) prob2 = @test_nowarn remake(prob; u0 = [y => 0.5]) @test is_variable(prob.f.initialization_data.initializeprob, p) From 51eeeeb27ee5094424bd92eb589f8b99f843c592 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 6 Dec 2024 17:43:57 +0530 Subject: [PATCH 0504/1337] fix: store and propagate `initialization_eqs` provided to Problem --- src/systems/nonlinear/initializesystem.jl | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/systems/nonlinear/initializesystem.jl b/src/systems/nonlinear/initializesystem.jl index e32f967f4a..cf6c6d4d42 100644 --- a/src/systems/nonlinear/initializesystem.jl +++ b/src/systems/nonlinear/initializesystem.jl @@ -21,6 +21,7 @@ function generate_initializesystem(sys::AbstractSystem; eqs_ics = Equation[] defs = copy(defaults(sys)) # copy so we don't modify sys.defaults additional_guesses = anydict(guesses) + additional_initialization_eqs = Vector{Equation}(initialization_eqs) guesses = merge(get_guesses(sys), additional_guesses) idxs_diff = isdiffeq.(eqs) @@ -191,7 +192,7 @@ function generate_initializesystem(sys::AbstractSystem; defs[k] = substitute(defs[k], paramsubs) end meta = InitializationSystemMetadata( - anydict(u0map), anydict(pmap), additional_guesses, extra_metadata) + anydict(u0map), anydict(pmap), additional_guesses, additional_initialization_eqs, extra_metadata) return NonlinearSystem(eqs_ics, vars, pars; @@ -207,6 +208,7 @@ struct InitializationSystemMetadata u0map::Dict{Any, Any} pmap::Dict{Any, Any} additional_guesses::Dict{Any, Any} + additional_initialization_eqs::Vector{Equation} extra_metadata::NamedTuple end @@ -299,6 +301,7 @@ function SciMLBase.remake_initialization_data( defs = defaults(sys) cmap, cs = get_cmap(sys) use_scc = true + initialization_eqs = Equation[] if SciMLBase.has_initializeprob(odefn) oldsys = odefn.initialization_data.initializeprob.f.sys @@ -308,6 +311,7 @@ function SciMLBase.remake_initialization_data( pmap = merge(meta.pmap, pmap) merge!(guesses, meta.additional_guesses) use_scc = get(meta.extra_metadata, :use_scc, true) + initialization_eqs = meta.additional_initialization_eqs end else # there is no initializeprob, so the original problem construction @@ -348,7 +352,7 @@ function SciMLBase.remake_initialization_data( op, missing_unknowns, missing_pars = build_operating_point( u0map, pmap, defs, cmap, dvs, ps) kws = maybe_build_initialization_problem( - sys, op, u0map, pmap, t0, defs, guesses, missing_unknowns; use_scc) + sys, op, u0map, pmap, t0, defs, guesses, missing_unknowns; use_scc, initialization_eqs) return get(kws, :initialization_data, nothing) end From 96f8d5de87813b300b61e3d8359b7231ec661529 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Sat, 14 Dec 2024 13:18:38 +0530 Subject: [PATCH 0505/1337] build: bump compats --- Project.toml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Project.toml b/Project.toml index 3c6126b7c0..d672c007c2 100644 --- a/Project.toml +++ b/Project.toml @@ -89,6 +89,7 @@ ConstructionBase = "1" DataInterpolations = "6.4" DataStructures = "0.17, 0.18" DeepDiffs = "1" +DelayDiffEq = "5.50" DiffEqBase = "6.157" DiffEqCallbacks = "2.16, 3, 4" DiffEqNoiseProcess = "5" @@ -117,7 +118,7 @@ Libdl = "1" LinearAlgebra = "1" MLStyle = "0.4.17" NaNMath = "0.3, 1" -NonlinearSolve = "3.14, 4" +NonlinearSolve = "4.3" OffsetArrays = "1" OrderedCollections = "1" OrdinaryDiffEq = "6.82.0" @@ -129,7 +130,7 @@ RecursiveArrayTools = "3.26" Reexport = "0.2, 1" RuntimeGeneratedFunctions = "0.5.9" SCCNonlinearSolve = "1.0.0" -SciMLBase = "2.66" +SciMLBase = "2.68.1" SciMLStructures = "1.0" Serialization = "1" Setfield = "0.7, 0.8, 1" @@ -137,7 +138,9 @@ SimpleNonlinearSolve = "0.1.0, 1, 2" SparseArrays = "1" SpecialFunctions = "0.7, 0.8, 0.9, 0.10, 1.0, 2" StaticArrays = "0.10, 0.11, 0.12, 1.0" -SymbolicIndexingInterface = "0.3.35" +StochasticDiffEq = "6.72.1" +StochasticDelayDiffEq = "1.8.1" +SymbolicIndexingInterface = "0.3.36" SymbolicUtils = "3.7" Symbolics = "6.19" URIs = "1" From 0a881e71d5f1b5fe77e170713e78b5a5ab3ec023 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 16 Dec 2024 16:22:15 +0530 Subject: [PATCH 0506/1337] fix: better handle reconstructing initializeprob with new types --- src/systems/diffeqs/abstractodesystem.jl | 5 ++ src/systems/nonlinear/initializesystem.jl | 69 ++++++++++------------- 2 files changed, 34 insertions(+), 40 deletions(-) diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index a6607e0c55..dd4165cb57 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -1310,6 +1310,11 @@ function InitializationProblem{iip, specialize}(sys::AbstractSystem, pmap = parammap, guesses, extra_metadata = (; use_scc)); fully_determined) end + meta = get_metadata(isys) + if meta isa InitializationSystemMetadata + @set! isys.metadata.oop_reconstruct_u0_p = ReconstructInitializeprob(sys, isys) + end + ts = get_tearing_state(isys) unassigned_vars = StructuralTransformations.singular_check(ts) if warn_initialize_determined && !isempty(unassigned_vars) diff --git a/src/systems/nonlinear/initializesystem.jl b/src/systems/nonlinear/initializesystem.jl index cf6c6d4d42..602fc7cac9 100644 --- a/src/systems/nonlinear/initializesystem.jl +++ b/src/systems/nonlinear/initializesystem.jl @@ -192,7 +192,8 @@ function generate_initializesystem(sys::AbstractSystem; defs[k] = substitute(defs[k], paramsubs) end meta = InitializationSystemMetadata( - anydict(u0map), anydict(pmap), additional_guesses, additional_initialization_eqs, extra_metadata) + anydict(u0map), anydict(pmap), additional_guesses, + additional_initialization_eqs, extra_metadata, nothing) return NonlinearSystem(eqs_ics, vars, pars; @@ -204,12 +205,30 @@ function generate_initializesystem(sys::AbstractSystem; kwargs...) end +struct ReconstructInitializeprob + getter::Any + setter::Any +end + +function ReconstructInitializeprob(srcsys::AbstractSystem, dstsys::AbstractSystem) + syms = [unknowns(dstsys); + reduce(vcat, reorder_parameters(dstsys, parameters(dstsys)); init = [])] + getter = getu(srcsys, syms) + setter = setsym_oop(dstsys, syms) + return ReconstructInitializeprob(getter, setter) +end + +function (rip::ReconstructInitializeprob)(srcvalp, dstvalp) + rip.setter(dstvalp, rip.getter(srcvalp)) +end + struct InitializationSystemMetadata u0map::Dict{Any, Any} pmap::Dict{Any, Any} additional_guesses::Dict{Any, Any} additional_initialization_eqs::Vector{Equation} extra_metadata::NamedTuple + oop_reconstruct_u0_p::Union{Nothing, ReconstructInitializeprob} end function is_parameter_solvable(p, pmap, defs, guesses) @@ -239,45 +258,15 @@ function SciMLBase.remake_initialization_data( if !SciMLBase.has_sys(oldinitprob.f) || !(oldinitprob.f.sys isa NonlinearSystem) return oldinitdata end - pidxs = ParameterIndex[] - pvals = [] - u0idxs = Int[] - u0vals = [] - for sym in variable_symbols(oldinitprob) - if is_variable(sys, sym) || has_observed_with_lhs(sys, sym) - u0 !== missing || continue - idx = variable_index(oldinitprob, sym) - push!(u0idxs, idx) - push!(u0vals, eltype(u0)(state_values(oldinitprob, idx))) - else - p !== missing || continue - idx = variable_index(oldinitprob, sym) - push!(u0idxs, idx) - push!(u0vals, typeof(getp(sys, sym)(p))(state_values(oldinitprob, idx))) - end - end - if p !== missing - for sym in parameter_symbols(oldinitprob) - push!(pidxs, parameter_index(oldinitprob, sym)) - if is_time_dependent(sys) && isequal(sym, get_iv(sys)) - push!(pvals, t0) - else - push!(pvals, getp(sys, sym)(p)) - end - end - end - if isempty(u0idxs) - newu0 = state_values(oldinitprob) - else - newu0 = remake_buffer( - oldinitprob.f.sys, state_values(oldinitprob), u0idxs, u0vals) - end - if isempty(pidxs) - newp = parameter_values(oldinitprob) + oldinitsys = oldinitprob.f.sys + meta = get_metadata(oldinitsys) + if meta isa InitializationSystemMetadata && meta.oop_reconstruct_u0_p !== nothing + reconstruct_fn = meta.oop_reconstruct_u0_p else - newp = remake_buffer( - oldinitprob.f.sys, parameter_values(oldinitprob), pidxs, pvals) + reconstruct_fn = ReconstructInitializeprob(sys, oldinitsys) end + new_initu0, new_initp = reconstruct_fn( + ProblemState(; u = newu0, p = newp, t = t0), oldinitprob) if oldinitprob.f.resid_prototype === nothing newf = oldinitprob.f else @@ -285,9 +274,9 @@ function SciMLBase.remake_initialization_data( SciMLBase.isinplace(oldinitprob.f), SciMLBase.specialization(oldinitprob.f)}( oldinitprob.f; resid_prototype = calculate_resid_prototype( - length(oldinitprob.f.resid_prototype), newu0, newp)) + length(oldinitprob.f.resid_prototype), new_initu0, new_initp)) end - initprob = remake(oldinitprob; f = newf, u0 = newu0, p = newp) + initprob = remake(oldinitprob; f = newf, u0 = new_initu0, p = new_initp) return SciMLBase.OverrideInitData(initprob, oldinitdata.update_initializeprob!, oldinitdata.initializeprobmap, oldinitdata.initializeprobpmap) end From 2e07200e03af428b127814af85f48fdeccdaf574 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 16 Dec 2024 16:22:51 +0530 Subject: [PATCH 0507/1337] test: fix incorrect initial values in tests --- test/nonlinearsystem.jl | 2 +- test/reduction.jl | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/test/nonlinearsystem.jl b/test/nonlinearsystem.jl index f766ca0131..cbd95a50d3 100644 --- a/test/nonlinearsystem.jl +++ b/test/nonlinearsystem.jl @@ -293,7 +293,7 @@ sys = structural_simplify(ns; conservative = true) eqs = [0 ~ σ * (y - x) 0 ~ x * (ρ - z) - y 0 ~ x * y - β * z] - guesses = [x => 1.0, y => 0.0, z => 0.0] + guesses = [x => 1.0, z => 0.0] ps = [σ => 10.0, ρ => 26.0, β => 8 / 3] @mtkbuild ns = NonlinearSystem(eqs) diff --git a/test/reduction.jl b/test/reduction.jl index 6d7a05b99e..fa9029a652 100644 --- a/test/reduction.jl +++ b/test/reduction.jl @@ -158,9 +158,7 @@ eqs = [u1 ~ u2 reducedsys = structural_simplify(sys) @test length(observed(reducedsys)) == 2 -u0 = [u1 => 1 - u2 => 1 - u3 => 0.3] +u0 = [u2 => 1] pp = [2] nlprob = NonlinearProblem(reducedsys, u0, [p => pp[1]]) reducedsol = solve(nlprob, NewtonRaphson()) From 878122305e616c0d0b4a40df493d58deb15d8227 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 24 Dec 2024 15:33:10 +0530 Subject: [PATCH 0508/1337] fix: fix `flatten_equations` for higher-dimension array equations --- src/systems/diffeqs/abstractodesystem.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index f4e29346ff..8e7b2ac97e 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -1230,7 +1230,7 @@ function flatten_equations(eqs) error("LHS ($(eq.lhs)) and RHS ($(eq.rhs)) must either both be array expressions or both scalar") size(eq.lhs) == size(eq.rhs) || error("Size of LHS ($(eq.lhs)) and RHS ($(eq.rhs)) must match: got $(size(eq.lhs)) and $(size(eq.rhs))") - return collect(eq.lhs) .~ collect(eq.rhs) + return vec(collect(eq.lhs) .~ collect(eq.rhs)) else eq end From 994b2db06507c26826e4950d3f70e19b69d6e09c Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 24 Dec 2024 15:34:38 +0530 Subject: [PATCH 0509/1337] fix: fix `timeseries_parameter_index` for array symbolics --- src/systems/index_cache.jl | 4 +++- test/symbolic_indexing_interface.jl | 11 +++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/systems/index_cache.jl b/src/systems/index_cache.jl index 4fc2f18bfd..623623da42 100644 --- a/src/systems/index_cache.jl +++ b/src/systems/index_cache.jl @@ -404,6 +404,7 @@ function SymbolicIndexingInterface.timeseries_parameter_index(ic::IndexCache, sy sym = get(ic.symbol_to_variable, sym, nothing) sym === nothing && return nothing end + sym = unwrap(sym) idx = check_index_map(ic.discrete_idx, sym) idx === nothing || return ParameterTimeseriesIndex(idx.clock_idx, (idx.buffer_idx, idx.idx_in_clock)) @@ -411,7 +412,8 @@ function SymbolicIndexingInterface.timeseries_parameter_index(ic::IndexCache, sy args = arguments(sym) idx = timeseries_parameter_index(ic, args[1]) idx === nothing && return nothing - ParameterIndex(idx.portion, (idx.idx..., args[2:end]...), idx.validate_size) + return ParameterTimeseriesIndex( + idx.timeseries_idx, (idx.parameter_idx..., args[2:end]...)) end function check_index_map(idxmap, sym) diff --git a/test/symbolic_indexing_interface.jl b/test/symbolic_indexing_interface.jl index e46c197dde..4a7cd926b4 100644 --- a/test/symbolic_indexing_interface.jl +++ b/test/symbolic_indexing_interface.jl @@ -224,3 +224,14 @@ end end @test isempty(get_all_timeseries_indexes(sys, a)) 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)] + @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)) + @test timeseries_parameter_index(sys, p[1, 1]) === + ParameterTimeseriesIndex(1, (1, 1, 1, 1)) +end From 1835a56c04e138a2b4afd07bf3f25994bfb19e4c Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 24 Dec 2024 16:10:52 +0530 Subject: [PATCH 0510/1337] test: fix `IfLifting` test --- test/if_lifting.jl | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/if_lifting.jl b/test/if_lifting.jl index d702355506..b72b468bf1 100644 --- a/test/if_lifting.jl +++ b/test/if_lifting.jl @@ -21,7 +21,9 @@ using ModelingToolkit: t_nounits as t, D_nounits as D, IfLifting, no_if_lift @test operation(only(equations(ss2)).rhs) === ifelse discvar = only(parameters(ss2)) - prob2 = ODEProblem(ss2, [x => 0.0], (0.0, 5.0)) + prob1 = ODEProblem(ss1, [ss1.x => 0.0], (0.0, 5.0)) + sol1 = solve(prob1, Tsit5()) + prob2 = ODEProblem(ss2, [ss2.x => 0.0], (0.0, 5.0)) sol2 = solve(prob2, Tsit5()) @test count(isapprox(pi), sol2.t) == 2 @test any(isapprox(pi), sol2.discretes[1].t) From 127cf3c36831595d57e423ff729fe600b367bcea Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 2 Jan 2025 18:44:38 +0530 Subject: [PATCH 0511/1337] build: bump OrdinaryDiffEqDefault compat --- Project.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Project.toml b/Project.toml index d672c007c2..b3ffc46164 100644 --- a/Project.toml +++ b/Project.toml @@ -123,6 +123,7 @@ OffsetArrays = "1" OrderedCollections = "1" OrdinaryDiffEq = "6.82.0" OrdinaryDiffEqCore = "1.13.0" +OrdinaryDiffEqDefault = "1.2" OrdinaryDiffEqNonlinearSolve = "1.3.0" PrecompileTools = "1" REPL = "1" @@ -166,6 +167,7 @@ OptimizationMOI = "fd9f6733-72f4-499f-8506-86b2bdd0dea1" OptimizationOptimJL = "36348300-93cb-4f02-beb5-3c3902f8871e" OrdinaryDiffEq = "1dea7af3-3e70-54e6-95c3-0bf5283fa5ed" OrdinaryDiffEqCore = "bbf590c4-e513-4bbe-9b18-05decba2e5d8" +OrdinaryDiffEqDefault = "50262376-6c5a-4cf5-baba-aaf4f84d72d7" OrdinaryDiffEqNonlinearSolve = "127b3ac7-2247-4354-8eb6-78cf4e7c58e8" Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" REPL = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb" From da03e00bc5499d6b58417fbcae0c7c4229dc8227 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 2 Jan 2025 20:24:57 +0530 Subject: [PATCH 0512/1337] test: add `OrdinaryDiffEqDefault` to the test environment --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index b3ffc46164..3d14a2dc65 100644 --- a/Project.toml +++ b/Project.toml @@ -183,4 +183,4 @@ Sundials = "c3572dad-4567-51f8-b174-8c6c989267f4" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [targets] -test = ["AmplNLWriter", "BenchmarkTools", "ControlSystemsBase", "DataInterpolations", "DelayDiffEq", "NonlinearSolve", "ForwardDiff", "Ipopt", "Ipopt_jll", "ModelingToolkitStandardLibrary", "Optimization", "OptimizationOptimJL", "OptimizationMOI", "OrdinaryDiffEq", "OrdinaryDiffEqCore", "REPL", "Random", "ReferenceTests", "SafeTestsets", "StableRNGs", "Statistics", "SteadyStateDiffEq", "Test", "StochasticDiffEq", "Sundials", "StochasticDelayDiffEq", "Pkg", "JET", "OrdinaryDiffEqNonlinearSolve"] +test = ["AmplNLWriter", "BenchmarkTools", "ControlSystemsBase", "DataInterpolations", "DelayDiffEq", "NonlinearSolve", "ForwardDiff", "Ipopt", "Ipopt_jll", "ModelingToolkitStandardLibrary", "Optimization", "OptimizationOptimJL", "OptimizationMOI", "OrdinaryDiffEq", "OrdinaryDiffEqCore", "OrdinaryDiffEqDefault", "REPL", "Random", "ReferenceTests", "SafeTestsets", "StableRNGs", "Statistics", "SteadyStateDiffEq", "Test", "StochasticDiffEq", "Sundials", "StochasticDelayDiffEq", "Pkg", "JET", "OrdinaryDiffEqNonlinearSolve"] From 59a41b1581a86d407fcdb8489510af8807335cea Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Sun, 5 Jan 2025 22:41:36 +0530 Subject: [PATCH 0513/1337] build: bump minor version --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 3d14a2dc65..7b0997823c 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "ModelingToolkit" uuid = "961ee093-0014-501f-94e3-6117800e7a78" authors = ["Yingbo Ma ", "Chris Rackauckas and contributors"] -version = "9.59.0" +version = "9.60.0" [deps] AbstractTrees = "1520ce14-60c1-5f80-bbc7-55ef81b5835c" From 080af2c536d411d06bf498cdedcc1c498e3e86fe Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 6 Jan 2025 15:27:53 +0530 Subject: [PATCH 0514/1337] fix: propagate `checkbounds` information to observed function generation --- src/systems/abstractsystem.jl | 18 ++++++++++++------ src/systems/diffeqs/abstractodesystem.jl | 6 ++++-- src/systems/diffeqs/sdesystem.jl | 3 ++- src/systems/discrete_system/discrete_system.jl | 3 ++- src/systems/jumps/jumpsystem.jl | 6 ++++-- src/systems/nonlinear/nonlinearsystem.jl | 6 ++++-- src/systems/optimization/optimizationsystem.jl | 2 +- 7 files changed, 29 insertions(+), 15 deletions(-) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index 168260ae69..64b75e80d0 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -785,7 +785,7 @@ end SymbolicIndexingInterface.supports_tuple_observed(::AbstractSystem) = true function SymbolicIndexingInterface.observed( - sys::AbstractSystem, sym; eval_expression = false, eval_module = @__MODULE__) + sys::AbstractSystem, sym; eval_expression = false, eval_module = @__MODULE__, checkbounds = true) if has_index_cache(sys) && (ic = get_index_cache(sys)) !== nothing if sym isa Symbol _sym = get(ic.symbol_to_variable, sym, nothing) @@ -808,7 +808,8 @@ function SymbolicIndexingInterface.observed( end end end - _fn = build_explicit_observed_function(sys, sym; eval_expression, eval_module) + _fn = build_explicit_observed_function( + sys, sym; eval_expression, eval_module, checkbounds) if is_time_dependent(sys) return _fn @@ -1671,11 +1672,14 @@ struct ObservedFunctionCache{S} steady_state::Bool eval_expression::Bool eval_module::Module + checkbounds::Bool end function ObservedFunctionCache( - sys; steady_state = false, eval_expression = false, eval_module = @__MODULE__) - return ObservedFunctionCache(sys, Dict(), steady_state, eval_expression, eval_module) + sys; steady_state = false, eval_expression = false, + eval_module = @__MODULE__, checkbounds = true) + return ObservedFunctionCache( + sys, Dict(), steady_state, eval_expression, eval_module, checkbounds) end # This is hit because ensemble problems do a deepcopy @@ -1685,7 +1689,9 @@ function Base.deepcopy_internal(ofc::ObservedFunctionCache, stackdict::IdDict) steady_state = ofc.steady_state eval_expression = ofc.eval_expression eval_module = ofc.eval_module - newofc = ObservedFunctionCache(sys, dict, steady_state, eval_expression, eval_module) + checkbounds = ofc.checkbounds + newofc = ObservedFunctionCache( + sys, dict, steady_state, eval_expression, eval_module, checkbounds) stackdict[ofc] = newofc return newofc end @@ -1694,7 +1700,7 @@ function (ofc::ObservedFunctionCache)(obsvar, args...) obs = get!(ofc.dict, value(obsvar)) do SymbolicIndexingInterface.observed( ofc.sys, obsvar; eval_expression = ofc.eval_expression, - eval_module = ofc.eval_module) + eval_module = ofc.eval_module, checkbounds = ofc.checkbounds) end if ofc.steady_state obs = let fn = obs diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index 20293068a9..b012bc73d4 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -436,7 +436,8 @@ function DiffEqBase.ODEFunction{iip, specialize}(sys::AbstractODESystem, ArrayInterface.restructure(u0 .* u0', M) end - observedfun = ObservedFunctionCache(sys; steady_state, eval_expression, eval_module) + observedfun = ObservedFunctionCache( + sys; steady_state, eval_expression, eval_module, checkbounds) jac_prototype = if sparse uElType = u0 === nothing ? Float64 : eltype(u0) @@ -522,7 +523,8 @@ function DiffEqBase.DAEFunction{iip}(sys::AbstractODESystem, dvs = unknowns(sys) _jac = nothing end - observedfun = ObservedFunctionCache(sys; eval_expression, eval_module) + observedfun = ObservedFunctionCache( + sys; eval_expression, eval_module, checkbounds = get(kwargs, :checkbounds, false)) jac_prototype = if sparse uElType = u0 === nothing ? Float64 : eltype(u0) diff --git a/src/systems/diffeqs/sdesystem.jl b/src/systems/diffeqs/sdesystem.jl index 0d600cfb5f..366e8a6d09 100644 --- a/src/systems/diffeqs/sdesystem.jl +++ b/src/systems/diffeqs/sdesystem.jl @@ -613,7 +613,8 @@ function DiffEqBase.SDEFunction{iip, specialize}(sys::SDESystem, dvs = unknowns( M = calculate_massmatrix(sys) _M = (u0 === nothing || M == I) ? M : ArrayInterface.restructure(u0 .* u0', M) - observedfun = ObservedFunctionCache(sys; eval_expression, eval_module) + observedfun = ObservedFunctionCache( + sys; eval_expression, eval_module, checkbounds = get(kwargs, :checkbounds, false)) SDEFunction{iip, specialize}(f, g; sys = sys, diff --git a/src/systems/discrete_system/discrete_system.jl b/src/systems/discrete_system/discrete_system.jl index 7458237333..bd5c72eec7 100644 --- a/src/systems/discrete_system/discrete_system.jl +++ b/src/systems/discrete_system/discrete_system.jl @@ -353,7 +353,8 @@ function SciMLBase.DiscreteFunction{iip, specialize}( f = SciMLBase.wrapfun_iip(f, (u0, u0, p, t)) end - observedfun = ObservedFunctionCache(sys) + observedfun = ObservedFunctionCache( + sys; eval_expression, eval_module, checkbounds = get(kwargs, :checkbounds, false)) DiscreteFunction{iip, specialize}(f; sys = sys, diff --git a/src/systems/jumps/jumpsystem.jl b/src/systems/jumps/jumpsystem.jl index efc5a9be7d..5c0a61771a 100644 --- a/src/systems/jumps/jumpsystem.jl +++ b/src/systems/jumps/jumpsystem.jl @@ -429,7 +429,8 @@ function DiffEqBase.DiscreteProblem(sys::JumpSystem, u0map, tspan::Union{Tuple, t = tspan === nothing ? nothing : tspan[1], use_union, tofloat = false, check_length = false) f = DiffEqBase.DISCRETE_INPLACE_DEFAULT - observedfun = ObservedFunctionCache(sys; eval_expression, eval_module) + observedfun = ObservedFunctionCache( + sys; eval_expression, eval_module, checkbounds = get(kwargs, :checkbounds, false)) df = DiscreteFunction{true, true}(f; sys = sys, observed = observedfun) DiscreteProblem(df, u0, tspan, p; kwargs...) @@ -527,7 +528,8 @@ function DiffEqBase.ODEProblem(sys::JumpSystem, u0map, tspan::Union{Tuple, Nothi t = tspan === nothing ? nothing : tspan[1], use_union, tofloat = false, check_length = false) f = (du, u, p, t) -> (du .= 0; nothing) - observedfun = ObservedFunctionCache(sys; eval_expression, eval_module) + observedfun = ObservedFunctionCache(sys; eval_expression, eval_module, + checkbounds = get(kwargs, :checkbounds, false)) df = ODEFunction(f; sys, observed = observedfun) return ODEProblem(df, u0, tspan, p; kwargs...) end diff --git a/src/systems/nonlinear/nonlinearsystem.jl b/src/systems/nonlinear/nonlinearsystem.jl index 8cd8175668..9ed830fc62 100644 --- a/src/systems/nonlinear/nonlinearsystem.jl +++ b/src/systems/nonlinear/nonlinearsystem.jl @@ -369,7 +369,8 @@ function SciMLBase.NonlinearFunction{iip}(sys::NonlinearSystem, dvs = unknowns(s _jac = nothing end - observedfun = ObservedFunctionCache(sys; eval_expression, eval_module) + observedfun = ObservedFunctionCache( + sys; eval_expression, eval_module, checkbounds = get(kwargs, :checkbounds, false)) if length(dvs) == length(equations(sys)) resid_prototype = nothing @@ -411,7 +412,8 @@ function SciMLBase.IntervalNonlinearFunction( f(u, p) = f_oop(u, p) f(u, p::MTKParameters) = f_oop(u, p...) - observedfun = ObservedFunctionCache(sys; eval_expression, eval_module) + observedfun = ObservedFunctionCache( + sys; eval_expression, eval_module, checkbounds = get(kwargs, :checkbounds, false)) IntervalNonlinearFunction{false}( f; observed = observedfun, sys = sys, initialization_data) diff --git a/src/systems/optimization/optimizationsystem.jl b/src/systems/optimization/optimizationsystem.jl index 0b20fdef79..860e063e35 100644 --- a/src/systems/optimization/optimizationsystem.jl +++ b/src/systems/optimization/optimizationsystem.jl @@ -420,7 +420,7 @@ function DiffEqBase.OptimizationProblem{iip}(sys::OptimizationSystem, u0map, hess_prototype = nothing end - observedfun = ObservedFunctionCache(sys; eval_expression, eval_module) + observedfun = ObservedFunctionCache(sys; eval_expression, eval_module, checkbounds) if length(cstr) > 0 @named cons_sys = ConstraintsSystem(cstr, dvs, ps; checks) From f9c833730b378ec8478a12a15d609b3e4a12af41 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 6 Jan 2025 15:29:17 +0530 Subject: [PATCH 0515/1337] build: bump Symbolics compat --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 7b0997823c..d1bde6371f 100644 --- a/Project.toml +++ b/Project.toml @@ -143,7 +143,7 @@ StochasticDiffEq = "6.72.1" StochasticDelayDiffEq = "1.8.1" SymbolicIndexingInterface = "0.3.36" SymbolicUtils = "3.7" -Symbolics = "6.19" +Symbolics = "6.22.1" URIs = "1" UnPack = "0.1, 1.0" Unitful = "1.1" From bc2c1287c24fd80ce12aa5068c6924c3395665c7 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 19 Dec 2024 13:37:32 +0530 Subject: [PATCH 0516/1337] feat: add analysis points --- src/ModelingToolkit.jl | 4 +- src/systems/analysis_points.jl | 649 +++++++++++++++++++++++++++++++++ src/systems/connectors.jl | 1 + 3 files changed, 653 insertions(+), 1 deletion(-) create mode 100644 src/systems/analysis_points.jl diff --git a/src/ModelingToolkit.jl b/src/ModelingToolkit.jl index 10aba4d8a9..486ef63460 100644 --- a/src/ModelingToolkit.jl +++ b/src/ModelingToolkit.jl @@ -145,6 +145,7 @@ include("systems/index_cache.jl") include("systems/parameter_buffer.jl") include("systems/abstractsystem.jl") include("systems/model_parsing.jl") +include("systems/analysis_points.jl") include("systems/connectors.jl") include("systems/imperative_affect.jl") include("systems/callbacks.jl") @@ -238,7 +239,8 @@ export SteadyStateProblem, SteadyStateProblemExpr export JumpProblem export NonlinearSystem, OptimizationSystem, ConstraintsSystem export alias_elimination, flatten -export connect, domain_connect, @connector, Connection, Flow, Stream, instream +export connect, domain_connect, @connector, Connection, AnalysisPoint, Flow, Stream, + instream export initial_state, transition, activeState, entry, ticksInState, timeInState export @component, @mtkmodel, @mtkbuild export isinput, isoutput, getbounds, hasbounds, getguess, hasguess, isdisturbance, diff --git a/src/systems/analysis_points.jl b/src/systems/analysis_points.jl new file mode 100644 index 0000000000..605eb32749 --- /dev/null +++ b/src/systems/analysis_points.jl @@ -0,0 +1,649 @@ +""" + $(TYPEDEF) + $(TYPEDSIGNATURES) + +Create an AnalysisPoint for linear analysis. Analysis points can be created by calling + +``` +connect(in, :ap_name, out...) +``` + +Where `in` is the input to the connection, and `out...` are the outputs. In the context of +ModelingToolkitStandardLibrary.jl, `in` is a `RealOutput` connector and `out...` are all +`RealInput` connectors. All involved connectors are required to either have an unknown named +`u` or a single unknown, all of which should have the same size. +""" +struct AnalysisPoint + input::Any + name::Symbol + outputs::Union{Nothing, Vector{Any}} +end + +AnalysisPoint() = AnalysisPoint(nothing, Symbol(), nothing) +AnalysisPoint(name::Symbol) = AnalysisPoint(nothing, name, nothing) + +Base.nameof(ap::AnalysisPoint) = ap.name + +Base.show(io::IO, ap::AnalysisPoint) = show(io, MIME"text/plain"(), ap) +function Base.show(io::IO, ::MIME"text/plain", ap::AnalysisPoint) + if ap.input === nothing + print(io, "0") + return + end + if get(io, :compact, false) + print(io, + "AnalysisPoint($(ap_var(ap.input)), $(ap_var.(ap.outputs)); name=$(ap.name))") + else + print(io, "AnalysisPoint(") + printstyled(io, ap.name, color = :cyan) + if ap.input !== nothing && ap.outputs !== nothing + print(io, " from ") + printstyled(io, ap_var(ap.input), color = :green) + print(io, " to ") + if length(ap.outputs) == 1 + printstyled(io, ap_var(ap.outputs[1]), color = :blue) + else + printstyled(io, "[", join(ap_var.(ap.outputs), ", "), "]", color = :blue) + end + end + print(io, ")") + end +end + +""" + $(TYPEDSIGNATURES) + +Convert an `AnalysisPoint` to a standard connection. +""" +function to_connection(ap::AnalysisPoint) + return connect(ap.input, ap.outputs...) +end + +""" + $(TYPEDSIGNATURES) + +Namespace an `AnalysisPoint` by namespacing the involved systems and the name of the point. +""" +function renamespace(sys, ap::AnalysisPoint) + return AnalysisPoint( + ap.input === nothing ? nothing : renamespace(sys, ap.input), + renamespace(sys, ap.name), + ap.outputs === nothing ? nothing : map(Base.Fix1(renamespace, sys), ap.outputs) + ) +end + +# create analysis points via `connect` +function Symbolics.connect(in, ap::AnalysisPoint, outs...) + return AnalysisPoint() ~ AnalysisPoint(in, ap.name, collect(outs)) +end + +""" + $(TYPEDSIGNATURES) + +Create an `AnalysisPoint` connection connecting `in` to `outs...`. +""" +function Symbolics.connect(in::AbstractSystem, name::Symbol, out, outs...) + return AnalysisPoint() ~ AnalysisPoint(in, name, [out; collect(outs)]) +end + +""" + $(TYPEDSIGNATURES) + +Return all the namespaces in `name`. Namespaces should be separated by `.` or +`$NAMESPACE_SEPARATOR`. +""" +namespace_hierarchy(name::Symbol) = map( + Symbol, split(string(name), ('.', NAMESPACE_SEPARATOR))) + +""" + $(TYPEDSIGNATURES) + +Remove all `AnalysisPoint`s in `sys` and any of its subsystems, replacing them by equivalent connections. +""" +function remove_analysis_points(sys::AbstractSystem) + eqs = map(get_eqs(sys)) do eq + eq.lhs isa AnalysisPoint ? to_connection(eq.rhs) : eq + end + @set! sys.eqs = eqs + @set! sys.systems = map(remove_analysis_points, get_systems(sys)) + + return sys +end + +""" + $(TYPEDSIGNATURES) + +Given a system involved in an `AnalysisPoint`, get the variable to be used in the +connection. This is the variable named `u` if present, and otherwise the only +variable in the system. If the system does not have a variable named `u` and +contains multiple variables, throw an error. +""" +function ap_var(sys) + if hasproperty(sys, :u) + return sys.u + end + x = unknowns(sys) + length(x) == 1 && return renamespace(sys, x[1]) + error("Could not determine the analysis-point variable in system $(nameof(sys)). To use an analysis point, apply it to a connection between causal blocks which have a variable named `u` or a single unknown of the same size.") +end + +""" + $(TYPEDEF) + +The supertype of all transformations that can be applied to an `AnalysisPoint`. All +concrete subtypes must implement `apply_transformation`. +""" +abstract type AnalysisPointTransformation end + +""" + apply_transformation(tf::AnalysisPointTransformation, sys::AbstractSystem) + +Apply the given analysis point transformation `tf` to the system `sys`. Throw an error if +any analysis points referred to in `tf` are not present in `sys`. Return a tuple +containing the modified system as the first element, and a tuple of the additional +variables added by the transformation as the second element. +""" +function apply_transformation end + +""" + $(TYPEDSIGNATURES) + +Given a namespaced subsystem `target` of root system `root`, return a modified copy of +`root` with `target` modified according to `fn` alongside any extra variables added +by `fn`. + +`fn` is a function which takes the instance of `target` present in the hierarchy of +`root`, and returns a 2-tuple consisting of the modified version of `target` and a tuple +of the extra variables added. +""" +modify_nested_subsystem(fn, root::AbstractSystem, target::AbstractSystem) = modify_nested_subsystem( + fn, root, nameof(target)) +""" + $(TYPEDSIGNATURES) + +Apply the modification to the system containing the namespaced analysis point `target`. +""" +modify_nested_subsystem(fn, root::AbstractSystem, target::AnalysisPoint) = modify_nested_subsystem( + fn, root, @view namespace_hierarchy(nameof(target))[1:(end - 1)]) +""" + $(TYPEDSIGNATURES) + +Apply the modification to the nested subsystem of `root` whose namespaced name matches +the provided name `target`. The namespace separator in `target` should be `.` or +`$NAMESPACE_SEPARATOR`. The `target` may include `nameof(root)` as the first namespace. +""" +modify_nested_subsystem(fn, root::AbstractSystem, target::Symbol) = modify_nested_subsystem( + fn, root, namespace_hierarchy(target)) + +""" + $(TYPEDSIGNATURES) + +Apply the modification to the nested subsystem of `root` where the name of the subsystem at +each level in the hierarchy is given by elements of `hierarchy`. For example, if +`hierarchy = [:a, :b, :c]`, the system being searched for will be `root.a.b.c`. Note that +the hierarchy may include the name of the root system, in which the first element will be +ignored. For example, `hierarchy = [:root, :a, :b, :c]` also searches for `root.a.b.c`. +An empty `hierarchy` will apply the modification to `root`. +""" +function modify_nested_subsystem( + fn, root::AbstractSystem, hierarchy::AbstractVector{Symbol}) + # no hierarchy, so just apply to the root + if isempty(hierarchy) + return fn(root) + end + # ignore the name of the root + if nameof(root) == hierarchy[1] + hierarchy = @view hierarchy[2:end] + end + + # recursive helper function which does the searching and modification + function _helper(sys::AbstractSystem, i::Int) + # we reached past the end, so everything matched and + # `sys` is the system to modify. + if i > length(hierarchy) + sys, vars = fn(sys) + else + cur = hierarchy[i] + idx = findfirst(subsys -> nameof(subsys) == cur, get_systems(sys)) + idx === nothing && + error("System $(join([nameof(root); hierarchy[1:i-1]], '.')) does not have a subsystem named $cur.") + + newsys, vars = _helper(get_systems(sys)[idx], i + 1) + @set! sys.systems[idx] = newsys + end + if i != 1 + vars = ntuple(Val(length(vars))) do i + renamespace(sys, vars[i]) + end + end + return sys, vars + end + + return _helper(root, 1) +end + +""" + $(TYPEDSIGNATURES) + +Given a system `sys` and analysis point `ap`, return the index in `get_eqs(sys)` +containing an equation which has as it's RHS an analysis point with name `nameof(ap)`. +""" +analysis_point_index(sys::AbstractSystem, ap::AnalysisPoint) = analysis_point_index( + sys, nameof(ap)) +""" + $(TYPEDSIGNATURES) + +Search for the analysis point with the given `name` in `get_eqs(sys)`. +""" +function analysis_point_index(sys::AbstractSystem, name::Symbol) + name = namespace_hierarchy(name)[end] + findfirst(get_eqs(sys)) do eq + eq.lhs isa AnalysisPoint && nameof(eq.rhs) == name + end +end + +""" + $(TYPEDSIGNATURES) + +Create a new variable of the same `symtype` and size as `var`, using `name` as the base +name for the new variable. `iv` denotes the independent variable of the system. Prefix +`d_` to the name of the new variable if `perturb == true`. Return the new symbolic +variable and the appropriate zero value for it. +""" +function get_analysis_variable(var, name, iv; perturb = true) + var = unwrap(var) + if perturb + name = Symbol(:d_, name) + end + if Symbolics.isarraysymbolic(var) + T = Array{eltype(symtype(var)), ndims(var)} + pvar = unwrap(only(@variables $name(iv)::T)) + pvar = setmetadata(pvar, Symbolics.ArrayShapeCtx, Symbolics.shape(var)) + default = zeros(symtype(var), size(var)) + else + T = symtype(var) + pvar = unwrap(only(@variables $name(iv)::T)) + default = zero(T) + end + return pvar, default +end + +#### PRIMITIVE TRANSFORMATIONS + +""" + $(TYPEDEF) + +A transformation which breaks the connection referred to by `ap`. If `add_input == true`, +it will add a new input variable which connects to the outputs of the analysis point. +`apply_transformation` returns the new input variable (if added) as the auxiliary +information. The new input variable will have the name `Symbol(:d_, nameof(ap))`. + +Note that this transformation will remove `ap`, causing any subsequent transformations +referring to it to fail. + +The added variable, if present, will have a default of zero, of the appropriate type and +size. + +## Fields + +$(TYPEDFIELDS) +""" +struct Break <: AnalysisPointTransformation + """ + The analysis point to break. + """ + ap::AnalysisPoint + """ + Whether to add a new input variable connected to all the outputs of `ap`. + """ + add_input::Bool +end + +""" + $(TYPEDSIGNATURES) + +`Break` the given analysis point `ap` without adding an input. +""" +Break(ap::AnalysisPoint) = Break(ap, false) + +function apply_transformation(tf::Break, sys::AbstractSystem) + modify_nested_subsystem(sys, tf.ap) do breaksys + # get analysis point + ap_idx = analysis_point_index(breaksys, tf.ap) + ap_idx === nothing && + error("Analysis point $(nameof(tf.ap)) not found in system $(nameof(sys)).") + breaksys_eqs = copy(get_eqs(breaksys)) + @set! breaksys.eqs = breaksys_eqs + + ap = breaksys_eqs[ap_idx].rhs + deleteat!(breaksys_eqs, ap_idx) + + tf.add_input || return sys, () + + ap_ivar = ap_var(ap.input) + new_var, new_def = get_analysis_variable(ap_ivar, nameof(ap), get_iv(sys)) + for outsys in ap.outputs + push!(breaksys_eqs, ap_var(outsys) ~ new_var) + end + defs = copy(get_defaults(breaksys)) + defs[new_var] = new_def + @set! breaksys.defaults = defs + unks = copy(get_unknowns(breaksys)) + push!(unks, new_var) + @set! breaksys.unknowns = unks + + return breaksys, (new_var,) + end +end + +""" + $(TYPEDEF) + +A transformation which returns the variable corresponding to the input of the analysis +point. + +`apply_transformation` returns the variable as auxiliary information. + +## Fields + +$(TYPEDFIELDS) +""" +struct GetInput <: AnalysisPointTransformation + """ + The analysis point to add the output to. + """ + ap::AnalysisPoint +end + +function apply_transformation(tf::GetInput, sys::AbstractSystem) + modify_nested_subsystem(sys, tf.ap) do ap_sys + # get the analysis point + ap_idx = analysis_point_index(ap_sys, tf.ap) + ap_idx === nothing && + error("Analysis point $(nameof(tf.ap)) not found in system $(nameof(sys)).") + # get the anlysis point + ap_sys_eqs = copy(get_eqs(ap_sys)) + ap = ap_sys_eqs[ap_idx].rhs + + # input variable + ap_ivar = ap_var(ap.input) + return ap_sys, (ap_ivar,) + end +end + +""" + $(TYPEDEF) + +A transformation that creates a new input variable which is added to the input of +the analysis point before connecting to the outputs. The new variable will have the name +`Symbol(:d_, nameof(ap))`. + +If `with_output == true`, also creates an additional new variable which has the value +provided to the outputs after the above modification. This new variable has the same name +as the analysis point. + +`apply_transformation` returns a 1-tuple of the perturbation variable if +`with_output == false` and a 2-tuple of the perturbation variable and output variable if +`with_output == true`. + +Removes the analysis point `ap`, so any subsequent transformations requiring it will fail. + +The added variable(s) will have a default of zero, of the appropriate type and size. + +## Fields + +$(TYPEDFIELDS) +""" +struct PerturbOutput <: AnalysisPointTransformation + """ + The analysis point to modify + """ + ap::AnalysisPoint + """ + Whether to add an additional output variable. + """ + with_output::Bool +end + +""" + $(TYPEDSIGNATURES) + +Add an input without an additional output variable. +""" +PerturbOutput(ap::AnalysisPoint) = PerturbOutput(ap, false) + +function apply_transformation(tf::PerturbOutput, sys::AbstractSystem) + modify_nested_subsystem(sys, tf.ap) do ap_sys + # get analysis point + ap_idx = analysis_point_index(ap_sys, tf.ap) + ap_idx === nothing && + error("Analysis point $(nameof(tf.ap)) not found in system $(nameof(sys)).") + # modified quations + ap_sys_eqs = copy(get_eqs(ap_sys)) + @set! ap_sys.eqs = ap_sys_eqs + ap = ap_sys_eqs[ap_idx].rhs + # remove analysis point + deleteat!(ap_sys_eqs, ap_idx) + + # add equations involving new variable + ap_ivar = ap_var(ap.input) + new_var, new_def = get_analysis_variable(ap_ivar, nameof(ap), get_iv(sys)) + for outsys in ap.outputs + push!(ap_sys_eqs, ap_var(outsys) ~ ap_ivar + new_var) + end + # add variable + unks = copy(get_unknowns(ap_sys)) + push!(unks, new_var) + @set! ap_sys.unknowns = unks + # add default + defs = copy(get_defaults(ap_sys)) + defs[new_var] = new_def + @set! ap_sys.defaults = defs + + tf.with_output || return ap_sys, (new_var,) + + # add output variable, equation, default + out_var, out_def = get_analysis_variable( + ap_ivar, nameof(ap), get_iv(sys); perturb = false) + defs[out_var] = out_def + push!(ap_sys_eqs, out_var ~ ap_ivar + new_var) + push!(unks, out_var) + + return ap_sys, (new_var, out_var) + end +end + +""" + $(TYPEDSIGNATURES) + +A transformation which adds a variable named `name` to the system containing the analysis +point `ap`. The added variable has the same type and size as the input of the analysis +point. +""" +struct AddVariable <: AnalysisPointTransformation + ap::AnalysisPoint + name::Symbol +end + +AddVariable(ap::AnalysisPoint) = AddVariable(ap, nameof(ap)) + +function apply_transformation(tf::AddVariable, sys::AbstractSystem) + modify_nested_subsystem(sys, tf.ap) do ap_sys + # get analysis point + ap_idx = analysis_point_index(ap_sys, tf.ap) + ap_idx === nothing && + error("Analysis point $(nameof(tf.ap)) not found in system $(nameof(sys)).") + ap_sys_eqs = copy(get_eqs(ap_sys)) + ap = ap_sys_eqs[ap_idx].rhs + + # add equations involving new variable + ap_ivar = ap_var(ap.input) + new_var, new_def = get_analysis_variable( + ap_ivar, tf.name, get_iv(sys); perturb = false) + # add variable + unks = copy(get_unknowns(ap_sys)) + push!(unks, new_var) + @set! ap_sys.unknowns = unks + return ap_sys, (new_var,) + end +end + +#### DERIVED TRANSFORMATIONS + +""" + $(TYPEDSIGNATURES) + +A transformation enable calculating the sensitivity function about the analysis point `ap`. +`apply_transformation` returns a 2-tuple `du, u` as auxiliary information. + +Removes the analysis point `ap`, so any subsequent transformations requiring it will fail. + +The added variables will have a default of zero, of the appropriate type and size. +""" +SensitivityTransform(ap::AnalysisPoint) = PerturbOutput(ap, true) + +""" + $(TYPEDEF) + +A transformation to enable calculating the complementary sensitivity function about the +analysis point `ap`. `apply_transformation` returns a 2-tuple `du, u` as auxiliary +information. + +Removes the analysis point `ap`, so any subsequent transformations requiring it will fail. + +The added variables will have a default of zero, of the appropriate type and size. +""" +struct ComplementarySensitivityTransform <: AnalysisPointTransformation + ap::AnalysisPoint +end + +function apply_transformation(cst::ComplementarySensitivityTransform, sys::AbstractSystem) + sys, (u,) = apply_transformation(GetInput(cst.ap), sys) + sys, (du,) = apply_transformation(AddVariable(cst.ap, Symbol(:comp_sens_du)), sys) + sys, (_du,) = apply_transformation(PerturbOutput(cst.ap), sys) + + # `PerturbOutput` adds the equation `input + _du ~ output` + # but comp sensitivity wants `output + du ~ input`. Thus, `du ~ -_du`. + eqs = copy(get_eqs(sys)) + @set! sys.eqs = eqs + push!(eqs, du ~ -_du) + + defs = copy(get_defaults(sys)) + @set! sys.defaults = defs + defs[du] = -_du + return sys, (du, u) +end + +struct LoopTransferTransform <: AnalysisPointTransformation + ap::AnalysisPoint +end + +function apply_transformation(tf::LoopTransferTransform, sys::AbstractSystem) + sys, (u,) = apply_transformation(GetInput(tf.ap), sys) + sys, (du,) = apply_transformation(Break(tf.ap, true), sys) + return sys, (du, u) +end + +### TODO: Move these + +canonicalize_ap(ap::Symbol) = [AnalysisPoint(ap)] +canonicalize_ap(ap::AnalysisPoint) = [ap] +canonicalize_ap(ap) = [ap] +function canonicalize_ap(aps::Vector) + mapreduce(canonicalize_ap, vcat, aps; init = []) +end + +function handle_loop_openings(sys::AbstractSystem, aps) + for ap in canonicalize_ap(aps) + sys, (outvar,) = apply_transformation(Break(ap, true), sys) + if Symbolics.isarraysymbolic(outvar) + push!(get_eqs(sys), outvar ~ zeros(size(outvar))) + else + push!(get_eqs(sys), outvar ~ 0) + end + end + return sys +end + +function get_sensitivity_function( + sys::AbstractSystem, aps; system_modifier = identity, loop_openings = [], kwargs...) + sys = handle_loop_openings(sys, loop_openings) + aps = canonicalize_ap(aps) + dus = [] + us = [] + for ap in aps + sys, (du, u) = apply_transformation(SensitivityTransform(ap), sys) + push!(dus, du) + push!(us, u) + end + linearization_function(system_modifier(sys), dus, us; kwargs...) +end + +function get_comp_sensitivity_function( + sys::AbstractSystem, aps; system_modifier = identity, loop_openings = [], kwargs...) + sys = handle_loop_openings(sys, loop_openings) + aps = canonicalize_ap(aps) + dus = [] + us = [] + for ap in aps + sys, (du, u) = apply_transformation(ComplementarySensitivityTransform(ap), sys) + push!(dus, du) + push!(us, u) + end + linearization_function(system_modifier(sys), dus, us; kwargs...) +end + +function get_looptransfer_function( + sys, aps; system_modifier = identity, loop_openings = [], kwargs...) + sys = handle_loop_openings(sys, loop_openings) + aps = canonicalize_ap(aps) + dus = [] + us = [] + for ap in aps + sys, (du, u) = apply_transformation(LoopTransferTransform(ap), sys) + push!(dus, du) + push!(us, u) + end + linearization_function(system_modifier(sys), dus, us; kwargs...) +end + +for f in [:get_sensitivity, :get_comp_sensitivity, :get_looptransfer] + @eval function $f(sys, ap, args...; kwargs...) + lin_fun, ssys = $(Symbol(f, :_function))(sys, ap, args...; kwargs...) + ModelingToolkit.linearize(ssys, lin_fun; kwargs...), ssys + end +end + +function open_loop(sys, ap::Union{Symbol, AnalysisPoint}; kwargs...) + if ap isa Symbol + ap = AnalysisPoint(ap) + end + tf = LoopTransferTransform(ap) + return apply_transformation(tf, sys) +end + +function linearization_function(sys::AbstractSystem, + inputs::Union{Symbol, Vector{Symbol}, AnalysisPoint, Vector{AnalysisPoint}}, + outputs; loop_openings = [], system_modifier = identity, kwargs...) + sys = handle_loop_openings(sys, loop_openings) + + inputs = canonicalize_ap(inputs) + outputs = canonicalize_ap(outputs) + + input_vars = [] + for input in inputs + sys, (input_var,) = apply_transformation(PerturbOutput(input), sys) + push!(input_vars, input_var) + end + output_vars = [] + for output in outputs + if output isa AnalysisPoint + sys, (output_var,) = apply_transformation(GetInput(output), sys) + push!(output_vars, output_var) + else + push!(output_vars, output) + end + end + + return linearization_function(system_modifier(sys), input_vars, output_vars; kwargs...) +end diff --git a/src/systems/connectors.jl b/src/systems/connectors.jl index 227b4624bf..f0f49acb85 100644 --- a/src/systems/connectors.jl +++ b/src/systems/connectors.jl @@ -481,6 +481,7 @@ end function expand_connections(sys::AbstractSystem, find = nothing, replace = nothing; debug = false, tol = 1e-10, scalarize = true) + sys = remove_analysis_points(sys) sys, (csets, domain_csets) = generate_connection_set(sys, find, replace; scalarize) ceqs, instream_csets = generate_connection_equations_and_stream_connections(csets) _sys = expand_instream(instream_csets, sys; debug = debug, tol = tol) From c43b3e9cd0f94658bf29f8cd01490a8e40082e6b Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 19 Dec 2024 13:37:40 +0530 Subject: [PATCH 0517/1337] test: test analysis points --- test/analysis_points.jl | 23 +++++++++++++++++++++++ test/runtests.jl | 1 + 2 files changed, 24 insertions(+) create mode 100644 test/analysis_points.jl diff --git a/test/analysis_points.jl b/test/analysis_points.jl new file mode 100644 index 0000000000..b6a9c8c21f --- /dev/null +++ b/test/analysis_points.jl @@ -0,0 +1,23 @@ +using ModelingToolkit, ModelingToolkitStandardLibrary +using Test +using ModelingToolkit: t_nounits as t, D_nounits as D + +@testset "AnalysisPoint is lowered to `connect`" begin + @named P = FirstOrder(k = 1, T = 1) + @named C = Gain(; k = -1) + + ap = AnalysisPoint(:plant_input) + eqs = [connect(P.output, C.input) + connect(C.output, ap, P.input)] + sys_ap = ODESystem(eqs, t, systems = [P, C], name = :hej) + sys_ap2 = @test_nowarn expand_connections(sys) + + @test all(eq -> !(eq.lhs isa AnalysisPoint), equations(sys_ap2)) + + eqs = [connect(P.output, C.input) + connect(C.output, P.input)] + sys_normal = ODESystem(eqs, t, systems = [P, C], name = :hej) + sys_normal2 = @test_nowarn expand_connections(sys) + + @test isequal(sys_ap2, sys_normal2) +end diff --git a/test/runtests.jl b/test/runtests.jl index 4a2ec80e6a..af83ffccd8 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -84,6 +84,7 @@ end @safetestset "print_tree" include("print_tree.jl") @safetestset "Constraints Test" include("constraints.jl") @safetestset "IfLifting Test" include("if_lifting.jl") + @safetestset "Analysis Points Test" include("analysis_points.jl") end end From eba5dc77d855bca64e4c94e0280b1535009b1f64 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 24 Dec 2024 11:24:54 +0530 Subject: [PATCH 0518/1337] refactor: format Project.toml --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 7b0997823c..e4eba3cfad 100644 --- a/Project.toml +++ b/Project.toml @@ -73,8 +73,8 @@ MTKBifurcationKitExt = "BifurcationKit" MTKChainRulesCoreExt = "ChainRulesCore" MTKDeepDiffsExt = "DeepDiffs" MTKHomotopyContinuationExt = "HomotopyContinuation" -MTKLabelledArraysExt = "LabelledArrays" MTKInfiniteOptExt = "InfiniteOpt" +MTKLabelledArraysExt = "LabelledArrays" [compat] AbstractTrees = "0.3, 0.4" From f0bab4a58daff1d4b6e984173631a1ba75c72fa3 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 30 Dec 2024 00:41:06 +0530 Subject: [PATCH 0519/1337] feat: allow accessing `AnalysisPoint` via `getproperty` syntax --- src/systems/abstractsystem.jl | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index 168260ae69..9b5ff7183d 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -1162,6 +1162,14 @@ function getvar(sys::AbstractSystem, name::Symbol; namespace = !iscomplete(sys)) end end + if has_eqs(sys) + for eq in get_eqs(sys) + if eq.lhs isa AnalysisPoint && nameof(eq.rhs) == name + return namespace ? renamespace(sys, eq.rhs) : eq.rhs + end + end + end + throw(ArgumentError("System $(nameof(sys)): variable $name does not exist")) end From 04ca326ca985bbc28a66b02f0f1ed89143919a77 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 30 Dec 2024 00:43:03 +0530 Subject: [PATCH 0520/1337] test: add tests from MTKStdlib --- test/analysis_points.jl | 242 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 239 insertions(+), 3 deletions(-) diff --git a/test/analysis_points.jl b/test/analysis_points.jl index b6a9c8c21f..6eae30314e 100644 --- a/test/analysis_points.jl +++ b/test/analysis_points.jl @@ -1,11 +1,14 @@ -using ModelingToolkit, ModelingToolkitStandardLibrary +using ModelingToolkit, ModelingToolkitStandardLibrary.Blocks +using OrdinaryDiffEq, LinearAlgebra using Test -using ModelingToolkit: t_nounits as t, D_nounits as D +using ModelingToolkit: t_nounits as t, D_nounits as D, AnalysisPoint, get_sensitivity, + get_comp_sensitivity, get_looptransfer +using Symbolics: NAMESPACE_SEPARATOR @testset "AnalysisPoint is lowered to `connect`" begin @named P = FirstOrder(k = 1, T = 1) @named C = Gain(; k = -1) - + ap = AnalysisPoint(:plant_input) eqs = [connect(P.output, C.input) connect(C.output, ap, P.input)] @@ -21,3 +24,236 @@ using ModelingToolkit: t_nounits as t, D_nounits as D @test isequal(sys_ap2, sys_normal2) end + +# also tests `connect(input, name::Symbol, outputs...)` syntax +@testset "AnalysisPoint is accessible via `getproperty`" begin + @named P = FirstOrder(k = 1, T = 1) + @named C = Gain(; k = -1) + + eqs = [connect(P.output, C.input), connect(C.output, :plant_input, P.input)] + sys_ap = ODESystem(eqs, t, systems = [P, C], name = :hej) + ap2 = @test_nowarn sys_ap.plant_input + @test nameof(ap2) == Symbol(join(["hej", "plant_input"], NAMESPACE_SEPARATOR)) + @named sys = ODESystem(Equation[], t; systems = [sys_ap]) + ap3 = @test_nowarn sys.hej.plant_input + @test nameof(ap3) == Symbol(join(["sys", "hej", "plant_input"], NAMESPACE_SEPARATOR)) + sys = complete(sys) + ap4 = sys.hej.plant_input + @test nameof(ap4) == Symbol(join(["hej", "plant_input"], NAMESPACE_SEPARATOR)) +end + +### Ported from MTKStdlib + +@named P = FirstOrder(k = 1, T = 1) +@named C = Gain(; k = -1) + +ap = AnalysisPoint(:plant_input) +eqs = [connect(P.output, C.input), connect(C.output, ap, P.input)] +sys = ODESystem(eqs, t, systems = [P, C], name = :hej) +@named nested_sys = ODESystem(Equation[], t; systems = [sys]) + +@testset "simplifies and solves" begin + ssys = structural_simplify(sys) + prob = ODEProblem(ssys, [P.x => 1], (0, 10)) + sol = solve(prob, Rodas5()) + @test norm(sol.u[1]) >= 1 + @test norm(sol.u[end]) < 1e-6 # This fails without the feedback through C +end + +@testset "get_sensitivity - $name" for (name, sys, ap) in [ + ("inner", sys, sys.plant_input), + ("nested", nested_sys, nested_sys.hej.plant_input), + ("inner - nonamespace", sys, :plant_input), + ("inner - Symbol", sys, nameof(sys.plant_input)), + ("nested - Symbol", nested_sys, nameof(nested_sys.hej.plant_input)) +] + matrices, _ = get_sensitivity(sys, ap) + @test matrices.A[] == -2 + @test matrices.B[] * matrices.C[] == -1 # either one negative + @test matrices.D[] == 1 +end + +@testset "get_comp_sensitivity - $name" for (name, sys, ap) in [ + ("inner", sys, sys.plant_input), + ("nested", nested_sys, nested_sys.hej.plant_input), + ("inner - nonamespace", sys, :plant_input), + ("inner - Symbol", sys, nameof(sys.plant_input)), + ("nested - Symbol", nested_sys, nameof(nested_sys.hej.plant_input)) +] + matrices, _ = get_comp_sensitivity(sys, ap) + @test matrices.A[] == -2 + @test matrices.B[] * matrices.C[] == 1 # both positive or negative + @test matrices.D[] == 0 +end + +#= +# Equivalent code using ControlSystems. This can be used to verify the expected results tested for above. +using ControlSystemsBase +P = tf(1.0, [1, 1]) +C = 1 # Negative feedback assumed in ControlSystems +S = sensitivity(P, C) # or feedback(1, P*C) +T = comp_sensitivity(P, C) # or feedback(P*C) +=# + +@testset "get_looptransfer - $name" for (name, sys, ap) in [ + ("inner", sys, sys.plant_input), + ("nested", nested_sys, nested_sys.hej.plant_input), + ("inner - nonamespace", sys, :plant_input), + ("inner - Symbol", sys, nameof(sys.plant_input)), + ("nested - Symbol", nested_sys, nameof(nested_sys.hej.plant_input)) +] + matrices, _ = get_looptransfer(sys, ap) + @test matrices.A[] == -1 + @test matrices.B[] * matrices.C[] == -1 # either one negative + @test matrices.D[] == 0 +end + +#= +# Equivalent code using ControlSystems. This can be used to verify the expected results tested for above. +using ControlSystemsBase +P = tf(1.0, [1, 1]) +C = -1 +L = P*C +=# + +@testset "open_loop - $name" for (name, sys, ap) in [ + ("inner", sys, sys.plant_input), + ("nested", nested_sys, nested_sys.hej.plant_input), + ("inner - nonamespace", sys, :plant_input), + ("inner - Symbol", sys, nameof(sys.plant_input)), + ("nested - Symbol", nested_sys, nameof(nested_sys.hej.plant_input)) +] + open_sys, (du, u) = open_loop(sys, ap) + matrices, _ = linearize(open_sys, [du], [u]) + @test matrices.A[] == -1 + @test matrices.B[] * matrices.C[] == -1 # either one negative + @test matrices.D[] == 0 +end + +# Multiple analysis points + +eqs = [connect(P.output, :plant_output, C.input) + connect(C.output, :plant_input, P.input)] +sys = ODESystem(eqs, t, systems = [P, C], name = :hej) +@named nested_sys = ODESystem(Equation[], t; systems = [sys]) + +@testset "get_sensitivity - $name" for (name, sys, ap) in [ + ("inner", sys, sys.plant_input), + ("nested", nested_sys, nested_sys.hej.plant_input), + ("inner - nonamespace", sys, :plant_input), + ("inner - Symbol", sys, nameof(sys.plant_input)), + ("nested - Symbol", nested_sys, nameof(nested_sys.hej.plant_input)) +] + matrices, _ = get_sensitivity(sys, ap) + @test matrices.A[] == -2 + @test matrices.B[] * matrices.C[] == -1 # either one negative + @test matrices.D[] == 1 +end + +@testset "linearize - $name" for (name, sys, inputap, outputap) in [ + ("inner", sys, sys.plant_input, sys.plant_output), + ("nested", nested_sys, nested_sys.hej.plant_input, nested_sys.hej.plant_output) +] + @testset "input - $(typeof(input)), output - $(typeof(output))" for (input, output) in [ + (inputap, outputap), + (nameof(inputap), outputap), + (inputap, nameof(outputap)), + (nameof(inputap), nameof(outputap)), + (inputap, [outputap]), + (nameof(inputap), [outputap]), + (inputap, [nameof(outputap)]), + (nameof(inputap), [nameof(outputap)]) + ] + if input isa Symbol + # broken because MTKStdlib defines the method for + # `input::Union{Symbol, Vector{Symbol}}` which we can't directly call + @test_broken linearize(sys, input, output) + linfun, ssys = @invoke linearization_function(sys::AbstractSystem, + input::Union{Symbol, Vector{Symbol}, AnalysisPoint, Vector{AnalysisPoint}}, + output::Any) + matrices = linearize(ssys, linfun) + else + matrices, _ = linearize(sys, input, output) + end + # Result should be the same as feedpack(P, 1), i.e., the closed-loop transfer function from plant input to plant output + @test matrices.A[] == -2 + @test matrices.B[] * matrices.C[] == 1 # both positive + @test matrices.D[] == 0 + end +end + +@testset "linearize - variable output - $name" for (name, sys, input, output) in [ + ("inner", sys, sys.plant_input, P.output.u), + ("nested", nested_sys, nested_sys.hej.plant_input, sys.P.output.u) +] + matrices, _ = linearize(sys, input, [output]) + @test matrices.A[] == -2 + @test matrices.B[] * matrices.C[] == 1 # both positive + @test matrices.D[] == 0 +end + +using ModelingToolkit, OrdinaryDiffEq, LinearAlgebra +using ModelingToolkitStandardLibrary.Mechanical.Rotational +using ModelingToolkitStandardLibrary.Blocks: Sine, PID, SecondOrder, Step, RealOutput +using ModelingToolkit: connect + +@testset "Complicated model" begin + # Parameters + m1 = 1 + m2 = 1 + k = 1000 # Spring stiffness + c = 10 # Damping coefficient + @named inertia1 = Inertia(; J = m1) + @named inertia2 = Inertia(; J = m2) + @named spring = Spring(; c = k) + @named damper = Damper(; d = c) + @named torque = Torque() + + function SystemModel(u = nothing; name = :model) + eqs = [connect(torque.flange, inertia1.flange_a) + connect(inertia1.flange_b, spring.flange_a, damper.flange_a) + connect(inertia2.flange_a, spring.flange_b, damper.flange_b)] + if u !== nothing + push!(eqs, connect(torque.tau, u.output)) + return ODESystem(eqs, t; + systems = [ + torque, + inertia1, + inertia2, + spring, + damper, + u + ], + name) + end + ODESystem(eqs, t; systems = [torque, inertia1, inertia2, spring, damper], name) + end + + @named r = Step(start_time = 0) + model = SystemModel() + @named pid = PID(k = 100, Ti = 0.5, Td = 1) + @named filt = SecondOrder(d = 0.9, w = 10) + @named sensor = AngleSensor() + @named er = Add(k2 = -1) + + connections = [connect(r.output, :r, filt.input) + connect(filt.output, er.input1) + connect(pid.ctr_output, :u, model.torque.tau) + connect(model.inertia2.flange_b, sensor.flange) + connect(sensor.phi, :y, er.input2) + connect(er.output, :e, pid.err_input)] + + closed_loop = ODESystem(connections, t, systems = [model, pid, filt, sensor, r, er], + name = :closed_loop, defaults = [ + model.inertia1.phi => 0.0, + model.inertia2.phi => 0.0, + model.inertia1.w => 0.0, + model.inertia2.w => 0.0, + filt.x => 0.0, + filt.xd => 0.0 + ]) + + sys = structural_simplify(closed_loop) + prob = ODEProblem(sys, unknowns(sys) .=> 0.0, (0.0, 4.0)) + sol = solve(prob, Rodas5P(), reltol = 1e-6, abstol = 1e-9) +end From 8a4ca21724b6631d271450b458d4a041db2ab5de Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 30 Dec 2024 00:43:23 +0530 Subject: [PATCH 0521/1337] feat: export `AnalysisPoint`-related tools --- src/ModelingToolkit.jl | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/ModelingToolkit.jl b/src/ModelingToolkit.jl index 486ef63460..29288a5742 100644 --- a/src/ModelingToolkit.jl +++ b/src/ModelingToolkit.jl @@ -293,4 +293,8 @@ export MTKParameters, reorder_dimension_by_tunables!, reorder_dimension_by_tunab export HomotopyContinuationProblem +export AnalysisPoint, Break, PerturbOutput, SampleInput, SensitivityTransform, + ComplementarySensitivityTransform, LoopTransferTransform, apply_transformation, + get_sensitivity_function, get_comp_sensitivity_function, get_looptransfer_function, + get_sensitivity, get_comp_sensitivity, get_looptransfer, open_loop end # module From 0750e62f415e7e66cc4abc1fd9b85a3a2b4a764f Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 30 Dec 2024 00:43:47 +0530 Subject: [PATCH 0522/1337] test: add downstream analysis points tests --- test/downstream/analysis_points.jl | 233 +++++++++++++++++++++++++++++ test/runtests.jl | 1 + 2 files changed, 234 insertions(+) create mode 100644 test/downstream/analysis_points.jl diff --git a/test/downstream/analysis_points.jl b/test/downstream/analysis_points.jl new file mode 100644 index 0000000000..5dc2b8957c --- /dev/null +++ b/test/downstream/analysis_points.jl @@ -0,0 +1,233 @@ +using ModelingToolkit, OrdinaryDiffEq, LinearAlgebra, ControlSystemsBase +using ModelingToolkitStandardLibrary.Mechanical.Rotational +using ModelingToolkitStandardLibrary.Blocks +using ModelingToolkit: connect, AnalysisPoint, t_nounits as t, D_nounits as D +import ControlSystemsBase as CS + +@testset "Complicated model" begin + # Parameters + m1 = 1 + m2 = 1 + k = 1000 # Spring stiffness + c = 10 # Damping coefficient + @named inertia1 = Inertia(; J = m1) + @named inertia2 = Inertia(; J = m2) + @named spring = Spring(; c = k) + @named damper = Damper(; d = c) + @named torque = Torque() + + function SystemModel(u = nothing; name = :model) + eqs = [connect(torque.flange, inertia1.flange_a) + connect(inertia1.flange_b, spring.flange_a, damper.flange_a) + connect(inertia2.flange_a, spring.flange_b, damper.flange_b)] + if u !== nothing + push!(eqs, connect(torque.tau, u.output)) + return ODESystem(eqs, t; + systems = [ + torque, + inertia1, + inertia2, + spring, + damper, + u + ], + name) + end + ODESystem(eqs, t; systems = [torque, inertia1, inertia2, spring, damper], name) + end + + @named r = Step(start_time = 0) + model = SystemModel() + @named pid = PID(k = 100, Ti = 0.5, Td = 1) + @named filt = SecondOrder(d = 0.9, w = 10) + @named sensor = AngleSensor() + @named er = Add(k2 = -1) + + connections = [connect(r.output, :r, filt.input) + connect(filt.output, er.input1) + connect(pid.ctr_output, :u, model.torque.tau) + connect(model.inertia2.flange_b, sensor.flange) + connect(sensor.phi, :y, er.input2) + connect(er.output, :e, pid.err_input)] + + closed_loop = ODESystem(connections, t, systems = [model, pid, filt, sensor, r, er], + name = :closed_loop, defaults = [ + model.inertia1.phi => 0.0, + model.inertia2.phi => 0.0, + model.inertia1.w => 0.0, + model.inertia2.w => 0.0, + filt.x => 0.0, + filt.xd => 0.0 + ]) + + sys = structural_simplify(closed_loop) + prob = ODEProblem(sys, unknowns(sys) .=> 0.0, (0.0, 4.0)) + sol = solve(prob, Rodas5P(), reltol = 1e-6, abstol = 1e-9) + + matrices, ssys = linearize(closed_loop, AnalysisPoint(:r), AnalysisPoint(:y)) + lsys = ss(matrices...) |> sminreal + @test lsys.nx == 8 + + stepres = ControlSystemsBase.step(c2d(lsys, 0.001), 4) + @test Array(stepres.y[:])≈Array(sol(0:0.001:4, idxs = model.inertia2.phi)) rtol=1e-4 + + matrices, ssys = get_sensitivity(closed_loop, :y) + So = ss(matrices...) + + matrices, ssys = get_sensitivity(closed_loop, :u) + Si = ss(matrices...) + + @test tf(So) ≈ tf(Si) +end + +@testset "Analysis points with subsystems" begin + @named P = FirstOrder(k = 1, T = 1) + @named C = Gain(; k = 1) + @named add = Blocks.Add(k2 = -1) + + eqs = [connect(P.output, :plant_output, add.input2) + connect(add.output, C.input) + connect(C.output, :plant_input, P.input)] + + # eqs = [connect(P.output, add.input2) + # connect(add.output, C.input) + # connect(C.output, P.input)] + + sys_inner = ODESystem(eqs, t, systems = [P, C, add], name = :inner) + + @named r = Constant(k = 1) + @named F = FirstOrder(k = 1, T = 3) + + eqs = [connect(r.output, F.input) + connect(F.output, sys_inner.add.input1)] + sys_outer = ODESystem(eqs, t, systems = [F, sys_inner, r], name = :outer) + + # test first that the structural_simplify works correctly + ssys = structural_simplify(sys_outer) + prob = ODEProblem(ssys, Pair[], (0, 10)) + @test_nowarn solve(prob, Rodas5()) + + matrices, _ = get_sensitivity(sys_outer, sys_outer.inner.plant_input) + lsys = sminreal(ss(matrices...)) + @test lsys.A[] == -2 + @test lsys.B[] * lsys.C[] == -1 # either one negative + @test lsys.D[] == 1 + + matrices_So, _ = get_sensitivity(sys_outer, sys_outer.inner.plant_output) + lsyso = sminreal(ss(matrices_So...)) + @test lsys == lsyso || lsys == -1 * lsyso * (-1) # Output and input sensitivities are equal for SISO systems +end + +@testset "multilevel system with loop openings" begin + @named P_inner = FirstOrder(k = 1, T = 1) + @named feedback = Feedback() + @named ref = Step() + @named sys_inner = ODESystem( + [connect(P_inner.output, :y, feedback.input2) + connect(feedback.output, :u, P_inner.input) + connect(ref.output, :r, feedback.input1)], + t, + systems = [P_inner, feedback, ref]) + + P_not_broken, _ = linearize(sys_inner, AnalysisPoint(:u), AnalysisPoint(:y)) + @test P_not_broken.A[] == -2 + P_broken, _ = linearize(sys_inner, AnalysisPoint(:u), AnalysisPoint(:y), + loop_openings = [AnalysisPoint(:u)]) + @test P_broken.A[] == -1 + P_broken, _ = linearize(sys_inner, AnalysisPoint(:u), AnalysisPoint(:y), + loop_openings = [AnalysisPoint(:y)]) + @test P_broken.A[] == -1 + + Sinner = sminreal(ss(get_sensitivity(sys_inner, :u)[1]...)) + + @named sys_inner = ODESystem( + [connect(P_inner.output, :y, feedback.input2) + connect(feedback.output, :u, P_inner.input)], + t, + systems = [P_inner, feedback]) + + @named P_outer = FirstOrder(k = rand(), T = rand()) + + @named sys_outer = ODESystem( + [connect(sys_inner.P_inner.output, :y2, P_outer.input) + connect(P_outer.output, :u2, sys_inner.feedback.input1)], + t, + systems = [P_outer, sys_inner]) + + Souter = sminreal(ss(get_sensitivity(sys_outer, :sys_inner_u)[1]...)) + + Sinner2 = sminreal(ss(get_sensitivity( + sys_outer, :sys_inner_u, loop_openings = [:y2])[1]...)) + + @test Sinner.nx == 1 + @test Sinner == Sinner2 + @test Souter.nx == 2 +end + +@testset "sensitivities in multivariate signals" begin + A = [-0.994 -0.0794; -0.006242 -0.0134] + B = [-0.181 -0.389; 1.1 1.12] + C = [1.74 0.72; -0.33 0.33] + D = [0.0 0.0; 0.0 0.0] + @named P = Blocks.StateSpace(A, B, C, D) + Pss = CS.ss(A, B, C, D) + + A = [-0.097;;] + B = [-0.138 -1.02] + C = [-0.076; 0.09;;] + D = [0.0 0.0; 0.0 0.0] + @named K = Blocks.StateSpace(A, B, C, D) + Kss = CS.ss(A, B, C, D) + + eqs = [connect(P.output, :plant_output, K.input) + connect(K.output, :plant_input, P.input)] + sys = ODESystem(eqs, t, systems = [P, K], name = :hej) + + matrices, _ = Blocks.get_sensitivity(sys, :plant_input) + S = CS.feedback(I(2), Kss * Pss, pos_feedback = true) + + @test CS.tf(CS.ss(matrices...)) ≈ CS.tf(S) + + matrices, _ = Blocks.get_comp_sensitivity(sys, :plant_input) + T = -CS.feedback(Kss * Pss, I(2), pos_feedback = true) + + # bodeplot([ss(matrices...), T]) + @test CS.tf(CS.ss(matrices...)) ≈ CS.tf(T) + + matrices, _ = Blocks.get_looptransfer( + sys, :plant_input) + L = Kss * Pss + @test CS.tf(CS.ss(matrices...)) ≈ CS.tf(L) + + matrices, _ = linearize(sys, :plant_input, :plant_output) + G = CS.feedback(Pss, Kss, pos_feedback = true) + @test CS.tf(CS.ss(matrices...)) ≈ CS.tf(G) +end + +@testset "multiple analysis points" begin + @named P = FirstOrder(k = 1, T = 1) + @named C = Gain(; k = 1) + @named add = Blocks.Add(k2 = -1) + + eqs = [connect(P.output, :plant_output, add.input2) + connect(add.output, C.input) + connect(C.output, :plant_input, P.input)] + + sys_inner = ODESystem(eqs, t, systems = [P, C, add], name = :inner) + + @named r = Constant(k = 1) + @named F = FirstOrder(k = 1, T = 3) + + eqs = [connect(r.output, F.input) + connect(F.output, sys_inner.add.input1)] + sys_outer = ODESystem(eqs, t, systems = [F, sys_inner, r], name = :outer) + + matrices, _ = get_sensitivity(sys_outer, [:, :inner_plant_output]) + + Ps = tf(1, [1, 1]) |> ss + Cs = tf(1) |> ss + + G = CS.ss(matrices...) |> sminreal + Si = CS.feedback(1, Cs * Ps) + @test tf(G[1, 1]) ≈ tf(Si) +end diff --git a/test/runtests.jl b/test/runtests.jl index af83ffccd8..d38aa86bb2 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -111,6 +111,7 @@ end @safetestset "Linearization Tests" include("downstream/linearize.jl") @safetestset "Linearization Dummy Derivative Tests" include("downstream/linearization_dd.jl") @safetestset "Inverse Models Test" include("downstream/inversemodel.jl") + @safetestset "Analysis Points Test" include("downstream/analysis_points.jl") end if GROUP == "All" || GROUP == "Extensions" From 469eda8837e7f7db533763e108e578fb9938bfe9 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 2 Jan 2025 23:33:41 +0530 Subject: [PATCH 0523/1337] feat: handle array variables as inputs/outputs of `linearization_function` --- src/systems/abstractsystem.jl | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index 9b5ff7183d..3790cb8eb0 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -2374,6 +2374,12 @@ function linearization_function(sys::AbstractSystem, inputs, op = Dict(op) inputs isa AbstractVector || (inputs = [inputs]) outputs isa AbstractVector || (outputs = [outputs]) + inputs = mapreduce(vcat, inputs; init = []) do var + symbolic_type(var) == ArraySymbolic() ? collect(var) : [var] + end + outputs = mapreduce(vcat, outputs; init = []) do var + symbolic_type(var) == ArraySymbolic() ? collect(var) : [var] + end ssys, diff_idxs, alge_idxs, input_idxs = io_preprocessing(sys, inputs, outputs; simplify, kwargs...) From ccfed451bfb8ba124fc4564eb27d3fd5111642f7 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 2 Jan 2025 23:34:02 +0530 Subject: [PATCH 0524/1337] fix: fix array variable handling in `get_analysis_variable` --- src/systems/analysis_points.jl | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/systems/analysis_points.jl b/src/systems/analysis_points.jl index 605eb32749..ef20377147 100644 --- a/src/systems/analysis_points.jl +++ b/src/systems/analysis_points.jl @@ -255,11 +255,11 @@ function get_analysis_variable(var, name, iv; perturb = true) if perturb name = Symbol(:d_, name) end - if Symbolics.isarraysymbolic(var) + if symbolic_type(var) == ArraySymbolic() T = Array{eltype(symtype(var)), ndims(var)} pvar = unwrap(only(@variables $name(iv)::T)) pvar = setmetadata(pvar, Symbolics.ArrayShapeCtx, Symbolics.shape(var)) - default = zeros(symtype(var), size(var)) + default = zeros(eltype(symtype(var)), size(var)) else T = symtype(var) pvar = unwrap(only(@variables $name(iv)::T)) @@ -429,7 +429,7 @@ function apply_transformation(tf::PerturbOutput, sys::AbstractSystem) ap_ivar = ap_var(ap.input) new_var, new_def = get_analysis_variable(ap_ivar, nameof(ap), get_iv(sys)) for outsys in ap.outputs - push!(ap_sys_eqs, ap_var(outsys) ~ ap_ivar + new_var) + push!(ap_sys_eqs, ap_var(outsys) ~ ap_ivar + wrap(new_var)) end # add variable unks = copy(get_unknowns(ap_sys)) @@ -446,7 +446,7 @@ function apply_transformation(tf::PerturbOutput, sys::AbstractSystem) out_var, out_def = get_analysis_variable( ap_ivar, nameof(ap), get_iv(sys); perturb = false) defs[out_var] = out_def - push!(ap_sys_eqs, out_var ~ ap_ivar + new_var) + push!(ap_sys_eqs, out_var ~ ap_ivar + wrap(new_var)) push!(unks, out_var) return ap_sys, (new_var, out_var) @@ -526,11 +526,11 @@ function apply_transformation(cst::ComplementarySensitivityTransform, sys::Abstr # but comp sensitivity wants `output + du ~ input`. Thus, `du ~ -_du`. eqs = copy(get_eqs(sys)) @set! sys.eqs = eqs - push!(eqs, du ~ -_du) + push!(eqs, du ~ -wrap(_du)) defs = copy(get_defaults(sys)) @set! sys.defaults = defs - defs[du] = -_du + defs[du] = -wrap(_du) return sys, (du, u) end From f9b62e2b3b16cf019fc355bbeec231e951e54409 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 2 Jan 2025 23:34:59 +0530 Subject: [PATCH 0525/1337] fix: fix naming of new variable in `ComplementarySensitivityTransform` --- src/systems/analysis_points.jl | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/systems/analysis_points.jl b/src/systems/analysis_points.jl index ef20377147..e71e67c691 100644 --- a/src/systems/analysis_points.jl +++ b/src/systems/analysis_points.jl @@ -519,7 +519,10 @@ end function apply_transformation(cst::ComplementarySensitivityTransform, sys::AbstractSystem) sys, (u,) = apply_transformation(GetInput(cst.ap), sys) - sys, (du,) = apply_transformation(AddVariable(cst.ap, Symbol(:comp_sens_du)), sys) + sys, (du,) = apply_transformation( + AddVariable( + cst.ap, Symbol(namespace_hierarchy(nameof(cst.ap))[end], :_comp_sens_du)), + sys) sys, (_du,) = apply_transformation(PerturbOutput(cst.ap), sys) # `PerturbOutput` adds the equation `input + _du ~ output` From ed5fa70d33f4b3c8e950a85bcddd71b63f3695ed Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 2 Jan 2025 23:35:41 +0530 Subject: [PATCH 0526/1337] fix: handle `loop_openings`, `system_modifier` kwargs in `get_*` linear analysis functions --- src/systems/analysis_points.jl | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/systems/analysis_points.jl b/src/systems/analysis_points.jl index e71e67c691..b8ec48eb68 100644 --- a/src/systems/analysis_points.jl +++ b/src/systems/analysis_points.jl @@ -611,8 +611,10 @@ function get_looptransfer_function( end for f in [:get_sensitivity, :get_comp_sensitivity, :get_looptransfer] - @eval function $f(sys, ap, args...; kwargs...) - lin_fun, ssys = $(Symbol(f, :_function))(sys, ap, args...; kwargs...) + @eval function $f( + sys, ap, args...; loop_openings = [], system_modifier = identity, kwargs...) + lin_fun, ssys = $(Symbol(f, :_function))( + sys, ap, args...; loop_openings, system_modifier, kwargs...) ModelingToolkit.linearize(ssys, lin_fun; kwargs...), ssys end end From c8a4127b6580c089f12e51a435268f5b62fb869f Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 2 Jan 2025 23:36:09 +0530 Subject: [PATCH 0527/1337] fix: fix handling of `loop_openings` in `linearization_function` --- src/systems/analysis_points.jl | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/systems/analysis_points.jl b/src/systems/analysis_points.jl index b8ec48eb68..180b730a98 100644 --- a/src/systems/analysis_points.jl +++ b/src/systems/analysis_points.jl @@ -630,25 +630,33 @@ end function linearization_function(sys::AbstractSystem, inputs::Union{Symbol, Vector{Symbol}, AnalysisPoint, Vector{AnalysisPoint}}, outputs; loop_openings = [], system_modifier = identity, kwargs...) - sys = handle_loop_openings(sys, loop_openings) - + loop_openings = Set(map(nameof, canonicalize_ap(loop_openings))) inputs = canonicalize_ap(inputs) outputs = canonicalize_ap(outputs) input_vars = [] for input in inputs - sys, (input_var,) = apply_transformation(PerturbOutput(input), sys) + if nameof(input) in loop_openings + delete!(loop_openings, nameof(input)) + sys, (input_var,) = apply_transformation(Break(input, true), sys) + else + sys, (input_var,) = apply_transformation(PerturbOutput(input), sys) + end push!(input_vars, input_var) end output_vars = [] for output in outputs if output isa AnalysisPoint - sys, (output_var,) = apply_transformation(GetInput(output), sys) - push!(output_vars, output_var) + sys, (output_var,) = apply_transformation(AddVariable(output), sys) + sys, (input_var,) = apply_transformation(GetInput(output), sys) + push!(get_eqs(sys), output_var ~ input_var) else - push!(output_vars, output) + output_var = output end + push!(output_vars, output_var) end + sys = handle_loop_openings(sys, collect(loop_openings)) + return linearization_function(system_modifier(sys), input_vars, output_vars; kwargs...) end From 0610d67ce61fc08f9fbb9bdb4f9151826144da82 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 2 Jan 2025 23:36:35 +0530 Subject: [PATCH 0528/1337] test: fix analysis point tests --- test/analysis_points.jl | 72 ++--------------------------------------- 1 file changed, 3 insertions(+), 69 deletions(-) diff --git a/test/analysis_points.jl b/test/analysis_points.jl index 6eae30314e..c888261db2 100644 --- a/test/analysis_points.jl +++ b/test/analysis_points.jl @@ -2,7 +2,7 @@ using ModelingToolkit, ModelingToolkitStandardLibrary.Blocks using OrdinaryDiffEq, LinearAlgebra using Test using ModelingToolkit: t_nounits as t, D_nounits as D, AnalysisPoint, get_sensitivity, - get_comp_sensitivity, get_looptransfer + get_comp_sensitivity, get_looptransfer, open_loop, AbstractSystem using Symbolics: NAMESPACE_SEPARATOR @testset "AnalysisPoint is lowered to `connect`" begin @@ -13,14 +13,14 @@ using Symbolics: NAMESPACE_SEPARATOR eqs = [connect(P.output, C.input) connect(C.output, ap, P.input)] sys_ap = ODESystem(eqs, t, systems = [P, C], name = :hej) - sys_ap2 = @test_nowarn expand_connections(sys) + sys_ap2 = @test_nowarn expand_connections(sys_ap) @test all(eq -> !(eq.lhs isa AnalysisPoint), equations(sys_ap2)) eqs = [connect(P.output, C.input) connect(C.output, P.input)] sys_normal = ODESystem(eqs, t, systems = [P, C], name = :hej) - sys_normal2 = @test_nowarn expand_connections(sys) + sys_normal2 = @test_nowarn expand_connections(sys_normal) @test isequal(sys_ap2, sys_normal2) end @@ -191,69 +191,3 @@ end @test matrices.B[] * matrices.C[] == 1 # both positive @test matrices.D[] == 0 end - -using ModelingToolkit, OrdinaryDiffEq, LinearAlgebra -using ModelingToolkitStandardLibrary.Mechanical.Rotational -using ModelingToolkitStandardLibrary.Blocks: Sine, PID, SecondOrder, Step, RealOutput -using ModelingToolkit: connect - -@testset "Complicated model" begin - # Parameters - m1 = 1 - m2 = 1 - k = 1000 # Spring stiffness - c = 10 # Damping coefficient - @named inertia1 = Inertia(; J = m1) - @named inertia2 = Inertia(; J = m2) - @named spring = Spring(; c = k) - @named damper = Damper(; d = c) - @named torque = Torque() - - function SystemModel(u = nothing; name = :model) - eqs = [connect(torque.flange, inertia1.flange_a) - connect(inertia1.flange_b, spring.flange_a, damper.flange_a) - connect(inertia2.flange_a, spring.flange_b, damper.flange_b)] - if u !== nothing - push!(eqs, connect(torque.tau, u.output)) - return ODESystem(eqs, t; - systems = [ - torque, - inertia1, - inertia2, - spring, - damper, - u - ], - name) - end - ODESystem(eqs, t; systems = [torque, inertia1, inertia2, spring, damper], name) - end - - @named r = Step(start_time = 0) - model = SystemModel() - @named pid = PID(k = 100, Ti = 0.5, Td = 1) - @named filt = SecondOrder(d = 0.9, w = 10) - @named sensor = AngleSensor() - @named er = Add(k2 = -1) - - connections = [connect(r.output, :r, filt.input) - connect(filt.output, er.input1) - connect(pid.ctr_output, :u, model.torque.tau) - connect(model.inertia2.flange_b, sensor.flange) - connect(sensor.phi, :y, er.input2) - connect(er.output, :e, pid.err_input)] - - closed_loop = ODESystem(connections, t, systems = [model, pid, filt, sensor, r, er], - name = :closed_loop, defaults = [ - model.inertia1.phi => 0.0, - model.inertia2.phi => 0.0, - model.inertia1.w => 0.0, - model.inertia2.w => 0.0, - filt.x => 0.0, - filt.xd => 0.0 - ]) - - sys = structural_simplify(closed_loop) - prob = ODEProblem(sys, unknowns(sys) .=> 0.0, (0.0, 4.0)) - sol = solve(prob, Rodas5P(), reltol = 1e-6, abstol = 1e-9) -end From bb23e6216360b625314e5491e28196220ffdc900 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 2 Jan 2025 23:36:48 +0530 Subject: [PATCH 0529/1337] test: fix and port over remaining analysis point downstream tests --- test/downstream/analysis_points.jl | 58 +++++++++++++++++++++++++----- 1 file changed, 50 insertions(+), 8 deletions(-) diff --git a/test/downstream/analysis_points.jl b/test/downstream/analysis_points.jl index 5dc2b8957c..85d271a547 100644 --- a/test/downstream/analysis_points.jl +++ b/test/downstream/analysis_points.jl @@ -1,7 +1,8 @@ using ModelingToolkit, OrdinaryDiffEq, LinearAlgebra, ControlSystemsBase using ModelingToolkitStandardLibrary.Mechanical.Rotational using ModelingToolkitStandardLibrary.Blocks -using ModelingToolkit: connect, AnalysisPoint, t_nounits as t, D_nounits as D +using ModelingToolkit: connect, AnalysisPoint, t_nounits as t, D_nounits as D, + get_sensitivity, get_comp_sensitivity, get_looptransfer, open_loop import ControlSystemsBase as CS @testset "Complicated model" begin @@ -154,10 +155,10 @@ end t, systems = [P_outer, sys_inner]) - Souter = sminreal(ss(get_sensitivity(sys_outer, :sys_inner_u)[1]...)) + Souter = sminreal(ss(get_sensitivity(sys_outer, sys_inner.u)[1]...)) Sinner2 = sminreal(ss(get_sensitivity( - sys_outer, :sys_inner_u, loop_openings = [:y2])[1]...)) + sys_outer, sys_inner.u, loop_openings = [:y2])[1]...)) @test Sinner.nx == 1 @test Sinner == Sinner2 @@ -183,23 +184,23 @@ end connect(K.output, :plant_input, P.input)] sys = ODESystem(eqs, t, systems = [P, K], name = :hej) - matrices, _ = Blocks.get_sensitivity(sys, :plant_input) + matrices, _ = get_sensitivity(sys, :plant_input) S = CS.feedback(I(2), Kss * Pss, pos_feedback = true) @test CS.tf(CS.ss(matrices...)) ≈ CS.tf(S) - matrices, _ = Blocks.get_comp_sensitivity(sys, :plant_input) + matrices, _ = get_comp_sensitivity(sys, :plant_input) T = -CS.feedback(Kss * Pss, I(2), pos_feedback = true) # bodeplot([ss(matrices...), T]) @test CS.tf(CS.ss(matrices...)) ≈ CS.tf(T) - matrices, _ = Blocks.get_looptransfer( + matrices, _ = get_looptransfer( sys, :plant_input) L = Kss * Pss @test CS.tf(CS.ss(matrices...)) ≈ CS.tf(L) - matrices, _ = linearize(sys, :plant_input, :plant_output) + matrices, _ = linearize(sys, AnalysisPoint(:plant_input), :plant_output) G = CS.feedback(Pss, Kss, pos_feedback = true) @test CS.tf(CS.ss(matrices...)) ≈ CS.tf(G) end @@ -222,7 +223,8 @@ end connect(F.output, sys_inner.add.input1)] sys_outer = ODESystem(eqs, t, systems = [F, sys_inner, r], name = :outer) - matrices, _ = get_sensitivity(sys_outer, [:, :inner_plant_output]) + matrices, _ = get_sensitivity( + sys_outer, [sys_outer.inner.plant_input, sys_outer.inner.plant_output]) Ps = tf(1, [1, 1]) |> ss Cs = tf(1) |> ss @@ -230,4 +232,44 @@ end G = CS.ss(matrices...) |> sminreal Si = CS.feedback(1, Cs * Ps) @test tf(G[1, 1]) ≈ tf(Si) + + So = CS.feedback(1, Ps * Cs) + @test tf(G[2, 2]) ≈ tf(So) + @test tf(G[1, 2]) ≈ tf(-CS.feedback(Cs, Ps)) + @test tf(G[2, 1]) ≈ tf(CS.feedback(Ps, Cs)) + + matrices, _ = get_comp_sensitivity( + sys_outer, [sys_outer.inner.plant_input, sys_outer.inner.plant_output]) + + G = CS.ss(matrices...) |> sminreal + Ti = CS.feedback(Cs * Ps) + @test tf(G[1, 1]) ≈ tf(Ti) + + To = CS.feedback(Ps * Cs) + @test tf(G[2, 2]) ≈ tf(To) + @test tf(G[1, 2]) ≈ tf(CS.feedback(Cs, Ps)) # The negative sign appears in a confusing place due to negative feedback not happening through Ps + @test tf(G[2, 1]) ≈ tf(-CS.feedback(Ps, Cs)) + + # matrices, _ = get_looptransfer(sys_outer, [:inner_plant_input, :inner_plant_output]) + matrices, _ = get_looptransfer(sys_outer, sys_outer.inner.plant_input) + L = CS.ss(matrices...) |> sminreal + @test tf(L) ≈ -tf(Cs * Ps) + + matrices, _ = get_looptransfer(sys_outer, sys_outer.inner.plant_output) + L = CS.ss(matrices...) |> sminreal + @test tf(L[1, 1]) ≈ -tf(Ps * Cs) + + # Calling looptransfer like below is not the intended way, but we can work out what it should return if we did so it remains a valid test + matrices, _ = get_looptransfer( + sys_outer, [sys_outer.inner.plant_input, sys_outer.inner.plant_output]) + L = CS.ss(matrices...) |> sminreal + @test tf(L[1, 1]) ≈ tf(0) + @test tf(L[2, 2]) ≈ tf(0) + @test sminreal(L[1, 2]) ≈ ss(-1) + @test tf(L[2, 1]) ≈ tf(Ps) + + matrices, _ = linearize( + sys_outer, [sys_outer.inner.plant_input], [sys_inner.plant_output]) + G = CS.ss(matrices...) |> sminreal + @test tf(G) ≈ tf(CS.feedback(Ps, Cs)) end From 89ed0cf57e6187c0a68c41a68f899aebf5eee516 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 3 Jan 2025 00:27:01 +0530 Subject: [PATCH 0530/1337] docs: add and update docstrings for analysis points and transformations --- src/systems/analysis_points.jl | 378 +++++++++++++++++++++++++++------ 1 file changed, 313 insertions(+), 65 deletions(-) diff --git a/src/systems/analysis_points.jl b/src/systems/analysis_points.jl index 180b730a98..9ff06109e5 100644 --- a/src/systems/analysis_points.jl +++ b/src/systems/analysis_points.jl @@ -1,25 +1,85 @@ """ $(TYPEDEF) - $(TYPEDSIGNATURES) + AnalysisPoint(input, name::Symbol, outputs::Vector) Create an AnalysisPoint for linear analysis. Analysis points can be created by calling ``` -connect(in, :ap_name, out...) +connect(out, :ap_name, in...) ``` -Where `in` is the input to the connection, and `out...` are the outputs. In the context of -ModelingToolkitStandardLibrary.jl, `in` is a `RealOutput` connector and `out...` are all -`RealInput` connectors. All involved connectors are required to either have an unknown named +Where `out` is the output being connected to the inputs `in...`. All involved +connectors (input and outputs) are required to either have an unknown named `u` or a single unknown, all of which should have the same size. + +See also [`get_sensitivity`](@ref), [`get_comp_sensitivity`](@ref), [`get_looptransfer`](@ref), [`open_loop`](@ref) + +# Fields + +$(TYPEDFIELDS) + +# Example + +```julia +using ModelingToolkit +using ModelingToolkitStandardLibrary.Blocks +using ModelingToolkit: t_nounits as t + +@named P = FirstOrder(k = 1, T = 1) +@named C = Gain(; k = -1) +t = ModelingToolkit.get_iv(P) + +eqs = [connect(P.output, C.input) + connect(C.output, :plant_input, P.input)] +sys = ODESystem(eqs, t, systems = [P, C], name = :feedback_system) + +matrices_S, _ = get_sensitivity(sys, :plant_input) # Compute the matrices of a state-space representation of the (input) sensitivity function. +matrices_T, _ = get_comp_sensitivity(sys, :plant_input) +``` + +Continued linear analysis and design can be performed using ControlSystemsBase.jl. +Create `ControlSystemsBase.StateSpace` objects using + +```julia +using ControlSystemsBase, Plots +S = ss(matrices_S...) +T = ss(matrices_T...) +bodeplot([S, T], lab = ["S" "T"]) +``` + +The sensitivity functions obtained this way should be equivalent to the ones obtained with the code below + +```julia +using ControlSystemsBase +P = tf(1.0, [1, 1]) +C = 1 # Negative feedback assumed in ControlSystems +S = sensitivity(P, C) # or feedback(1, P*C) +T = comp_sensitivity(P, C) # or feedback(P*C) +``` """ struct AnalysisPoint + """ + The input to the connection. In the context of ModelingToolkitStandardLibrary.jl, + this is a `RealOutput` connector. + """ input::Any + """ + The name of the analysis point. + """ name::Symbol + """ + The outputs of the connection. In the context of ModelingToolkitStandardLibrary.jl, + these are all `RealInput` connectors. + """ outputs::Union{Nothing, Vector{Any}} end AnalysisPoint() = AnalysisPoint(nothing, Symbol(), nothing) +""" + $(TYPEDSIGNATURES) + +Create an `AnalysisPoint` with the given name, with no input or outputs specified. +""" AnalysisPoint(name::Symbol) = AnalysisPoint(nothing, name, nothing) Base.nameof(ap::AnalysisPoint) = ap.name @@ -78,9 +138,35 @@ function Symbolics.connect(in, ap::AnalysisPoint, outs...) end """ - $(TYPEDSIGNATURES) + connect(output_connector, ap_name::Symbol, input_connector; verbose = true) + connect(output_connector, ap::AnalysisPoint, input_connector; verbose = true) + +Connect `output_connector` and `input_connector` with an [`AnalysisPoint`](@ref) inbetween. +The incoming connection `output_connector` is expected to be an output connector (for +example, `ModelingToolkitStandardLibrary.Blocks.RealOutput`), and vice versa. + +*PLEASE NOTE*: The connection is assumed to be *causal*, meaning that -Create an `AnalysisPoint` connection connecting `in` to `outs...`. +```julia +@named P = FirstOrder(k = 1, T = 1) +@named C = Gain(; k = -1) +connect(C.output, :plant_input, P.input) +``` + +is correct, whereas + +```julia +connect(P.input, :plant_input, C.output) +``` + +typically is not (unless the model is an inverse model). + +# Arguments: + + - `output_connector`: An output connector + - `input_connector`: An input connector + - `ap`: An explicitly created [`AnalysisPoint`](@ref) + - `ap_name`: If a name is given, an [`AnalysisPoint`](@ref) with the given name will be created automatically. """ function Symbolics.connect(in::AbstractSystem, name::Symbol, out, outs...) return AnalysisPoint() ~ AnalysisPoint(in, name, [out; collect(outs)]) @@ -198,19 +284,23 @@ function modify_nested_subsystem( # recursive helper function which does the searching and modification function _helper(sys::AbstractSystem, i::Int) - # we reached past the end, so everything matched and - # `sys` is the system to modify. if i > length(hierarchy) + # we reached past the end, so everything matched and + # `sys` is the system to modify. sys, vars = fn(sys) else + # find the subsystem with the given name and error otherwise cur = hierarchy[i] idx = findfirst(subsys -> nameof(subsys) == cur, get_systems(sys)) idx === nothing && error("System $(join([nameof(root); hierarchy[1:i-1]], '.')) does not have a subsystem named $cur.") + # recurse into new subsystem newsys, vars = _helper(get_systems(sys)[idx], i + 1) + # update this system with modified subsystem @set! sys.systems[idx] = newsys end + # only namespace variables from inner systems if i != 1 vars = ntuple(Val(length(vars))) do i renamespace(sys, vars[i]) @@ -270,6 +360,15 @@ end #### PRIMITIVE TRANSFORMATIONS +const DOC_WILL_REMOVE_AP = """ + Note that this transformation will remove `ap`, causing any subsequent transformations \ + referring to it to fail.\ + """ + +const DOC_ADDED_VARIABLE = """ + The added variable(s) will have a default of zero, of the appropriate type and size.\ + """ + """ $(TYPEDEF) @@ -278,11 +377,9 @@ it will add a new input variable which connects to the outputs of the analysis p `apply_transformation` returns the new input variable (if added) as the auxiliary information. The new input variable will have the name `Symbol(:d_, nameof(ap))`. -Note that this transformation will remove `ap`, causing any subsequent transformations -referring to it to fail. +$DOC_WILL_REMOVE_AP -The added variable, if present, will have a default of zero, of the appropriate type and -size. +$DOC_ADDED_VARIABLE ## Fields @@ -340,9 +437,7 @@ end $(TYPEDEF) A transformation which returns the variable corresponding to the input of the analysis -point. - -`apply_transformation` returns the variable as auxiliary information. +point. Does not modify the system. ## Fields @@ -350,7 +445,7 @@ $(TYPEDFIELDS) """ struct GetInput <: AnalysisPointTransformation """ - The analysis point to add the output to. + The analysis point to get the input of. """ ap::AnalysisPoint end @@ -380,15 +475,12 @@ the analysis point before connecting to the outputs. The new variable will have If `with_output == true`, also creates an additional new variable which has the value provided to the outputs after the above modification. This new variable has the same name -as the analysis point. +as the analysis point and will be the second variable in the tuple of new variables returned +from `apply_transformation`. -`apply_transformation` returns a 1-tuple of the perturbation variable if -`with_output == false` and a 2-tuple of the perturbation variable and output variable if -`with_output == true`. +$DOC_WILL_REMOVE_AP -Removes the analysis point `ap`, so any subsequent transformations requiring it will fail. - -The added variable(s) will have a default of zero, of the appropriate type and size. +$DOC_ADDED_VARIABLE ## Fields @@ -454,17 +546,33 @@ function apply_transformation(tf::PerturbOutput, sys::AbstractSystem) end """ - $(TYPEDSIGNATURES) + $(TYPEDEF) A transformation which adds a variable named `name` to the system containing the analysis -point `ap`. The added variable has the same type and size as the input of the analysis -point. +point `ap`. $DOC_ADDED_VARIABLE + +# Fields + +$(TYPEDFIELDS) """ struct AddVariable <: AnalysisPointTransformation + """ + The analysis point in the system to modify, and whose input should be used as the + template for the new variable. + """ ap::AnalysisPoint + """ + The name of the added variable. + """ name::Symbol end +""" + $(TYPEDSIGNATURES) + +Add a new variable to the system containing analysis point `ap` with the same name as the +analysis point. +""" AddVariable(ap::AnalysisPoint) = AddVariable(ap, nameof(ap)) function apply_transformation(tf::AddVariable, sys::AbstractSystem) @@ -494,11 +602,12 @@ end $(TYPEDSIGNATURES) A transformation enable calculating the sensitivity function about the analysis point `ap`. -`apply_transformation` returns a 2-tuple `du, u` as auxiliary information. +The returned added variables are `(du, u)` where `du` is the perturbation added to the +input, and `u` is the output after perturbation. -Removes the analysis point `ap`, so any subsequent transformations requiring it will fail. +$DOC_WILL_REMOVE_AP -The added variables will have a default of zero, of the appropriate type and size. +$DOC_ADDED_VARIABLE """ SensitivityTransform(ap::AnalysisPoint) = PerturbOutput(ap, true) @@ -506,14 +615,21 @@ SensitivityTransform(ap::AnalysisPoint) = PerturbOutput(ap, true) $(TYPEDEF) A transformation to enable calculating the complementary sensitivity function about the -analysis point `ap`. `apply_transformation` returns a 2-tuple `du, u` as auxiliary -information. +analysis point `ap`. The returned added variables are `(du, u)` where `du` is the +perturbation added to the outputs and `u` is the input to the analysis point. + +$DOC_WILL_REMOVE_AP + +$DOC_ADDED_VARIABLE -Removes the analysis point `ap`, so any subsequent transformations requiring it will fail. +# Fields -The added variables will have a default of zero, of the appropriate type and size. +$(TYPEDFIELDS) """ struct ComplementarySensitivityTransform <: AnalysisPointTransformation + """ + The analysis point to modify. + """ ap::AnalysisPoint end @@ -537,7 +653,25 @@ function apply_transformation(cst::ComplementarySensitivityTransform, sys::Abstr return sys, (du, u) end +""" + $(TYPEDEF) + +A transformation to enable calculating the loop transfer function about the analysis point +`ap`. The returned added variables are `(du, u)` where `du` feeds into the outputs of `ap` +and `u` is the input of `ap`. + +$DOC_WILL_REMOVE_AP + +$DOC_ADDED_VARIABLE + +# Fields + +$(TYPEDFIELDS) +""" struct LoopTransferTransform <: AnalysisPointTransformation + """ + The analysis point to modify. + """ ap::AnalysisPoint end @@ -547,8 +681,13 @@ function apply_transformation(tf::LoopTransferTransform, sys::AbstractSystem) return sys, (du, u) end -### TODO: Move these +""" + $(TYPEDSIGNATURES) +A utility function to get the "canonical" form of a list of analysis points. Always returns +a list of values. Any value that cannot be turned into an `AnalysisPoint` (i.e. isn't +already an `AnalysisPoint` or `Symbol`) is simply wrapped in an array. +""" canonicalize_ap(ap::Symbol) = [AnalysisPoint(ap)] canonicalize_ap(ap::AnalysisPoint) = [ap] canonicalize_ap(ap) = [ap] @@ -556,6 +695,11 @@ function canonicalize_ap(aps::Vector) mapreduce(canonicalize_ap, vcat, aps; init = []) end +""" + $(TYPEDSIGNATURES) + +Given a list of analysis points, break the connection for each and set the output to zero. +""" function handle_loop_openings(sys::AbstractSystem, aps) for ap in canonicalize_ap(aps) sys, (outvar,) = apply_transformation(Break(ap, true), sys) @@ -568,63 +712,124 @@ function handle_loop_openings(sys::AbstractSystem, aps) return sys end -function get_sensitivity_function( - sys::AbstractSystem, aps; system_modifier = identity, loop_openings = [], kwargs...) +const DOC_LOOP_OPENINGS = """ + - `loop_openings`: A list of analysis points whose connections should be removed and + the outputs set to zero as a part of the linear analysis. +""" + +const DOC_SYS_MODIFIER = """ + - `system_modifier`: A function taking the transformed system and applying any + additional transformations, returning the modified system. The modified system + is passed to `linearization_function`. +""" +""" + $(TYPEDSIGNATURES) + +Utility function for linear analyses that apply a transformation `transform`, which +returns the added variables `(du, u)`, to each of the analysis points in `aps` and then +calls `linearization_function` with all the `du`s as inputs and `u`s as outputs. Returns +the linearization function and modified, simplified system. + +# Keyword arguments + +$DOC_LOOP_OPENINGS +$DOC_SYS_MODIFIER + +All other keyword arguments are forwarded to `linearization_function`. +""" +function get_linear_analysis_function( + sys::AbstractSystem, transform, aps; system_modifier = identity, loop_openings = [], kwargs...) sys = handle_loop_openings(sys, loop_openings) aps = canonicalize_ap(aps) dus = [] us = [] for ap in aps - sys, (du, u) = apply_transformation(SensitivityTransform(ap), sys) + sys, (du, u) = apply_transformation(transform(ap), sys) push!(dus, du) push!(us, u) end linearization_function(system_modifier(sys), dus, us; kwargs...) end -function get_comp_sensitivity_function( - sys::AbstractSystem, aps; system_modifier = identity, loop_openings = [], kwargs...) - sys = handle_loop_openings(sys, loop_openings) - aps = canonicalize_ap(aps) - dus = [] - us = [] - for ap in aps - sys, (du, u) = apply_transformation(ComplementarySensitivityTransform(ap), sys) - push!(dus, du) - push!(us, u) - end - linearization_function(system_modifier(sys), dus, us; kwargs...) +""" + $(TYPEDSIGNATURES) + +Return the sensitivity function for the analysis point(s) `aps`, and the modified system +simplified with the appropriate inputs and outputs. + +# Keyword Arguments + +$DOC_LOOP_OPENINGS +$DOC_SYS_MODIFIER + +All other keyword arguments are forwarded to `linearization_function`. +""" +function get_sensitivity_function(sys::AbstractSystem, aps; kwargs...) + get_linear_analysis_function(sys, SensitivityTransform, aps; kwargs...) end -function get_looptransfer_function( - sys, aps; system_modifier = identity, loop_openings = [], kwargs...) - sys = handle_loop_openings(sys, loop_openings) - aps = canonicalize_ap(aps) - dus = [] - us = [] - for ap in aps - sys, (du, u) = apply_transformation(LoopTransferTransform(ap), sys) - push!(dus, du) - push!(us, u) - end - linearization_function(system_modifier(sys), dus, us; kwargs...) +""" + $(TYPEDSIGNATURES) + +Return the complementary sensitivity function for the analysis point(s) `aps`, and the +modified system simplified with the appropriate inputs and outputs. + +# Keyword Arguments + +$DOC_LOOP_OPENINGS +$DOC_SYS_MODIFIER + +All other keyword arguments are forwarded to `linearization_function`. +""" +function get_comp_sensitivity_function(sys::AbstractSystem, aps; kwargs...) + get_linear_analysis_function(sys, ComplementarySensitivityTransform, aps; kwargs...) +end + +""" + $(TYPEDSIGNATURES) + +Return the loop-transfer function for the analysis point(s) `aps`, and the modified +system simplified with the appropriate inputs and outputs. + +# Keyword Arguments + +$DOC_LOOP_OPENINGS +$DOC_SYS_MODIFIER + +All other keyword arguments are forwarded to `linearization_function`. +""" +function get_looptransfer_function(sys::AbstractSystem, aps; kwargs...) + get_linear_analysis_function(sys, LoopTransferTransform, aps; kwargs...) end for f in [:get_sensitivity, :get_comp_sensitivity, :get_looptransfer] + utility_fun = Symbol(f, :_function) @eval function $f( sys, ap, args...; loop_openings = [], system_modifier = identity, kwargs...) - lin_fun, ssys = $(Symbol(f, :_function))( + lin_fun, ssys = $(utility_fun)( sys, ap, args...; loop_openings, system_modifier, kwargs...) ModelingToolkit.linearize(ssys, lin_fun; kwargs...), ssys end end -function open_loop(sys, ap::Union{Symbol, AnalysisPoint}; kwargs...) +""" + $(TYPEDSIGNATURES) + +Apply `LoopTransferTransform` to the analysis point `ap` and return the +result of `apply_transformation`. + +# Keyword Arguments + +- `system_modifier`: a function which takes the modified system and returns a new system + with any required further modifications peformed. +""" +function open_loop(sys, ap::Union{Symbol, AnalysisPoint}; system_modifier = identity) if ap isa Symbol ap = AnalysisPoint(ap) end tf = LoopTransferTransform(ap) - return apply_transformation(tf, sys) + sys, vars = apply_transformation(tf, sys) + return system_modifier(sys), vars end function linearization_function(sys::AbstractSystem, @@ -660,3 +865,46 @@ function linearization_function(sys::AbstractSystem, return linearization_function(system_modifier(sys), input_vars, output_vars; kwargs...) end + +@doc """ + get_sensitivity(sys, ap::AnalysisPoint; kwargs) + get_sensitivity(sys, ap_name::Symbol; kwargs) + +Compute the sensitivity function in analysis point `ap`. The sensitivity function is obtained by introducing an infinitesimal perturbation `d` at the input of `ap`, linearizing the system and computing the transfer function between `d` and the output of `ap`. + +# Arguments: + + - `kwargs`: Are sent to `ModelingToolkit.linearize` + +See also [`get_comp_sensitivity`](@ref), [`get_looptransfer`](@ref). +""" get_sensitivity + +@doc """ + get_comp_sensitivity(sys, ap::AnalysisPoint; kwargs) + get_comp_sensitivity(sys, ap_name::Symbol; kwargs) + +Compute the complementary sensitivity function in analysis point `ap`. The complementary sensitivity function is obtained by introducing an infinitesimal perturbation `d` at the output of `ap`, linearizing the system and computing the transfer function between `d` and the input of `ap`. + +# Arguments: + + - `kwargs`: Are sent to `ModelingToolkit.linearize` + +See also [`get_sensitivity`](@ref), [`get_looptransfer`](@ref). +""" get_comp_sensitivity + +@doc """ + get_looptransfer(sys, ap::AnalysisPoint; kwargs) + get_looptransfer(sys, ap_name::Symbol; kwargs) + +Compute the (linearized) loop-transfer function in analysis point `ap`, from `ap.out` to `ap.in`. + +!!! info "Negative feedback" + + Feedback loops often use negative feedback, and the computed loop-transfer function will in this case have the negative feedback included. Standard analysis tools often assume a loop-transfer function without the negative gain built in, and the result of this function may thus need negation before use. + +# Arguments: + + - `kwargs`: Are sent to `ModelingToolkit.linearize` + +See also [`get_sensitivity`](@ref), [`get_comp_sensitivity`](@ref), [`open_loop`](@ref). +""" get_looptransfer From ddceb6b6127c89e78308d04275c0e652b7bc87b3 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 3 Jan 2025 17:29:22 +0530 Subject: [PATCH 0531/1337] refactor: use version of Stdlib which disables old `AnalysisPoint` --- Project.toml | 1 + test/analysis_points.jl | 15 ++------------- test/downstream/Project.toml | 3 +++ test/downstream/analysis_points.jl | 3 +-- 4 files changed, 7 insertions(+), 15 deletions(-) diff --git a/Project.toml b/Project.toml index e4eba3cfad..256b1de6fc 100644 --- a/Project.toml +++ b/Project.toml @@ -117,6 +117,7 @@ Latexify = "0.11, 0.12, 0.13, 0.14, 0.15, 0.16" Libdl = "1" LinearAlgebra = "1" MLStyle = "0.4.17" +ModelingToolkitStandardLibrary = "2.19" NaNMath = "0.3, 1" NonlinearSolve = "4.3" OffsetArrays = "1" diff --git a/test/analysis_points.jl b/test/analysis_points.jl index c888261db2..9361b5a686 100644 --- a/test/analysis_points.jl +++ b/test/analysis_points.jl @@ -1,8 +1,7 @@ using ModelingToolkit, ModelingToolkitStandardLibrary.Blocks using OrdinaryDiffEq, LinearAlgebra using Test -using ModelingToolkit: t_nounits as t, D_nounits as D, AnalysisPoint, get_sensitivity, - get_comp_sensitivity, get_looptransfer, open_loop, AbstractSystem +using ModelingToolkit: t_nounits as t, D_nounits as D, AnalysisPoint, AbstractSystem using Symbolics: NAMESPACE_SEPARATOR @testset "AnalysisPoint is lowered to `connect`" begin @@ -164,17 +163,7 @@ end (inputap, [nameof(outputap)]), (nameof(inputap), [nameof(outputap)]) ] - if input isa Symbol - # broken because MTKStdlib defines the method for - # `input::Union{Symbol, Vector{Symbol}}` which we can't directly call - @test_broken linearize(sys, input, output) - linfun, ssys = @invoke linearization_function(sys::AbstractSystem, - input::Union{Symbol, Vector{Symbol}, AnalysisPoint, Vector{AnalysisPoint}}, - output::Any) - matrices = linearize(ssys, linfun) - else - matrices, _ = linearize(sys, input, output) - end + matrices, _ = linearize(sys, input, output) # Result should be the same as feedpack(P, 1), i.e., the closed-loop transfer function from plant input to plant output @test matrices.A[] == -2 @test matrices.B[] * matrices.C[] == 1 # both positive diff --git a/test/downstream/Project.toml b/test/downstream/Project.toml index b8776e1e4f..ade09e797b 100644 --- a/test/downstream/Project.toml +++ b/test/downstream/Project.toml @@ -5,3 +5,6 @@ ModelingToolkit = "961ee093-0014-501f-94e3-6117800e7a78" ModelingToolkitStandardLibrary = "16a59e39-deab-5bd0-87e4-056b12336739" OrdinaryDiffEq = "1dea7af3-3e70-54e6-95c3-0bf5283fa5ed" SymbolicIndexingInterface = "2efcf032-c050-4f8e-a9bb-153293bab1f5" + +[compat] +ModelingToolkitStandardLibrary = "2.19" diff --git a/test/downstream/analysis_points.jl b/test/downstream/analysis_points.jl index 85d271a547..fabca0f493 100644 --- a/test/downstream/analysis_points.jl +++ b/test/downstream/analysis_points.jl @@ -1,8 +1,7 @@ using ModelingToolkit, OrdinaryDiffEq, LinearAlgebra, ControlSystemsBase using ModelingToolkitStandardLibrary.Mechanical.Rotational using ModelingToolkitStandardLibrary.Blocks -using ModelingToolkit: connect, AnalysisPoint, t_nounits as t, D_nounits as D, - get_sensitivity, get_comp_sensitivity, get_looptransfer, open_loop +using ModelingToolkit: connect, t_nounits as t, D_nounits as D import ControlSystemsBase as CS @testset "Complicated model" begin From de17b878e35ca7fb05c5af2a262f1c9ef799c51e Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 6 Jan 2025 12:31:29 +0530 Subject: [PATCH 0532/1337] feat: validate causality of analysis points --- src/systems/analysis_points.jl | 54 ++++++++++++++++++++++++++++------ test/analysis_points.jl | 10 +++++++ 2 files changed, 55 insertions(+), 9 deletions(-) diff --git a/src/systems/analysis_points.jl b/src/systems/analysis_points.jl index 9ff06109e5..a2b7fb7ea8 100644 --- a/src/systems/analysis_points.jl +++ b/src/systems/analysis_points.jl @@ -72,6 +72,36 @@ struct AnalysisPoint these are all `RealInput` connectors. """ outputs::Union{Nothing, Vector{Any}} + + function AnalysisPoint(input, name::Symbol, outputs; verbose = true) + # input to analysis point should be an output variable + if verbose && input !== nothing + var = ap_var(input) + isoutput(var) || ap_warning(1, name, true) + end + # outputs of analysis points should be input variables + if verbose && outputs !== nothing + for (i, output) in enumerate(outputs) + var = ap_var(output) + isinput(var) || ap_warning(2 + i, name, false) + end + end + + return new(input, name, outputs) + end +end + +function ap_warning(arg::Int, name::Symbol, should_be_output) + causality = should_be_output ? "output" : "input" + @warn """ + The $(arg)-th argument to analysis point $(name) was not a $causality. This is supported in \ + order to handle inverse models, but may not be what you intended. + + If you are building a forward mode (causal), you may want to swap this argument with \ + one on the opposite side of the name of the analysis point provided to `connect`. \ + Learn more about the causality of analysis points in the docstring for `AnalysisPoint`. \ + Silence this message using `connect(out, :name, in...; warn = false)`. + """ end AnalysisPoint() = AnalysisPoint(nothing, Symbol(), nothing) @@ -133,8 +163,8 @@ function renamespace(sys, ap::AnalysisPoint) end # create analysis points via `connect` -function Symbolics.connect(in, ap::AnalysisPoint, outs...) - return AnalysisPoint() ~ AnalysisPoint(in, ap.name, collect(outs)) +function Symbolics.connect(in, ap::AnalysisPoint, outs...; verbose = true) + return AnalysisPoint() ~ AnalysisPoint(in, ap.name, collect(outs); verbose) end """ @@ -161,15 +191,21 @@ connect(P.input, :plant_input, C.output) typically is not (unless the model is an inverse model). -# Arguments: +# Arguments + +- `output_connector`: An output connector +- `input_connector`: An input connector +- `ap`: An explicitly created [`AnalysisPoint`](@ref) +- `ap_name`: If a name is given, an [`AnalysisPoint`](@ref) with the given name will be + created automatically. + +# Keyword arguments - - `output_connector`: An output connector - - `input_connector`: An input connector - - `ap`: An explicitly created [`AnalysisPoint`](@ref) - - `ap_name`: If a name is given, an [`AnalysisPoint`](@ref) with the given name will be created automatically. +- `verbose`: Warn if an input is connected to an output (reverse causality). Silence this + warning if you are analyzing an inverse model. """ -function Symbolics.connect(in::AbstractSystem, name::Symbol, out, outs...) - return AnalysisPoint() ~ AnalysisPoint(in, name, [out; collect(outs)]) +function Symbolics.connect(in::AbstractSystem, name::Symbol, out, outs...; verbose = true) + return AnalysisPoint() ~ AnalysisPoint(in, name, [out; collect(outs)]; verbose) end """ diff --git a/test/analysis_points.jl b/test/analysis_points.jl index 9361b5a686..8c93e919b9 100644 --- a/test/analysis_points.jl +++ b/test/analysis_points.jl @@ -24,6 +24,16 @@ using Symbolics: NAMESPACE_SEPARATOR @test isequal(sys_ap2, sys_normal2) end +@testset "Inverse causality throws a warning" begin + @named P = FirstOrder(k = 1, T = 1) + @named C = Gain(; k = -1) + + ap = AnalysisPoint(:plant_input) + @test_warn ["1-th argument", "plant_input", "not a output"] connect( + P.input, ap, C.output) + @test_nowarn connect(P.input, ap, C.output; verbose = false) +end + # also tests `connect(input, name::Symbol, outputs...)` syntax @testset "AnalysisPoint is accessible via `getproperty`" begin @named P = FirstOrder(k = 1, T = 1) From 50973353bb7da69dc35d838f90177f5ae1b461d1 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 6 Jan 2025 14:06:29 +0530 Subject: [PATCH 0533/1337] fix: fix `isvarkind` for `Symbolics.Arr` --- src/variables.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/variables.jl b/src/variables.jl index 3b0d64e7ea..c45c4b0f00 100644 --- a/src/variables.jl +++ b/src/variables.jl @@ -90,7 +90,7 @@ struct Equality <: AbstractConnectType end # Equality connection struct Flow <: AbstractConnectType end # sum to 0 struct Stream <: AbstractConnectType end # special stream connector -isvarkind(m, x::Num) = isvarkind(m, value(x)) +isvarkind(m, x::Union{Num, Symbolics.Arr}) = isvarkind(m, value(x)) function isvarkind(m, x) iskind = getmetadata(x, m, nothing) iskind !== nothing && return iskind From 2b1f525cb036dadec10bb8c34503e480240e4d5c Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 6 Jan 2025 14:55:13 +0530 Subject: [PATCH 0534/1337] fix: remove ambiguities in namespacing of analysis points --- src/systems/analysis_points.jl | 34 ++++++++--------- test/analysis_points.jl | 59 ++++++++++++------------------ test/downstream/analysis_points.jl | 16 ++++---- 3 files changed, 48 insertions(+), 61 deletions(-) diff --git a/src/systems/analysis_points.jl b/src/systems/analysis_points.jl index a2b7fb7ea8..87f278d4dc 100644 --- a/src/systems/analysis_points.jl +++ b/src/systems/analysis_points.jl @@ -314,9 +314,10 @@ function modify_nested_subsystem( return fn(root) end # ignore the name of the root - if nameof(root) == hierarchy[1] - hierarchy = @view hierarchy[2:end] + if nameof(root) != hierarchy[1] + error("The name of the root system $(nameof(root)) must be included in the name passed to `modify_nested_subsystem`") end + hierarchy = @view hierarchy[2:end] # recursive helper function which does the searching and modification function _helper(sys::AbstractSystem, i::Int) @@ -722,13 +723,14 @@ end A utility function to get the "canonical" form of a list of analysis points. Always returns a list of values. Any value that cannot be turned into an `AnalysisPoint` (i.e. isn't -already an `AnalysisPoint` or `Symbol`) is simply wrapped in an array. +already an `AnalysisPoint` or `Symbol`) is simply wrapped in an array. `Symbol` names of +`AnalysisPoint`s are namespaced with `sys`. """ -canonicalize_ap(ap::Symbol) = [AnalysisPoint(ap)] -canonicalize_ap(ap::AnalysisPoint) = [ap] -canonicalize_ap(ap) = [ap] -function canonicalize_ap(aps::Vector) - mapreduce(canonicalize_ap, vcat, aps; init = []) +canonicalize_ap(sys::AbstractSystem, ap::Symbol) = [AnalysisPoint(renamespace(sys, ap))] +canonicalize_ap(sys::AbstractSystem, ap::AnalysisPoint) = [ap] +canonicalize_ap(sys::AbstractSystem, ap) = [ap] +function canonicalize_ap(sys::AbstractSystem, aps::Vector) + mapreduce(Base.Fix1(canonicalize_ap, sys), vcat, aps; init = []) end """ @@ -737,7 +739,7 @@ end Given a list of analysis points, break the connection for each and set the output to zero. """ function handle_loop_openings(sys::AbstractSystem, aps) - for ap in canonicalize_ap(aps) + for ap in canonicalize_ap(sys, aps) sys, (outvar,) = apply_transformation(Break(ap, true), sys) if Symbolics.isarraysymbolic(outvar) push!(get_eqs(sys), outvar ~ zeros(size(outvar))) @@ -776,7 +778,7 @@ All other keyword arguments are forwarded to `linearization_function`. function get_linear_analysis_function( sys::AbstractSystem, transform, aps; system_modifier = identity, loop_openings = [], kwargs...) sys = handle_loop_openings(sys, loop_openings) - aps = canonicalize_ap(aps) + aps = canonicalize_ap(sys, aps) dus = [] us = [] for ap in aps @@ -860,9 +862,7 @@ result of `apply_transformation`. with any required further modifications peformed. """ function open_loop(sys, ap::Union{Symbol, AnalysisPoint}; system_modifier = identity) - if ap isa Symbol - ap = AnalysisPoint(ap) - end + ap = only(canonicalize_ap(sys, ap)) tf = LoopTransferTransform(ap) sys, vars = apply_transformation(tf, sys) return system_modifier(sys), vars @@ -871,9 +871,9 @@ end function linearization_function(sys::AbstractSystem, inputs::Union{Symbol, Vector{Symbol}, AnalysisPoint, Vector{AnalysisPoint}}, outputs; loop_openings = [], system_modifier = identity, kwargs...) - loop_openings = Set(map(nameof, canonicalize_ap(loop_openings))) - inputs = canonicalize_ap(inputs) - outputs = canonicalize_ap(outputs) + loop_openings = Set(map(nameof, canonicalize_ap(sys, loop_openings))) + inputs = canonicalize_ap(sys, inputs) + outputs = canonicalize_ap(sys, outputs) input_vars = [] for input in inputs @@ -897,7 +897,7 @@ function linearization_function(sys::AbstractSystem, push!(output_vars, output_var) end - sys = handle_loop_openings(sys, collect(loop_openings)) + sys = handle_loop_openings(sys, map(AnalysisPoint, collect(loop_openings))) return linearization_function(system_modifier(sys), input_vars, output_vars; kwargs...) end diff --git a/test/analysis_points.jl b/test/analysis_points.jl index 8c93e919b9..9e8e3fb455 100644 --- a/test/analysis_points.jl +++ b/test/analysis_points.jl @@ -2,6 +2,7 @@ using ModelingToolkit, ModelingToolkitStandardLibrary.Blocks using OrdinaryDiffEq, LinearAlgebra using Test using ModelingToolkit: t_nounits as t, D_nounits as D, AnalysisPoint, AbstractSystem +import ModelingToolkit as MTK using Symbolics: NAMESPACE_SEPARATOR @testset "AnalysisPoint is lowered to `connect`" begin @@ -69,26 +70,21 @@ sys = ODESystem(eqs, t, systems = [P, C], name = :hej) @test norm(sol.u[end]) < 1e-6 # This fails without the feedback through C end -@testset "get_sensitivity - $name" for (name, sys, ap) in [ +test_cases = [ ("inner", sys, sys.plant_input), ("nested", nested_sys, nested_sys.hej.plant_input), - ("inner - nonamespace", sys, :plant_input), - ("inner - Symbol", sys, nameof(sys.plant_input)), - ("nested - Symbol", nested_sys, nameof(nested_sys.hej.plant_input)) + ("inner - Symbol", sys, :plant_input), + ("nested - Symbol", nested_sys, nameof(sys.plant_input)) ] + +@testset "get_sensitivity - $name" for (name, sys, ap) in test_cases matrices, _ = get_sensitivity(sys, ap) @test matrices.A[] == -2 @test matrices.B[] * matrices.C[] == -1 # either one negative @test matrices.D[] == 1 end -@testset "get_comp_sensitivity - $name" for (name, sys, ap) in [ - ("inner", sys, sys.plant_input), - ("nested", nested_sys, nested_sys.hej.plant_input), - ("inner - nonamespace", sys, :plant_input), - ("inner - Symbol", sys, nameof(sys.plant_input)), - ("nested - Symbol", nested_sys, nameof(nested_sys.hej.plant_input)) -] +@testset "get_comp_sensitivity - $name" for (name, sys, ap) in test_cases matrices, _ = get_comp_sensitivity(sys, ap) @test matrices.A[] == -2 @test matrices.B[] * matrices.C[] == 1 # both positive or negative @@ -104,13 +100,7 @@ S = sensitivity(P, C) # or feedback(1, P*C) T = comp_sensitivity(P, C) # or feedback(P*C) =# -@testset "get_looptransfer - $name" for (name, sys, ap) in [ - ("inner", sys, sys.plant_input), - ("nested", nested_sys, nested_sys.hej.plant_input), - ("inner - nonamespace", sys, :plant_input), - ("inner - Symbol", sys, nameof(sys.plant_input)), - ("nested - Symbol", nested_sys, nameof(nested_sys.hej.plant_input)) -] +@testset "get_looptransfer - $name" for (name, sys, ap) in test_cases matrices, _ = get_looptransfer(sys, ap) @test matrices.A[] == -1 @test matrices.B[] * matrices.C[] == -1 # either one negative @@ -125,13 +115,7 @@ C = -1 L = P*C =# -@testset "open_loop - $name" for (name, sys, ap) in [ - ("inner", sys, sys.plant_input), - ("nested", nested_sys, nested_sys.hej.plant_input), - ("inner - nonamespace", sys, :plant_input), - ("inner - Symbol", sys, nameof(sys.plant_input)), - ("nested - Symbol", nested_sys, nameof(nested_sys.hej.plant_input)) -] +@testset "open_loop - $name" for (name, sys, ap) in test_cases open_sys, (du, u) = open_loop(sys, ap) matrices, _ = linearize(open_sys, [du], [u]) @test matrices.A[] == -1 @@ -146,13 +130,14 @@ eqs = [connect(P.output, :plant_output, C.input) sys = ODESystem(eqs, t, systems = [P, C], name = :hej) @named nested_sys = ODESystem(Equation[], t; systems = [sys]) -@testset "get_sensitivity - $name" for (name, sys, ap) in [ +test_cases = [ ("inner", sys, sys.plant_input), ("nested", nested_sys, nested_sys.hej.plant_input), - ("inner - nonamespace", sys, :plant_input), - ("inner - Symbol", sys, nameof(sys.plant_input)), - ("nested - Symbol", nested_sys, nameof(nested_sys.hej.plant_input)) + ("inner - Symbol", sys, :plant_input), + ("nested - Symbol", nested_sys, nameof(sys.plant_input)) ] + +@testset "get_sensitivity - $name" for (name, sys, ap) in test_cases matrices, _ = get_sensitivity(sys, ap) @test matrices.A[] == -2 @test matrices.B[] * matrices.C[] == -1 # either one negative @@ -163,15 +148,19 @@ end ("inner", sys, sys.plant_input, sys.plant_output), ("nested", nested_sys, nested_sys.hej.plant_input, nested_sys.hej.plant_output) ] + inputname = Symbol(join( + MTK.namespace_hierarchy(nameof(inputap))[2:end], NAMESPACE_SEPARATOR)) + outputname = Symbol(join( + MTK.namespace_hierarchy(nameof(outputap))[2:end], NAMESPACE_SEPARATOR)) @testset "input - $(typeof(input)), output - $(typeof(output))" for (input, output) in [ (inputap, outputap), - (nameof(inputap), outputap), - (inputap, nameof(outputap)), - (nameof(inputap), nameof(outputap)), + (inputname, outputap), + (inputap, outputname), + (inputname, outputname), (inputap, [outputap]), - (nameof(inputap), [outputap]), - (inputap, [nameof(outputap)]), - (nameof(inputap), [nameof(outputap)]) + (inputname, [outputap]), + (inputap, [outputname]), + (inputname, [outputname]) ] matrices, _ = linearize(sys, input, output) # Result should be the same as feedpack(P, 1), i.e., the closed-loop transfer function from plant input to plant output diff --git a/test/downstream/analysis_points.jl b/test/downstream/analysis_points.jl index fabca0f493..7a433cb504 100644 --- a/test/downstream/analysis_points.jl +++ b/test/downstream/analysis_points.jl @@ -64,7 +64,7 @@ import ControlSystemsBase as CS prob = ODEProblem(sys, unknowns(sys) .=> 0.0, (0.0, 4.0)) sol = solve(prob, Rodas5P(), reltol = 1e-6, abstol = 1e-9) - matrices, ssys = linearize(closed_loop, AnalysisPoint(:r), AnalysisPoint(:y)) + matrices, ssys = linearize(closed_loop, :r, :y) lsys = ss(matrices...) |> sminreal @test lsys.nx == 8 @@ -129,13 +129,11 @@ end t, systems = [P_inner, feedback, ref]) - P_not_broken, _ = linearize(sys_inner, AnalysisPoint(:u), AnalysisPoint(:y)) + P_not_broken, _ = linearize(sys_inner, :u, :y) @test P_not_broken.A[] == -2 - P_broken, _ = linearize(sys_inner, AnalysisPoint(:u), AnalysisPoint(:y), - loop_openings = [AnalysisPoint(:u)]) + P_broken, _ = linearize(sys_inner, :u, :y, loop_openings = [:u]) @test P_broken.A[] == -1 - P_broken, _ = linearize(sys_inner, AnalysisPoint(:u), AnalysisPoint(:y), - loop_openings = [AnalysisPoint(:y)]) + P_broken, _ = linearize(sys_inner, :u, :y, loop_openings = [:y]) @test P_broken.A[] == -1 Sinner = sminreal(ss(get_sensitivity(sys_inner, :u)[1]...)) @@ -154,10 +152,10 @@ end t, systems = [P_outer, sys_inner]) - Souter = sminreal(ss(get_sensitivity(sys_outer, sys_inner.u)[1]...)) + Souter = sminreal(ss(get_sensitivity(sys_outer, sys_outer.sys_inner.u)[1]...)) Sinner2 = sminreal(ss(get_sensitivity( - sys_outer, sys_inner.u, loop_openings = [:y2])[1]...)) + sys_outer, sys_outer.sys_inner.u, loop_openings = [:y2])[1]...)) @test Sinner.nx == 1 @test Sinner == Sinner2 @@ -268,7 +266,7 @@ end @test tf(L[2, 1]) ≈ tf(Ps) matrices, _ = linearize( - sys_outer, [sys_outer.inner.plant_input], [sys_inner.plant_output]) + sys_outer, [sys_outer.inner.plant_input], [nameof(sys_inner.plant_output)]) G = CS.ss(matrices...) |> sminreal @test tf(G) ≈ tf(CS.feedback(Ps, Cs)) end From c93df8fb418af2db96ca215028657ae8f9f73e5f Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 6 Jan 2025 15:13:50 +0530 Subject: [PATCH 0535/1337] refactor: do not export individual analysis point transformations --- src/ModelingToolkit.jl | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/ModelingToolkit.jl b/src/ModelingToolkit.jl index 29288a5742..7462122998 100644 --- a/src/ModelingToolkit.jl +++ b/src/ModelingToolkit.jl @@ -293,8 +293,7 @@ export MTKParameters, reorder_dimension_by_tunables!, reorder_dimension_by_tunab export HomotopyContinuationProblem -export AnalysisPoint, Break, PerturbOutput, SampleInput, SensitivityTransform, - ComplementarySensitivityTransform, LoopTransferTransform, apply_transformation, - get_sensitivity_function, get_comp_sensitivity_function, get_looptransfer_function, - get_sensitivity, get_comp_sensitivity, get_looptransfer, open_loop +export AnalysisPoint, get_sensitivity_function, get_comp_sensitivity_function, + get_looptransfer_function, get_sensitivity, get_comp_sensitivity, get_looptransfer, + open_loop end # module From a7650737cbcfdd99def579c193b59c4dedec04d1 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 6 Jan 2025 15:14:22 +0530 Subject: [PATCH 0536/1337] docs: port over linear analysis tutorial from MTKStdlib --- docs/Project.toml | 3 + docs/pages.jl | 3 +- docs/src/tutorials/linear_analysis.md | 152 ++++++++++++++++++++++++++ 3 files changed, 157 insertions(+), 1 deletion(-) create mode 100644 docs/src/tutorials/linear_analysis.md diff --git a/docs/Project.toml b/docs/Project.toml index a358455503..be80250603 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -1,6 +1,7 @@ [deps] BenchmarkTools = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf" BifurcationKit = "0f109fa4-8a5d-4b75-95aa-f515264e7665" +ControlSystemsBase = "aaaaaaaa-a6ca-5380-bf3e-84a91bcd477e" DataInterpolations = "82cc6244-b520-54b8-b5a6-8a565e85f1d0" DifferentialEquations = "0c46a032-eb83-5123-abaf-570d42b7fbaa" Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f" @@ -8,6 +9,7 @@ Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" DynamicQuantities = "06fc5a27-2a28-4c7c-a15d-362465fb6821" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" ModelingToolkit = "961ee093-0014-501f-94e3-6117800e7a78" +ModelingToolkitStandardLibrary = "16a59e39-deab-5bd0-87e4-056b12336739" NonlinearSolve = "8913a72c-1f9b-4ce2-8d82-65094dcecaec" Optim = "429524aa-4258-5aef-a3af-852621145aeb" Optimization = "7f7a1694-90dd-40f0-9382-eb1efda571ba" @@ -31,6 +33,7 @@ Distributions = "0.25" Documenter = "1" DynamicQuantities = "^0.11.2, 0.12, 1" ModelingToolkit = "8.33, 9" +ModelingToolkitStandardLibrary = "2.19" NonlinearSolve = "3, 4" Optim = "1.7" Optimization = "3.9, 4" diff --git a/docs/pages.jl b/docs/pages.jl index 2af487adf8..8a95de4ac5 100644 --- a/docs/pages.jl +++ b/docs/pages.jl @@ -13,7 +13,8 @@ pages = [ "tutorials/bifurcation_diagram_computation.md", "tutorials/SampledData.md", "tutorials/domain_connections.md", - "tutorials/callable_params.md"], + "tutorials/callable_params.md", + "tutorials/linear_analysis.md"], "Examples" => Any[ "Basic Examples" => Any["examples/higher_order.md", "examples/spring_mass.md", diff --git a/docs/src/tutorials/linear_analysis.md b/docs/src/tutorials/linear_analysis.md new file mode 100644 index 0000000000..7fcb67f9fb --- /dev/null +++ b/docs/src/tutorials/linear_analysis.md @@ -0,0 +1,152 @@ +# Linear Analysis + +Linear analysis refers to the process of linearizing a nonlinear model and analysing the resulting linear dynamical system. To facilitate linear analysis, ModelingToolkit provides the concept of an [`AnalysisPoint`](@ref), which can be inserted in-between two causal blocks (such as those from `ModelingToolkitStandardLibrary.Blocks` sub module). Once a model containing analysis points is built, several operations are available: + + - [`get_sensitivity`](@ref) get the [sensitivity function (wiki)](https://en.wikipedia.org/wiki/Sensitivity_(control_systems)), $S(s)$, as defined in the field of control theory. + - [`get_comp_sensitivity`](@ref) get the complementary sensitivity function $T(s) : S(s)+T(s)=1$. + - [`get_looptransfer`](@ref) get the (open) loop-transfer function where the loop starts and ends in the analysis point. For a typical simple feedback connection with a plant $P(s)$ and a controller $C(s)$, the loop-transfer function at the plant output is $P(s)C(s)$. + - [`linearize`](@ref) can be called with two analysis points denoting the input and output of the linearized system. + - [`open_loop`](@ref) return a new (nonlinear) system where the loop has been broken in the analysis point, i.e., the connection the analysis point usually implies has been removed. + +An analysis point can be created explicitly using the constructor [`AnalysisPoint`](@ref), or automatically when connecting two causal components using `connect`: + +```julia +connect(comp1.output, :analysis_point_name, comp2.input) +``` + +A single output can also be connected to multiple inputs: + +```julia +connect(comp1.output, :analysis_point_name, comp2.input, comp3.input, comp4.input) +``` + +!!! warning "Causality" + + Analysis points are *causal*, i.e., they imply a directionality for the flow of information. The order of the connections in the connect statement is thus important, i.e., `connect(out, :name, in)` is different from `connect(in, :name, out)`. + +The directionality of an analysis point can be thought of as an arrow in a block diagram, where the name of the analysis point applies to the arrow itself. + +``` +┌─────┐ ┌─────┐ +│ │ name │ │ +│ out├────────►│in │ +│ │ │ │ +└─────┘ └─────┘ +``` + +This is signified by the name being the middle argument to `connect`. + +Of the above mentioned functions, all except for [`open_loop`](@ref) return the output of [`ModelingToolkit.linearize`](@ref), which is + +```julia +matrices, simplified_sys = linearize(...) +# matrices = (; A, B, C, D) +``` + +i.e., `matrices` is a named tuple containing the matrices of a linear state-space system on the form + +```math +\begin{aligned} +\dot x &= Ax + Bu\\ +y &= Cx + Du +\end{aligned} +``` + +## Example + +The following example builds a simple closed-loop system with a plant $P$ and a controller $C$. Two analysis points are inserted, one before and one after $P$. We then derive a number of sensitivity functions and show the corresponding code using the package ControlSystemBase.jl + +```@example LINEAR_ANALYSIS +using ModelingToolkitStandardLibrary.Blocks, ModelingToolkit +@named P = FirstOrder(k = 1, T = 1) # A first-order system with pole in -1 +@named C = Gain(-1) # A P controller +t = ModelingToolkit.get_iv(P) +eqs = [connect(P.output, :plant_output, C.input) # Connect with an automatically created analysis point called :plant_output + connect(C.output, :plant_input, P.input)] +sys = ODESystem(eqs, t, systems = [P, C], name = :feedback_system) + +matrices_S = get_sensitivity(sys, :plant_input)[1] # Compute the matrices of a state-space representation of the (input)sensitivity function. +matrices_T = get_comp_sensitivity(sys, :plant_input)[1] +``` + +Continued linear analysis and design can be performed using ControlSystemsBase.jl. +We create `ControlSystemsBase.StateSpace` objects using + +```@example LINEAR_ANALYSIS +using ControlSystemsBase, Plots +S = ss(matrices_S...) +T = ss(matrices_T...) +bodeplot([S, T], lab = ["S" "" "T" ""], plot_title = "Bode plot of sensitivity functions", + margin = 5Plots.mm) +``` + +The sensitivity functions obtained this way should be equivalent to the ones obtained with the code below + +```@example LINEAR_ANALYSIS_CS +using ControlSystemsBase +P = tf(1.0, [1, 1]) |> ss +C = 1 # Negative feedback assumed in ControlSystems +S = sensitivity(P, C) # or feedback(1, P*C) +T = comp_sensitivity(P, C) # or feedback(P*C) +``` + +We may also derive the loop-transfer function $L(s) = P(s)C(s)$ using + +```@example LINEAR_ANALYSIS +matrices_L = get_looptransfer(sys, :plant_output)[1] +L = ss(matrices_L...) +``` + +which is equivalent to the following with ControlSystems + +```@example LINEAR_ANALYSIS_CS +L = P * (-C) # Add the minus sign to build the negative feedback into the controller +``` + +To obtain the transfer function between two analysis points, we call `linearize` + +```@example LINEAR_ANALYSIS +using ModelingToolkit # hide +matrices_PS = linearize(sys, :plant_input, :plant_output)[1] +``` + +this particular transfer function should be equivalent to the linear system `P(s)S(s)`, i.e., equivalent to + +```@example LINEAR_ANALYSIS_CS +feedback(P, C) +``` + +### Obtaining transfer functions + +A statespace system from [ControlSystemsBase](https://juliacontrol.github.io/ControlSystems.jl/stable/man/creating_systems/) can be converted to a transfer function using the function `tf`: + +```@example LINEAR_ANALYSIS_CS +tf(S) +``` + +## Gain and phase margins + +Further linear analysis can be performed using the [analysis methods from ControlSystemsBase](https://juliacontrol.github.io/ControlSystems.jl/stable/lib/analysis/). For example, calculating the gain and phase margins of a system can be done using + +```@example LINEAR_ANALYSIS_CS +margin(P) +``` + +(they are infinite for this system). A Nyquist plot can be produced using + +```@example LINEAR_ANALYSIS_CS +nyquistplot(P) +``` + +## Index + +```@index +Pages = ["linear_analysis.md"] +``` + +```@autodocs +Modules = [ModelingToolkit] +Pages = ["systems/analysis_points.jl"] +Order = [:function, :type] +Private = false +``` From 520daf49d589b49c05c172d8a29e353170bf6c05 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 6 Jan 2025 18:08:13 +0530 Subject: [PATCH 0537/1337] docs: remove DifferentialEquations.jl from docs environment --- docs/Project.toml | 2 -- docs/src/basics/Composition.md | 2 +- docs/src/examples/perturbation.md | 2 +- docs/src/examples/sparse_jacobians.md | 2 +- docs/src/examples/spring_mass.md | 2 +- docs/src/tutorials/acausal_components.md | 2 +- docs/src/tutorials/modelingtoolkitize.md | 4 ++-- docs/src/tutorials/ode_modeling.md | 6 +++--- docs/src/tutorials/programmatically_generating.md | 2 +- 9 files changed, 11 insertions(+), 13 deletions(-) diff --git a/docs/Project.toml b/docs/Project.toml index be80250603..40fcb16581 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -3,7 +3,6 @@ BenchmarkTools = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf" BifurcationKit = "0f109fa4-8a5d-4b75-95aa-f515264e7665" ControlSystemsBase = "aaaaaaaa-a6ca-5380-bf3e-84a91bcd477e" DataInterpolations = "82cc6244-b520-54b8-b5a6-8a565e85f1d0" -DifferentialEquations = "0c46a032-eb83-5123-abaf-570d42b7fbaa" Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f" Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" DynamicQuantities = "06fc5a27-2a28-4c7c-a15d-362465fb6821" @@ -28,7 +27,6 @@ Unitful = "1986cc42-f94f-5a68-af5c-568840ba703d" BenchmarkTools = "1.3" BifurcationKit = "0.4" DataInterpolations = "6.5" -DifferentialEquations = "7.6" Distributions = "0.25" Documenter = "1" DynamicQuantities = "^0.11.2, 0.12, 1" diff --git a/docs/src/basics/Composition.md b/docs/src/basics/Composition.md index 78dec8445b..2e5d4be831 100644 --- a/docs/src/basics/Composition.md +++ b/docs/src/basics/Composition.md @@ -56,7 +56,7 @@ x0 = [decay1.x => 1.0 p = [decay1.a => 0.1 decay2.a => 0.2] -using DifferentialEquations +using OrdinaryDiffEq prob = ODEProblem(simplified_sys, x0, (0.0, 100.0), p) sol = solve(prob, Tsit5()) sol[decay2.f] diff --git a/docs/src/examples/perturbation.md b/docs/src/examples/perturbation.md index 407248709d..0d1a493cb4 100644 --- a/docs/src/examples/perturbation.md +++ b/docs/src/examples/perturbation.md @@ -49,7 +49,7 @@ These are the ODEs we want to solve. Now construct an `ODESystem`, which automat To solve the `ODESystem`, we generate an `ODEProblem` with initial conditions $x(0) = 0$, and $ẋ(0) = 1$, and solve it: ```@example perturbation -using DifferentialEquations +using OrdinaryDiffEq u0 = Dict([unknowns(sys) .=> 0.0; D(y[0]) => 1.0]) # nonzero initial velocity prob = ODEProblem(sys, u0, (0.0, 3.0)) sol = solve(prob) diff --git a/docs/src/examples/sparse_jacobians.md b/docs/src/examples/sparse_jacobians.md index 1ed3b1733c..03bd80d432 100644 --- a/docs/src/examples/sparse_jacobians.md +++ b/docs/src/examples/sparse_jacobians.md @@ -11,7 +11,7 @@ First, let's start out with an implementation of the 2-dimensional Brusselator partial differential equation discretized using finite differences: ```@example sparsejac -using DifferentialEquations, ModelingToolkit +using OrdinaryDiffEq, ModelingToolkit const N = 32 const xyd_brusselator = range(0, stop = 1, length = N) diff --git a/docs/src/examples/spring_mass.md b/docs/src/examples/spring_mass.md index e733b11724..355e5c20b2 100644 --- a/docs/src/examples/spring_mass.md +++ b/docs/src/examples/spring_mass.md @@ -5,7 +5,7 @@ In this tutorial, we will build a simple component-based model of a spring-mass ## Copy-Paste Example ```@example component -using ModelingToolkit, Plots, DifferentialEquations, LinearAlgebra +using ModelingToolkit, Plots, OrdinaryDiffEq, LinearAlgebra using ModelingToolkit: t_nounits as t, D_nounits as D using Symbolics: scalarize diff --git a/docs/src/tutorials/acausal_components.md b/docs/src/tutorials/acausal_components.md index c91ba29670..096b576d29 100644 --- a/docs/src/tutorials/acausal_components.md +++ b/docs/src/tutorials/acausal_components.md @@ -20,7 +20,7 @@ equalities before solving. Let's see this in action. ## Copy-Paste Example ```@example acausal -using ModelingToolkit, Plots, DifferentialEquations +using ModelingToolkit, Plots, OrdinaryDiffEq using ModelingToolkit: t_nounits as t, D_nounits as D @connector Pin begin diff --git a/docs/src/tutorials/modelingtoolkitize.md b/docs/src/tutorials/modelingtoolkitize.md index e272a2237c..545879f842 100644 --- a/docs/src/tutorials/modelingtoolkitize.md +++ b/docs/src/tutorials/modelingtoolkitize.md @@ -33,10 +33,10 @@ to improve a simulation code before it's passed to the solver. ## Example Usage: Generating an Analytical Jacobian Expression for an ODE Code Take, for example, the Robertson ODE -defined as an `ODEProblem` for DifferentialEquations.jl: +defined as an `ODEProblem` for OrdinaryDiffEq.jl: ```@example mtkize -using DifferentialEquations, ModelingToolkit +using OrdinaryDiffEq, ModelingToolkit function rober(du, u, p, t) y₁, y₂, y₃ = u k₁, k₂, k₃ = p diff --git a/docs/src/tutorials/ode_modeling.md b/docs/src/tutorials/ode_modeling.md index 755e70ef46..0a4cd80803 100644 --- a/docs/src/tutorials/ode_modeling.md +++ b/docs/src/tutorials/ode_modeling.md @@ -34,7 +34,7 @@ using ModelingToolkit: t_nounits as t, D_nounits as D end end -using DifferentialEquations: solve +using OrdinaryDiffEq @mtkbuild fol = FOL() prob = ODEProblem(fol, [], (0.0, 10.0), []) sol = solve(prob) @@ -84,10 +84,10 @@ Note that equations in MTK use the tilde character (`~`) as equality sign. `@mtkbuild` creates an instance of `FOL` named as `fol`. -After construction of the ODE, you can solve it using [DifferentialEquations.jl](https://docs.sciml.ai/DiffEqDocs/stable/): +After construction of the ODE, you can solve it using [OrdinaryDiffEq.jl](https://docs.sciml.ai/DiffEqDocs/stable/): ```@example ode2 -using DifferentialEquations +using OrdinaryDiffEq using Plots prob = ODEProblem(fol, [], (0.0, 10.0), []) diff --git a/docs/src/tutorials/programmatically_generating.md b/docs/src/tutorials/programmatically_generating.md index 76d12dba2a..9fc1db1834 100644 --- a/docs/src/tutorials/programmatically_generating.md +++ b/docs/src/tutorials/programmatically_generating.md @@ -49,7 +49,7 @@ eqs = [D(x) ~ (h - x) / τ] # create an array of equations # Note: Complete models cannot be subsystems of other models! fol = structural_simplify(model) prob = ODEProblem(fol, [], (0.0, 10.0), []) -using DifferentialEquations: solve +using OrdinaryDiffEq sol = solve(prob) using Plots From 53f32e29c519a8bc48d61fd3b80a733d99bac049 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 18 Nov 2024 16:28:03 +0530 Subject: [PATCH 0538/1337] fix: `@mtkmodel` no longer makes `@variables` callable --- src/systems/model_parsing.jl | 2 +- test/model_parsing.jl | 21 ++++++++++++++++++++- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/src/systems/model_parsing.jl b/src/systems/model_parsing.jl index 5402659b1c..52e46597bf 100644 --- a/src/systems/model_parsing.jl +++ b/src/systems/model_parsing.jl @@ -497,7 +497,7 @@ function generate_var!(dict, a, b, varclass, mod; vd isa Vector && (vd = first(vd)) vd[a] = Dict{Symbol, Any}() var = if indices === nothing - Symbolics.variable(a, T = SymbolicUtils.FnType{Tuple{Any}, type})(iv) + first(@variables $a(iv)::type) else vd[a][:size] = Tuple(lastindex.(indices)) first(@variables $a(iv)[indices...]::type) diff --git a/test/model_parsing.jl b/test/model_parsing.jl index 6ab7636b32..baaa4651bf 100644 --- a/test/model_parsing.jl +++ b/test/model_parsing.jl @@ -1,4 +1,4 @@ -using ModelingToolkit, Test +using ModelingToolkit, Symbolics, Test using ModelingToolkit: get_connector_type, get_defaults, get_gui_metadata, get_systems, get_ps, getdefault, getname, readable_code, scalarize, symtype, VariableDescription, RegularConnector, @@ -990,3 +990,22 @@ struct CustomStruct end @named sys = MyModel(p = CustomStruct()) @test ModelingToolkit.defaults(sys)[@nonamespace sys.p] == CustomStruct() end + +@testset "Variables are not callable symbolics" begin + @mtkmodel Example begin + @variables begin + x(t) + y(t) + end + @equations begin + x ~ y + end + end + @named ex = Example() + vars = Symbolics.get_variables(only(equations(ex))) + @test length(vars) == 2 + for u in Symbolics.unwrap.(unknowns(ex)) + @test !Symbolics.hasmetadata(u, Symbolics.CallWithParent) + @test any(isequal(u), vars) + end +end From 25fd631c599b5c885d1faadddbceff5787c23533 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 20 Nov 2024 21:47:38 +0530 Subject: [PATCH 0539/1337] build: bump Symbolics compat --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 7b0997823c..2e4812c50f 100644 --- a/Project.toml +++ b/Project.toml @@ -143,7 +143,7 @@ StochasticDiffEq = "6.72.1" StochasticDelayDiffEq = "1.8.1" SymbolicIndexingInterface = "0.3.36" SymbolicUtils = "3.7" -Symbolics = "6.19" +Symbolics = "6.22" URIs = "1" UnPack = "0.1, 1.0" Unitful = "1.1" From 69e05f5b5031acc3902acf9edd5430dad75efa73 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 7 Jan 2025 12:35:57 +0530 Subject: [PATCH 0540/1337] docs: ignore Scholarpedia link in Documenter linkcheck --- docs/make.jl | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/make.jl b/docs/make.jl index f380867e6c..36a27bf598 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -25,7 +25,12 @@ makedocs(sitename = "ModelingToolkit.jl", modules = [ModelingToolkit], clean = true, doctest = false, linkcheck = true, warnonly = [:docs_block, :missing_docs, :cross_references], - linkcheck_ignore = ["https://epubs.siam.org/doi/10.1137/0903023"], + linkcheck_ignore = [ + "https://epubs.siam.org/doi/10.1137/0903023", + # this link tends to fail linkcheck stochastically and often takes much longer to succeed + # even in the browser it takes ages + "http://www.scholarpedia.org/article/Differential-algebraic_equations" + ], format = Documenter.HTML(; assets = ["assets/favicon.ico"], mathengine, From da923ca2f583ad9f8dcffdcabe486d7f60d3d1b5 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 8 Jan 2025 11:33:01 +0530 Subject: [PATCH 0541/1337] fix: minor bug fix in `PolynomialTransformation` --- src/systems/nonlinear/homotopy_continuation.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/systems/nonlinear/homotopy_continuation.jl b/src/systems/nonlinear/homotopy_continuation.jl index 00c6c7f1b0..09e11199e8 100644 --- a/src/systems/nonlinear/homotopy_continuation.jl +++ b/src/systems/nonlinear/homotopy_continuation.jl @@ -348,7 +348,8 @@ function PolynomialTransformation(sys::NonlinearSystem) # Is there a better way to check for uniqueness? `simplify` is relatively slow # (maybe use the threaded version?) and `expand` can blow up expression size. # Could metatheory help? - all_non_poly_terms = mapreduce(d -> d.non_polynomial_terms, vcat, polydata) + all_non_poly_terms = mapreduce( + d -> d.non_polynomial_terms, vcat, polydata; init = BasicSymbolic[]) unique!(all_non_poly_terms) # each variable can only be replaced by one non-polynomial expression involving From cb79bf269ad39aae8bf916b2c7060e6fa4176c51 Mon Sep 17 00:00:00 2001 From: Herman Sletmoen Date: Wed, 8 Jan 2025 15:59:18 +0100 Subject: [PATCH 0542/1337] Handle any error with logged functions --- src/debugging.jl | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/debugging.jl b/src/debugging.jl index 06e3edf0d8..80bf3a8f10 100644 --- a/src/debugging.jl +++ b/src/debugging.jl @@ -1,23 +1,23 @@ const LOGGED_FUN = Set([log, sqrt, (^), /, inv]) -is_legal(::typeof(/), a, b) = is_legal(inv, b) -is_legal(::typeof(inv), a) = !iszero(a) -is_legal(::Union{typeof(log), typeof(sqrt)}, a) = a isa Complex || a >= zero(a) -is_legal(::typeof(^), a, b) = a isa Complex || b isa Complex || isinteger(b) || a >= zero(a) +struct LoggedFunctionException <: Exception + msg::String +end struct LoggedFun{F} f::F args::Any end +Base.showerror(io::IO, err::LoggedFunctionException) = print(io, err.msg) Base.nameof(lf::LoggedFun) = nameof(lf.f) SymbolicUtils.promote_symtype(::LoggedFun, Ts...) = Real function (lf::LoggedFun)(args...) - f = lf.f - symbolic_args = lf.args - if is_legal(f, args...) - f(args...) - else - args_str = join(string.(symbolic_args .=> args), ", ", ", and ") - throw(DomainError(args, "$(lf.f) errors with input(s): $args_str")) + try + return lf.f(args...) # try to call with numerical input, as usual + catch err + throw(LoggedFunctionException( + "Function $(lf.f)($(join(lf.args, ", "))) errors with input" * + join("\n " .* string.(lf.args .=> args)) # one line for each "var => val" for readability + )) # Julia automatically attaches original error message end end From 075b90898239f5b0819885b4a8a167644679dd9c Mon Sep 17 00:00:00 2001 From: Herman Sletmoen Date: Wed, 8 Jan 2025 16:01:38 +0100 Subject: [PATCH 0543/1337] Update test (remove legacy branch) --- test/odesystem.jl | 25 +++++++++---------------- 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/test/odesystem.jl b/test/odesystem.jl index 85d135b338..c07594f726 100644 --- a/test/odesystem.jl +++ b/test/odesystem.jl @@ -931,22 +931,15 @@ testdict = Dict([:name => "test"]) @named sys = ODESystem(eqs, t, metadata = testdict) @test get_metadata(sys) == testdict -@variables P(t)=0 Q(t)=2 -∂t = D - -eqs = [∂t(Q) ~ 1 / sin(P) - ∂t(P) ~ log(-cos(Q))] -@named sys = ODESystem(eqs, t, [P, Q], []) -sys = complete(debug_system(sys)); -prob = ODEProblem(sys, [], (0, 1.0)); -du = zero(prob.u0); -if VERSION < v"1.8" - @test_throws DomainError prob.f(du, [1, 0], prob.p, 0.0) - @test_throws DomainError prob.f(du, [0, 2], prob.p, 0.0) -else - @test_throws "-cos(Q(t))" prob.f(du, [1, 0], prob.p, 0.0) - @test_throws "sin(P(t))" prob.f(du, [0, 2], prob.p, 0.0) -end +@variables P(t) = NaN Q(t) = NaN +@named sys = ODESystem([ + D(Q) ~ 1 / sin(P) + D(P) ~ log(-cos(Q)) +], t, [P, Q], []) +sys = complete(debug_system(sys)) +prob = ODEProblem(sys, [], (0.0, 1.0)) +@test_throws "log(-cos(Q(t))) errors" prob.f([1, 0], prob.p, 0.0) +@test prob.f([0, 2], prob.p, 0.0)[1] == 1 / 0 let @variables x(t) = 1 From e1cc46ed9333a1b978a1fbc11b40b6768d459db7 Mon Sep 17 00:00:00 2001 From: Herman Sletmoen Date: Wed, 8 Jan 2025 16:13:41 +0100 Subject: [PATCH 0544/1337] Update debug_system() docstring --- src/systems/abstractsystem.jl | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index 168260ae69..9aac35dad6 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -2266,19 +2266,17 @@ Replace functions with singularities with a function that errors with symbolic information. E.g. ```julia-repl -julia> sys = debug_system(sys); +julia> sys = debug_system(sys) -julia> sys = complete(sys); +julia> sys = complete(sys) -julia> prob = ODEProblem(sys, [], (0, 1.0)); +julia> prob = ODEProblem(sys, [1.0, 0.0], (0.0, 1.0)) -julia> du = zero(prob.u0); - -julia> prob.f(du, prob.u0, prob.p, 0.0) -ERROR: DomainError with (-1.0,): -log errors with input(s): -cos(Q(t)) => -1.0 +julia> prob.f(prob.u0, prob.p, 0.0) +ERROR: Function log(-cos(Q(t))) errors with input + -cos(Q(t)) => -1.0 Stacktrace: - [1] (::ModelingToolkit.LoggedFun{typeof(log)})(args::Float64) + [1] (::ModelingToolkit.LoggedFun{typeof(log)})(num_args::Float64) ... ``` """ From f2c94b5d70d52085ed49b694e18ec89818876aea Mon Sep 17 00:00:00 2001 From: Herman Sletmoen Date: Wed, 8 Jan 2025 16:13:54 +0100 Subject: [PATCH 0545/1337] Hint at flattening system in error message --- src/systems/abstractsystem.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index 9aac35dad6..1205a3d370 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -2282,7 +2282,7 @@ Stacktrace: """ function debug_system(sys::AbstractSystem) if has_systems(sys) && !isempty(get_systems(sys)) - error("debug_system only works on systems with no sub-systems!") + error("debug_system(sys) only works on systems with no sub-systems! Consider flattening it with flatten(sys) or structural_simplify(sys) first.") end if has_eqs(sys) @set! sys.eqs = debug_sub.(equations(sys)) From dcfbe2d00f1ef2d501e1948492e5dfd89ec9dead Mon Sep 17 00:00:00 2001 From: Herman Sletmoen Date: Wed, 8 Jan 2025 16:46:15 +0100 Subject: [PATCH 0546/1337] Error on non-finite values when debugging systems --- src/debugging.jl | 17 +++++++++++------ src/systems/abstractsystem.jl | 14 +++++--------- test/odesystem.jl | 2 +- 3 files changed, 17 insertions(+), 16 deletions(-) diff --git a/src/debugging.jl b/src/debugging.jl index 80bf3a8f10..830d32afce 100644 --- a/src/debugging.jl +++ b/src/debugging.jl @@ -7,18 +7,23 @@ struct LoggedFun{F} f::F args::Any end +LoggedFunctionException(lf::LoggedFun, args, msg) = LoggedFunctionException( + "Function $(lf.f)($(join(lf.args, ", "))) " * msg * " with input" * + join("\n " .* string.(lf.args .=> args)) # one line for each "var => val" for readability +) Base.showerror(io::IO, err::LoggedFunctionException) = print(io, err.msg) Base.nameof(lf::LoggedFun) = nameof(lf.f) SymbolicUtils.promote_symtype(::LoggedFun, Ts...) = Real function (lf::LoggedFun)(args...) - try - return lf.f(args...) # try to call with numerical input, as usual + val = try + lf.f(args...) # try to call with numerical input, as usual catch err - throw(LoggedFunctionException( - "Function $(lf.f)($(join(lf.args, ", "))) errors with input" * - join("\n " .* string.(lf.args .=> args)) # one line for each "var => val" for readability - )) # Julia automatically attaches original error message + throw(LoggedFunctionException(lf, args, "errors")) # Julia automatically attaches original error message end + if !isfinite(val) + throw(LoggedFunctionException(lf, args, "output non-finite value $val")) + end + return val end function logged_fun(f, args...) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index 1205a3d370..492ded28ee 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -2266,18 +2266,14 @@ Replace functions with singularities with a function that errors with symbolic information. E.g. ```julia-repl -julia> sys = debug_system(sys) +julia> sys = debug_system(complete(sys)) -julia> sys = complete(sys) - -julia> prob = ODEProblem(sys, [1.0, 0.0], (0.0, 1.0)) +julia> prob = ODEProblem(sys, [0.0, 2.0], (0.0, 1.0)) julia> prob.f(prob.u0, prob.p, 0.0) -ERROR: Function log(-cos(Q(t))) errors with input - -cos(Q(t)) => -1.0 -Stacktrace: - [1] (::ModelingToolkit.LoggedFun{typeof(log)})(num_args::Float64) - ... +ERROR: Function /(1, sin(P(t))) output non-finite value Inf with input + 1 => 1 + sin(P(t)) => 0.0 ``` """ function debug_system(sys::AbstractSystem) diff --git a/test/odesystem.jl b/test/odesystem.jl index c07594f726..cc2fe89ff3 100644 --- a/test/odesystem.jl +++ b/test/odesystem.jl @@ -939,7 +939,7 @@ testdict = Dict([:name => "test"]) sys = complete(debug_system(sys)) prob = ODEProblem(sys, [], (0.0, 1.0)) @test_throws "log(-cos(Q(t))) errors" prob.f([1, 0], prob.p, 0.0) -@test prob.f([0, 2], prob.p, 0.0)[1] == 1 / 0 +@test_throws "/(1, sin(P(t))) output non-finite value" prob.f([0, 2], prob.p, 0.0) let @variables x(t) = 1 From f5562dc215102895a6c0296877be4a886321067f Mon Sep 17 00:00:00 2001 From: Herman Sletmoen Date: Wed, 8 Jan 2025 17:07:35 +0100 Subject: [PATCH 0547/1337] Toggle extra non-finite value error with keyword argument --- src/debugging.jl | 13 +++++++------ src/systems/abstractsystem.jl | 12 +++++++----- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/src/debugging.jl b/src/debugging.jl index 830d32afce..c8b7372c5d 100644 --- a/src/debugging.jl +++ b/src/debugging.jl @@ -6,6 +6,7 @@ end struct LoggedFun{F} f::F args::Any + error_nonfinite::Bool end LoggedFunctionException(lf::LoggedFun, args, msg) = LoggedFunctionException( "Function $(lf.f)($(join(lf.args, ", "))) " * msg * " with input" * @@ -20,22 +21,22 @@ function (lf::LoggedFun)(args...) catch err throw(LoggedFunctionException(lf, args, "errors")) # Julia automatically attaches original error message end - if !isfinite(val) + if lf.error_nonfinite && !isfinite(val) throw(LoggedFunctionException(lf, args, "output non-finite value $val")) end return val end -function logged_fun(f, args...) +function logged_fun(f, args...; error_nonfinite = true) # remember to update error_nonfinite in debug_system() docstring # Currently we don't really support complex numbers - term(LoggedFun(f, args), args..., type = Real) + term(LoggedFun(f, args, error_nonfinite), args..., type = Real) end -debug_sub(eq::Equation) = debug_sub(eq.lhs) ~ debug_sub(eq.rhs) -function debug_sub(ex) +debug_sub(eq::Equation; kw...) = debug_sub(eq.lhs; kw...) ~ debug_sub(eq.rhs; kw...) +function debug_sub(ex; kw...) iscall(ex) || return ex f = operation(ex) args = map(debug_sub, arguments(ex)) - f in LOGGED_FUN ? logged_fun(f, args...) : + f in LOGGED_FUN ? logged_fun(f, args...; kw...) : maketerm(typeof(ex), f, args, metadata(ex)) end diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index 492ded28ee..c3525c542b 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -2260,10 +2260,12 @@ macro mtkbuild(exprs...) end """ -$(SIGNATURES) + debug_system(sys::AbstractSystem; error_nonfinite = true) Replace functions with singularities with a function that errors with symbolic -information. E.g. +information. If `error_nonfinite`, debugged functions that output nonfinite values +(like `Inf` or `NaN`) also display errors, even though the raw function itself +does not throw an exception (like `1/0`). For example: ```julia-repl julia> sys = debug_system(complete(sys)) @@ -2276,15 +2278,15 @@ ERROR: Function /(1, sin(P(t))) output non-finite value Inf with input sin(P(t)) => 0.0 ``` """ -function debug_system(sys::AbstractSystem) +function debug_system(sys::AbstractSystem; kw...) if has_systems(sys) && !isempty(get_systems(sys)) error("debug_system(sys) only works on systems with no sub-systems! Consider flattening it with flatten(sys) or structural_simplify(sys) first.") end if has_eqs(sys) - @set! sys.eqs = debug_sub.(equations(sys)) + @set! sys.eqs = debug_sub.(equations(sys); kw...) end if has_observed(sys) - @set! sys.observed = debug_sub.(observed(sys)) + @set! sys.observed = debug_sub.(observed(sys); kw...) end return sys end From 94576dbec700682eef80054dce76f1dcf6c78518 Mon Sep 17 00:00:00 2001 From: Herman Sletmoen Date: Wed, 8 Jan 2025 17:27:32 +0100 Subject: [PATCH 0548/1337] Let user pass list of functions that should be debugged --- src/debugging.jl | 10 ++++------ src/systems/abstractsystem.jl | 17 ++++++++++------- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/src/debugging.jl b/src/debugging.jl index c8b7372c5d..f6652183cb 100644 --- a/src/debugging.jl +++ b/src/debugging.jl @@ -1,5 +1,3 @@ -const LOGGED_FUN = Set([log, sqrt, (^), /, inv]) - struct LoggedFunctionException <: Exception msg::String end @@ -32,11 +30,11 @@ function logged_fun(f, args...; error_nonfinite = true) # remember to update err term(LoggedFun(f, args, error_nonfinite), args..., type = Real) end -debug_sub(eq::Equation; kw...) = debug_sub(eq.lhs; kw...) ~ debug_sub(eq.rhs; kw...) -function debug_sub(ex; kw...) +debug_sub(eq::Equation, funcs; kw...) = debug_sub(eq.lhs, funcs; kw...) ~ debug_sub(eq.rhs, funcs; kw...) +function debug_sub(ex, funcs; kw...) iscall(ex) || return ex f = operation(ex) - args = map(debug_sub, arguments(ex)) - f in LOGGED_FUN ? logged_fun(f, args...; kw...) : + args = map(ex -> debug_sub(ex, funcs; kw...), arguments(ex)) + f in funcs ? logged_fun(f, args...; kw...) : maketerm(typeof(ex), f, args, metadata(ex)) end diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index c3525c542b..796ff73eba 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -2260,11 +2260,11 @@ macro mtkbuild(exprs...) end """ - debug_system(sys::AbstractSystem; error_nonfinite = true) + debug_system(sys::AbstractSystem; functions = [log, sqrt, (^), /, inv, asin, acos], error_nonfinite = true) -Replace functions with singularities with a function that errors with symbolic -information. If `error_nonfinite`, debugged functions that output nonfinite values -(like `Inf` or `NaN`) also display errors, even though the raw function itself +Wrap `functions` in `sys` so any error thrown in them shows helpful symbolic-numeric +information about its input. If `error_nonfinite`, functions that output nonfinite +values (like `Inf` or `NaN`) also display errors, even though the raw function itself does not throw an exception (like `1/0`). For example: ```julia-repl @@ -2278,15 +2278,18 @@ ERROR: Function /(1, sin(P(t))) output non-finite value Inf with input sin(P(t)) => 0.0 ``` """ -function debug_system(sys::AbstractSystem; kw...) +function debug_system(sys::AbstractSystem; functions = [log, sqrt, (^), /, inv, asin, acos], kw...) + if !(functions isa Set) + functions = Set(functions) # more efficient "in" lookup + end if has_systems(sys) && !isempty(get_systems(sys)) error("debug_system(sys) only works on systems with no sub-systems! Consider flattening it with flatten(sys) or structural_simplify(sys) first.") end if has_eqs(sys) - @set! sys.eqs = debug_sub.(equations(sys); kw...) + @set! sys.eqs = debug_sub.(equations(sys), Ref(functions); kw...) end if has_observed(sys) - @set! sys.observed = debug_sub.(observed(sys); kw...) + @set! sys.observed = debug_sub.(observed(sys), Ref(functions); kw...) end return sys end From ca788528b7046163100b1821f16907721c98c9f5 Mon Sep 17 00:00:00 2001 From: Herman Sletmoen Date: Wed, 8 Jan 2025 17:39:46 +0100 Subject: [PATCH 0549/1337] Format --- src/debugging.jl | 14 +++++++++----- src/systems/abstractsystem.jl | 3 ++- test/odesystem.jl | 8 +++----- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/src/debugging.jl b/src/debugging.jl index f6652183cb..a1a168d8dd 100644 --- a/src/debugging.jl +++ b/src/debugging.jl @@ -6,10 +6,12 @@ struct LoggedFun{F} args::Any error_nonfinite::Bool end -LoggedFunctionException(lf::LoggedFun, args, msg) = LoggedFunctionException( - "Function $(lf.f)($(join(lf.args, ", "))) " * msg * " with input" * - join("\n " .* string.(lf.args .=> args)) # one line for each "var => val" for readability -) +function LoggedFunctionException(lf::LoggedFun, args, msg) + LoggedFunctionException( + "Function $(lf.f)($(join(lf.args, ", "))) " * msg * " with input" * + join("\n " .* string.(lf.args .=> args)) # one line for each "var => val" for readability + ) +end Base.showerror(io::IO, err::LoggedFunctionException) = print(io, err.msg) Base.nameof(lf::LoggedFun) = nameof(lf.f) SymbolicUtils.promote_symtype(::LoggedFun, Ts...) = Real @@ -30,7 +32,9 @@ function logged_fun(f, args...; error_nonfinite = true) # remember to update err term(LoggedFun(f, args, error_nonfinite), args..., type = Real) end -debug_sub(eq::Equation, funcs; kw...) = debug_sub(eq.lhs, funcs; kw...) ~ debug_sub(eq.rhs, funcs; kw...) +function debug_sub(eq::Equation, funcs; kw...) + debug_sub(eq.lhs, funcs; kw...) ~ debug_sub(eq.rhs, funcs; kw...) +end function debug_sub(ex, funcs; kw...) iscall(ex) || return ex f = operation(ex) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index 796ff73eba..e6c07bcf9c 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -2278,7 +2278,8 @@ ERROR: Function /(1, sin(P(t))) output non-finite value Inf with input sin(P(t)) => 0.0 ``` """ -function debug_system(sys::AbstractSystem; functions = [log, sqrt, (^), /, inv, asin, acos], kw...) +function debug_system( + sys::AbstractSystem; functions = [log, sqrt, (^), /, inv, asin, acos], kw...) if !(functions isa Set) functions = Set(functions) # more efficient "in" lookup end diff --git a/test/odesystem.jl b/test/odesystem.jl index cc2fe89ff3..94f676461b 100644 --- a/test/odesystem.jl +++ b/test/odesystem.jl @@ -931,11 +931,9 @@ testdict = Dict([:name => "test"]) @named sys = ODESystem(eqs, t, metadata = testdict) @test get_metadata(sys) == testdict -@variables P(t) = NaN Q(t) = NaN -@named sys = ODESystem([ - D(Q) ~ 1 / sin(P) - D(P) ~ log(-cos(Q)) -], t, [P, Q], []) +@variables P(t)=NaN Q(t)=NaN +eqs = [D(Q) ~ 1 / sin(P), D(P) ~ log(-cos(Q))] +@named sys = ODESystem(eqs, t, [P, Q], []) sys = complete(debug_system(sys)) prob = ODEProblem(sys, [], (0.0, 1.0)) @test_throws "log(-cos(Q(t))) errors" prob.f([1, 0], prob.p, 0.0) From ef1f089cbd493272e2b9297c2189aa2a6029d72f Mon Sep 17 00:00:00 2001 From: vyudu Date: Wed, 8 Jan 2025 15:12:12 -0500 Subject: [PATCH 0550/1337] up --- src/systems/diffeqs/abstractodesystem.jl | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index d8ff71c324..87ab83f10b 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -930,16 +930,6 @@ end get_callback(prob::BVProblem) = error("BVP solvers do not support callbacks.") -@inline function SymbolicUtils.Code.create_array(::Type{<:Base.ReinterpretArray}, ::Nothing, - ::Val{1}, ::Val{dims}, elems...) where {dims} - [elems...] -end - -@inline function SymbolicUtils.Code.create_array( - ::Type{<:Base.ReinterpretArray}, T, ::Val{1}, ::Val{dims}, elems...) where {dims} - T[elems...] -end - """ ```julia DiffEqBase.DAEProblem{iip}(sys::AbstractODESystem, du0map, u0map, tspan, From 9921abe9fce3250ffa2d2535e61c31fcffeb701a Mon Sep 17 00:00:00 2001 From: Torkel Loman Date: Thu, 9 Jan 2025 11:24:58 +0000 Subject: [PATCH 0551/1337] DifferentialEquations -> OrdinaryDiffEqDefault --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d93a18e3f7..12570c35a3 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ lower it to a first order system, symbolically generate the Jacobian function for the numerical integrator, and solve it. ```julia -using DifferentialEquations, ModelingToolkit +using OrdinaryDiffEqDefault, ModelingToolkit using ModelingToolkit: t_nounits as t, D_nounits as D @parameters σ ρ β From 2a25200bcb6a1ad57c4329b6383ba844eb73c2f7 Mon Sep 17 00:00:00 2001 From: vyudu Date: Thu, 9 Jan 2025 17:36:38 -0500 Subject: [PATCH 0552/1337] extend BVProblem for constraint equations --- src/systems/diffeqs/abstractodesystem.jl | 74 +++++++++++++++++-- test/bvproblem.jl | 90 ++++++++++++++++++++++++ 2 files changed, 160 insertions(+), 4 deletions(-) diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index f5c1c288d7..74b1bf7596 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -873,7 +873,7 @@ function SciMLBase.BVProblem(sys::AbstractODESystem, kwargs...) BVProblem{false, SciMLBase.FullSpecialize}(sys, u0map, args...; kwargs...) end - +o function SciMLBase.BVProblem{true}(sys::AbstractODESystem, args...; kwargs...) BVProblem{true, SciMLBase.AutoSpecialize}(sys, args...; kwargs...) end @@ -908,11 +908,32 @@ function SciMLBase.BVProblem{iip, specialize}(sys::AbstractODESystem, u0map = [] kwargs1 = merge(kwargs1, (callback = cbs,)) end + # Handle algebraic equations + stidxmap = Dict([v => i for (i, v) in enumerate(unknowns(sys))]) + pidxmap = Dict([v => i for (i, v) in enumerate(parameters(sys))]) + ns = length(stmap) + ne = length(get_alg_eqs(sys)) + # Define the boundary conditions. - bc = if iip - (residual, u, p, t) -> (residual .= u[1] .- u0) + bc = if has_alg_eqs(sys) + if iip + (residual,u,p,t) -> begin + residual[1:ns] .= u[1] .- u0 + residual[ns+1:ns+ne] .= sub_u_p_into_symeq.(get_alg_eqs(sys)) + end + else + (u,p,t) -> begin + resid = vcat(u[1] - u0, sub_u_p_into_symeq.(get_alg_eqs(sys))) + end + end else - (u, p, t) -> (u[1] - u0) + if iip + (residual,u,p,t) -> begin + residual .= u[1] .- u0 + end + else + (u,p,t) -> (u[1] - u0) + end end return BVProblem{iip}(f, bc, u0, tspan, p; kwargs1..., kwargs...) @@ -920,6 +941,51 @@ end get_callback(prob::BVProblem) = error("BVP solvers do not support callbacks.") +# Helper to create the dictionary that will substitute numeric values for u, p into the algebraic equations in the ODESystem. Used to construct the boundary condition function. +# Take a system with variables x,y, parameters g +# +# 1 + x + y → 1 + u[1][1] + u[1][2] +# x(0.5) → u(0.5)[1] +# x(0.5)*g(0.5) → u(0.5)[1]*p[1] + +function sub_u_p_into_symeq(eq, u, p, stidxmap, pidxmap) + iv = ModelingToolkit.get_iv(sys) + eq = Symbolics.unwrap(eq) + + stmap = Dict([st => u[1][i] for st => i in stidxmap]) + pmap = Dict([pa => p[i] for pa => i in pidxmap]) + eq = Symbolics.substitute(eq, merge(stmap, pmap)) + + csyms = [] + # Find most nested calls, substitute those first. + while !isempty(find_callable_syms!(csyms, eq)) + for sym in csyms + t = arguments(sym)[1] + x = operation(sym) + + if isparameter(x) + eq = Symbolics.substitute(eq, Dict(x(t) => p[pidxmap[x(iv)]])) + elseif isvariable(x) + eq = Symbolics.substitute(eq, Dict(x(t) => u(val)[stidxmap[x(iv)]])) + end + end + empty!(csyms) + end + eq +end + +function find_callable_syms!(csyms, ex) + ex = Symbolics.unwrap(ex) + + if iscall(ex) + operation(ex) isa Symbolic && (arguments(ex)[1] isa Symbolic) && push!(csyms, ex) # only add leaf nodes + for arg in arguments(ex) + find_callable_syms!(csyms, arg) + end + end + csyms +end + """ ```julia DiffEqBase.DAEProblem{iip}(sys::AbstractODESystem, du0map, u0map, tspan, diff --git a/test/bvproblem.jl b/test/bvproblem.jl index c5a302147d..2d5535325a 100644 --- a/test/bvproblem.jl +++ b/test/bvproblem.jl @@ -2,6 +2,7 @@ using BoundaryValueDiffEq, OrdinaryDiffEq using ModelingToolkit using ModelingToolkit: t_nounits as t, D_nounits as D +### Test Collocation solvers on simple problems solvers = [MIRK4, RadauIIa5, LobattoIIIa3] @parameters α=7.5 β=4.0 γ=8.0 δ=5.0 @@ -68,3 +69,92 @@ for solver in solvers @test isapprox(sol.u[end], osol.u[end]; atol = 0.01) @test sol.u[1] == [π / 2, π / 2] end + +################################################### +### TESTING ODESystem with Constraint Equations ### +################################################### + +# Cartesian pendulum from the docs. Testing that initialization is satisfied. +let + @parameters g + @variables x(t) y(t) [state_priority = 10] λ(t) + eqs = [D(D(x)) ~ λ * x + D(D(y)) ~ λ * y - g + x^2 + y^2 ~ 1] + @mtkbuild pend = ODESystem(eqs, t) + + tspan = (0.0, 1.5) + u0map = [x => 1, y => 0] + parammap = [g => 1] + guesses = [λ => 1] + + prob = ODEProblem(pend, u0map, tspan, pmap; guesses) + sol = solve(prob, Rodas5P()) + + bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(pend, u0map, tspan, parammap; guesses) + + for solver in solvers + sol = solve(bvp, solver(), dt = 0.01) + @test isapprox(sol.u[end], osol.u[end]; atol = 0.01) + conditions = getfield.(equations(pend)[3:end], :rhs) + @test [sol[conditions][1]; sol[x][1] - 1; sol[y][1]] ≈ 0 + end + + bvp2 = SciMLBase.BVProblem{false, SciMLBase.FullSpecialize}(pend, u0map, tspan, parammap) + for solver in solvers + sol = solve(bvp, solver(), dt = 0.01) + @test isapprox(sol.u[end], osol.u[end]; atol = 0.01) + conditions = getfield.(equations(pend)[3:end], :rhs) + @test [sol[conditions][1]; sol[x][1] - 1; sol[y][1]] ≈ 0 + end +end + +# Adding a midpoint boundary condition. +let + @parameters g + @variables x(..) y(t) [state_priority = 10] λ(t) + eqs = [D(D(x(t))) ~ λ * x(t) + D(D(y)) ~ λ * y - g + x(t)^2 + y^2 ~ 1 + x(0.5) ~ 1] + @mtkbuild pend = ODESystem(eqs, t) + + tspan = (0.0, 1.5) + u0map = [x(t) => 0.6, y => 0.8] + parammap = [g => 1] + guesses = [λ => 1] + + prob = ODEProblem(pend, u0map, tspan, pmap; guesses, check_length = false) + sol = solve(prob, Rodas5P()) + + bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(pend, u0map, tspan, parammap; guesses = guesses, check_length = false) + + for solver in solvers + sol = solve(bvp, solver(), dt = 0.01) + @test isapprox(sol.u[end], osol.u[end]; atol = 0.01) + conditions = getfield.(equations(pend)[3:end], :rhs) + @test [sol[conditions][1]; sol[x][1] - 1; sol[y][1]] ≈ 0 + @test sol.u[1] == [π / 2, π / 2] + end + + bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(pend, u0map, tspan, parammap; guesses) + + for solver in solvers + sol = solve(bvp, solver(), dt = 0.01) + @test isapprox(sol.u[end], osol.u[end]; atol = 0.01) + conditions = getfield.(equations(pend)[3:end], :rhs) + @test [sol[conditions][1]; sol[x][1] - 1; sol[y][1]] ≈ 0 + end + + bvp2 = SciMLBase.BVProblem{false, SciMLBase.FullSpecialize}(pend, u0map, tspan, parammap) + for solver in solvers + sol = solve(bvp, solver(), dt = 0.01) + @test isapprox(sol.u[end], osol.u[end]; atol = 0.01) + conditions = getfield.(equations(pend)[3:end], :rhs) + @test [sol[conditions][1]; sol[x][1] - 1; sol[y][1]] ≈ 0 + end +end + +# Testing a more complicated case with multiple constraints. +let +end From 761f5ea75a39e4264c765d6d00b37b302581cf47 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 9 Jan 2025 13:01:27 +0530 Subject: [PATCH 0553/1337] feat: allow remade initialization systems to be incomplete --- src/systems/diffeqs/abstractodesystem.jl | 9 ++++++++- src/systems/nonlinear/initializesystem.jl | 9 ++++++++- src/systems/problem_utils.jl | 5 ++++- test/initializationsystem.jl | 24 +++++++++++++++++++++++ 4 files changed, 44 insertions(+), 3 deletions(-) diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index b012bc73d4..3da5b6a2d0 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -1294,6 +1294,7 @@ function InitializationProblem{iip, specialize}(sys::AbstractSystem, fully_determined = nothing, check_units = true, use_scc = true, + allow_incomplete = false, kwargs...) where {iip, specialize} if !iscomplete(sys) error("A completed system is required. Call `complete` or `structural_simplify` on the system before creating an `ODEProblem`") @@ -1335,7 +1336,13 @@ function InitializationProblem{iip, specialize}(sys::AbstractSystem, # TODO: throw on uninitialized arrays filter!(x -> !(x isa Symbolics.Arr), uninit) if !isempty(uninit) - throw(IncompleteInitializationError(uninit)) + allow_incomplete || throw(IncompleteInitializationError(uninit)) + # for incomplete initialization, we will add the missing variables as parameters. + # they will be updated by `update_initializeprob!` and `initializeprobmap` will + # use them to construct the new `u0`. + newparams = map(toparam, uninit) + append!(get_ps(isys), newparams) + isys = complete(isys) end neqs = length(equations(isys)) diff --git a/src/systems/nonlinear/initializesystem.jl b/src/systems/nonlinear/initializesystem.jl index 602fc7cac9..c3bbd92317 100644 --- a/src/systems/nonlinear/initializesystem.jl +++ b/src/systems/nonlinear/initializesystem.jl @@ -317,6 +317,12 @@ function SciMLBase.remake_initialization_data( u0map[dvs[i]] = newu0[i] end end + # ensure all unknowns have guesses in case they weren't given one + # and become solvable + for i in eachindex(dvs) + haskey(guesses, dvs[i]) && continue + guesses[dvs[i]] = newu0[i] + end if p === missing # the user didn't pass `p` to `remake`, so they want to retain # existing values. Fill all parameters in `pmap` so that none of @@ -341,7 +347,8 @@ function SciMLBase.remake_initialization_data( op, missing_unknowns, missing_pars = build_operating_point( u0map, pmap, defs, cmap, dvs, ps) kws = maybe_build_initialization_problem( - sys, op, u0map, pmap, t0, defs, guesses, missing_unknowns; use_scc, initialization_eqs) + sys, op, u0map, pmap, t0, defs, guesses, missing_unknowns; + use_scc, initialization_eqs, allow_incomplete = true) return get(kws, :initialization_data, nothing) end diff --git a/src/systems/problem_utils.jl b/src/systems/problem_utils.jl index 6ce5704daa..4cbf495d87 100644 --- a/src/systems/problem_utils.jl +++ b/src/systems/problem_utils.jl @@ -545,7 +545,10 @@ function maybe_build_initialization_problem( (!is_time_dependent(sys) || t !== nothing) initializeprob = ModelingToolkit.InitializationProblem( sys, t, u0map, pmap; guesses, kwargs...) - initializeprobmap = getu(initializeprob, unknowns(sys)) + + all_init_syms = Set(all_symbols(initializeprob)) + solved_unknowns = filter(var -> var in all_init_syms, unknowns(sys)) + initializeprobmap = getu(initializeprob, solved_unknowns) punknowns = [p for p in all_variable_symbols(initializeprob) diff --git a/test/initializationsystem.jl b/test/initializationsystem.jl index 2745b9d81f..ecf6899624 100644 --- a/test/initializationsystem.jl +++ b/test/initializationsystem.jl @@ -1282,3 +1282,27 @@ end @test SciMLBase.successful_retcode(solve(newprob)) end + +@testset "Issue#3295: Incomplete initialization of pure-ODE systems" begin + @variables X(t) Y(t) + @parameters p d + eqs = [ + D(X) ~ p - d * X, + D(Y) ~ p - d * Y + ] + @mtkbuild osys = ODESystem(eqs, t) + + # Make problem. + u0_vals = [X => 4, Y => 5.0] + tspan = (0.0, 10.0) + p_vals = [p => 1.0, d => 0.1] + oprob = ODEProblem(osys, u0_vals, tspan, p_vals) + integ = init(oprob) + @test integ[X] ≈ 4.0 + @test integ[Y] ≈ 5.0 + # Attempt to `remake`. + rp = remake(oprob; u0 = [Y => 7]) + integ = init(rp) + @test integ[X] ≈ 4.0 + @test integ[Y] ≈ 7.0 +end From fa0df9d7e215376967acc2f00e06feaea7e8d730 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 10 Jan 2025 14:22:38 +0530 Subject: [PATCH 0554/1337] test: fix initialization test --- test/initializationsystem.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/initializationsystem.jl b/test/initializationsystem.jl index ecf6899624..6f8c961b45 100644 --- a/test/initializationsystem.jl +++ b/test/initializationsystem.jl @@ -116,7 +116,7 @@ end x′ end @variables begin - p(t) = p′ + p(t) x(t) = x′ dm(t) = 0 f(t) = p′ * A From 755b3b6eb5d7ce31b94e1912e8ceeb1504129c6e Mon Sep 17 00:00:00 2001 From: Herman Sletmoen Date: Fri, 10 Jan 2025 15:23:21 +0100 Subject: [PATCH 0555/1337] Add documentation page for debugging --- docs/pages.jl | 1 + docs/src/basics/Debugging.md | 44 ++++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+) create mode 100644 docs/src/basics/Debugging.md diff --git a/docs/pages.jl b/docs/pages.jl index 2af487adf8..5dd869625c 100644 --- a/docs/pages.jl +++ b/docs/pages.jl @@ -31,6 +31,7 @@ pages = [ "basics/InputOutput.md", "basics/MTKLanguage.md", "basics/Validation.md", + "basics/Debugging.md", "basics/DependencyGraphs.md", "basics/Precompilation.md", "basics/FAQ.md"], diff --git a/docs/src/basics/Debugging.md b/docs/src/basics/Debugging.md new file mode 100644 index 0000000000..29fdc5f215 --- /dev/null +++ b/docs/src/basics/Debugging.md @@ -0,0 +1,44 @@ +# Debugging + +Every (mortal) modeler writes models that contain mistakes or are susceptible to numerical errors in their hunt for the perfect model. +Debugging such errors is part of the modeling process, and ModelingToolkit includes some functionality that helps with this. + +For example, consider an ODE model with "dangerous" functions (here `√`): + +```@example debug +using ModelingToolkit, OrdinaryDiffEq +using ModelingToolkit: t_nounits as t, D_nounits as D + +@variables u1(t) u2(t) u3(t) +eqs = [D(u1) ~ -√(u1), D(u2) ~ -√(u2), D(u3) ~ -√(u3)] +defaults = [u1 => 1.0, u2 => 2.0, u3 => 3.0] +@named sys = ODESystem(eqs, t; defaults) +sys = structural_simplify(sys) +``` + +This problem causes the ODE solver to crash: + +```@example debug +prob = ODEProblem(sys, [], (0.0, 10.0), []) +sol = solve(prob, Tsit5()) +``` + +This suggests *that* something went wrong, but not exactly *what* went wrong and *where* it did. +In such situations, the `debug_system` function is helpful: + +```@example debug +try # workaround to show Documenter.jl error (https://github.com/JuliaDocs/Documenter.jl/issues/1420#issuecomment-770539595) # hide +dsys = debug_system(sys; functions = [sqrt]) +dprob = ODEProblem(dsys, [], (0.0, 10.0), []) +dsol = solve(dprob, Tsit5()) +catch err # hide +showerror(stderr, err) # hide +end # hide +``` + +Now we see that it crashed because `u1` decreased so much that it became negative and outside the domain of the `√` function. +We could have figured that out ourselves, but it is not always so obvious for more complex models. + +```@docs +debug_system +``` From 3bc5e155ff6260d536e48cea872d55de4a7b2b62 Mon Sep 17 00:00:00 2001 From: Herman Sletmoen Date: Fri, 10 Jan 2025 16:03:46 +0100 Subject: [PATCH 0556/1337] Use @repl block instead of @example to show errors in documentation without workarounds --- docs/src/basics/Debugging.md | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/docs/src/basics/Debugging.md b/docs/src/basics/Debugging.md index 29fdc5f215..d5c51ec0c1 100644 --- a/docs/src/basics/Debugging.md +++ b/docs/src/basics/Debugging.md @@ -18,22 +18,18 @@ sys = structural_simplify(sys) This problem causes the ODE solver to crash: -```@example debug -prob = ODEProblem(sys, [], (0.0, 10.0), []) -sol = solve(prob, Tsit5()) +```@repl debug +prob = ODEProblem(sys, [], (0.0, 10.0), []); +sol = solve(prob, Tsit5()); ``` This suggests *that* something went wrong, but not exactly *what* went wrong and *where* it did. In such situations, the `debug_system` function is helpful: -```@example debug -try # workaround to show Documenter.jl error (https://github.com/JuliaDocs/Documenter.jl/issues/1420#issuecomment-770539595) # hide -dsys = debug_system(sys; functions = [sqrt]) -dprob = ODEProblem(dsys, [], (0.0, 10.0), []) -dsol = solve(dprob, Tsit5()) -catch err # hide -showerror(stderr, err) # hide -end # hide +```@repl debug +dsys = debug_system(sys; functions = [sqrt]); +dprob = ODEProblem(dsys, [], (0.0, 10.0), []); +dsol = solve(dprob, Tsit5()); ``` Now we see that it crashed because `u1` decreased so much that it became negative and outside the domain of the `√` function. From 50504abbcf0149d50bd6e858ae1c5f368f8d2835 Mon Sep 17 00:00:00 2001 From: vyudu Date: Sat, 11 Jan 2025 13:57:59 -0500 Subject: [PATCH 0557/1337] adding tests --- Project.toml | 6 +- src/systems/diffeqs/abstractodesystem.jl | 140 ++++++++++--- test/bvproblem.jl | 242 ++++++++++++++--------- 3 files changed, 261 insertions(+), 127 deletions(-) diff --git a/Project.toml b/Project.toml index 98fd119f0c..b0bd4381b6 100644 --- a/Project.toml +++ b/Project.toml @@ -82,6 +82,7 @@ ArrayInterface = "6, 7" BifurcationKit = "0.4" BlockArrays = "1.1" BoundaryValueDiffEq = "5.12.0" +BoundaryValueDiffEqAscher = "1.1.0" ChainRulesCore = "1" Combinatorics = "1" CommonSolve = "0.2.4" @@ -140,8 +141,8 @@ SimpleNonlinearSolve = "0.1.0, 1, 2" SparseArrays = "1" SpecialFunctions = "0.7, 0.8, 0.9, 0.10, 1.0, 2" StaticArrays = "0.10, 0.11, 0.12, 1.0" -StochasticDiffEq = "6.72.1" StochasticDelayDiffEq = "1.8.1" +StochasticDiffEq = "6.72.1" SymbolicIndexingInterface = "0.3.36" SymbolicUtils = "3.7" Symbolics = "6.19" @@ -154,6 +155,7 @@ julia = "1.9" AmplNLWriter = "7c4d4715-977e-5154-bfe0-e096adeac482" BenchmarkTools = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf" BoundaryValueDiffEq = "764a87c0-6b3e-53db-9096-fe964310641d" +BoundaryValueDiffEqAscher = "7227322d-7511-4e07-9247-ad6ff830280e" ControlSystemsBase = "aaaaaaaa-a6ca-5380-bf3e-84a91bcd477e" DataInterpolations = "82cc6244-b520-54b8-b5a6-8a565e85f1d0" DeepDiffs = "ab62b9b5-e342-54a8-a765-a90f495de1a6" @@ -185,4 +187,4 @@ Sundials = "c3572dad-4567-51f8-b174-8c6c989267f4" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [targets] -test = ["AmplNLWriter", "BenchmarkTools", "ControlSystemsBase", "DataInterpolations", "DelayDiffEq", "NonlinearSolve", "ForwardDiff", "Ipopt", "Ipopt_jll", "ModelingToolkitStandardLibrary", "Optimization", "OptimizationOptimJL", "OptimizationMOI", "OrdinaryDiffEq", "OrdinaryDiffEqCore", "OrdinaryDiffEqDefault", "REPL", "Random", "ReferenceTests", "SafeTestsets", "StableRNGs", "Statistics", "SteadyStateDiffEq", "Test", "StochasticDiffEq", "Sundials", "StochasticDelayDiffEq", "Pkg", "JET", "OrdinaryDiffEqNonlinearSolve"] +test = ["AmplNLWriter", "BenchmarkTools", "BoundaryValueDiffEq", "BoundaryValueDiffEqAscher", "ControlSystemsBase", "DataInterpolations", "DelayDiffEq", "NonlinearSolve", "ForwardDiff", "Ipopt", "Ipopt_jll", "ModelingToolkitStandardLibrary", "Optimization", "OptimizationOptimJL", "OptimizationMOI", "OrdinaryDiffEq", "OrdinaryDiffEqCore", "OrdinaryDiffEqDefault", "REPL", "Random", "ReferenceTests", "SafeTestsets", "StableRNGs", "Statistics", "SteadyStateDiffEq", "Test", "StochasticDiffEq", "Sundials", "StochasticDelayDiffEq", "Pkg", "JET", "OrdinaryDiffEqNonlinearSolve"] diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index 74b1bf7596..3c45b1b2f8 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -853,15 +853,47 @@ get_callback(prob::ODEProblem) = prob.kwargs[:callback] ```julia SciMLBase.BVProblem{iip}(sys::AbstractODESystem, u0map, tspan, parammap = DiffEqBase.NullParameters(); + constraints = nothing, guesses = nothing, version = nothing, tgrad = false, jac = true, sparse = true, simplify = false, kwargs...) where {iip} ``` -Create a `BVProblem` from the [`ODESystem`](@ref). The arguments `dvs` and +Create a boundary value problem from the [`ODESystem`](@ref). The arguments `dvs` and `ps` are used to set the order of the dependent variable and parameter vectors, -respectively. `u0map` should be used to specify the initial condition. +respectively. `u0map` is used to specify fixed initial values for the states. + +Every variable must have either an initial guess supplied using `guesses` or +a fixed initial value specified using `u0map`. + +`constraints` are used to specify boundary conditions to the ODESystem in the +form of equations. These values should specify values that state variables should +take at specific points, as in `x(0.5) ~ 1`). More general constraints that +should hold over the entire solution, such as `x(t)^2 + y(t)^2`, should be +specified as one of the equations used to build the `ODESystem`. Below is an example. + +```julia + @parameters g + @variables x(..) y(t) [state_priority = 10] λ(t) + eqs = [D(D(x(t))) ~ λ * x(t) + D(D(y)) ~ λ * y - g + x(t)^2 + y^2 ~ 1] + @mtkbuild pend = ODESystem(eqs, t) + + tspan = (0.0, 1.5) + u0map = [x(t) => 0.6, y => 0.8] + parammap = [g => 1] + guesses = [λ => 1] + constraints = [x(0.5) ~ 1] + + bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(pend, u0map, tspan, parammap; constraints, guesses, check_length = false) +``` + +If no `constraints` are specified, the problem will be treated as an initial value problem. + +If the `ODESystem` has algebraic equations like `x(t)^2 + y(t)^2`, the resulting +`BVProblem` must be solved using BVDAE solvers, such as Ascher. """ function SciMLBase.BVProblem(sys::AbstractODESystem, args...; kwargs...) BVProblem{true}(sys, args...; kwargs...) @@ -873,7 +905,7 @@ function SciMLBase.BVProblem(sys::AbstractODESystem, kwargs...) BVProblem{false, SciMLBase.FullSpecialize}(sys, u0map, args...; kwargs...) end -o + function SciMLBase.BVProblem{true}(sys::AbstractODESystem, args...; kwargs...) BVProblem{true, SciMLBase.AutoSpecialize}(sys, args...; kwargs...) end @@ -885,6 +917,7 @@ end function SciMLBase.BVProblem{iip, specialize}(sys::AbstractODESystem, u0map = [], tspan = get_tspan(sys), parammap = DiffEqBase.NullParameters(); + constraints = nothing, guesses = nothing, version = nothing, tgrad = false, callback = nothing, check_length = true, @@ -892,38 +925,63 @@ function SciMLBase.BVProblem{iip, specialize}(sys::AbstractODESystem, u0map = [] eval_expression = false, eval_module = @__MODULE__, kwargs...) where {iip, specialize} + if !iscomplete(sys) error("A completed system is required. Call `complete` or `structural_simplify` on the system before creating an `BVProblem`") end + !isnothing(callbacks) && error("BVP solvers do not support callbacks.") - f, u0, p = process_SciMLProblem(ODEFunction{iip, specialize}, sys, u0map, parammap; - t = tspan !== nothing ? tspan[1] : tspan, - check_length, warn_initialize_determined, eval_expression, eval_module, kwargs...) + iv = get_iv(sys) + constraintsts = nothing + constraintps = nothing + sts = unknowns(sys) + ps = parameters(sys) - cbs = process_events(sys; callback, eval_expression, eval_module, kwargs...) - kwargs = filter_kwargs(kwargs) + if !isnothing(constraints) + constraints isa Equation || + constraints isa Vector{Equation} || + error("Constraints must be specified as an equation or a vector of equations.") - kwargs1 = (;) - if cbs !== nothing - kwargs1 = merge(kwargs1, (callback = cbs,)) + (length(constraints) + length(u0map) > length(sts)) && + error("The BVProblem is overdetermined. The total number of conditions (# constraints + # fixed initial values given by u0map) cannot exceed the total number of states.") + + constraintsts = OrderedSet() + constraintps = OrderedSet() + + for eq in constraints + collect_vars!(constraintsts, constraintps, eq, iv) + validate_constraint_syms(eq, constraintsts, constraintps, Set(sts), Set(ps), iv) + empty!(constraintsts) + empty!(constraintps) + end end - # Handle algebraic equations - stidxmap = Dict([v => i for (i, v) in enumerate(unknowns(sys))]) - pidxmap = Dict([v => i for (i, v) in enumerate(parameters(sys))]) - ns = length(stmap) - ne = length(get_alg_eqs(sys)) + f, u0, p = process_SciMLProblem(ODEFunction{iip, specialize}, sys, u0map, parammap; + t = tspan !== nothing ? tspan[1] : tspan, guesses, + check_length, warn_initialize_determined, eval_expression, eval_module, kwargs...) + + stidxmap = Dict([v => i for (i, v) in enumerate(sts)]) + pidxmap = Dict([v => i for (i, v) in enumerate(ps)]) + + # Indices of states that have initial constraints. + u0i = has_alg_eqs(sys) ? collect(1:length(sts)) : [stidxmap[k] for k in keys(u0map)] + ni = length(u0i) - # Define the boundary conditions. - bc = if has_alg_eqs(sys) + bc = if !isnothing(constraints) + ne = length(constraints) if iip (residual,u,p,t) -> begin - residual[1:ns] .= u[1] .- u0 - residual[ns+1:ns+ne] .= sub_u_p_into_symeq.(get_alg_eqs(sys)) + residual[1:ni] .= u[1][u0i] .- u0[u0i] + residual[ni+1:ni+ne] .= map(constraints) do cons + sub_u_p_into_symeq(cons.rhs - cons.lhs, u, p, stidxmap, pidxmap, iv, tspan) + end end else (u,p,t) -> begin - resid = vcat(u[1] - u0, sub_u_p_into_symeq.(get_alg_eqs(sys))) + consresid = map(constraints) do cons + sub_u_p_into_symeq(cons.rhs-cons.lhs, u, p, stidxmap, pidxmap, iv, tspan) + end + resid = vcat(u[1][u0i] - u0[u0i], consresid) end end else @@ -941,32 +999,54 @@ end get_callback(prob::BVProblem) = error("BVP solvers do not support callbacks.") -# Helper to create the dictionary that will substitute numeric values for u, p into the algebraic equations in the ODESystem. Used to construct the boundary condition function. +# Validate that all the variables in the BVP constraints are well-formed states or parameters. +function validate_constraint_syms(eq, constraintsts, constraintps, sts, ps, iv) + ModelingToolkit.check_variables(constraintsts) + ModelingToolkit.check_parameters(constraintps) + + for var in constraintsts + if arguments(var) == iv + var ∈ sts || error("Constraint equation $eq contains a variable $var that is not a variable of the ODESystem.") + error("Constraint equation $eq contains a variable $var that does not have a specified argument. Such equations should be specified as algebraic equations to the ODESystem rather than a boundary constraints.") + else + operation(var)(iv) ∈ sts || error("Constraint equation $eq contains a variable $(operation(var)) that is not a variable of the ODESystem.") + end + end + + for var in constraintps + if !iscall(var) + var ∈ ps || error("Constraint equation $eq contains a parameter $var that is not a parameter of the ODESystem.") + else + operation(var) ∈ ps || error("Constraint equations contain a parameter $var that is not a parameter of the ODESystem.") + end + end +end + +# Helper to substitute numeric values for u, p into the algebraic equations in the ODESystem. Used to construct the boundary condition function. # Take a system with variables x,y, parameters g # -# 1 + x + y → 1 + u[1][1] + u[1][2] +# 1 + x(0) + y(0) → 1 + u[1][1] + u[1][2] # x(0.5) → u(0.5)[1] # x(0.5)*g(0.5) → u(0.5)[1]*p[1] - -function sub_u_p_into_symeq(eq, u, p, stidxmap, pidxmap) - iv = ModelingToolkit.get_iv(sys) +function sub_u_p_into_symeq(eq, u, p, stidxmap, pidxmap, iv, tspan) eq = Symbolics.unwrap(eq) - stmap = Dict([st => u[1][i] for st => i in stidxmap]) - pmap = Dict([pa => p[i] for pa => i in pidxmap]) + stmap = Dict([st => u[1][i] for (st, i) in stidxmap]) + pmap = Dict([pa => p[i] for (pa, i) in pidxmap]) eq = Symbolics.substitute(eq, merge(stmap, pmap)) csyms = [] # Find most nested calls, substitute those first. while !isempty(find_callable_syms!(csyms, eq)) for sym in csyms - t = arguments(sym)[1] x = operation(sym) + t = arguments(sym)[1] + prog = (tspan[2] - tspan[1])/(t - tspan[1]) # 1 / the % of the timespan elapsed if isparameter(x) eq = Symbolics.substitute(eq, Dict(x(t) => p[pidxmap[x(iv)]])) elseif isvariable(x) - eq = Symbolics.substitute(eq, Dict(x(t) => u(val)[stidxmap[x(iv)]])) + eq = Symbolics.substitute(eq, Dict(x(t) => u[Int(end ÷ prog)][stidxmap[x(iv)]])) end end empty!(csyms) diff --git a/test/bvproblem.jl b/test/bvproblem.jl index 2d5535325a..6432c5ae02 100644 --- a/test/bvproblem.jl +++ b/test/bvproblem.jl @@ -1,80 +1,85 @@ -using BoundaryValueDiffEq, OrdinaryDiffEq +using BoundaryValueDiffEq, OrdinaryDiffEq, BoundaryValueDiffEqAscher using ModelingToolkit using ModelingToolkit: t_nounits as t, D_nounits as D ### Test Collocation solvers on simple problems solvers = [MIRK4, RadauIIa5, LobattoIIIa3] +daesolvers = [Ascher2, Ascher4, Ascher6] -@parameters α=7.5 β=4.0 γ=8.0 δ=5.0 -@variables x(t)=1.0 y(t)=2.0 - -eqs = [D(x) ~ α * x - β * x * y, - D(y) ~ -γ * y + δ * x * y] - -u0map = [x => 1.0, y => 2.0] -parammap = [α => 7.5, β => 4, γ => 8.0, δ => 5.0] -tspan = (0.0, 10.0) - -@mtkbuild lotkavolterra = ODESystem(eqs, t) -op = ODEProblem(lotkavolterra, u0map, tspan, parammap) -osol = solve(op, Vern9()) - -bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}( - lotkavolterra, u0map, tspan, parammap; eval_expression = true) - -for solver in solvers - sol = solve(bvp, solver(), dt = 0.01) - @test isapprox(sol.u[end], osol.u[end]; atol = 0.01) - @test sol.u[1] == [1.0, 2.0] -end - -# Test out of place -bvp2 = SciMLBase.BVProblem{false, SciMLBase.AutoSpecialize}( - lotkavolterra, u0map, tspan, parammap; eval_expression = true) - -for solver in solvers - sol = solve(bvp2, solver(), dt = 0.01) - @test isapprox(sol.u[end], osol.u[end]; atol = 0.01) - @test sol.u[1] == [1.0, 2.0] +let + @parameters α=7.5 β=4.0 γ=8.0 δ=5.0 + @variables x(t)=1.0 y(t)=2.0 + + eqs = [D(x) ~ α * x - β * x * y, + D(y) ~ -γ * y + δ * x * y] + + u0map = [x => 1.0, y => 2.0] + parammap = [α => 7.5, β => 4, γ => 8.0, δ => 5.0] + tspan = (0.0, 10.0) + + @mtkbuild lotkavolterra = ODESystem(eqs, t) + op = ODEProblem(lotkavolterra, u0map, tspan, parammap) + osol = solve(op, Vern9()) + + bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}( + lotkavolterra, u0map, tspan, parammap; eval_expression = true) + + for solver in solvers + sol = solve(bvp, solver(), dt = 0.01) + @test isapprox(sol.u[end], osol.u[end]; atol = 0.01) + @test sol.u[1] == [1.0, 2.0] + end + + # Test out of place + bvp2 = SciMLBase.BVProblem{false, SciMLBase.AutoSpecialize}( + lotkavolterra, u0map, tspan, parammap; eval_expression = true) + + for solver in solvers + sol = solve(bvp2, solver(), dt = 0.01) + @test isapprox(sol.u[end], osol.u[end]; atol = 0.01) + @test sol.u[1] == [1.0, 2.0] + end end ### Testing on pendulum - -@parameters g=9.81 L=1.0 -@variables θ(t) = π / 2 - -eqs = [D(D(θ)) ~ -(g / L) * sin(θ)] - -@mtkbuild pend = ODESystem(eqs, t) - -u0map = [θ => π / 2, D(θ) => π / 2] -parammap = [:L => 1.0, :g => 9.81] -tspan = (0.0, 6.0) - -op = ODEProblem(pend, u0map, tspan, parammap) -osol = solve(op, Vern9()) - -bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(pend, u0map, tspan, parammap) -for solver in solvers - sol = solve(bvp, solver(), dt = 0.01) - @test isapprox(sol.u[end], osol.u[end]; atol = 0.01) - @test sol.u[1] == [π / 2, π / 2] -end - -# Test out-of-place -bvp2 = SciMLBase.BVProblem{false, SciMLBase.FullSpecialize}(pend, u0map, tspan, parammap) - -for solver in solvers - sol = solve(bvp2, solver(), dt = 0.01) - @test isapprox(sol.u[end], osol.u[end]; atol = 0.01) - @test sol.u[1] == [π / 2, π / 2] +let + @parameters g=9.81 L=1.0 + @variables θ(t) = π / 2 + + eqs = [D(D(θ)) ~ -(g / L) * sin(θ)] + + @mtkbuild pend = ODESystem(eqs, t) + + u0map = [θ => π / 2, D(θ) => π / 2] + parammap = [:L => 1.0, :g => 9.81] + tspan = (0.0, 6.0) + + op = ODEProblem(pend, u0map, tspan, parammap) + osol = solve(op, Vern9()) + + bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(pend, u0map, tspan, parammap) + for solver in solvers + sol = solve(bvp, solver(), dt = 0.01) + @test isapprox(sol.u[end], osol.u[end]; atol = 0.01) + @test sol.u[1] == [π / 2, π / 2] + end + + # Test out-of-place + bvp2 = SciMLBase.BVProblem{false, SciMLBase.FullSpecialize}(pend, u0map, tspan, parammap) + + for solver in solvers + sol = solve(bvp2, solver(), dt = 0.01) + @test isapprox(sol.u[end], osol.u[end]; atol = 0.01) + @test sol.u[1] == [π / 2, π / 2] + end end ################################################### -### TESTING ODESystem with Constraint Equations ### +### ODESystem with Constraint Equations, DAEs with constraints ### ################################################### -# Cartesian pendulum from the docs. Testing that initialization is satisfied. +# Cartesian pendulum from the docs. +# DAE IVP solved using BoundaryValueDiffEq solvers. let @parameters g @variables x(t) y(t) [state_priority = 10] λ(t) @@ -109,14 +114,74 @@ let end end -# Adding a midpoint boundary condition. +function test_solvers(solvers, prob, u0map, constraints, equations = []; dt = 0.01) + for solver in solvers + sol = solve(bvp, solver(); dt) + + for (k, v) in u0map + @test sol[k][1] == v + end + + for cons in constraints + @test sol[cons.rhs - cons.lhs] ≈ 0 + end + + for eq in equations + @test sol[eq] ≈ 0 + end + end +end + +# Simple ODESystem with BVP constraints. +let + @parameters α=7.5 β=4.0 γ=8.0 δ=5.0 + @variables x(..) y(t) + + eqs = [D(x) ~ α * x - β * x * y, + D(y) ~ -γ * y + δ * x * y] + + u0map = [y => 2.0] + parammap = [α => 7.5, β => 4, γ => 8.0, δ => 5.0] + tspan = (0.0, 10.0) + guesses = [x => 1.0] + + @mtkbuild lotkavolterra = ODESystem(eqs, t) + op = ODEProblem(lotkavolterra, u0map, tspan, parammap, guesses = guesses) + + constraints = [x(6.) ~ 3] + bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(pend, u0map, tspan, parammap; guesses, constraints) + test_solvers(solvers, bvp, u0map, constraints) + + # Testing that more complicated constraints give correct solutions. + constraints = [y(2.) + x(8.) ~ 12] + bvp = SciMLBase.BVProblem{false, SciMLBase.FullSpecialize}(pend, u0map, tspan, parammap; guesses, constraints) + test_solvers(solvers, bvp, u0map, constraints) + + constraints = [α * β - x(6.) ~ 24] + bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(pend, u0map, tspan, parammap; guesses, constraints) + test_solvers(solvers, bvp, u0map, constraints) + + # Testing that errors are properly thrown when malformed constraints are given. + @variables bad(..) + constraints = [x(1.) + bad(3.) ~ 10] + @test_throws Exception bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(pend, u0map, tspan, parammap; guesses, constraints) + + constraints = [x(t) + y(t) ~ 3] + @test_throws Exception bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(pend, u0map, tspan, parammap; guesses, constraints) + + @parameters bad2 + constraints = [bad2 + x(0.) ~ 3] + @test_throws Exception bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(pend, u0map, tspan, parammap; guesses, constraints) +end + +# Adding a midpoint boundary constraint. +# Solve using BVDAE solvers. let @parameters g @variables x(..) y(t) [state_priority = 10] λ(t) eqs = [D(D(x(t))) ~ λ * x(t) D(D(y)) ~ λ * y - g - x(t)^2 + y^2 ~ 1 - x(0.5) ~ 1] + x(t)^2 + y^2 ~ 1] @mtkbuild pend = ODESystem(eqs, t) tspan = (0.0, 1.5) @@ -124,37 +189,24 @@ let parammap = [g => 1] guesses = [λ => 1] - prob = ODEProblem(pend, u0map, tspan, pmap; guesses, check_length = false) - sol = solve(prob, Rodas5P()) + constraints = [x(0.5) ~ 1] + bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(pend, u0map, tspan, parammap; constraints, guesses, check_length = false) + test_solvers(daesolvers, bvp, u0map, constraints) - bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(pend, u0map, tspan, parammap; guesses = guesses, check_length = false) - - for solver in solvers - sol = solve(bvp, solver(), dt = 0.01) - @test isapprox(sol.u[end], osol.u[end]; atol = 0.01) - conditions = getfield.(equations(pend)[3:end], :rhs) - @test [sol[conditions][1]; sol[x][1] - 1; sol[y][1]] ≈ 0 - @test sol.u[1] == [π / 2, π / 2] - end + bvp2 = SciMLBase.BVProblem{false, SciMLBase.FullSpecialize}(pend, u0map, tspan, parammap) + test_solvers(daesolvers, bvp2, u0map, constraints, get_alg_eqs(pend)) - bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(pend, u0map, tspan, parammap; guesses) - - for solver in solvers - sol = solve(bvp, solver(), dt = 0.01) - @test isapprox(sol.u[end], osol.u[end]; atol = 0.01) - conditions = getfield.(equations(pend)[3:end], :rhs) - @test [sol[conditions][1]; sol[x][1] - 1; sol[y][1]] ≈ 0 - end + # More complicated constraints. + u0map = [x(t) => 0.6] + guesses = [λ => 1, y(t) => 0.8] - bvp2 = SciMLBase.BVProblem{false, SciMLBase.FullSpecialize}(pend, u0map, tspan, parammap) - for solver in solvers - sol = solve(bvp, solver(), dt = 0.01) - @test isapprox(sol.u[end], osol.u[end]; atol = 0.01) - conditions = getfield.(equations(pend)[3:end], :rhs) - @test [sol[conditions][1]; sol[x][1] - 1; sol[y][1]] ≈ 0 - end -end + constraints = [x(0.5) ~ 1, + x(0.3)^3 + y(0.6)^2 ~ 0.5] + bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(pend, u0map, tspan, parammap; constraints, guesses, check_length = false) + test_solvers(daesolvers, bvp, u0map, constraints, get_alg_eqs(pend)) -# Testing a more complicated case with multiple constraints. -let + constraints = [x(0.4) * g ~ y(0.2), + y(0.7) ~ 0.3] + bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(pend, u0map, tspan, parammap; constraints, guesses, check_length = false) + test_solvers(daesolvers, bvp, u0map, constraints, get_alg_eqs(pend)) end From 5d082ab05dc674b005ef1a724aa7273a712c2b66 Mon Sep 17 00:00:00 2001 From: vyudu Date: Sat, 11 Jan 2025 16:20:55 -0500 Subject: [PATCH 0558/1337] up --- src/systems/diffeqs/abstractodesystem.jl | 8 +++---- test/bvproblem.jl | 27 ++++++++++++------------ 2 files changed, 18 insertions(+), 17 deletions(-) diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index 3c45b1b2f8..99b34ec1a8 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -917,7 +917,7 @@ end function SciMLBase.BVProblem{iip, specialize}(sys::AbstractODESystem, u0map = [], tspan = get_tspan(sys), parammap = DiffEqBase.NullParameters(); - constraints = nothing, guesses = nothing, + constraints = nothing, guesses = Dict(), version = nothing, tgrad = false, callback = nothing, check_length = true, @@ -929,7 +929,7 @@ function SciMLBase.BVProblem{iip, specialize}(sys::AbstractODESystem, u0map = [] if !iscomplete(sys) error("A completed system is required. Call `complete` or `structural_simplify` on the system before creating an `BVProblem`") end - !isnothing(callbacks) && error("BVP solvers do not support callbacks.") + !isnothing(callback) && error("BVP solvers do not support callbacks.") iv = get_iv(sys) constraintsts = nothing @@ -964,7 +964,7 @@ function SciMLBase.BVProblem{iip, specialize}(sys::AbstractODESystem, u0map = [] pidxmap = Dict([v => i for (i, v) in enumerate(ps)]) # Indices of states that have initial constraints. - u0i = has_alg_eqs(sys) ? collect(1:length(sts)) : [stidxmap[k] for k in keys(u0map)] + u0i = has_alg_eqs(sys) ? collect(1:length(sts)) : [stidxmap[k] for (k,v) in u0map] ni = length(u0i) bc = if !isnothing(constraints) @@ -994,7 +994,7 @@ function SciMLBase.BVProblem{iip, specialize}(sys::AbstractODESystem, u0map = [] end end - return BVProblem{iip}(f, bc, u0, tspan, p; kwargs1..., kwargs...) + return BVProblem{iip}(f, bc, u0, tspan, p; kwargs...) end get_callback(prob::BVProblem) = error("BVP solvers do not support callbacks.") diff --git a/test/bvproblem.jl b/test/bvproblem.jl index 6432c5ae02..e864433f3c 100644 --- a/test/bvproblem.jl +++ b/test/bvproblem.jl @@ -44,13 +44,14 @@ end ### Testing on pendulum let @parameters g=9.81 L=1.0 - @variables θ(t) = π / 2 + @variables θ(t) = π / 2 θ_t(t) - eqs = [D(D(θ)) ~ -(g / L) * sin(θ)] + eqs = [D(θ) ~ θ_t + D(θ_t) ~ -(g / L) * sin(θ)] @mtkbuild pend = ODESystem(eqs, t) - u0map = [θ => π / 2, D(θ) => π / 2] + u0map = [θ => π / 2, θ_t => π / 2] parammap = [:L => 1.0, :g => 9.81] tspan = (0.0, 6.0) @@ -74,9 +75,9 @@ let end end -################################################### -### ODESystem with Constraint Equations, DAEs with constraints ### -################################################### +################################################################## +### ODESystem with constraint equations, DAEs with constraints ### +################################################################## # Cartesian pendulum from the docs. # DAE IVP solved using BoundaryValueDiffEq solvers. @@ -90,19 +91,19 @@ let tspan = (0.0, 1.5) u0map = [x => 1, y => 0] - parammap = [g => 1] - guesses = [λ => 1] + pmap = [g => 1] + guess = [λ => 1] - prob = ODEProblem(pend, u0map, tspan, pmap; guesses) - sol = solve(prob, Rodas5P()) + prob = ODEProblem(pend, u0map, tspan, pmap; guesses = guess) + osol = solve(prob, Rodas5P()) - bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(pend, u0map, tspan, parammap; guesses) + bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(pend, u0map, tspan, parammap; guesses = guess) for solver in solvers - sol = solve(bvp, solver(), dt = 0.01) + sol = solve(bvp, solver(), dt = 0.001) @test isapprox(sol.u[end], osol.u[end]; atol = 0.01) conditions = getfield.(equations(pend)[3:end], :rhs) - @test [sol[conditions][1]; sol[x][1] - 1; sol[y][1]] ≈ 0 + @test isapprox([sol[conditions][1]; sol[x][1] - 1; sol[y][1]], zeros(5), atol = 0.001) end bvp2 = SciMLBase.BVProblem{false, SciMLBase.FullSpecialize}(pend, u0map, tspan, parammap) From 31afd3ce4a0f86b14ef1d8f2633d381ed1dfba6a Mon Sep 17 00:00:00 2001 From: sivasathyaseeelan Date: Sun, 12 Jan 2025 17:53:22 +0530 Subject: [PATCH 0559/1337] added tests for SDEFunctionExpr Signed-off-by: sivasathyaseeelan --- test/sdesystem.jl | 52 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/test/sdesystem.jl b/test/sdesystem.jl index 8069581dcc..d25f9b3111 100644 --- a/test/sdesystem.jl +++ b/test/sdesystem.jl @@ -868,3 +868,55 @@ end @test length(ModelingToolkit.get_noiseeqs(sys)) == 1 @test length(observed(sys)) == 1 end + +@testset "SDEFunctionExpr" begin + @parameters σ ρ β + @variables x(tt) y(tt) z(tt) + + eqs = [D(x) ~ σ * (y - x), + D(y) ~ x * (ρ - z) - y, + D(z) ~ x * y - β * z] + + noiseeqs = [0.1 * x, + 0.1 * y, + 0.1 * z] + + @named sys = ODESystem(eqs, tt, [x, y, z], [σ, ρ, β]) + + @named de = SDESystem(eqs, noiseeqs, tt, [x, y, z], [σ, ρ, β], tspan = (0.0, 10.0)) + de = complete(de) + + f = SDEFunctionExpr(de) + @test f isa Expr + + @testset "Configuration Tests" begin + # Test with `tgrad` + f_tgrad = SDEFunctionExpr(de; tgrad = true) + @test f_tgrad isa Expr + + # Test with `jac` + f_jac = SDEFunctionExpr(de; jac = true) + @test f_jac isa Expr + + # Test with sparse Jacobian + f_sparse = SDEFunctionExpr(de; sparse = true) + @test f_sparse isa Expr + end + + @testset "Mass Matrix Handling" begin + # Test with default mass matrix + @test eval(SDEFunctionExpr(de).args[7]) == UniformScaling{Bool}(true) + + # Test with non-trivial mass matrix + u0 = [1.0, 2.0, 3.0] + f_mass = SDEFunctionExpr(de, u0 = u0) + @test f_mass isa Expr + end + + @testset "Ordering Tests" begin + dvs = [z, y, x] + ps = [β, ρ, σ] + f_order = SDEFunctionExpr(de, dvs, ps) + @test f_order isa Expr + end +end \ No newline at end of file From 0a4fca7025004218018481818fc29c9e17808251 Mon Sep 17 00:00:00 2001 From: vyudu Date: Mon, 13 Jan 2025 11:21:15 -0500 Subject: [PATCH 0560/1337] add error --- src/systems/diffeqs/sdesystem.jl | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/systems/diffeqs/sdesystem.jl b/src/systems/diffeqs/sdesystem.jl index 366e8a6d09..95b5a4cc59 100644 --- a/src/systems/diffeqs/sdesystem.jl +++ b/src/systems/diffeqs/sdesystem.jl @@ -737,6 +737,7 @@ function DiffEqBase.SDEProblem{iip, specialize}( if !iscomplete(sys) error("A completed `SDESystem` is required. Call `complete` or `structural_simplify` on the system before creating an `SDEProblem`") end + f, u0, p = process_SciMLProblem( SDEFunction{iip, specialize}, sys, u0map, parammap; check_length, t = tspan === nothing ? nothing : tspan[1], kwargs...) @@ -767,6 +768,19 @@ function DiffEqBase.SDEProblem{iip, specialize}( noise_rate_prototype = noise_rate_prototype, kwargs...) end +function DiffEqBase.SDEProblem{iip, specialize}( + sys::ODESystem, u0map = [], tspan = get_tspan(sys), + parammap = DiffEqBase.NullParameters(); + sparsenoise = nothing, check_length = true, + callback = nothing, kwargs...) where {iip, specialize} + + if any(ModelingToolkit.isbrownian, unknowns(sys)) + error("SDESystem constructed by defining Brownian variables with @brownian must be simplified by calling `structural_simplify` before a SDEProblem can be constructed.") + else + error("Cannot construct SDEProblem from an ODESystem.") + end +end + """ ```julia DiffEqBase.SDEProblem{iip}(sys::SDESystem, u0map, tspan, p = parammap; From 9eee7fd0c895612558b997be37e985f61e3f0f6c Mon Sep 17 00:00:00 2001 From: vyudu Date: Mon, 13 Jan 2025 11:26:56 -0500 Subject: [PATCH 0561/1337] up --- src/systems/diffeqs/sdesystem.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/systems/diffeqs/sdesystem.jl b/src/systems/diffeqs/sdesystem.jl index 95b5a4cc59..26f26b4263 100644 --- a/src/systems/diffeqs/sdesystem.jl +++ b/src/systems/diffeqs/sdesystem.jl @@ -777,7 +777,7 @@ function DiffEqBase.SDEProblem{iip, specialize}( if any(ModelingToolkit.isbrownian, unknowns(sys)) error("SDESystem constructed by defining Brownian variables with @brownian must be simplified by calling `structural_simplify` before a SDEProblem can be constructed.") else - error("Cannot construct SDEProblem from an ODESystem.") + error("Cannot construct SDEProblem from a normal ODESystem.") end end From 65b9912a470f298cd92f542a36c4835134e72d92 Mon Sep 17 00:00:00 2001 From: vyudu Date: Mon, 13 Jan 2025 12:06:02 -0500 Subject: [PATCH 0562/1337] init --- src/systems/diffeqs/odesystem.jl | 2 ++ src/systems/diffeqs/sdesystem.jl | 6 ++++-- test/odesystem.jl | 26 ++++++++++++++++++++++++++ test/sdesystem.jl | 26 ++++++++++++++++++++++++++ 4 files changed, 58 insertions(+), 2 deletions(-) diff --git a/src/systems/diffeqs/odesystem.jl b/src/systems/diffeqs/odesystem.jl index 61f16fd926..aafd1c254a 100644 --- a/src/systems/diffeqs/odesystem.jl +++ b/src/systems/diffeqs/odesystem.jl @@ -373,6 +373,8 @@ function Base.:(==)(sys1::ODESystem, sys2::ODESystem) _eq_unordered(get_eqs(sys1), get_eqs(sys2)) && _eq_unordered(get_unknowns(sys1), get_unknowns(sys2)) && _eq_unordered(get_ps(sys1), get_ps(sys2)) && + _eq_unordered(continuous_events(sys1), continuous_events(sys2)) && + _eq_unordered(discrete_events(sys1), discrete_events(sys2)) && all(s1 == s2 for (s1, s2) in zip(get_systems(sys1), get_systems(sys2))) end diff --git a/src/systems/diffeqs/sdesystem.jl b/src/systems/diffeqs/sdesystem.jl index 366e8a6d09..5e25941333 100644 --- a/src/systems/diffeqs/sdesystem.jl +++ b/src/systems/diffeqs/sdesystem.jl @@ -279,11 +279,13 @@ function Base.:(==)(sys1::SDESystem, sys2::SDESystem) iv2 = get_iv(sys2) isequal(iv1, iv2) && isequal(nameof(sys1), nameof(sys2)) && - isequal(get_eqs(sys1), get_eqs(sys2)) && - isequal(get_noiseeqs(sys1), get_noiseeqs(sys2)) && + _eq_unordered(get_eqs(sys1), get_eqs(sys2)) && + _eq_unordered(get_noiseeqs(sys1), get_noiseeqs(sys2)) && isequal(get_is_scalar_noise(sys1), get_is_scalar_noise(sys2)) && _eq_unordered(get_unknowns(sys1), get_unknowns(sys2)) && _eq_unordered(get_ps(sys1), get_ps(sys2)) && + _eq_unordered(continuous_events(sys1), continuous_events(sys2)) && + _eq_unordered(discrete_events(sys1), discrete_events(sys2)) && all(s1 == s2 for (s1, s2) in zip(get_systems(sys1), get_systems(sys2))) end diff --git a/test/odesystem.jl b/test/odesystem.jl index 85d135b338..e230c5e9a3 100644 --- a/test/odesystem.jl +++ b/test/odesystem.jl @@ -1552,3 +1552,29 @@ end expected_tstops = unique!(sort!(vcat(0.0:0.075:10.0, 0.1, 0.2, 0.65, 0.35, 0.45))) @test all(x -> any(isapprox(x, atol = 1e-6), sol2.t), expected_tstops) end + +# Test `isequal` +let + @variables X(t) + @parameters p d + 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]] + + osys1 = complete(ODESystem([eq], t; name = :osys, continuous_events)) + osys2 = complete(ODESystem([eq], t; name = :osys)) + @test osys1 !== osys2 + + osys1 = complete(ODESystem([eq], t; name = :osys, discrete_events)) + osys2 = complete(ODESystem([eq], t; name = :osys)) + @test osys1 !== osys2 + + osys1 = complete(ODESystem([eq], t; name = :osys, continuous_events)) + osys2 = complete(ODESystem([eq], t; name = :osys, discrete_events)) + @test osys1 !== osys2 +end diff --git a/test/sdesystem.jl b/test/sdesystem.jl index 8069581dcc..7ff7d5e7c8 100644 --- a/test/sdesystem.jl +++ b/test/sdesystem.jl @@ -868,3 +868,29 @@ end @test length(ModelingToolkit.get_noiseeqs(sys)) == 1 @test length(observed(sys)) == 1 end + +# Test `isequal` handles events +let + @variables X(t) + @parameters p d + @brownian a + seq = D(X) ~ p - d*X + a + @mtkbuild ssys1 = System([eq], t; name = :ssys) + @mtkbuild ssys2 = System([eq], t; name = :ssys) + @test ssys1 == ssys2 # true + + continuous_events = [[X ~ 1.0] => [X ~ X + 5.0]] + discrete_events = [5.0 => [d ~ d / 2.0]] + + @mtkbuild ssys1 = System([eq], t; name = :ssys, continuous_events) + @mtkbuild ssys2 = System([eq], t; name = :ssys) + @test ssys1 !== ssys2 + + @mtkbuild ssys1 = System([eq], t; name = :ssys, discrete_events) + @mtkbuild ssys2 = System([eq], t; name = :ssys) + @test ssys1 !== ssys2 + + @mtkbuild ssys1 = System([eq], t; name = :ssys, continuous_events) + @mtkbuild ssys2 = System([eq], t; name = :ssys, discrete_events) + @test ssys1 !== ssys2 +end From fe704edc86b39d5e72df3091140b22f6b5bca38d Mon Sep 17 00:00:00 2001 From: sivasathyaseeelan Date: Tue, 14 Jan 2025 03:24:34 +0530 Subject: [PATCH 0563/1337] modified test Signed-off-by: sivasathyaseeelan --- test/sdesystem.jl | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/test/sdesystem.jl b/test/sdesystem.jl index d25f9b3111..1593458b63 100644 --- a/test/sdesystem.jl +++ b/test/sdesystem.jl @@ -903,16 +903,6 @@ end @test f_sparse isa Expr end - @testset "Mass Matrix Handling" begin - # Test with default mass matrix - @test eval(SDEFunctionExpr(de).args[7]) == UniformScaling{Bool}(true) - - # Test with non-trivial mass matrix - u0 = [1.0, 2.0, 3.0] - f_mass = SDEFunctionExpr(de, u0 = u0) - @test f_mass isa Expr - end - @testset "Ordering Tests" begin dvs = [z, y, x] ps = [β, ρ, σ] From b83e003babe1347439d05ea87a8b90995f1dcc82 Mon Sep 17 00:00:00 2001 From: vyudu Date: Tue, 14 Jan 2025 00:59:31 -0500 Subject: [PATCH 0564/1337] refactor the bc creation function --- src/systems/diffeqs/abstractodesystem.jl | 189 ++++++++++--------- test/bvproblem.jl | 230 ++++++++++++++++------- 2 files changed, 264 insertions(+), 155 deletions(-) diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index 99b34ec1a8..25347a17cd 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -931,68 +931,60 @@ function SciMLBase.BVProblem{iip, specialize}(sys::AbstractODESystem, u0map = [] end !isnothing(callback) && error("BVP solvers do not support callbacks.") - iv = get_iv(sys) + has_alg_eqs(sys) && error("The BVProblem currently does not support ODESystems with algebraic equations.") # Remove this when the BVDAE solvers get updated, the codegen should work when it does. + constraintsts = nothing constraintps = nothing sts = unknowns(sys) ps = parameters(sys) - if !isnothing(constraints) + # Constraint validation + f_cons = if !isnothing(constraints) constraints isa Equation || constraints isa Vector{Equation} || error("Constraints must be specified as an equation or a vector of equations.") (length(constraints) + length(u0map) > length(sts)) && - error("The BVProblem is overdetermined. The total number of conditions (# constraints + # fixed initial values given by u0map) cannot exceed the total number of states.") - - constraintsts = OrderedSet() - constraintps = OrderedSet() - - for eq in constraints - collect_vars!(constraintsts, constraintps, eq, iv) - validate_constraint_syms(eq, constraintsts, constraintps, Set(sts), Set(ps), iv) - empty!(constraintsts) - empty!(constraintps) - end + error("The BVProblem is overdetermined. The total number of conditions (# constraints + # fixed initial values given by u0map) cannot exceed the total number of states.") end - f, u0, p = process_SciMLProblem(ODEFunction{iip, specialize}, sys, u0map, parammap; + # ODESystems without algebraic equations should use both fixed values + guesses + # for initialization. + _u0map = has_alg_eqs(sys) ? u0map : merge(Dict(u0map), Dict(guesses)) + f, u0, p = process_SciMLProblem(ODEFunction{iip, specialize}, sys, _u0map, parammap; t = tspan !== nothing ? tspan[1] : tspan, guesses, check_length, warn_initialize_determined, eval_expression, eval_module, kwargs...) stidxmap = Dict([v => i for (i, v) in enumerate(sts)]) - pidxmap = Dict([v => i for (i, v) in enumerate(ps)]) - - # Indices of states that have initial constraints. - u0i = has_alg_eqs(sys) ? collect(1:length(sts)) : [stidxmap[k] for (k,v) in u0map] - ni = length(u0i) - - bc = if !isnothing(constraints) - ne = length(constraints) - if iip - (residual,u,p,t) -> begin - residual[1:ni] .= u[1][u0i] .- u0[u0i] - residual[ni+1:ni+ne] .= map(constraints) do cons - sub_u_p_into_symeq(cons.rhs - cons.lhs, u, p, stidxmap, pidxmap, iv, tspan) - end - end - else - (u,p,t) -> begin - consresid = map(constraints) do cons - sub_u_p_into_symeq(cons.rhs-cons.lhs, u, p, stidxmap, pidxmap, iv, tspan) - end - resid = vcat(u[1][u0i] - u0[u0i], consresid) - end - end - else - if iip - (residual,u,p,t) -> begin - residual .= u[1] .- u0 - end - else - (u,p,t) -> (u[1] - u0) - end - end + u0_idxs = has_alg_eqs(sys) ? collect(1:length(sts)) : [stidxmap[k] for (k,v) in u0map] + + # bc = if !isnothing(constraints) && iip + # (residual,u,p,t) -> begin + # println(u(0.5)) + # residual[1:ni] .= u[1][u0i] .- u0[u0i] + # for (i, cons) in enumerate(constraints) + # residual[ni+i] = eval_symbolic_residual(cons, u, p, stidxmap, pidxmap, iv, tspan) + # end + # end + + # elseif !isnothing(constraints) && !iip + # (u,p,t) -> begin + # consresid = map(constraints) do cons + # eval_symbolic_residual(cons, u, p, stidxmap, pidxmap, iv, tspan) + # end + # resid = vcat(u[1][u0i] - u0[u0i], consresid) + # end + + # elseif iip + # (residual,u,p,t) -> begin + # println(u(0.5)) + # residual .= u[1] .- u0 + # end + + # else + # (u,p,t) -> (u[1] - u0) + # end + bc = process_constraints(sys, constraints, u0, u0_idxs, tspan, iip) return BVProblem{iip}(f, bc, u0, tspan, p; kwargs...) end @@ -1001,11 +993,10 @@ get_callback(prob::BVProblem) = error("BVP solvers do not support callbacks.") # Validate that all the variables in the BVP constraints are well-formed states or parameters. function validate_constraint_syms(eq, constraintsts, constraintps, sts, ps, iv) - ModelingToolkit.check_variables(constraintsts) - ModelingToolkit.check_parameters(constraintps) - for var in constraintsts - if arguments(var) == iv + if length(arguments(var)) > 1 + error("Too many arguments for variable $var.") + elseif arguments(var) == iv var ∈ sts || error("Constraint equation $eq contains a variable $var that is not a variable of the ODESystem.") error("Constraint equation $eq contains a variable $var that does not have a specified argument. Such equations should be specified as algebraic equations to the ODESystem rather than a boundary constraints.") else @@ -1017,53 +1008,81 @@ function validate_constraint_syms(eq, constraintsts, constraintps, sts, ps, iv) if !iscall(var) var ∈ ps || error("Constraint equation $eq contains a parameter $var that is not a parameter of the ODESystem.") else + length(arguments(var)) > 1 && error("Too many arguments for parameter $var.") operation(var) ∈ ps || error("Constraint equations contain a parameter $var that is not a parameter of the ODESystem.") end end end -# Helper to substitute numeric values for u, p into the algebraic equations in the ODESystem. Used to construct the boundary condition function. -# Take a system with variables x,y, parameters g -# -# 1 + x(0) + y(0) → 1 + u[1][1] + u[1][2] -# x(0.5) → u(0.5)[1] -# x(0.5)*g(0.5) → u(0.5)[1]*p[1] -function sub_u_p_into_symeq(eq, u, p, stidxmap, pidxmap, iv, tspan) - eq = Symbolics.unwrap(eq) - - stmap = Dict([st => u[1][i] for (st, i) in stidxmap]) - pmap = Dict([pa => p[i] for (pa, i) in pidxmap]) - eq = Symbolics.substitute(eq, merge(stmap, pmap)) - - csyms = [] - # Find most nested calls, substitute those first. - while !isempty(find_callable_syms!(csyms, eq)) - for sym in csyms - x = operation(sym) - t = arguments(sym)[1] - prog = (tspan[2] - tspan[1])/(t - tspan[1]) # 1 / the % of the timespan elapsed - - if isparameter(x) - eq = Symbolics.substitute(eq, Dict(x(t) => p[pidxmap[x(iv)]])) - elseif isvariable(x) - eq = Symbolics.substitute(eq, Dict(x(t) => u[Int(end ÷ prog)][stidxmap[x(iv)]])) +""" + process_constraints(sys, constraints, u0, tspan, iip) + + Given an ODESystem with some constraints, generate the boundary condition function. +""" +function process_constraints(sys::ODESystem, constraints, u0, u0_idxs, tspan, iip) + + iv = get_iv(sys) + sts = get_unknowns(sys) + ps = get_ps(sys) + np = length(ps) + ns = length(sts) + + stidxmap = Dict([v => i for (i, v) in enumerate(sts)]) + pidxmap = Dict([v => i for (i, v) in enumerate(ps)]) + + @variables sol(..)[1:ns] p[1:np] + exprs = Any[] + + constraintsts = OrderedSet() + constraintps = OrderedSet() + + !isnothing(constraints) && for cons in constraints + collect_vars!(constraintsts, constraintps, cons, iv) + validate_constraint_syms(cons, constraintsts, constraintps, Set(sts), Set(ps), iv) + expr = cons.rhs - cons.lhs + + for st in constraintsts + x = operation(st) + t = arguments(st)[1] + idx = stidxmap[x(iv)] + + expr = Symbolics.substitute(expr, Dict(x(t) => sol(t)[idx])) + end + + for var in constraintps + if iscall(var) + x = operation(var) + t = arguments(var)[1] + idx = pidxmap[x] + + expr = Symbolics.substitute(expr, Dict(x(t) => p[idx])) + else + idx = pidxmap[var] + expr = Symbolics.substitute(expr, Dict(var => p[idx])) end end - empty!(csyms) + + empty!(constraintsts) + empty!(constraintps) + push!(exprs, expr) end - eq -end -function find_callable_syms!(csyms, ex) - ex = Symbolics.unwrap(ex) + init_cond_exprs = Any[] - if iscall(ex) - operation(ex) isa Symbolic && (arguments(ex)[1] isa Symbolic) && push!(csyms, ex) # only add leaf nodes - for arg in arguments(ex) - find_callable_syms!(csyms, arg) + for i in u0_idxs + expr = sol(tspan[1])[i] - u0[i] + push!(init_cond_exprs, expr) + end + + exprs = vcat(init_cond_exprs, exprs) + bcs = Symbolics.build_function(exprs, sol, p, expression = Val{false}) + if iip + return (resid, u, p, t) -> begin + bcs[2](resid, u, p) end + else + return (u, p, t) -> bcs[1](u, p) end - csyms end """ diff --git a/test/bvproblem.jl b/test/bvproblem.jl index e864433f3c..7e0d45b128 100644 --- a/test/bvproblem.jl +++ b/test/bvproblem.jl @@ -1,3 +1,5 @@ +### TODO: update when BoundaryValueDiffEqAscher is updated to use the normal boundary condition conventions + using BoundaryValueDiffEq, OrdinaryDiffEq, BoundaryValueDiffEqAscher using ModelingToolkit using ModelingToolkit: t_nounits as t, D_nounits as D @@ -81,51 +83,140 @@ end # Cartesian pendulum from the docs. # DAE IVP solved using BoundaryValueDiffEq solvers. +# let +# @parameters g +# @variables x(t) y(t) [state_priority = 10] λ(t) +# eqs = [D(D(x)) ~ λ * x +# D(D(y)) ~ λ * y - g +# x^2 + y^2 ~ 1] +# @mtkbuild pend = ODESystem(eqs, t) +# +# tspan = (0.0, 1.5) +# u0map = [x => 1, y => 0] +# pmap = [g => 1] +# guess = [λ => 1] +# +# prob = ODEProblem(pend, u0map, tspan, pmap; guesses = guess) +# osol = solve(prob, Rodas5P()) +# +# zeta = [0., 0., 0., 0., 0.] +# bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(pend, u0map, tspan, parammap; guesses = guess) +# +# for solver in solvers +# sol = solve(bvp, solver(zeta), dt = 0.001) +# @test isapprox(sol.u[end], osol.u[end]; atol = 0.01) +# conditions = getfield.(equations(pend)[3:end], :rhs) +# @test isapprox([sol[conditions][1]; sol[x][1] - 1; sol[y][1]], zeros(5), atol = 0.001) +# end +# +# bvp2 = SciMLBase.BVProblem{false, SciMLBase.FullSpecialize}(pend, u0map, tspan, parammap) +# for solver in solvers +# sol = solve(bvp, solver(zeta), dt = 0.01) +# @test isapprox(sol.u[end], osol.u[end]; atol = 0.01) +# conditions = getfield.(equations(pend)[3:end], :rhs) +# @test [sol[conditions][1]; sol[x][1] - 1; sol[y][1]] ≈ 0 +# end +# end + +# Test generation of boundary condition function. let - @parameters g - @variables x(t) y(t) [state_priority = 10] λ(t) - eqs = [D(D(x)) ~ λ * x - D(D(y)) ~ λ * y - g - x^2 + y^2 ~ 1] - @mtkbuild pend = ODESystem(eqs, t) - - tspan = (0.0, 1.5) - u0map = [x => 1, y => 0] - pmap = [g => 1] - guess = [λ => 1] - - prob = ODEProblem(pend, u0map, tspan, pmap; guesses = guess) - osol = solve(prob, Rodas5P()) - - bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(pend, u0map, tspan, parammap; guesses = guess) + @parameters α=7.5 β=4.0 γ=8.0 δ=5.0 + @variables x(..) y(t) + eqs = [D(x(t)) ~ α * x(t) - β * x(t) * y, + D(y) ~ -γ * y + δ * x(t) * y] - for solver in solvers - sol = solve(bvp, solver(), dt = 0.001) - @test isapprox(sol.u[end], osol.u[end]; atol = 0.01) - conditions = getfield.(equations(pend)[3:end], :rhs) - @test isapprox([sol[conditions][1]; sol[x][1] - 1; sol[y][1]], zeros(5), atol = 0.001) + tspan = (0., 10.) + @mtkbuild lksys = ODESystem(eqs, t) + + function lotkavolterra!(du, u, p, t) + du[1] = p[1]*u[1] - p[2]*u[1]*u[2] + du[2] = -p[3]*u[2] + p[4]*u[1]*u[2] end - bvp2 = SciMLBase.BVProblem{false, SciMLBase.FullSpecialize}(pend, u0map, tspan, parammap) - for solver in solvers - sol = solve(bvp, solver(), dt = 0.01) - @test isapprox(sol.u[end], osol.u[end]; atol = 0.01) - conditions = getfield.(equations(pend)[3:end], :rhs) - @test [sol[conditions][1]; sol[x][1] - 1; sol[y][1]] ≈ 0 + function lotkavolterra(u, p, t) + [p[1]*u[1] - p[2]*u[1]*u[2], -p[3]*u[2] + p[4]*u[1]*u[2]] + end + # Compare the built bc function to the actual constructed one. + function bc!(resid, u, p, t) + resid[1] = u[1][1] - 1. + resid[2] = u[1][2] - 2. + nothing + end + function bc(u, p, t) + [u[1][1] - 1., u[1][2] - 2.] + end + + constraints = nothing + u0 = [1., 2.]; p = [7.5, 4., 8., 5.] + genbc_iip = ModelingToolkit.process_constraints(lksys, constraints, u0, [1, 2], tspan, true) + genbc_oop = ModelingToolkit.process_constraints(lksys, constraints, u0, [1, 2], tspan, false) + + bvpi1 = SciMLBase.BVProblem(lotkavolterra!, bc!, [1,2], tspan, p) + bvpi2 = SciMLBase.BVProblem(lotkavolterra!, genbc_iip, [1,2], tspan, p) + + sol1 = solve(bvpi1, MIRK4(), dt = 0.01) + sol2 = solve(bvpi2, MIRK4(), dt = 0.01) + @test sol1 ≈ sol2 + + bvpo1 = BVProblem(lotkavolterra, bc, [1,2], tspan, p) + bvpo2 = BVProblem(lotkavolterra, genbc_oop, [1,2], tspan, p) + + sol1 = solve(bvpo1, MIRK4(), dt = 0.01) + sol2 = solve(bvpo2, MIRK4(), dt = 0.01) + @test sol1 ≈ sol2 + + # Test with a constraint. + constraints = [x(0.5) ~ 1.] + + function bc!(resid, u, p, t) + resid[1] = u[1][2] - 2. + resid[2] = u(0.5)[1] - 1. + end + function bc(u, p, t) + [u[1][2] - 2., u(0.5)[1] - 1.] end + + u0 = [1., 2.] + genbc_iip = ModelingToolkit.process_constraints(lksys, constraints, u0, [2], tspan, true) + genbc_oop = ModelingToolkit.process_constraints(lksys, constraints, u0, [2], tspan, false) + + bvpi1 = SciMLBase.BVProblem(lotkavolterra!, bc!, [1,2], tspan, p) + bvpi2 = SciMLBase.BVProblem(lotkavolterra!, genbc_iip, [1,2], tspan, p) + + sol1 = solve(bvpi1, MIRK4(), dt = 0.01) + sol2 = solve(bvpi2, MIRK4(), dt = 0.01) + @test sol1 ≈ sol2 # don't get true equality here, not sure why + + bvpo1 = BVProblem(lotkavolterra, bc, [1,2], tspan, p) + bvpo2 = BVProblem(lotkavolterra, genbc_oop, [1,2], tspan, p) + + sol1 = solve(bvpo1, MIRK4(), dt = 0.01) + sol2 = solve(bvpo2, MIRK4(), dt = 0.01) + @test sol1 ≈ sol2 end function test_solvers(solvers, prob, u0map, constraints, equations = []; dt = 0.01) for solver in solvers - sol = solve(bvp, solver(); dt) + sol = solve(prob, solver(); dt) + @test successful_retcode(sol.retcode) + p = prob.p; t = sol.t; bc = prob.f.bc + ns = length(prob.u0) + + if isinplace(bvp.f) + resid = zeros(ns) + bc!(resid, sol, p, t) + @test isapprox(zeros(ns), resid) + else + @test isapprox(zeros(ns), bc(sol, p, t)) + end for (k, v) in u0map @test sol[k][1] == v end - for cons in constraints - @test sol[cons.rhs - cons.lhs] ≈ 0 - end + # for cons in constraints + # @test sol[cons.rhs - cons.lhs] ≈ 0 + # end for eq in equations @test sol[eq] ≈ 0 @@ -138,19 +229,18 @@ let @parameters α=7.5 β=4.0 γ=8.0 δ=5.0 @variables x(..) y(t) - eqs = [D(x) ~ α * x - β * x * y, - D(y) ~ -γ * y + δ * x * y] + eqs = [D(x(t)) ~ α * x(t) - β * x(t) * y, + D(y) ~ -γ * y + δ * x(t) * y] u0map = [y => 2.0] parammap = [α => 7.5, β => 4, γ => 8.0, δ => 5.0] tspan = (0.0, 10.0) - guesses = [x => 1.0] + guesses = [x(t) => 1.0] @mtkbuild lotkavolterra = ODESystem(eqs, t) - op = ODEProblem(lotkavolterra, u0map, tspan, parammap, guesses = guesses) constraints = [x(6.) ~ 3] - bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(pend, u0map, tspan, parammap; guesses, constraints) + bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(lotkavolterra, u0map, tspan, parammap; guesses, constraints) test_solvers(solvers, bvp, u0map, constraints) # Testing that more complicated constraints give correct solutions. @@ -177,37 +267,37 @@ end # Adding a midpoint boundary constraint. # Solve using BVDAE solvers. -let - @parameters g - @variables x(..) y(t) [state_priority = 10] λ(t) - eqs = [D(D(x(t))) ~ λ * x(t) - D(D(y)) ~ λ * y - g - x(t)^2 + y^2 ~ 1] - @mtkbuild pend = ODESystem(eqs, t) - - tspan = (0.0, 1.5) - u0map = [x(t) => 0.6, y => 0.8] - parammap = [g => 1] - guesses = [λ => 1] - - constraints = [x(0.5) ~ 1] - bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(pend, u0map, tspan, parammap; constraints, guesses, check_length = false) - test_solvers(daesolvers, bvp, u0map, constraints) - - bvp2 = SciMLBase.BVProblem{false, SciMLBase.FullSpecialize}(pend, u0map, tspan, parammap) - test_solvers(daesolvers, bvp2, u0map, constraints, get_alg_eqs(pend)) - - # More complicated constraints. - u0map = [x(t) => 0.6] - guesses = [λ => 1, y(t) => 0.8] - - constraints = [x(0.5) ~ 1, - x(0.3)^3 + y(0.6)^2 ~ 0.5] - bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(pend, u0map, tspan, parammap; constraints, guesses, check_length = false) - test_solvers(daesolvers, bvp, u0map, constraints, get_alg_eqs(pend)) - - constraints = [x(0.4) * g ~ y(0.2), - y(0.7) ~ 0.3] - bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(pend, u0map, tspan, parammap; constraints, guesses, check_length = false) - test_solvers(daesolvers, bvp, u0map, constraints, get_alg_eqs(pend)) -end +# let +# @parameters g +# @variables x(..) y(t) [state_priority = 10] λ(t) +# eqs = [D(D(x(t))) ~ λ * x(t) +# D(D(y)) ~ λ * y - g +# x(t)^2 + y^2 ~ 1] +# @mtkbuild pend = ODESystem(eqs, t) +# +# tspan = (0.0, 1.5) +# u0map = [x(t) => 0.6, y => 0.8] +# parammap = [g => 1] +# guesses = [λ => 1] +# +# constraints = [x(0.5) ~ 1] +# bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(pend, u0map, tspan, parammap; constraints, guesses, check_length = false) +# test_solvers(daesolvers, bvp, u0map, constraints) +# +# bvp2 = SciMLBase.BVProblem{false, SciMLBase.FullSpecialize}(pend, u0map, tspan, parammap) +# test_solvers(daesolvers, bvp2, u0map, constraints, get_alg_eqs(pend)) +# +# # More complicated constraints. +# u0map = [x(t) => 0.6] +# guesses = [λ => 1, y(t) => 0.8] +# +# constraints = [x(0.5) ~ 1, +# x(0.3)^3 + y(0.6)^2 ~ 0.5] +# bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(pend, u0map, tspan, parammap; constraints, guesses, check_length = false) +# test_solvers(daesolvers, bvp, u0map, constraints, get_alg_eqs(pend)) +# +# constraints = [x(0.4) * g ~ y(0.2), +# y(0.7) ~ 0.3] +# bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(pend, u0map, tspan, parammap; constraints, guesses, check_length = false) +# test_solvers(daesolvers, bvp, u0map, constraints, get_alg_eqs(pend)) +# end From db5eb66ea29533e51aefd6ea49ea04e0257f3201 Mon Sep 17 00:00:00 2001 From: vyudu Date: Tue, 14 Jan 2025 13:46:31 -0500 Subject: [PATCH 0565/1337] up --- src/systems/diffeqs/abstractodesystem.jl | 33 +----- test/bvproblem.jl | 144 ++++++++++++----------- 2 files changed, 76 insertions(+), 101 deletions(-) diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index 25347a17cd..0e8435942c 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -939,7 +939,7 @@ function SciMLBase.BVProblem{iip, specialize}(sys::AbstractODESystem, u0map = [] ps = parameters(sys) # Constraint validation - f_cons = if !isnothing(constraints) + if !isnothing(constraints) constraints isa Equation || constraints isa Vector{Equation} || error("Constraints must be specified as an equation or a vector of equations.") @@ -958,32 +958,6 @@ function SciMLBase.BVProblem{iip, specialize}(sys::AbstractODESystem, u0map = [] stidxmap = Dict([v => i for (i, v) in enumerate(sts)]) u0_idxs = has_alg_eqs(sys) ? collect(1:length(sts)) : [stidxmap[k] for (k,v) in u0map] - # bc = if !isnothing(constraints) && iip - # (residual,u,p,t) -> begin - # println(u(0.5)) - # residual[1:ni] .= u[1][u0i] .- u0[u0i] - # for (i, cons) in enumerate(constraints) - # residual[ni+i] = eval_symbolic_residual(cons, u, p, stidxmap, pidxmap, iv, tspan) - # end - # end - - # elseif !isnothing(constraints) && !iip - # (u,p,t) -> begin - # consresid = map(constraints) do cons - # eval_symbolic_residual(cons, u, p, stidxmap, pidxmap, iv, tspan) - # end - # resid = vcat(u[1][u0i] - u0[u0i], consresid) - # end - - # elseif iip - # (residual,u,p,t) -> begin - # println(u(0.5)) - # residual .= u[1] .- u0 - # end - - # else - # (u,p,t) -> (u[1] - u0) - # end bc = process_constraints(sys, constraints, u0, u0_idxs, tspan, iip) return BVProblem{iip}(f, bc, u0, tspan, p; kwargs...) @@ -1075,11 +1049,10 @@ function process_constraints(sys::ODESystem, constraints, u0, u0_idxs, tspan, ii end exprs = vcat(init_cond_exprs, exprs) + @show exprs bcs = Symbolics.build_function(exprs, sol, p, expression = Val{false}) if iip - return (resid, u, p, t) -> begin - bcs[2](resid, u, p) - end + return (resid, u, p, t) -> bcs[2](resid, u, p) else return (u, p, t) -> bcs[1](u, p) end diff --git a/test/bvproblem.jl b/test/bvproblem.jl index 7e0d45b128..42762b8a3f 100644 --- a/test/bvproblem.jl +++ b/test/bvproblem.jl @@ -2,7 +2,9 @@ using BoundaryValueDiffEq, OrdinaryDiffEq, BoundaryValueDiffEqAscher using ModelingToolkit +using SciMLBase using ModelingToolkit: t_nounits as t, D_nounits as D +import ModelingToolkit: process_constraints ### Test Collocation solvers on simple problems solvers = [MIRK4, RadauIIa5, LobattoIIIa3] @@ -81,46 +83,9 @@ end ### ODESystem with constraint equations, DAEs with constraints ### ################################################################## -# Cartesian pendulum from the docs. -# DAE IVP solved using BoundaryValueDiffEq solvers. -# let -# @parameters g -# @variables x(t) y(t) [state_priority = 10] λ(t) -# eqs = [D(D(x)) ~ λ * x -# D(D(y)) ~ λ * y - g -# x^2 + y^2 ~ 1] -# @mtkbuild pend = ODESystem(eqs, t) -# -# tspan = (0.0, 1.5) -# u0map = [x => 1, y => 0] -# pmap = [g => 1] -# guess = [λ => 1] -# -# prob = ODEProblem(pend, u0map, tspan, pmap; guesses = guess) -# osol = solve(prob, Rodas5P()) -# -# zeta = [0., 0., 0., 0., 0.] -# bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(pend, u0map, tspan, parammap; guesses = guess) -# -# for solver in solvers -# sol = solve(bvp, solver(zeta), dt = 0.001) -# @test isapprox(sol.u[end], osol.u[end]; atol = 0.01) -# conditions = getfield.(equations(pend)[3:end], :rhs) -# @test isapprox([sol[conditions][1]; sol[x][1] - 1; sol[y][1]], zeros(5), atol = 0.001) -# end -# -# bvp2 = SciMLBase.BVProblem{false, SciMLBase.FullSpecialize}(pend, u0map, tspan, parammap) -# for solver in solvers -# sol = solve(bvp, solver(zeta), dt = 0.01) -# @test isapprox(sol.u[end], osol.u[end]; atol = 0.01) -# conditions = getfield.(equations(pend)[3:end], :rhs) -# @test [sol[conditions][1]; sol[x][1] - 1; sol[y][1]] ≈ 0 -# end -# end - # Test generation of boundary condition function. let - @parameters α=7.5 β=4.0 γ=8.0 δ=5.0 + @parameters α=1.5 β=1.0 γ=3.0 δ=1.0 @variables x(..) y(t) eqs = [D(x(t)) ~ α * x(t) - β * x(t) * y, D(y) ~ -γ * y + δ * x(t) * y] @@ -130,11 +95,11 @@ let function lotkavolterra!(du, u, p, t) du[1] = p[1]*u[1] - p[2]*u[1]*u[2] - du[2] = -p[3]*u[2] + p[4]*u[1]*u[2] + du[2] = -p[4]*u[2] + p[3]*u[1]*u[2] end function lotkavolterra(u, p, t) - [p[1]*u[1] - p[2]*u[1]*u[2], -p[3]*u[2] + p[4]*u[1]*u[2]] + [p[1]*u[1] - p[2]*u[1]*u[2], -p[4]*u[2] + p[3]*u[1]*u[2]] end # Compare the built bc function to the actual constructed one. function bc!(resid, u, p, t) @@ -146,23 +111,22 @@ let [u[1][1] - 1., u[1][2] - 2.] end - constraints = nothing - u0 = [1., 2.]; p = [7.5, 4., 8., 5.] - genbc_iip = ModelingToolkit.process_constraints(lksys, constraints, u0, [1, 2], tspan, true) - genbc_oop = ModelingToolkit.process_constraints(lksys, constraints, u0, [1, 2], tspan, false) + u0 = [1., 2.]; p = [1.5, 1., 3., 1.] + genbc_iip = ModelingToolkit.process_constraints(lksys, nothing, u0, [1, 2], tspan, true) + genbc_oop = ModelingToolkit.process_constraints(lksys, nothing, u0, [1, 2], tspan, false) - bvpi1 = SciMLBase.BVProblem(lotkavolterra!, bc!, [1,2], tspan, p) - bvpi2 = SciMLBase.BVProblem(lotkavolterra!, genbc_iip, [1,2], tspan, p) + bvpi1 = SciMLBase.BVProblem(lotkavolterra!, bc!, [1.,2.], tspan, p) + bvpi2 = SciMLBase.BVProblem(lotkavolterra!, genbc_iip, [1.,2.], tspan, p) - sol1 = solve(bvpi1, MIRK4(), dt = 0.01) - sol2 = solve(bvpi2, MIRK4(), dt = 0.01) + sol1 = solve(bvpi1, MIRK4(), dt = 0.05) + sol2 = solve(bvpi2, MIRK4(), dt = 0.05) @test sol1 ≈ sol2 bvpo1 = BVProblem(lotkavolterra, bc, [1,2], tspan, p) bvpo2 = BVProblem(lotkavolterra, genbc_oop, [1,2], tspan, p) - sol1 = solve(bvpo1, MIRK4(), dt = 0.01) - sol2 = solve(bvpo2, MIRK4(), dt = 0.01) + sol1 = solve(bvpo1, MIRK4(), dt = 0.05) + sol2 = solve(bvpo2, MIRK4(), dt = 0.05) @test sol1 ≈ sol2 # Test with a constraint. @@ -180,28 +144,28 @@ let genbc_iip = ModelingToolkit.process_constraints(lksys, constraints, u0, [2], tspan, true) genbc_oop = ModelingToolkit.process_constraints(lksys, constraints, u0, [2], tspan, false) - bvpi1 = SciMLBase.BVProblem(lotkavolterra!, bc!, [1,2], tspan, p) - bvpi2 = SciMLBase.BVProblem(lotkavolterra!, genbc_iip, [1,2], tspan, p) - - sol1 = solve(bvpi1, MIRK4(), dt = 0.01) - sol2 = solve(bvpi2, MIRK4(), dt = 0.01) + bvpi1 = SciMLBase.BVProblem(lotkavolterra!, bc!, [1.,2.], tspan, p) + bvpi2 = SciMLBase.BVProblem(lotkavolterra!, genbc_iip, [1.,2.], tspan, p) + bvpi3 = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(lksys, u0map, tspan, parammap; guesses, constraints) + + sol1 = solve(bvpi1, MIRK4(), dt = 0.05) + sol2 = solve(bvpi2, MIRK4(), dt = 0.05) @test sol1 ≈ sol2 # don't get true equality here, not sure why bvpo1 = BVProblem(lotkavolterra, bc, [1,2], tspan, p) bvpo2 = BVProblem(lotkavolterra, genbc_oop, [1,2], tspan, p) - sol1 = solve(bvpo1, MIRK4(), dt = 0.01) - sol2 = solve(bvpo2, MIRK4(), dt = 0.01) + sol1 = solve(bvpo1, MIRK4(), dt = 0.05) + sol2 = solve(bvpo2, MIRK4(), dt = 0.05) @test sol1 ≈ sol2 end -function test_solvers(solvers, prob, u0map, constraints, equations = []; dt = 0.01) +function test_solvers(solvers, prob, u0map, constraints, equations = []; dt = 0.05) for solver in solvers sol = solve(prob, solver(); dt) - @test successful_retcode(sol.retcode) + @test SciMLBase.successful_retcode(sol.retcode) p = prob.p; t = sol.t; bc = prob.f.bc ns = length(prob.u0) - if isinplace(bvp.f) resid = zeros(ns) bc!(resid, sol, p, t) @@ -226,45 +190,83 @@ end # Simple ODESystem with BVP constraints. let - @parameters α=7.5 β=4.0 γ=8.0 δ=5.0 + t = ModelingToolkit.t_nounits; D = ModelingToolkit.D_nounits + @parameters α=1.5 β=1.0 γ=3.0 δ=1.0 @variables x(..) y(t) eqs = [D(x(t)) ~ α * x(t) - β * x(t) * y, D(y) ~ -γ * y + δ * x(t) * y] - u0map = [y => 2.0] + u0map = [] parammap = [α => 7.5, β => 4, γ => 8.0, δ => 5.0] tspan = (0.0, 10.0) - guesses = [x(t) => 1.0] + guesses = [x(t) => 1.0, y => 2.] @mtkbuild lotkavolterra = ODESystem(eqs, t) - constraints = [x(6.) ~ 3] + constraints = [x(6.) ~ 1.5] bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(lotkavolterra, u0map, tspan, parammap; guesses, constraints) test_solvers(solvers, bvp, u0map, constraints) # Testing that more complicated constraints give correct solutions. - constraints = [y(2.) + x(8.) ~ 12] - bvp = SciMLBase.BVProblem{false, SciMLBase.FullSpecialize}(pend, u0map, tspan, parammap; guesses, constraints) + constraints = [y(2.) + x(8.) ~ 2.] + bvp = SciMLBase.BVProblem{false, SciMLBase.FullSpecialize}(lotkavolterra, u0map, tspan, parammap; guesses, constraints) test_solvers(solvers, bvp, u0map, constraints) - constraints = [α * β - x(6.) ~ 24] - bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(pend, u0map, tspan, parammap; guesses, constraints) + constraints = [α * β - x(6.) ~ 0.5] + bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(lotkavolterra, u0map, tspan, parammap; guesses, constraints) test_solvers(solvers, bvp, u0map, constraints) # Testing that errors are properly thrown when malformed constraints are given. @variables bad(..) constraints = [x(1.) + bad(3.) ~ 10] - @test_throws Exception bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(pend, u0map, tspan, parammap; guesses, constraints) + @test_throws Exception bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(lotkavolterra, u0map, tspan, parammap; guesses, constraints) constraints = [x(t) + y(t) ~ 3] - @test_throws Exception bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(pend, u0map, tspan, parammap; guesses, constraints) + @test_throws Exception bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(lotkavolterra, u0map, tspan, parammap; guesses, constraints) @parameters bad2 constraints = [bad2 + x(0.) ~ 3] - @test_throws Exception bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(pend, u0map, tspan, parammap; guesses, constraints) + @test_throws Exception bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(lotkavolterra, u0map, tspan, parammap; guesses, constraints) end +# Cartesian pendulum from the docs. +# DAE IVP solved using BoundaryValueDiffEq solvers. +# let +# @parameters g +# @variables x(t) y(t) [state_priority = 10] λ(t) +# eqs = [D(D(x)) ~ λ * x +# D(D(y)) ~ λ * y - g +# x^2 + y^2 ~ 1] +# @mtkbuild pend = ODESystem(eqs, t) +# +# tspan = (0.0, 1.5) +# u0map = [x => 1, y => 0] +# pmap = [g => 1] +# guess = [λ => 1] +# +# prob = ODEProblem(pend, u0map, tspan, pmap; guesses = guess) +# osol = solve(prob, Rodas5P()) +# +# zeta = [0., 0., 0., 0., 0.] +# bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(pend, u0map, tspan, parammap; guesses = guess) +# +# for solver in solvers +# sol = solve(bvp, solver(zeta), dt = 0.001) +# @test isapprox(sol.u[end], osol.u[end]; atol = 0.01) +# conditions = getfield.(equations(pend)[3:end], :rhs) +# @test isapprox([sol[conditions][1]; sol[x][1] - 1; sol[y][1]], zeros(5), atol = 0.001) +# end +# +# bvp2 = SciMLBase.BVProblem{false, SciMLBase.FullSpecialize}(pend, u0map, tspan, parammap) +# for solver in solvers +# sol = solve(bvp, solver(zeta), dt = 0.01) +# @test isapprox(sol.u[end], osol.u[end]; atol = 0.01) +# conditions = getfield.(equations(pend)[3:end], :rhs) +# @test [sol[conditions][1]; sol[x][1] - 1; sol[y][1]] ≈ 0 +# end +# end + # Adding a midpoint boundary constraint. # Solve using BVDAE solvers. # let From ea15cc7519dc2451879b63e71d6241f1a9aef1fc Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 15 Jan 2025 15:11:43 +0530 Subject: [PATCH 0566/1337] build: bump SymbolicUtils compat --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 91a3c37109..93d535d085 100644 --- a/Project.toml +++ b/Project.toml @@ -143,7 +143,7 @@ StaticArrays = "0.10, 0.11, 0.12, 1.0" StochasticDiffEq = "6.72.1" StochasticDelayDiffEq = "1.8.1" SymbolicIndexingInterface = "0.3.36" -SymbolicUtils = "3.7" +SymbolicUtils = "3.10" Symbolics = "6.22.1" URIs = "1" UnPack = "0.1, 1.0" From e591cd0526a87c0706379722a9b343253f8abee6 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 9 Jan 2025 16:56:02 +0530 Subject: [PATCH 0567/1337] feat: add support for causal connections of variables --- src/systems/abstractsystem.jl | 9 ++- src/systems/analysis_points.jl | 19 ++++- src/systems/connectors.jl | 123 +++++++++++++++++++++++++++++++++ 3 files changed, 149 insertions(+), 2 deletions(-) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index ccc749baeb..3dc9cda3ba 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -1875,6 +1875,13 @@ Equivalent to `length(equations(expand_connections(sys))) - length(filter(eq -> function n_expanded_connection_equations(sys::AbstractSystem) # TODO: what about inputs? isconnector(sys) && return length(get_unknowns(sys)) + sys = remove_analysis_points(sys) + n_variable_connect_eqs = 0 + for eq in equations(sys) + is_causal_variable_connection(eq.rhs) || continue + n_variable_connect_eqs += length(get_systems(eq.rhs)) - 1 + end + sys, (csets, _) = generate_connection_set(sys) ceqs, instream_csets = generate_connection_equations_and_stream_connections(csets) n_outer_stream_variables = 0 @@ -1897,7 +1904,7 @@ function n_expanded_connection_equations(sys::AbstractSystem) # n_toplevel_unused_flows += count(x->get_connection_type(x) === Flow && !(x in toplevel_flows), get_unknowns(m)) #end - nextras = n_outer_stream_variables + length(ceqs) + nextras = n_outer_stream_variables + length(ceqs) + n_variable_connect_eqs end function Base.show( diff --git a/src/systems/analysis_points.jl b/src/systems/analysis_points.jl index 87f278d4dc..9ce5fe2bc1 100644 --- a/src/systems/analysis_points.jl +++ b/src/systems/analysis_points.jl @@ -208,6 +208,14 @@ function Symbolics.connect(in::AbstractSystem, name::Symbol, out, outs...; verbo return AnalysisPoint() ~ AnalysisPoint(in, name, [out; collect(outs)]; verbose) end +function Symbolics.connect( + in::ConnectableSymbolicT, name::Symbol, out::ConnectableSymbolicT, + outs::ConnectableSymbolicT...; verbose = true) + allvars = (in, out, outs...) + validate_causal_variables_connection(allvars) + return AnalysisPoint() ~ AnalysisPoint(in, name, [out; collect(outs)]; verbose) +end + """ $(TYPEDSIGNATURES) @@ -240,7 +248,7 @@ connection. This is the variable named `u` if present, and otherwise the only variable in the system. If the system does not have a variable named `u` and contains multiple variables, throw an error. """ -function ap_var(sys) +function ap_var(sys::AbstractSystem) if hasproperty(sys, :u) return sys.u end @@ -249,6 +257,15 @@ function ap_var(sys) error("Could not determine the analysis-point variable in system $(nameof(sys)). To use an analysis point, apply it to a connection between causal blocks which have a variable named `u` or a single unknown of the same size.") end +""" + $(TYPEDSIGNATURES) + +For an `AnalysisPoint` involving causal variables. Simply return the variable. +""" +function ap_var(var::ConnectableSymbolicT) + return var +end + """ $(TYPEDEF) diff --git a/src/systems/connectors.jl b/src/systems/connectors.jl index f0f49acb85..db3349f546 100644 --- a/src/systems/connectors.jl +++ b/src/systems/connectors.jl @@ -68,6 +68,93 @@ SymbolicUtils.promote_symtype(::typeof(instream), _) = Real isconnector(s::AbstractSystem) = has_connector_type(s) && get_connector_type(s) !== nothing +""" + $(TYPEDEF) + +Utility struct which wraps a symbolic variable used in a `Connection` to enable `Base.show` +to work. +""" +struct SymbolicWithNameof + var::Any +end + +function Base.nameof(x::SymbolicWithNameof) + return Symbol(x.var) +end + +is_causal_variable_connection(c) = false +function is_causal_variable_connection(c::Connection) + all(x -> x isa SymbolicWithNameof, get_systems(c)) +end + +const ConnectableSymbolicT = Union{BasicSymbolic, Num, Symbolics.Arr} + +const CAUSAL_CONNECTION_ERR = """ +Only causal variables can be used in a `connect` statement. The first argument must \ +be a single output variable and all subsequent variables must be input variables. +""" + +function VariableNotOutputError(var) + ArgumentError(""" + $CAUSAL_CONNECTION_ERR Expected $var to be marked as an output with `[output = true]` \ + in the variable metadata. + """) +end + +function VariableNotInputError(var) + ArgumentError(""" + $CAUSAL_CONNECTION_ERR Expected $var to be marked an input with `[input = true]` \ + in the variable metadata. + """) +end + +""" + $(TYPEDSIGNATURES) + +Perform validation for a connect statement involving causal variables. +""" +function validate_causal_variables_connection(allvars) + var1 = allvars[1] + var2 = allvars[2] + vars = Base.tail(Base.tail(allvars)) + for var in allvars + vtype = getvariabletype(var) + vtype === VARIABLE || + throw(ArgumentError("Expected $var to be of kind `$VARIABLE`. Got `$vtype`.")) + end + if length(unique(allvars)) !== length(allvars) + throw(ArgumentError("Expected all connection variables to be unique. Got variables $allvars which contains duplicate entries.")) + end + allsizes = map(size, allvars) + if !allequal(allsizes) + throw(ArgumentError("Expected all connection variables to have the same size. Got variables $allvars with sizes $allsizes respectively.")) + end + isoutput(var1) || throw(VariableNotOutputError(var1)) + isinput(var2) || throw(VariableNotInputError(var2)) + for var in vars + isinput(var) || throw(VariableNotInputError(var)) + end +end + +""" + $(TYPEDSIGNATURES) + +Connect multiple causal variables. The first variable must be an output, and all subsequent +variables must be inputs. The statement `connect(var1, var2, var3, ...)` expands to: + +```julia +var1 ~ var2 +var1 ~ var3 +# ... +``` +""" +function Symbolics.connect(var1::ConnectableSymbolicT, var2::ConnectableSymbolicT, + vars::ConnectableSymbolicT...) + allvars = (var1, var2, vars...) + validate_causal_variables_connection(allvars) + return Equation(Connection(), Connection(map(SymbolicWithNameof, allvars))) +end + function flowvar(sys::AbstractSystem) sts = get_unknowns(sys) for s in sts @@ -329,6 +416,10 @@ function generate_connection_set!(connectionsets, domain_csets, for eq in eqs′ lhs = eq.lhs rhs = eq.rhs + + # causal variable connections will be expanded before we get here, + # but this guard is useful for `n_expanded_connection_equations`. + is_causal_variable_connection(rhs) && continue if find !== nothing && find(rhs, _getname(namespace)) neweq, extra_unknown = replace(rhs, _getname(namespace)) if extra_unknown isa AbstractArray @@ -479,9 +570,41 @@ function domain_defaults(sys, domain_csets) def end +""" + $(TYPEDSIGNATURES) + +Recursively descend through the hierarchy of `sys` and expand all connection equations +of causal variables. Return the modified system. +""" +function expand_variable_connections(sys::AbstractSystem) + eqs = copy(get_eqs(sys)) + valid_idxs = trues(length(eqs)) + additional_eqs = Equation[] + + for (i, eq) in enumerate(eqs) + eq.lhs isa Connection || continue + connection = eq.rhs + elements = connection.systems + is_causal_variable_connection(connection) || continue + + valid_idxs[i] = false + elements = map(x -> x.var, elements) + outvar = first(elements) + for invar in Iterators.drop(elements, 1) + push!(additional_eqs, outvar ~ invar) + end + end + eqs = [eqs[valid_idxs]; additional_eqs] + subsystems = map(expand_variable_connections, get_systems(sys)) + @set! sys.eqs = eqs + @set! sys.systems = subsystems + return sys +end + function expand_connections(sys::AbstractSystem, find = nothing, replace = nothing; debug = false, tol = 1e-10, scalarize = true) sys = remove_analysis_points(sys) + sys = expand_variable_connections(sys) sys, (csets, domain_csets) = generate_connection_set(sys, find, replace; scalarize) ceqs, instream_csets = generate_connection_equations_and_stream_connections(csets) _sys = expand_instream(instream_csets, sys; debug = debug, tol = tol) From 0df55eb2414cecd4bae7fc30a7b57c4c4ed7309a Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 9 Jan 2025 16:56:11 +0530 Subject: [PATCH 0568/1337] test: test causal connections of variables --- test/causal_variables_connection.jl | 98 +++++++++++++++++++++++++++++ test/runtests.jl | 1 + 2 files changed, 99 insertions(+) create mode 100644 test/causal_variables_connection.jl diff --git a/test/causal_variables_connection.jl b/test/causal_variables_connection.jl new file mode 100644 index 0000000000..c22e8319d4 --- /dev/null +++ b/test/causal_variables_connection.jl @@ -0,0 +1,98 @@ +using ModelingToolkit, ModelingToolkitStandardLibrary.Blocks +using ModelingToolkit: t_nounits as t, D_nounits as D + +@testset "Error checking" begin + @variables begin + x(t) + y(t), [input = true] + z(t), [output = true] + w(t) + v(t), [input = true] + u(t), [output = true] + xarr(t)[1:4], [output = true] + yarr(t)[1:2, 1:2], [input = true] + end + @parameters begin + p, [input = true] + q, [output = true] + end + + @test_throws ["p", "kind", "VARIABLE", "PARAMETER"] connect(z, p) + @test_throws ["q", "kind", "VARIABLE", "PARAMETER"] connect(q, y) + @test_throws ["p", "kind", "VARIABLE", "PARAMETER"] connect(z, y, p) + + @test_throws ["unique"] connect(z, y, y) + + @test_throws ["same size"] connect(xarr, yarr) + + @test_throws ["Expected", "x", "output = true", "metadata"] connect(x, y) + @test_throws ["Expected", "y", "output = true", "metadata"] connect(y, v) + + @test_throws ["Expected", "x", "input = true", "metadata"] connect(z, x) + @test_throws ["Expected", "x", "input = true", "metadata"] connect(z, y, x) + @test_throws ["Expected", "u", "input = true", "metadata"] connect(z, u) + @test_throws ["Expected", "u", "input = true", "metadata"] connect(z, y, u) +end + +@testset "Connection expansion" begin + @named P = FirstOrder(k = 1, T = 1) + @named C = Gain(; k = -1) + + eqs = [connect(P.output.u, C.input.u) + connect(C.output.u, P.input.u)] + sys1 = ODESystem(eqs, t, systems = [P, C], name = :hej) + sys = expand_connections(sys1) + @test any(isequal(P.output.u ~ C.input.u), equations(sys)) + @test any(isequal(C.output.u ~ P.input.u), equations(sys)) + + @named sysouter = ODESystem(Equation[], t; systems = [sys1]) + sys = expand_connections(sysouter) + @test any(isequal(sys1.P.output.u ~ sys1.C.input.u), equations(sys)) + @test any(isequal(sys1.C.output.u ~ sys1.P.input.u), equations(sys)) +end + +@testset "With Analysis Points" begin + @named P = FirstOrder(k = 1, T = 1) + @named C = Gain(; k = -1) + + ap = AnalysisPoint(:plant_input) + eqs = [connect(P.output, C.input), connect(C.output.u, ap, P.input.u)] + sys = ODESystem(eqs, t, systems = [P, C], name = :hej) + @named nested_sys = ODESystem(Equation[], t; systems = [sys]) + + test_cases = [ + ("inner", sys, sys.plant_input), + ("nested", nested_sys, nested_sys.hej.plant_input), + ("inner - Symbol", sys, :plant_input), + ("nested - Symbol", nested_sys, nameof(sys.plant_input)) + ] + + @testset "get_sensitivity - $name" for (name, sys, ap) in test_cases + matrices, _ = get_sensitivity(sys, ap) + @test matrices.A[] == -2 + @test matrices.B[] * matrices.C[] == -1 # either one negative + @test matrices.D[] == 1 + end + + @testset "get_comp_sensitivity - $name" for (name, sys, ap) in test_cases + matrices, _ = get_comp_sensitivity(sys, ap) + @test matrices.A[] == -2 + @test matrices.B[] * matrices.C[] == 1 # both positive or negative + @test matrices.D[] == 0 + end + + @testset "get_looptransfer - $name" for (name, sys, ap) in test_cases + matrices, _ = get_looptransfer(sys, ap) + @test matrices.A[] == -1 + @test matrices.B[] * matrices.C[] == -1 # either one negative + @test matrices.D[] == 0 + end + + @testset "open_loop - $name" for (name, sys, ap) in test_cases + open_sys, (du, u) = open_loop(sys, ap) + matrices, _ = linearize(open_sys, [du], [u]) + @test matrices.A[] == -1 + @test matrices.B[] * matrices.C[] == -1 # either one negative + @test matrices.D[] == 0 + end +end diff --git a/test/runtests.jl b/test/runtests.jl index d38aa86bb2..95154e550e 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -85,6 +85,7 @@ end @safetestset "Constraints Test" include("constraints.jl") @safetestset "IfLifting Test" include("if_lifting.jl") @safetestset "Analysis Points Test" include("analysis_points.jl") + @safetestset "Causal Variables Connection Test" include("causal_variables_connection.jl") end end From b2b57eabd21a4bf66c76df11f336ba2ab78c0496 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 10 Jan 2025 12:05:42 +0530 Subject: [PATCH 0569/1337] refactor: rearrange includes --- src/ModelingToolkit.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ModelingToolkit.jl b/src/ModelingToolkit.jl index 7462122998..b63112bde1 100644 --- a/src/ModelingToolkit.jl +++ b/src/ModelingToolkit.jl @@ -145,8 +145,8 @@ include("systems/index_cache.jl") include("systems/parameter_buffer.jl") include("systems/abstractsystem.jl") include("systems/model_parsing.jl") -include("systems/analysis_points.jl") include("systems/connectors.jl") +include("systems/analysis_points.jl") include("systems/imperative_affect.jl") include("systems/callbacks.jl") include("systems/problem_utils.jl") From f6728dd21adc31b74d399ef622b72ea1f6ab3e0d Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 15 Jan 2025 15:12:34 +0530 Subject: [PATCH 0570/1337] build: bump SymbolicUtils compat --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 91a3c37109..93d535d085 100644 --- a/Project.toml +++ b/Project.toml @@ -143,7 +143,7 @@ StaticArrays = "0.10, 0.11, 0.12, 1.0" StochasticDiffEq = "6.72.1" StochasticDelayDiffEq = "1.8.1" SymbolicIndexingInterface = "0.3.36" -SymbolicUtils = "3.7" +SymbolicUtils = "3.10" Symbolics = "6.22.1" URIs = "1" UnPack = "0.1, 1.0" From 3133177381cd3b5d0a44ea7e8df559cfdc648167 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 15 Jan 2025 15:20:51 +0530 Subject: [PATCH 0571/1337] fix: fix printing of `AbstractSystem` with subsystems --- src/systems/abstractsystem.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index ccc749baeb..2bafe55396 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -1919,7 +1919,7 @@ function Base.show( nrows > 0 && hint && print(io, " see hierarchy($name)") for i in 1:nrows sub = subs[i] - name = String(nameof(sub)) + local name = String(nameof(sub)) print(io, "\n ", name) desc = description(sub) if !isempty(desc) From eff59072a629c2e5901715db9943d3ae9da4b604 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 15 Jan 2025 15:21:38 +0530 Subject: [PATCH 0572/1337] fix: handle all symtypes in `subexpressions_not_involving_vars!` --- src/utils.jl | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/utils.jl b/src/utils.jl index c3011c2a79..46b7ad2de4 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -1108,10 +1108,15 @@ returns the modified `expr`. """ function subexpressions_not_involving_vars!(expr, vars, state::Dict{Any, Any}) expr = unwrap(expr) - symbolic_type(expr) == NotSymbolic() && return expr + if symbolic_type(expr) == NotSymbolic() + if is_array_of_symbolics(expr) + return map(expr) do el + subexpressions_not_involving_vars!(el, vars, state) + end + end + return expr + end iscall(expr) || return expr - is_variable_floatingpoint(expr) || return expr - symtype(expr) <: Union{Real, AbstractArray{<:Real}} || return expr Symbolics.shape(expr) == Symbolics.Unknown() && return expr haskey(state, expr) && return state[expr] vs = ModelingToolkit.vars(expr) @@ -1143,7 +1148,6 @@ function subexpressions_not_involving_vars!(expr, vars, state::Dict{Any, Any}) return op(indep_term, dep_term) end newargs = map(args) do arg - symbolic_type(arg) != NotSymbolic() || is_array_of_symbolics(arg) || return arg subexpressions_not_involving_vars!(arg, vars, state) end return maketerm(typeof(expr), op, newargs, metadata(expr)) From 5c9e14862cae7ed381fe194bf709f7de97c809f3 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 15 Jan 2025 15:21:58 +0530 Subject: [PATCH 0573/1337] feat: support caching of different types of subexpressions in `SCCNonlinearProblem` --- src/ModelingToolkit.jl | 3 +- src/systems/nonlinear/nonlinearsystem.jl | 113 ++++++++++++++++++----- 2 files changed, 91 insertions(+), 25 deletions(-) diff --git a/src/ModelingToolkit.jl b/src/ModelingToolkit.jl index 7462122998..d88120afa0 100644 --- a/src/ModelingToolkit.jl +++ b/src/ModelingToolkit.jl @@ -54,7 +54,8 @@ import SCCNonlinearSolve using Reexport using RecursiveArrayTools import Graphs: SimpleDiGraph, add_edge!, incidence_matrix -import BlockArrays: BlockedArray, Block, blocksize, blocksizes +import BlockArrays: BlockArray, BlockedArray, Block, blocksize, blocksizes, blockpush!, + undef_blocks, blocks import CommonSolve import EnumX diff --git a/src/systems/nonlinear/nonlinearsystem.jl b/src/systems/nonlinear/nonlinearsystem.jl index fe18d0de35..bc3c1451e1 100644 --- a/src/systems/nonlinear/nonlinearsystem.jl +++ b/src/systems/nonlinear/nonlinearsystem.jl @@ -573,29 +573,37 @@ function DiffEqBase.NonlinearLeastSquaresProblem{iip}(sys::NonlinearSystem, u0ma NonlinearLeastSquaresProblem{iip}(f, u0, p; filter_kwargs(kwargs)...) end +const TypeT = Union{DataType, UnionAll} + struct CacheWriter{F} fn::F end function (cw::CacheWriter)(p, sols) - cw.fn(p.caches[1], sols, p...) + cw.fn(p.caches..., sols, p...) end -function CacheWriter(sys::AbstractSystem, exprs, solsyms, obseqs::Vector{Equation}; +function CacheWriter(sys::AbstractSystem, buffer_types::Vector{TypeT}, + exprs::Dict{TypeT, Vector{Any}}, solsyms, obseqs::Vector{Equation}; eval_expression = false, eval_module = @__MODULE__) ps = parameters(sys) rps = reorder_parameters(sys, ps) obs_assigns = [eq.lhs ← eq.rhs for eq in obseqs] cmap, cs = get_cmap(sys) cmap_assigns = [eq.lhs ← eq.rhs for eq in cmap] + + outsyms = [Symbol(:out, i) for i in eachindex(buffer_types)] + body = map(eachindex(buffer_types), buffer_types) do i, T + Symbol(:tmp, i) ← SetArray(true, outsyms[i], get(exprs, T, [])) + end fn = Func( - [:out, DestructuredArgs(DestructuredArgs.(solsyms)), + [outsyms..., DestructuredArgs(DestructuredArgs.(solsyms)), DestructuredArgs.(rps)...], [], - SetArray(true, :out, exprs) + Let(body, :()) ) |> wrap_assignments(false, obs_assigns)[2] |> wrap_parameter_dependencies(sys, false)[2] |> - wrap_array_vars(sys, exprs; dvs = nothing, inputs = [])[2] |> + wrap_array_vars(sys, []; dvs = nothing, inputs = [])[2] |> wrap_assignments(false, cmap_assigns)[2] |> toexpr return CacheWriter(eval_or_rgf(fn; eval_expression, eval_module)) end @@ -677,8 +685,16 @@ function SciMLBase.SCCNonlinearProblem{iip}(sys::NonlinearSystem, u0map, explicitfuns = [] nlfuns = [] - prevobsidxs = Int[] - cachesize = 0 + prevobsidxs = BlockArray(undef_blocks, Vector{Int}, Int[]) + # Cache buffer types and corresponding sizes. Stored as a pair of arrays instead of a + # dict to maintain a consistent order of buffers across SCCs + cachetypes = TypeT[] + cachesizes = Int[] + # explicitfun! related information for each SCC + # We need to compute buffer sizes before doing any codegen + scc_cachevars = Dict{TypeT, Vector{Any}}[] + scc_cacheexprs = Dict{TypeT, Vector{Any}}[] + scc_eqs = Vector{Equation}[] for (i, (escc, vscc)) in enumerate(zip(eq_sccs, var_sccs)) # subset unknowns and equations _dvs = dvs[vscc] @@ -690,6 +706,7 @@ function SciMLBase.SCCNonlinearProblem{iip}(sys::NonlinearSystem, u0map, _obs = obs[obsidxs] # get all subexpressions in the RHS which we can precompute in the cache + # precomputed subexpressions should not contain `banned_vars` banned_vars = Set{Any}(vcat(_dvs, getproperty.(_obs, (:lhs,)))) for var in banned_vars iscall(var) || continue @@ -706,37 +723,85 @@ function SciMLBase.SCCNonlinearProblem{iip}(sys::NonlinearSystem, u0map, _eqs[i].rhs, banned_vars, state) end - # cached variables and their corresponding expressions - cachevars = Any[obs[i].lhs for i in prevobsidxs] - cacheexprs = Any[obs[i].lhs for i in prevobsidxs] + # map from symtype to cached variables and their expressions + cachevars = Dict{Union{DataType, UnionAll}, Vector{Any}}() + cacheexprs = Dict{Union{DataType, UnionAll}, Vector{Any}}() + # observed of previous SCCs are in the cache + # NOTE: When we get proper CSE, we can substitute these + # and then use `subexpressions_not_involving_vars!` + for i in prevobsidxs + T = symtype(obs[i].lhs) + buf = get!(() -> Any[], cachevars, T) + push!(buf, obs[i].lhs) + + buf = get!(() -> Any[], cacheexprs, T) + push!(buf, obs[i].lhs) + end + for (k, v) in state - push!(cachevars, unwrap(v)) - push!(cacheexprs, unwrap(k)) + k = unwrap(k) + v = unwrap(v) + T = symtype(k) + buf = get!(() -> Any[], cachevars, T) + push!(buf, v) + buf = get!(() -> Any[], cacheexprs, T) + push!(buf, k) end - cachesize = max(cachesize, length(cachevars)) + + # update the sizes of cache buffers + for (T, buf) in cachevars + idx = findfirst(isequal(T), cachetypes) + if idx === nothing + push!(cachetypes, T) + push!(cachesizes, 0) + idx = lastindex(cachetypes) + end + cachesizes[idx] = max(cachesizes[idx], length(buf)) + end + + push!(scc_cachevars, cachevars) + push!(scc_cacheexprs, cacheexprs) + push!(scc_eqs, _eqs) + blockpush!(prevobsidxs, obsidxs) + end + + for (i, (escc, vscc)) in enumerate(zip(eq_sccs, var_sccs)) + _dvs = dvs[vscc] + _eqs = scc_eqs[i] + obsidxs = prevobsidxs[Block(i)] + _prevobsidxs = reduce(vcat, blocks(prevobsidxs)[1:(i - 1)]; init = Int[]) + _obs = obs[obsidxs] + cachevars = scc_cachevars[i] + cacheexprs = scc_cacheexprs[i] if isempty(cachevars) push!(explicitfuns, Returns(nothing)) else solsyms = getindex.((dvs,), view(var_sccs, 1:(i - 1))) push!(explicitfuns, - CacheWriter(sys, cacheexprs, solsyms, obs[prevobsidxs]; + CacheWriter(sys, cachetypes, cacheexprs, solsyms, obs[_prevobsidxs]; eval_expression, eval_module)) end + + cachebufsyms = Tuple(map(cachetypes) do T + get(cachevars, T, []) + end) f = SCCNonlinearFunction{iip}( - sys, _eqs, _dvs, _obs, (cachevars,); eval_expression, eval_module, kwargs...) + sys, _eqs, _dvs, _obs, cachebufsyms; eval_expression, eval_module, kwargs...) push!(nlfuns, f) - append!(cachevars, _dvs) - append!(cacheexprs, _dvs) - for i in obsidxs - push!(cachevars, obs[i].lhs) - push!(cacheexprs, obs[i].rhs) - end - append!(prevobsidxs, obsidxs) end - if cachesize != 0 - p = rebuild_with_caches(p, BufferTemplate(eltype(u0), cachesize)) + if !isempty(cachetypes) + templates = map(cachetypes, cachesizes) do T, n + # Real refers to `eltype(u0)` + if T == Real + T = eltype(u0) + elseif T <: Array && eltype(T) == Real + T = Array{eltype(u0), ndims(T)} + end + BufferTemplate(T, n) + end + p = rebuild_with_caches(p, templates...) end subprobs = [] From 2ef4392c29c3376fd965141f50211ad3f4c38242 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 15 Jan 2025 15:22:07 +0530 Subject: [PATCH 0574/1337] test: test caching of different symtypes in `SCCNonlinearProblem` --- test/scc_nonlinear_problem.jl | 92 +++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) diff --git a/test/scc_nonlinear_problem.jl b/test/scc_nonlinear_problem.jl index fdf1646343..57f3d72fb7 100644 --- a/test/scc_nonlinear_problem.jl +++ b/test/scc_nonlinear_problem.jl @@ -161,3 +161,95 @@ end @test SciMLBase.successful_retcode(sccsol) @test val[] == 1 end + +import ModelingToolkitStandardLibrary.Blocks as B +import ModelingToolkitStandardLibrary.Mechanical.Translational as T +import ModelingToolkitStandardLibrary.Hydraulic.IsothermalCompressible as IC + +@testset "Caching of subexpressions of different types" begin + liquid_pressure(rho, rho_0, bulk) = (rho / rho_0 - 1) * bulk + gas_pressure(rho, rho_0, p_gas, rho_gas) = rho * ((0 - p_gas) / (rho_0 - rho_gas)) + full_pressure(rho, rho_0, bulk, p_gas, rho_gas) = ifelse( + rho >= rho_0, liquid_pressure(rho, rho_0, bulk), + gas_pressure(rho, rho_0, p_gas, rho_gas)) + + @component function Volume(; + #parameters + area, + direction = +1, + x_int, + name) + pars = @parameters begin + area = area + x_int = x_int + rho_0 = 1000 + bulk = 1e9 + p_gas = -1000 + rho_gas = 1 + end + + vars = @variables begin + x(t) = x_int + dx(t), [guess = 0] + p(t), [guess = 0] + f(t), [guess = 0] + rho(t), [guess = 0] + m(t), [guess = 0] + dm(t), [guess = 0] + end + + systems = @named begin + port = IC.HydraulicPort() + flange = T.MechanicalPort() + end + + eqs = [ + # connectors + port.p ~ p + port.dm ~ dm + flange.v * direction ~ dx + flange.f * direction ~ -f + + # differentials + D(x) ~ dx + D(m) ~ dm + + # physics + p ~ full_pressure(rho, rho_0, bulk, p_gas, rho_gas) + f ~ p * area + m ~ rho * x * area] + + return ODESystem(eqs, t, vars, pars; name, systems) + end + + systems = @named begin + fluid = IC.HydraulicFluid(; bulk_modulus = 1e9) + + src1 = IC.Pressure(;) + src2 = IC.Pressure(;) + + vol1 = Volume(; area = 0.01, direction = +1, x_int = 0.1) + vol2 = Volume(; area = 0.01, direction = +1, x_int = 0.1) + + mass = T.Mass(; m = 10) + + sin1 = B.Sine(; frequency = 0.5, amplitude = +0.5e5, offset = 10e5) + sin2 = B.Sine(; frequency = 0.5, amplitude = -0.5e5, offset = 10e5) + end + + eqs = [connect(fluid, src1.port) + connect(fluid, src2.port) + connect(src1.port, vol1.port) + connect(src2.port, vol2.port) + connect(vol1.flange, mass.flange, vol2.flange) + connect(src1.p, sin1.output) + connect(src2.p, sin2.output)] + + initialization_eqs = [mass.s ~ 0.0 + mass.v ~ 0.0] + + @mtkbuild sys = ODESystem(eqs, t, [], []; systems, initialization_eqs) + prob = ODEProblem(sys, [], (0, 5)) + sol = solve(prob) + @test SciMLBase.successful_retcode(sol) +end From 562b77a6b13bc60889278da2de1b7a5422dbb461 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 15 Jan 2025 17:35:41 +0530 Subject: [PATCH 0575/1337] fix: fix `CacheWriter` codegen with array symbolics --- src/systems/nonlinear/nonlinearsystem.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/systems/nonlinear/nonlinearsystem.jl b/src/systems/nonlinear/nonlinearsystem.jl index bc3c1451e1..3059da4cb4 100644 --- a/src/systems/nonlinear/nonlinearsystem.jl +++ b/src/systems/nonlinear/nonlinearsystem.jl @@ -580,7 +580,7 @@ struct CacheWriter{F} end function (cw::CacheWriter)(p, sols) - cw.fn(p.caches..., sols, p...) + cw.fn(p.caches, sols, p...) end function CacheWriter(sys::AbstractSystem, buffer_types::Vector{TypeT}, @@ -594,10 +594,10 @@ function CacheWriter(sys::AbstractSystem, buffer_types::Vector{TypeT}, outsyms = [Symbol(:out, i) for i in eachindex(buffer_types)] body = map(eachindex(buffer_types), buffer_types) do i, T - Symbol(:tmp, i) ← SetArray(true, outsyms[i], get(exprs, T, [])) + Symbol(:tmp, i) ← SetArray(true, :(out[$i]), get(exprs, T, [])) end fn = Func( - [outsyms..., DestructuredArgs(DestructuredArgs.(solsyms)), + [:out, DestructuredArgs(DestructuredArgs.(solsyms)), DestructuredArgs.(rps)...], [], Let(body, :()) From 1febdae62742d4694f46597a8414d089d954682e Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 15 Jan 2025 17:36:06 +0530 Subject: [PATCH 0576/1337] fix: fix handling of scalarized array symbolics in `subexpressions_not_involving_vars!` --- src/utils.jl | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/utils.jl b/src/utils.jl index 46b7ad2de4..5e4f0b52d2 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -1116,20 +1116,25 @@ function subexpressions_not_involving_vars!(expr, vars, state::Dict{Any, Any}) end return expr end + any(isequal(expr), vars) && return expr iscall(expr) || return expr Symbolics.shape(expr) == Symbolics.Unknown() && return expr haskey(state, expr) && return state[expr] - vs = ModelingToolkit.vars(expr) - intersect!(vs, vars) - if isempty(vs) + op = operation(expr) + args = arguments(expr) + # if this is a `getindex` and the getindex-ed value is a `Sym` + # or it is not a called parameter + # OR + # none of `vars` are involved in `expr` + if op === getindex && (issym(args[1]) || !iscalledparameter(args[1])) || + (vs = ModelingToolkit.vars(expr); intersect!(vs, vars); isempty(vs)) sym = gensym(:subexpr) stype = symtype(expr) var = similar_variable(expr, sym) state[expr] = var return var end - op = operation(expr) - args = arguments(expr) + if (op == (+) || op == (*)) && symbolic_type(expr) !== ArraySymbolic() indep_args = [] dep_args = [] From 6ce9e85b2b8f902f913533d29d9f9f4337e194a8 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 15 Jan 2025 17:36:19 +0530 Subject: [PATCH 0577/1337] fix: fix handling of SCC observed equations in `SCCNonlinearProblem` --- src/systems/nonlinear/nonlinearsystem.jl | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/systems/nonlinear/nonlinearsystem.jl b/src/systems/nonlinear/nonlinearsystem.jl index 3059da4cb4..6b0b0cc759 100644 --- a/src/systems/nonlinear/nonlinearsystem.jl +++ b/src/systems/nonlinear/nonlinearsystem.jl @@ -695,6 +695,7 @@ function SciMLBase.SCCNonlinearProblem{iip}(sys::NonlinearSystem, u0map, scc_cachevars = Dict{TypeT, Vector{Any}}[] scc_cacheexprs = Dict{TypeT, Vector{Any}}[] scc_eqs = Vector{Equation}[] + scc_obs = Vector{Equation}[] for (i, (escc, vscc)) in enumerate(zip(eq_sccs, var_sccs)) # subset unknowns and equations _dvs = dvs[vscc] @@ -708,10 +709,8 @@ function SciMLBase.SCCNonlinearProblem{iip}(sys::NonlinearSystem, u0map, # get all subexpressions in the RHS which we can precompute in the cache # precomputed subexpressions should not contain `banned_vars` banned_vars = Set{Any}(vcat(_dvs, getproperty.(_obs, (:lhs,)))) - for var in banned_vars - iscall(var) || continue - operation(var) === getindex || continue - push!(banned_vars, arguments(var)[1]) + filter!(banned_vars) do var + symbolic_type(var) != ArraySymbolic() || all(x -> var[i] in banned_vars, eachindex(var)) end state = Dict() for i in eachindex(_obs) @@ -762,15 +761,15 @@ function SciMLBase.SCCNonlinearProblem{iip}(sys::NonlinearSystem, u0map, push!(scc_cachevars, cachevars) push!(scc_cacheexprs, cacheexprs) push!(scc_eqs, _eqs) + push!(scc_obs, _obs) blockpush!(prevobsidxs, obsidxs) end for (i, (escc, vscc)) in enumerate(zip(eq_sccs, var_sccs)) _dvs = dvs[vscc] _eqs = scc_eqs[i] - obsidxs = prevobsidxs[Block(i)] _prevobsidxs = reduce(vcat, blocks(prevobsidxs)[1:(i - 1)]; init = Int[]) - _obs = obs[obsidxs] + _obs = scc_obs[i] cachevars = scc_cachevars[i] cacheexprs = scc_cacheexprs[i] From c7977aec93e28c47a89e75fff6cf1beb1cf41ad2 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 15 Jan 2025 18:11:12 +0530 Subject: [PATCH 0578/1337] test: refactor initialization test --- test/initializationsystem.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/initializationsystem.jl b/test/initializationsystem.jl index 6f8c961b45..987c2e091a 100644 --- a/test/initializationsystem.jl +++ b/test/initializationsystem.jl @@ -32,7 +32,7 @@ initprob = ModelingToolkit.InitializationProblem(pend, 0.0, [x => 1, y => 0], [g @test initprob isa NonlinearLeastSquaresProblem sol = solve(initprob) @test SciMLBase.successful_retcode(sol) -@test sol.u == [0.0, 0.0, 0.0, 0.0] +@test all(iszero, sol.u) @test maximum(abs.(sol[conditions])) < 1e-14 initprob = ModelingToolkit.InitializationProblem( From e802946e597d8f7fa7932ed328cb3acb1599e2c3 Mon Sep 17 00:00:00 2001 From: vyudu Date: Wed, 15 Jan 2025 16:10:55 -0500 Subject: [PATCH 0579/1337] test update --- src/systems/diffeqs/abstractodesystem.jl | 10 ++-- test/bvproblem.jl | 76 +++++++++++++----------- 2 files changed, 44 insertions(+), 42 deletions(-) diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index 0e8435942c..d935a94eb8 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -860,12 +860,11 @@ SciMLBase.BVProblem{iip}(sys::AbstractODESystem, u0map, tspan, kwargs...) where {iip} ``` -Create a boundary value problem from the [`ODESystem`](@ref). The arguments `dvs` and -`ps` are used to set the order of the dependent variable and parameter vectors, -respectively. `u0map` is used to specify fixed initial values for the states. +Create a boundary value problem from the [`ODESystem`](@ref). -Every variable must have either an initial guess supplied using `guesses` or -a fixed initial value specified using `u0map`. +`u0map` is used to specify fixed initial values for the states. Every variable +must have either an initial guess supplied using `guesses` or a fixed initial +value specified using `u0map`. `constraints` are used to specify boundary conditions to the ODESystem in the form of equations. These values should specify values that state variables should @@ -1049,7 +1048,6 @@ function process_constraints(sys::ODESystem, constraints, u0, u0_idxs, tspan, ii end exprs = vcat(init_cond_exprs, exprs) - @show exprs bcs = Symbolics.build_function(exprs, sol, p, expression = Val{false}) if iip return (resid, u, p, t) -> bcs[2](resid, u, p) diff --git a/test/bvproblem.jl b/test/bvproblem.jl index 42762b8a3f..e6645c060d 100644 --- a/test/bvproblem.jl +++ b/test/bvproblem.jl @@ -83,14 +83,14 @@ end ### ODESystem with constraint equations, DAEs with constraints ### ################################################################## -# Test generation of boundary condition function. +# Test generation of boundary condition function using `process_constraints`. Compare solutions to manually written boundary conditions let @parameters α=1.5 β=1.0 γ=3.0 δ=1.0 - @variables x(..) y(t) - eqs = [D(x(t)) ~ α * x(t) - β * x(t) * y, - D(y) ~ -γ * y + δ * x(t) * y] + @variables x(..) y(..) + eqs = [D(x(t)) ~ α * x(t) - β * x(t) * y(t), + D(y(t)) ~ -γ * y(t) + δ * x(t) * y(t)] - tspan = (0., 10.) + tspan = (0., 1.) @mtkbuild lksys = ODESystem(eqs, t) function lotkavolterra!(du, u, p, t) @@ -111,7 +111,7 @@ let [u[1][1] - 1., u[1][2] - 2.] end - u0 = [1., 2.]; p = [1.5, 1., 3., 1.] + u0 = [1., 2.]; p = [1.5, 1., 1., 3.] genbc_iip = ModelingToolkit.process_constraints(lksys, nothing, u0, [1, 2], tspan, true) genbc_oop = ModelingToolkit.process_constraints(lksys, nothing, u0, [1, 2], tspan, false) @@ -130,48 +130,54 @@ let @test sol1 ≈ sol2 # Test with a constraint. - constraints = [x(0.5) ~ 1.] + constraints = [y(0.5) ~ 2.] function bc!(resid, u, p, t) - resid[1] = u[1][2] - 2. - resid[2] = u(0.5)[1] - 1. + resid[1] = u(0.0)[1] - 1. + resid[2] = u(0.5)[2] - 2. end function bc(u, p, t) - [u[1][2] - 2., u(0.5)[1] - 1.] + [u(0.0)[1] - 1., u(0.5)[2] - 2.] end - u0 = [1., 2.] - genbc_iip = ModelingToolkit.process_constraints(lksys, constraints, u0, [2], tspan, true) - genbc_oop = ModelingToolkit.process_constraints(lksys, constraints, u0, [2], tspan, false) + u0 = [1, 1.] + genbc_iip = ModelingToolkit.process_constraints(lksys, constraints, u0, [1], tspan, true) + genbc_oop = ModelingToolkit.process_constraints(lksys, constraints, u0, [1], tspan, false) - bvpi1 = SciMLBase.BVProblem(lotkavolterra!, bc!, [1.,2.], tspan, p) - bvpi2 = SciMLBase.BVProblem(lotkavolterra!, genbc_iip, [1.,2.], tspan, p) - bvpi3 = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(lksys, u0map, tspan, parammap; guesses, constraints) + bvpi1 = SciMLBase.BVProblem(lotkavolterra!, bc!, u0, tspan, p) + bvpi2 = SciMLBase.BVProblem(lotkavolterra!, genbc_iip, u0, tspan, p) + bvpi3 = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(lksys, [x(t) => 1.], tspan; guesses = [y(t) => 1.], constraints) + bvpi4 = SciMLBase.BVProblem{true, SciMLBase.FullSpecialize}(lksys, [x(t) => 1.], tspan; guesses = [y(t) => 1.], constraints) - sol1 = solve(bvpi1, MIRK4(), dt = 0.05) - sol2 = solve(bvpi2, MIRK4(), dt = 0.05) - @test sol1 ≈ sol2 # don't get true equality here, not sure why + @btime sol1 = solve(bvpi1, MIRK4(), dt = 0.01) + @btime sol2 = solve(bvpi2, MIRK4(), dt = 0.01) + @btime sol3 = solve(bvpi3, MIRK4(), dt = 0.01) + @btime sol4 = solve(bvpi4, MIRK4(), dt = 0.01) + @test sol1 ≈ sol2 ≈ sol3 ≈ sol4 # don't get true equality here, not sure why bvpo1 = BVProblem(lotkavolterra, bc, [1,2], tspan, p) bvpo2 = BVProblem(lotkavolterra, genbc_oop, [1,2], tspan, p) + bvpo3 = SciMLBase.BVProblem{false, SciMLBase.FullSpecialize}(lksys, [x(t) => 1.], tspan, parammap; guesses = [y(t) => 1.], constraints) - sol1 = solve(bvpo1, MIRK4(), dt = 0.05) - sol2 = solve(bvpo2, MIRK4(), dt = 0.05) - @test sol1 ≈ sol2 + @btime sol1 = solve(bvpo1, MIRK4(), dt = 0.05) + @btime sol2 = solve(bvpo2, MIRK4(), dt = 0.05) + @btime sol3 = solve(bvpo3, MIRK4(), dt = 0.05) + @test sol1 ≈ sol2 ≈ sol3 end -function test_solvers(solvers, prob, u0map, constraints, equations = []; dt = 0.05) +function test_solvers(solvers, prob, u0map, constraints, equations = []; dt = 0.05, atol = 1e-4) for solver in solvers - sol = solve(prob, solver(); dt) + println("Solver: $solver") + @btime sol = solve(prob, solver(), dt = dt, abstol = atol) @test SciMLBase.successful_retcode(sol.retcode) p = prob.p; t = sol.t; bc = prob.f.bc ns = length(prob.u0) if isinplace(bvp.f) resid = zeros(ns) bc!(resid, sol, p, t) - @test isapprox(zeros(ns), resid) + @test isapprox(zeros(ns), resid; atol) else - @test isapprox(zeros(ns), bc(sol, p, t)) + @test isapprox(zeros(ns), bc(sol, p, t); atol) end for (k, v) in u0map @@ -190,23 +196,21 @@ end # Simple ODESystem with BVP constraints. let - t = ModelingToolkit.t_nounits; D = ModelingToolkit.D_nounits @parameters α=1.5 β=1.0 γ=3.0 δ=1.0 - @variables x(..) y(t) + @variables x(..) y(..) - eqs = [D(x(t)) ~ α * x(t) - β * x(t) * y, - D(y) ~ -γ * y + δ * x(t) * y] + eqs = [D(x(t)) ~ α * x(t) - β * x(t) * y(t), + D(y(t)) ~ -γ * y(t) + δ * x(t) * y(t)] u0map = [] - parammap = [α => 7.5, β => 4, γ => 8.0, δ => 5.0] tspan = (0.0, 10.0) - guesses = [x(t) => 1.0, y => 2.] + guesses = [x(t) => 4.0, y(t) => 2.] - @mtkbuild lotkavolterra = ODESystem(eqs, t) + @mtkbuild lksys = ODESystem(eqs, t) - constraints = [x(6.) ~ 1.5] - bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(lotkavolterra, u0map, tspan, parammap; guesses, constraints) - test_solvers(solvers, bvp, u0map, constraints) + constraints = [x(6.) ~ 3.5, x(3.) ~ 7.] + bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(lksys, u0map, tspan; guesses, constraints) + test_solvers(solvers, bvp, u0map, constraints; dt = 0.1) # Testing that more complicated constraints give correct solutions. constraints = [y(2.) + x(8.) ~ 2.] From e74e047bf333fc197ecd0b5390f22aa2651a4431 Mon Sep 17 00:00:00 2001 From: vyudu Date: Wed, 15 Jan 2025 17:20:43 -0500 Subject: [PATCH 0580/1337] fix --- test/bvproblem.jl | 44 ++++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/test/bvproblem.jl b/test/bvproblem.jl index e6645c060d..a8de6af44d 100644 --- a/test/bvproblem.jl +++ b/test/bvproblem.jl @@ -7,7 +7,7 @@ using ModelingToolkit: t_nounits as t, D_nounits as D import ModelingToolkit: process_constraints ### Test Collocation solvers on simple problems -solvers = [MIRK4, RadauIIa5, LobattoIIIa3] +solvers = [MIRK4, RadauIIa5] daesolvers = [Ascher2, Ascher4, Ascher6] let @@ -149,32 +149,32 @@ let bvpi3 = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(lksys, [x(t) => 1.], tspan; guesses = [y(t) => 1.], constraints) bvpi4 = SciMLBase.BVProblem{true, SciMLBase.FullSpecialize}(lksys, [x(t) => 1.], tspan; guesses = [y(t) => 1.], constraints) - @btime sol1 = solve(bvpi1, MIRK4(), dt = 0.01) - @btime sol2 = solve(bvpi2, MIRK4(), dt = 0.01) - @btime sol3 = solve(bvpi3, MIRK4(), dt = 0.01) - @btime sol4 = solve(bvpi4, MIRK4(), dt = 0.01) + sol1 = @btime solve($bvpi1, MIRK4(), dt = 0.01) + sol2 = @btime solve($bvpi2, MIRK4(), dt = 0.01) + sol3 = @btime solve($bvpi3, MIRK4(), dt = 0.01) + sol4 = @btime solve($bvpi4, MIRK4(), dt = 0.01) @test sol1 ≈ sol2 ≈ sol3 ≈ sol4 # don't get true equality here, not sure why bvpo1 = BVProblem(lotkavolterra, bc, [1,2], tspan, p) bvpo2 = BVProblem(lotkavolterra, genbc_oop, [1,2], tspan, p) - bvpo3 = SciMLBase.BVProblem{false, SciMLBase.FullSpecialize}(lksys, [x(t) => 1.], tspan, parammap; guesses = [y(t) => 1.], constraints) + bvpo3 = SciMLBase.BVProblem{false, SciMLBase.FullSpecialize}(lksys, [x(t) => 1.], tspan; guesses = [y(t) => 1.], constraints) - @btime sol1 = solve(bvpo1, MIRK4(), dt = 0.05) - @btime sol2 = solve(bvpo2, MIRK4(), dt = 0.05) - @btime sol3 = solve(bvpo3, MIRK4(), dt = 0.05) + sol1 = @btime solve($bvpo1, MIRK4(), dt = 0.05) + sol2 = @btime solve($bvpo2, MIRK4(), dt = 0.05) + sol3 = @btime solve($bvpo3, MIRK4(), dt = 0.05) @test sol1 ≈ sol2 ≈ sol3 end function test_solvers(solvers, prob, u0map, constraints, equations = []; dt = 0.05, atol = 1e-4) for solver in solvers println("Solver: $solver") - @btime sol = solve(prob, solver(), dt = dt, abstol = atol) + sol = @btime solve($prob, $solver(), dt = $dt, abstol = $atol) @test SciMLBase.successful_retcode(sol.retcode) p = prob.p; t = sol.t; bc = prob.f.bc ns = length(prob.u0) if isinplace(bvp.f) resid = zeros(ns) - bc!(resid, sol, p, t) + bc(resid, sol, p, t) @test isapprox(zeros(ns), resid; atol) else @test isapprox(zeros(ns), bc(sol, p, t); atol) @@ -203,35 +203,35 @@ let D(y(t)) ~ -γ * y(t) + δ * x(t) * y(t)] u0map = [] - tspan = (0.0, 10.0) + tspan = (0.0, 1.0) guesses = [x(t) => 4.0, y(t) => 2.] @mtkbuild lksys = ODESystem(eqs, t) - constraints = [x(6.) ~ 3.5, x(3.) ~ 7.] + constraints = [x(.6) ~ 3.5, x(.3) ~ 7.] bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(lksys, u0map, tspan; guesses, constraints) - test_solvers(solvers, bvp, u0map, constraints; dt = 0.1) + test_solvers(solvers, bvp, u0map, constraints; dt = 0.05) # Testing that more complicated constraints give correct solutions. - constraints = [y(2.) + x(8.) ~ 2.] - bvp = SciMLBase.BVProblem{false, SciMLBase.FullSpecialize}(lotkavolterra, u0map, tspan, parammap; guesses, constraints) - test_solvers(solvers, bvp, u0map, constraints) + constraints = [y(.2) + x(.8) ~ 3., y(.3) + x(.5) ~ 5.] + bvp = SciMLBase.BVProblem{false, SciMLBase.FullSpecialize}(lksys, u0map, tspan; guesses, constraints, jac = true) + test_solvers(solvers, bvp, u0map, constraints; dt = 0.05) - constraints = [α * β - x(6.) ~ 0.5] - bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(lotkavolterra, u0map, tspan, parammap; guesses, constraints) + constraints = [α * β - x(.6) ~ 0.0, y(.2) ~ 3.] + bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(lksys, u0map, tspan; guesses, constraints) test_solvers(solvers, bvp, u0map, constraints) # Testing that errors are properly thrown when malformed constraints are given. @variables bad(..) constraints = [x(1.) + bad(3.) ~ 10] - @test_throws Exception bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(lotkavolterra, u0map, tspan, parammap; guesses, constraints) + @test_throws Exception bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(lksys, u0map, tspan; guesses, constraints) constraints = [x(t) + y(t) ~ 3] - @test_throws Exception bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(lotkavolterra, u0map, tspan, parammap; guesses, constraints) + @test_throws Exception bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(lksys, u0map, tspan; guesses, constraints) @parameters bad2 constraints = [bad2 + x(0.) ~ 3] - @test_throws Exception bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(lotkavolterra, u0map, tspan, parammap; guesses, constraints) + @test_throws Exception bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(lksys, u0map, tspan; guesses, constraints) end # Cartesian pendulum from the docs. From 143d5c3c0c04b9713644ae989f6b691a427b7d86 Mon Sep 17 00:00:00 2001 From: vyudu Date: Thu, 16 Jan 2025 11:05:21 -0500 Subject: [PATCH 0581/1337] init --- src/systems/abstractsystem.jl | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index cbd07ee9bf..fe2978e28b 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -1010,7 +1010,9 @@ for prop in [:eqs :tstops :index_cache :is_scalar_noise - :isscheduled] + :isscheduled + :input + :misc] fname_get = Symbol(:get_, prop) fname_has = Symbol(:has_, prop) @eval begin From 80b25491f749bf4de1837f784ce0a6f62911beed Mon Sep 17 00:00:00 2001 From: vyudu Date: Thu, 16 Jan 2025 14:51:47 -0500 Subject: [PATCH 0582/1337] add getters for noise, unit, connect, misc --- src/ModelingToolkit.jl | 4 +- src/variables.jl | 93 +++++++++++++++++++++++++++++----- test/test_variable_metadata.jl | 44 ++++++++++++++++ 3 files changed, 128 insertions(+), 13 deletions(-) diff --git a/src/ModelingToolkit.jl b/src/ModelingToolkit.jl index 743dc8ce76..cf3ca5ada0 100644 --- a/src/ModelingToolkit.jl +++ b/src/ModelingToolkit.jl @@ -246,7 +246,9 @@ export initial_state, transition, activeState, entry, ticksInState, timeInState export @component, @mtkmodel, @mtkbuild export isinput, isoutput, getbounds, hasbounds, getguess, hasguess, isdisturbance, istunable, getdist, hasdist, - tunable_parameters, isirreducible, getdescription, hasdescription + tunable_parameters, isirreducible, getdescription, hasdescription, + hasnoise, getnoise, hasunit, getunit, hasconnect, getconnect, + hasmisc, getmisc export ode_order_lowering, dae_order_lowering, liouville_transform export PDESystem export Differential, expand_derivatives, @derivatives diff --git a/src/variables.jl b/src/variables.jl index c45c4b0f00..dab24dbbd1 100644 --- a/src/variables.jl +++ b/src/variables.jl @@ -29,7 +29,7 @@ ModelingToolkit.dump_variable_metadata(p) """ function dump_variable_metadata(var) uvar = unwrap(var) - vartype, name = get(uvar.metadata, VariableSource, (:unknown, :unknown)) + vartype, name = Symbolics.getmetadata(uvar, VariableSource, (:unknown, :unknown)) type = symtype(uvar) if type <: AbstractArray shape = Symbolics.shape(var) @@ -39,14 +39,14 @@ function dump_variable_metadata(var) else shape = nothing end - unit = get(uvar.metadata, VariableUnit, nothing) - connect = get(uvar.metadata, VariableConnectType, nothing) - noise = get(uvar.metadata, VariableNoiseType, nothing) + unit = getunit(uvar) + connect = getconnect(uvar) + noise = getnoise(uvar) input = isinput(uvar) || nothing output = isoutput(uvar) || nothing - irreducible = get(uvar.metadata, VariableIrreducible, nothing) - state_priority = get(uvar.metadata, VariableStatePriority, nothing) - misc = get(uvar.metadata, VariableMisc, nothing) + irreducible = isirreducible(var) + state_priority = Symbolics.getmetadata(uvar, VariableStatePriority, nothing) + misc = getmisc(uvar) bounds = hasbounds(uvar) ? getbounds(uvar) : nothing desc = getdescription(var) if desc == "" @@ -57,12 +57,13 @@ function dump_variable_metadata(var) disturbance = isdisturbance(uvar) || nothing tunable = istunable(uvar, isparameter(uvar)) dist = getdist(uvar) - type = symtype(uvar) + variable_type = getvariabletype(uvar) meta = ( var = var, vartype, name, + variable_type, shape, unit, connect, @@ -85,11 +86,28 @@ function dump_variable_metadata(var) return NamedTuple(k => v for (k, v) in pairs(meta) if v !== nothing) end +### Connect abstract type AbstractConnectType end struct Equality <: AbstractConnectType end # Equality connection struct Flow <: AbstractConnectType end # sum to 0 struct Stream <: AbstractConnectType end # special stream connector +""" + getconnect(x) + +Get the connect type of x. See also [`hasconnect`](@ref). +""" +getconnect(x) = getconnect(unwrap(x)) +getconnect(x::Symbolic) = Symbolics.getmetadata(x, VariableConnectType, nothing) +""" + hasconnect(x) + +Determine whether variable `x` has a connect type. See also [`getconnect`](@ref). +""" +hasconnect(x) = getconnect(x) !== nothing +setconnect(x, t::Type{T}) where T <: AbstractConnectType = setmetadata(x, VariableConnectType, t) + +### Input, Output, Irreducible isvarkind(m, x::Union{Num, Symbolics.Arr}) = isvarkind(m, value(x)) function isvarkind(m, x) iskind = getmetadata(x, m, nothing) @@ -98,15 +116,17 @@ function isvarkind(m, x) getmetadata(x, m, false) end -setinput(x, v) = setmetadata(x, VariableInput, v) -setoutput(x, v) = setmetadata(x, VariableOutput, v) -setio(x, i, o) = setoutput(setinput(x, i), o) +setinput(x, v::Bool) = setmetadata(x, VariableInput, v) +setoutput(x, v::Bool) = setmetadata(x, VariableOutput, v) +setio(x, i::Bool, o::Bool) = setoutput(setinput(x, i), o) + isinput(x) = isvarkind(VariableInput, x) isoutput(x) = isvarkind(VariableOutput, x) + # Before the solvability check, we already have handled IO variables, so # irreducibility is independent from IO. isirreducible(x) = isvarkind(VariableIrreducible, x) -setirreducible(x, v) = setmetadata(x, VariableIrreducible, v) +setirreducible(x, v::Bool) = setmetadata(x, VariableIrreducible, v) state_priority(x) = convert(Float64, getmetadata(x, VariableStatePriority, 0.0))::Float64 function default_toterm(x) @@ -545,3 +565,52 @@ function get_default_or_guess(x) return getguess(x) end end + +## Miscellaneous metadata ====================================================================== +""" + getmisc(x) + +Fetch any miscellaneous data associated with symbolic variable `x`. +See also [`hasmisc(x)`](@ref). +""" +getmisc(x) = getmisc(unwrap(x)) +getmisc(x::Symbolic) = Symbolics.getmetadata(x, VariableMisc, nothing) +""" + hasmisc(x) + +Determine whether a symbolic variable `x` has misc +metadata associated with it. + +See also [`getmisc(x)`](@ref). +""" +hasmisc(x) = getmisc(x) !== nothing +setmisc(x, miscdata) = setmetadata(x, VariableMisc, miscdata) + +## Units ====================================================================== +""" + getunit(x) + +Alias for [`get_unit(x)`](@ref). +""" +getunit(x) = get_unit(x) +""" + hasunit(x) + +Check if the variable `x` has a unit. +""" +hasunit(x) = getunit(x) !== nothing + +## Noise ====================================================================== +""" + getnoise(x) + +Get the noise type of variable `x`. +""" +getnoise(x) = getnoise(unwrap(x)) +getnoise(x::Symbolic) = Symbolics.getmetadata(x, VariableNoiseType, nothing) +""" + hasnoise(x) + +Determine if variable `x` has a noise type. +""" +hasnoise(x) = getnoise(x) !== nothing diff --git a/test/test_variable_metadata.jl b/test/test_variable_metadata.jl index 7b093be366..106bce1166 100644 --- a/test/test_variable_metadata.jl +++ b/test/test_variable_metadata.jl @@ -1,4 +1,5 @@ using ModelingToolkit +using DynamicQuantities # Bounds @variables u [bounds = (-1, 1)] @@ -185,3 +186,46 @@ params_meta = ModelingToolkit.dump_parameters(sys) params_meta = Dict([ModelingToolkit.getname(meta.var) => meta for meta in params_meta]) @test params_meta[:p].default == 3.0 @test isequal(params_meta[:q].dependency, 2p) + +# Noise +@variables x [noise = 1] +@test hasnoise(x) +@test getnoise(x) == 1 +@test ModelingToolkit.dump_variable_metadata(x).noise == 1 + +# Connect +@variables x [connect = Flow] +@test hasconnect(x) +@test getconnect(x) == Flow +@test ModelingToolkit.dump_variable_metadata(x).connect == Flow +x = ModelingToolkit.setconnect(x, ModelingToolkit.Stream) +@test getconnect(x) == ModelingToolkit.Stream + +struct BadConnect end +@test_throws Exception ModelingToolkit.setconnect(x, BadConnect) + +# Unit +@variables x [unit = u"s"] +@test hasunit(x) +@test getunit(x) == u"s" +@test ModelingToolkit.dump_variable_metadata(x).unit == u"s" + +# Misc data +@variables x [misc = [:good]] +@test hasmisc(x) +@test getmisc(x) == [:good] +x = ModelingToolkit.setmisc(x, "okay") +@test getmisc(x) == "okay" + +# Variable Type +@variables x +@test ModelingToolkit.getvariabletype(x) == ModelingToolkit.VARIABLE +@test ModelingToolkit.dump_variable_metadata(x).variable_type == ModelingToolkit.VARIABLE +x = ModelingToolkit.toparam(x) +@test ModelingToolkit.getvariabletype(x) == ModelingToolkit.PARAMETER + +@parameters y +@test ModelingToolkit.getvariabletype(y) == ModelingToolkit.PARAMETER + +@brownian z +@test ModelingToolkit.getvariabletype(z) == ModelingToolkit.BROWNIAN From a5db689b7892043e4492240f340fc07bb81bf2bb Mon Sep 17 00:00:00 2001 From: vyudu Date: Thu, 16 Jan 2025 14:52:44 -0500 Subject: [PATCH 0583/1337] up --- src/systems/abstractsystem.jl | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index fe2978e28b..cbd07ee9bf 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -1010,9 +1010,7 @@ for prop in [:eqs :tstops :index_cache :is_scalar_noise - :isscheduled - :input - :misc] + :isscheduled] fname_get = Symbol(:get_, prop) fname_has = Symbol(:has_, prop) @eval begin From 8e4cb3ca3be7f89247762109a73c44678f29e82d Mon Sep 17 00:00:00 2001 From: sivasathyaseeelan Date: Fri, 17 Jan 2025 01:51:37 +0530 Subject: [PATCH 0584/1337] added test for dae_order_lowering Signed-off-by: sivasathyaseeelan --- test/odesystem.jl | 57 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/test/odesystem.jl b/test/odesystem.jl index 85d135b338..76c47a8f1d 100644 --- a/test/odesystem.jl +++ b/test/odesystem.jl @@ -1552,3 +1552,60 @@ end expected_tstops = unique!(sort!(vcat(0.0:0.075:10.0, 0.1, 0.2, 0.65, 0.35, 0.45))) @test all(x -> any(isapprox(x, atol = 1e-6), sol2.t), expected_tstops) end + +@testset "dae_order_lowering basic test" begin + @parameters a + @variables x(t) y(t) z(t) + @named dae_sys = ODESystem([ + D(x) ~ y, + 0 ~ x + z, + 0 ~ x - y + z + ], t, [z, y, x], []) + + lowered_dae_sys = dae_order_lowering(dae_sys) + @variables x1(t) y1(t) z1(t) + expected_eqs = [ + 0 ~ x + z, + 0 ~ x - y + z, + Differential(t)(x) ~ y + ] + lowered_eqs = equations(lowered_dae_sys) + sorted_lowered_eqs = sort(lowered_eqs, by=string) + sorted_expected_eqs = sort(expected_eqs, by=string) + @test sorted_lowered_eqs == sorted_expected_eqs + + expected_vars = Set([z, y, x]) + lowered_vars = Set(unknowns(lowered_dae_sys)) + @test lowered_vars == expected_vars +end + +@testset "dae_order_lowering test with structural_simplify" begin + @variables x(t) y(t) z(t) + @parameters M b k + eqs = [ + D(D(x)) ~ -b / M * D(x) - k / M * x, + 0 ~ y - D(x), + 0 ~ z - x + ] + ps = [M, b, k] + default_u0 = [ + D(x) => 0.0, x => 10.0, y => 0.0, z => 10.0 + ] + default_p = [M => 1.0, b => 1.0, k => 1.0] + @named dae_sys = ODESystem(eqs, t, [x, y, z], ps; defaults = [default_u0; default_p]) + + simplified_dae_sys = structural_simplify(dae_sys) + + lowered_dae_sys = dae_order_lowering(simplified_dae_sys) + lowered_dae_sys = complete(lowered_dae_sys) + + tspan = (0.0, 10.0) + prob = ODEProblem(lowered_dae_sys, nothing, tspan) + sol = solve(prob, Tsit5()) + + @test sol.t[end] == tspan[end] + @test sum(abs, sol.u[end]) < 1 + + prob = ODEProblem{false}(lowered_dae_sys; u0_constructor = x -> SVector(x...)) + @test prob.u0 isa SVector +end \ No newline at end of file From 55e607bc8bb3742137a0c950782d74b6465f9706 Mon Sep 17 00:00:00 2001 From: Herman Sletmoen Date: Fri, 17 Jan 2025 13:27:05 +0100 Subject: [PATCH 0585/1337] Substitute dummy derivatives in initialization problem --- src/systems/diffeqs/abstractodesystem.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index b012bc73d4..c4da03e916 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -1366,6 +1366,7 @@ function InitializationProblem{iip, specialize}(sys::AbstractSystem, end u0map = merge(ModelingToolkit.guesses(sys), todict(guesses), todict(u0map)) + u0map = Dict(diff2term(var) => val for (var, val) in u0map) # replace D(x) -> x_t etc. fullmap = merge(u0map, parammap) u0T = Union{} for sym in unknowns(isys) From 94ff612da58463ae09586aa052c755e7c9724f9a Mon Sep 17 00:00:00 2001 From: Herman Sletmoen Date: Fri, 17 Jan 2025 14:13:20 +0100 Subject: [PATCH 0586/1337] Add test --- test/extensions/ad.jl | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/test/extensions/ad.jl b/test/extensions/ad.jl index 954b868a1e..75ec3ef753 100644 --- a/test/extensions/ad.jl +++ b/test/extensions/ad.jl @@ -4,6 +4,7 @@ using Zygote using SymbolicIndexingInterface using SciMLStructures using OrdinaryDiffEq +using NonlinearSolve using SciMLSensitivity using ForwardDiff using ChainRulesCore @@ -103,3 +104,22 @@ vals = (1.0f0, 3ones(Float32, 3)) tangent = rand_tangent(ps) fwd, back = ChainRulesCore.rrule(remake_buffer, sys, ps, idxs, vals) @inferred back(tangent) + +@testset "Dual type promotion in remake with dummy derivatives" begin # https://github.com/SciML/ModelingToolkit.jl/issues/3336 + # Throw ball straight up into the air + @variables y(t) + eqs = [D(D(y)) ~ -9.81] + initialization_eqs = [y^2 ~ 0] # initialize y = 0 in a way that builds an initialization problem + @named sys = ODESystem(eqs, t; initialization_eqs) + sys = structural_simplify(sys) + + # Find initial throw velocity that reaches exactly 10 m after 1 s + dprob0 = ODEProblem(sys, [D(y) => NaN], (0.0, 1.0), []; guesses = [y => 0.0]) + nprob = NonlinearProblem((ics, _) -> begin + dprob = remake(dprob0, u0 = Dict(D(y) => ics[1])) + dsol = solve(dprob, Tsit5()) + return [dsol[y][end] - 10.0] + end, [1.0]) + nsol = solve(nprob, NewtonRaphson()) + @test nsol[1] ≈ 10.0/1.0 + 9.81*1.0/2 # anal free fall solution is y = v0*t - g*t^2/2 -> v0 = y/t + g*t/2 +end From 6bd9ad4cd7a4befd1f54b56da1b83707b77b89b6 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 16 Jan 2025 13:28:20 +0530 Subject: [PATCH 0587/1337] fix: fix `generate_initializesystem(::JumpSystem)` --- src/systems/nonlinear/initializesystem.jl | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/systems/nonlinear/initializesystem.jl b/src/systems/nonlinear/initializesystem.jl index c3bbd92317..0b556eef68 100644 --- a/src/systems/nonlinear/initializesystem.jl +++ b/src/systems/nonlinear/initializesystem.jl @@ -13,7 +13,9 @@ function generate_initializesystem(sys::AbstractSystem; check_units = true, check_defguess = false, name = nameof(sys), extra_metadata = (;), kwargs...) eqs = equations(sys) - eqs = filter(x -> x isa Equation, eqs) + if !(eqs isa Vector{Equation}) + eqs = Equation[x for x in eqs if x isa Equation] + end trueobs, eqs = unhack_observed(observed(sys), eqs) vars = unique([unknowns(sys); getfield.(trueobs, :lhs)]) vars_set = Set(vars) # for efficient in-lookup From 3ea956cd2d6e144a2574a2de5d9d22de90498c62 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 16 Jan 2025 13:28:49 +0530 Subject: [PATCH 0588/1337] fix: retain `initialization_data` in `DiscreteProblem` --- src/systems/jumps/jumpsystem.jl | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/systems/jumps/jumpsystem.jl b/src/systems/jumps/jumpsystem.jl index 5c0a61771a..f39b5fa46a 100644 --- a/src/systems/jumps/jumpsystem.jl +++ b/src/systems/jumps/jumpsystem.jl @@ -425,14 +425,15 @@ function DiffEqBase.DiscreteProblem(sys::JumpSystem, u0map, tspan::Union{Tuple, error("The passed in JumpSystem contains `Equation`s or continuous events, please use a problem type that supports these features, such as ODEProblem.") end - _, u0, p = process_SciMLProblem(EmptySciMLFunction, sys, u0map, parammap; + _f, u0, p = process_SciMLProblem(EmptySciMLFunction, sys, u0map, parammap; t = tspan === nothing ? nothing : tspan[1], use_union, tofloat = false, check_length = false) f = DiffEqBase.DISCRETE_INPLACE_DEFAULT observedfun = ObservedFunctionCache( sys; eval_expression, eval_module, checkbounds = get(kwargs, :checkbounds, false)) - df = DiscreteFunction{true, true}(f; sys = sys, observed = observedfun) + df = DiscreteFunction{true, true}(f; sys = sys, observed = observedfun, + initialization_data = get(_f.kwargs, :initialization_data, nothing)) DiscreteProblem(df, u0, tspan, p; kwargs...) end From 9a78fe67cdf74564247a215fd5ccbbe8b834e8c0 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 16 Jan 2025 13:29:04 +0530 Subject: [PATCH 0589/1337] test: test initialization of `DiscreteProblem(::JumpSystem)` --- test/initializationsystem.jl | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/test/initializationsystem.jl b/test/initializationsystem.jl index 987c2e091a..7d2f2ed22a 100644 --- a/test/initializationsystem.jl +++ b/test/initializationsystem.jl @@ -1306,3 +1306,24 @@ end @test integ[X] ≈ 4.0 @test integ[Y] ≈ 7.0 end + +@testset "Issue#3297: `generate_initializesystem(::JumpSystem)`" begin + @parameters β γ S0 + @variables S(t)=S0 I(t) R(t) + rate₁ = β * S * I + affect₁ = [S ~ S - 1, I ~ I + 1] + rate₂ = γ * I + affect₂ = [I ~ I - 1, R ~ R + 1] + j₁ = ConstantRateJump(rate₁, affect₁) + j₂ = ConstantRateJump(rate₂, affect₂) + j₃ = MassActionJump(2 * β + γ, [R => 1], [S => 1, R => -1]) + @mtkbuild js = JumpSystem([j₁, j₂, j₃], t, [S, I, R], [β, γ, S0]) + + u0s = [I => 1, R => 0] + ps = [S0 => 999, β => 0.01, γ => 0.001] + dprob = DiscreteProblem(js, u0s, (0.0, 10.0), ps) + @test dprob.f.initialization_data !== nothing + sol = solve(dprob, FunctionMap()) + @test sol[S, 1] ≈ 999 + @test SciMLBase.successful_retcode(sol) +end From c5b7a639ff5c127241d2444407b2c7d7429b6196 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 16 Jan 2025 17:33:51 +0530 Subject: [PATCH 0590/1337] test: add `JumpProblem` to initialization test --- test/initializationsystem.jl | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/test/initializationsystem.jl b/test/initializationsystem.jl index 7d2f2ed22a..62b6bba707 100644 --- a/test/initializationsystem.jl +++ b/test/initializationsystem.jl @@ -1,5 +1,5 @@ using ModelingToolkit, OrdinaryDiffEq, NonlinearSolve, Test -using StochasticDiffEq, DelayDiffEq, StochasticDelayDiffEq +using StochasticDiffEq, DelayDiffEq, StochasticDelayDiffEq, JumpProcesses using ForwardDiff using SymbolicIndexingInterface, SciMLStructures using SciMLStructures: Tunable @@ -1326,4 +1326,9 @@ end sol = solve(dprob, FunctionMap()) @test sol[S, 1] ≈ 999 @test SciMLBase.successful_retcode(sol) + + jprob = JumpProblem(js, dprob) + sol = solve(jprob, SSAStepper()) + @test sol[S, 1] ≈ 999 + @test SciMLBase.successful_retcode(sol) end From 29d1a18bacdd03708c2b3e33e37d22fdaf5a6cce Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 17 Jan 2025 16:42:43 +0530 Subject: [PATCH 0591/1337] build: bump SciMLBase and OrdinaryDiffEqCore compats --- Project.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Project.toml b/Project.toml index 93d535d085..7ccb4d2110 100644 --- a/Project.toml +++ b/Project.toml @@ -123,7 +123,7 @@ NonlinearSolve = "4.3" OffsetArrays = "1" OrderedCollections = "1" OrdinaryDiffEq = "6.82.0" -OrdinaryDiffEqCore = "1.13.0" +OrdinaryDiffEqCore = "1.15.0" OrdinaryDiffEqDefault = "1.2" OrdinaryDiffEqNonlinearSolve = "1.3.0" PrecompileTools = "1" @@ -132,7 +132,7 @@ RecursiveArrayTools = "3.26" Reexport = "0.2, 1" RuntimeGeneratedFunctions = "0.5.9" SCCNonlinearSolve = "1.0.0" -SciMLBase = "2.68.1" +SciMLBase = "2.71" SciMLStructures = "1.0" Serialization = "1" Setfield = "0.7, 0.8, 1" From fa0494683bc00b2b4346d2f6cfe5976841116597 Mon Sep 17 00:00:00 2001 From: vyudu Date: Fri, 17 Jan 2025 10:49:59 -0500 Subject: [PATCH 0592/1337] remove noise, document new getters/setters --- docs/src/basics/Variable_metadata.md | 33 +++++++++++++++++++++++++++- src/variables.jl | 22 ++----------------- test/test_variable_metadata.jl | 2 ++ 3 files changed, 36 insertions(+), 21 deletions(-) diff --git a/docs/src/basics/Variable_metadata.md b/docs/src/basics/Variable_metadata.md index e9bdabfe38..ecb94ea878 100644 --- a/docs/src/basics/Variable_metadata.md +++ b/docs/src/basics/Variable_metadata.md @@ -54,6 +54,10 @@ using ModelingToolkit: t_nounits as t, D_nounits as D @variables i(t) [connect = Flow] @variables k(t) [connect = Stream] +hasconnect(i) +``` +```@example connect +getconnect(k) ``` ## Input or output @@ -177,8 +181,35 @@ A variable can be marked `irreducible` to prevent it from being moved to an `observed` state. This forces the variable to be computed during solving so that it can be accessed in [callbacks](@ref events) -```julia +```@example metadata @variable important_value [irreducible = true] +isirreducible(important_value) +``` + +## Units + +Units for variables can be designated using symbolic metadata. For more information, please see the [model validation and units](@ref units) section of the docs. Note that `getunit` is equivalent to `get_unit`. + +```@example metadata +@variable speed [unit=u"m/s"] +hasunit(speed) +``` +```@example metadata +getunit(speed) +``` + +## Miscellaneous metadata + +User-defined metadata can be added using the `misc` metadata. This can be queried +using the `hasmisc` and `getmisc` functions. + +```@example metadata +@variables u [misc = :conserved_parameter] y [misc = [2, 4, 6]] +hasmisc(u) +``` + +```@example metadata +getmisc(y) ``` ## Additional functions diff --git a/src/variables.jl b/src/variables.jl index dab24dbbd1..9cd7e72c79 100644 --- a/src/variables.jl +++ b/src/variables.jl @@ -8,7 +8,6 @@ struct VariableStatePriority end struct VariableMisc end Symbolics.option_to_metadata_type(::Val{:unit}) = VariableUnit Symbolics.option_to_metadata_type(::Val{:connect}) = VariableConnectType -Symbolics.option_to_metadata_type(::Val{:noise}) = VariableNoiseType Symbolics.option_to_metadata_type(::Val{:input}) = VariableInput Symbolics.option_to_metadata_type(::Val{:output}) = VariableOutput Symbolics.option_to_metadata_type(::Val{:irreducible}) = VariableIrreducible @@ -29,7 +28,7 @@ ModelingToolkit.dump_variable_metadata(p) """ function dump_variable_metadata(var) uvar = unwrap(var) - vartype, name = Symbolics.getmetadata(uvar, VariableSource, (:unknown, :unknown)) + variable_source, name = Symbolics.getmetadata(uvar, VariableSource, (:unknown, :unknown)) type = symtype(uvar) if type <: AbstractArray shape = Symbolics.shape(var) @@ -41,7 +40,6 @@ function dump_variable_metadata(var) end unit = getunit(uvar) connect = getconnect(uvar) - noise = getnoise(uvar) input = isinput(uvar) || nothing output = isoutput(uvar) || nothing irreducible = isirreducible(var) @@ -61,13 +59,12 @@ function dump_variable_metadata(var) meta = ( var = var, - vartype, + variable_source, name, variable_type, shape, unit, connect, - noise, input, output, irreducible, @@ -599,18 +596,3 @@ getunit(x) = get_unit(x) Check if the variable `x` has a unit. """ hasunit(x) = getunit(x) !== nothing - -## Noise ====================================================================== -""" - getnoise(x) - -Get the noise type of variable `x`. -""" -getnoise(x) = getnoise(unwrap(x)) -getnoise(x::Symbolic) = Symbolics.getmetadata(x, VariableNoiseType, nothing) -""" - hasnoise(x) - -Determine if variable `x` has a noise type. -""" -hasnoise(x) = getnoise(x) !== nothing diff --git a/test/test_variable_metadata.jl b/test/test_variable_metadata.jl index 106bce1166..b6e05ce4e0 100644 --- a/test/test_variable_metadata.jl +++ b/test/test_variable_metadata.jl @@ -221,8 +221,10 @@ x = ModelingToolkit.setmisc(x, "okay") @variables x @test ModelingToolkit.getvariabletype(x) == ModelingToolkit.VARIABLE @test ModelingToolkit.dump_variable_metadata(x).variable_type == ModelingToolkit.VARIABLE +@test ModelingToolkit.dump_variable_metadata(x).variable_source == :variables x = ModelingToolkit.toparam(x) @test ModelingToolkit.getvariabletype(x) == ModelingToolkit.PARAMETER +@test ModelingToolkit.dump_variable_metadata(x).variable_source == :variables @parameters y @test ModelingToolkit.getvariabletype(y) == ModelingToolkit.PARAMETER From baea3814840a525200d50a9f84f8e96c7cccb24a Mon Sep 17 00:00:00 2001 From: vyudu Date: Fri, 17 Jan 2025 11:19:14 -0500 Subject: [PATCH 0593/1337] add check --- src/systems/diffeqs/odesystem.jl | 1 + src/systems/diffeqs/sdesystem.jl | 1 + src/systems/discrete_system/discrete_system.jl | 1 + src/systems/jumps/jumpsystem.jl | 1 + src/systems/nonlinear/nonlinearsystem.jl | 3 +++ src/systems/optimization/optimizationsystem.jl | 3 +++ src/systems/pde/pdesystem.jl | 3 +++ src/utils.jl | 9 +++++++++ 8 files changed, 22 insertions(+) diff --git a/src/systems/diffeqs/odesystem.jl b/src/systems/diffeqs/odesystem.jl index 61f16fd926..b31f39ec22 100644 --- a/src/systems/diffeqs/odesystem.jl +++ b/src/systems/diffeqs/odesystem.jl @@ -202,6 +202,7 @@ struct ODESystem <: AbstractODESystem check_parameters(ps, iv) check_equations(deqs, iv) check_equations(equations(cevents), iv) + check_var_types(ODESystem, dvs) end if checks == true || (checks & CheckUnits) > 0 u = __get_unit_type(dvs, ps, iv) diff --git a/src/systems/diffeqs/sdesystem.jl b/src/systems/diffeqs/sdesystem.jl index 366e8a6d09..eeedce1d46 100644 --- a/src/systems/diffeqs/sdesystem.jl +++ b/src/systems/diffeqs/sdesystem.jl @@ -170,6 +170,7 @@ struct SDESystem <: AbstractODESystem check_parameters(ps, iv) check_equations(deqs, iv) check_equations(neqs, dvs) + check_var_types(SDESystem, dvs) if size(neqs, 1) != length(deqs) throw(ArgumentError("Noise equations ill-formed. Number of rows must match number of drift equations. size(neqs,1) = $(size(neqs,1)) != length(deqs) = $(length(deqs))")) end diff --git a/src/systems/discrete_system/discrete_system.jl b/src/systems/discrete_system/discrete_system.jl index bd5c72eec7..fb6dfe0236 100644 --- a/src/systems/discrete_system/discrete_system.jl +++ b/src/systems/discrete_system/discrete_system.jl @@ -122,6 +122,7 @@ struct DiscreteSystem <: AbstractTimeDependentSystem check_independent_variables([iv]) check_variables(dvs, iv) check_parameters(ps, iv) + check_var_types(DiscreteSystem, dvs) end if checks == true || (checks & CheckUnits) > 0 u = __get_unit_type(dvs, ps, iv) diff --git a/src/systems/jumps/jumpsystem.jl b/src/systems/jumps/jumpsystem.jl index 5c0a61771a..e5dbb185e2 100644 --- a/src/systems/jumps/jumpsystem.jl +++ b/src/systems/jumps/jumpsystem.jl @@ -147,6 +147,7 @@ struct JumpSystem{U <: ArrayPartition} <: AbstractTimeDependentSystem check_independent_variables([iv]) check_variables(unknowns, iv) check_parameters(ps, iv) + check_var_types(JumpSystem, unknowns) end if checks == true || (checks & CheckUnits) > 0 u = __get_unit_type(unknowns, ps, iv) diff --git a/src/systems/nonlinear/nonlinearsystem.jl b/src/systems/nonlinear/nonlinearsystem.jl index 6b0b0cc759..687d55b9bb 100644 --- a/src/systems/nonlinear/nonlinearsystem.jl +++ b/src/systems/nonlinear/nonlinearsystem.jl @@ -115,6 +115,9 @@ struct NonlinearSystem <: AbstractTimeIndependentSystem tearing_state = nothing, substitutions = nothing, complete = false, index_cache = nothing, parent = nothing, isscheduled = false; checks::Union{Bool, Int} = true) + if checks == true || (checks & CheckComponents) > 0 + check_var_types(NonlinearSystem, unknowns) + end if checks == true || (checks & CheckUnits) > 0 u = __get_unit_type(unknowns, ps) check_units(u, eqs) diff --git a/src/systems/optimization/optimizationsystem.jl b/src/systems/optimization/optimizationsystem.jl index 860e063e35..de4696bfa2 100644 --- a/src/systems/optimization/optimizationsystem.jl +++ b/src/systems/optimization/optimizationsystem.jl @@ -73,6 +73,9 @@ struct OptimizationSystem <: AbstractOptimizationSystem gui_metadata = nothing, complete = false, index_cache = nothing, parent = nothing, isscheduled = false; checks::Union{Bool, Int} = true) + if checks == true || (checks & CheckComponents) > 0 + check_var_types(OptimizationSystem, unknowns) + end if checks == true || (checks & CheckUnits) > 0 u = __get_unit_type(unknowns, ps) unwrap(op) isa Symbolic && check_units(u, op) diff --git a/src/systems/pde/pdesystem.jl b/src/systems/pde/pdesystem.jl index eac540e401..e067f0e149 100644 --- a/src/systems/pde/pdesystem.jl +++ b/src/systems/pde/pdesystem.jl @@ -102,6 +102,9 @@ struct PDESystem <: ModelingToolkit.AbstractMultivariateSystem checks::Union{Bool, Int} = true, description = "", name) + if checks == true || (checks & CheckComponents) > 0 + check_var_types(PDESystem, dvs) + end if checks == true || (checks & CheckUnits) > 0 u = __get_unit_type(dvs, ivs, ps) check_units(u, eqs) diff --git a/src/utils.jl b/src/utils.jl index 5e4f0b52d2..7324a07115 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -155,6 +155,14 @@ function check_variables(dvs, iv) end end +function check_var_types(sys_type::Type{T}, dvs) where T <: AbstractSystem + any(u -> !(symtype(u) <: Number), dvs) && error("The type of unknown variables must be a numeric type.") + if sys_type == ODESystem || sys_type == SDESystem || sys_type == PDESystem + any(u -> !(symtype(u) <: Float), dvs) && error("The type of unknown variables for an SDESystem, PDESystem, or ODESystem must be a float.") + end + nothing +end + function check_lhs(eq::Equation, op, dvs::Set) v = unwrap(eq.lhs) _iszero(v) && return @@ -1182,3 +1190,4 @@ function guesses_from_metadata!(guesses, vars) guesses[vars[i]] = varguesses[i] end end + From 42f36c5876d27e6947f7ea5d94cfa21d279c80d2 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 17 Jan 2025 22:12:38 +0530 Subject: [PATCH 0594/1337] fix: respect `checkbounds` in `build_explicit_observed_function` --- src/systems/diffeqs/odesystem.jl | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/src/systems/diffeqs/odesystem.jl b/src/systems/diffeqs/odesystem.jl index 61f16fd926..d9e126baba 100644 --- a/src/systems/diffeqs/odesystem.jl +++ b/src/systems/diffeqs/odesystem.jl @@ -629,7 +629,15 @@ function build_explicit_observed_function(sys, ts; oop_fn = Func(args, [], pre(Let(obsexprs, return_value, - false))) |> array_wrapper[1] |> oop_mtkp_wrapper |> toexpr + false)), [Expr(:meta, :propagate_inbounds)]) |> array_wrapper[1] |> oop_mtkp_wrapper |> toexpr + + if !checkbounds + oop_fn.args[end] = quote + @inbounds begin + $(oop_fn.args[end]) + end + end + end oop_fn = expression ? oop_fn : eval_or_rgf(oop_fn; eval_expression, eval_module) if !isscalar @@ -639,6 +647,18 @@ function build_explicit_observed_function(sys, ts; wrap_code = mtkparams_wrapper .∘ array_wrapper .∘ wrap_assignments(isscalar, obsexprs), expression = Val{true})[2] + if !checkbounds + iip_fn.args[end] = quote + @inbounds begin + $(iip_fn.args[end]) + end + end + end + iip_fn.args[end] = quote + $(Expr(:meta, :propagate_inbounds)) + $(iip_fn.args[end]) + end + if !expression iip_fn = eval_or_rgf(iip_fn; eval_expression, eval_module) end From 74e9d56d083530dfefe86055cca3e5be2f6ab235 Mon Sep 17 00:00:00 2001 From: vyudu Date: Fri, 17 Jan 2025 11:44:57 -0500 Subject: [PATCH 0595/1337] up --- src/utils.jl | 2 +- test/odesystem.jl | 7 +++++++ test/sdesystem.jl | 9 +++++++++ 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/utils.jl b/src/utils.jl index 7324a07115..f66055a65c 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -158,7 +158,7 @@ end function check_var_types(sys_type::Type{T}, dvs) where T <: AbstractSystem any(u -> !(symtype(u) <: Number), dvs) && error("The type of unknown variables must be a numeric type.") if sys_type == ODESystem || sys_type == SDESystem || sys_type == PDESystem - any(u -> !(symtype(u) <: Float), dvs) && error("The type of unknown variables for an SDESystem, PDESystem, or ODESystem must be a float.") + any(u -> !(symtype(u) <: AbstractFloat), dvs) && error("The type of unknown variables for an SDESystem, PDESystem, or ODESystem must be a float.") end nothing end diff --git a/test/odesystem.jl b/test/odesystem.jl index 85d135b338..c354399c3a 100644 --- a/test/odesystem.jl +++ b/test/odesystem.jl @@ -1552,3 +1552,10 @@ end expected_tstops = unique!(sort!(vcat(0.0:0.075:10.0, 0.1, 0.2, 0.65, 0.35, 0.45))) @test all(x -> any(isapprox(x, atol = 1e-6), sol2.t), expected_tstops) end + +let + @parameters p d + @variables X(t)::Int64 + eq = D(X) ~ p - d*X + @test_throws Exception @mtkbuild osys = ODESystem([eq], t) +end diff --git a/test/sdesystem.jl b/test/sdesystem.jl index 8069581dcc..95a5358db6 100644 --- a/test/sdesystem.jl +++ b/test/sdesystem.jl @@ -868,3 +868,12 @@ end @test length(ModelingToolkit.get_noiseeqs(sys)) == 1 @test length(observed(sys)) == 1 end + +# Test validating types of states +let + @parameters p d + @variables X(t)::Int64 + @brownian z + eq2 = D(X) ~ p - d*X + z + @test_throws Exception @mtkbuild ssys = System([eq2], t) +end From 5ff099a89e6b8ae5129f0ca2e8a38fb76908de11 Mon Sep 17 00:00:00 2001 From: vyudu Date: Fri, 17 Jan 2025 11:58:05 -0500 Subject: [PATCH 0596/1337] document state priority --- docs/src/basics/Variable_metadata.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/src/basics/Variable_metadata.md b/docs/src/basics/Variable_metadata.md index ecb94ea878..d8e0bfc2c5 100644 --- a/docs/src/basics/Variable_metadata.md +++ b/docs/src/basics/Variable_metadata.md @@ -186,6 +186,15 @@ it can be accessed in [callbacks](@ref events) isirreducible(important_value) ``` +## State Priority + +When a model is structurally simplified, the algorithm will try to ensure that the variables with higher state priority become states of the system. A variable's state priority is a number set using the `state_priority` metadata. + +```@example metadata +@variable important_dof [state_priority = 10] unimportant_dof [state_priority = 2] +state_priority(important_dof) +``` + ## Units Units for variables can be designated using symbolic metadata. For more information, please see the [model validation and units](@ref units) section of the docs. Note that `getunit` is equivalent to `get_unit`. From cb068e8fcd9e79fea502a9443acdf96d5d3b6c23 Mon Sep 17 00:00:00 2001 From: vyudu Date: Fri, 17 Jan 2025 12:16:34 -0500 Subject: [PATCH 0597/1337] remove noise --- src/ModelingToolkit.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ModelingToolkit.jl b/src/ModelingToolkit.jl index cf3ca5ada0..b4533f72a0 100644 --- a/src/ModelingToolkit.jl +++ b/src/ModelingToolkit.jl @@ -247,7 +247,7 @@ export @component, @mtkmodel, @mtkbuild export isinput, isoutput, getbounds, hasbounds, getguess, hasguess, isdisturbance, istunable, getdist, hasdist, tunable_parameters, isirreducible, getdescription, hasdescription, - hasnoise, getnoise, hasunit, getunit, hasconnect, getconnect, + hasunit, getunit, hasconnect, getconnect, hasmisc, getmisc export ode_order_lowering, dae_order_lowering, liouville_transform export PDESystem From 0d64d282d1c56bfff888e08cf1171f3dcafab4cb Mon Sep 17 00:00:00 2001 From: vyudu Date: Fri, 17 Jan 2025 12:24:42 -0500 Subject: [PATCH 0598/1337] up --- docs/src/basics/Variable_metadata.md | 2 +- src/variables.jl | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/src/basics/Variable_metadata.md b/docs/src/basics/Variable_metadata.md index d8e0bfc2c5..d36c91697e 100644 --- a/docs/src/basics/Variable_metadata.md +++ b/docs/src/basics/Variable_metadata.md @@ -197,7 +197,7 @@ state_priority(important_dof) ## Units -Units for variables can be designated using symbolic metadata. For more information, please see the [model validation and units](@ref units) section of the docs. Note that `getunit` is equivalent to `get_unit`. +Units for variables can be designated using symbolic metadata. For more information, please see the [model validation and units](@ref units) section of the docs. Note that `getunit` is not equivalent to `get_unit` - the former is a metadata getter for individual variables (and is provided so the same interface function for `unit` exists like other metadata), while the latter is used to handle more general symbolic expressions. ```@example metadata @variable speed [unit=u"m/s"] diff --git a/src/variables.jl b/src/variables.jl index 9cd7e72c79..2f33e2eb29 100644 --- a/src/variables.jl +++ b/src/variables.jl @@ -587,9 +587,9 @@ setmisc(x, miscdata) = setmetadata(x, VariableMisc, miscdata) """ getunit(x) -Alias for [`get_unit(x)`](@ref). +Fetch the unit associated with variable `x`. This function is a metadata getter for an individual variable, while `get_unit` is used for unit inference on more complicated sdymbolic expressions. """ -getunit(x) = get_unit(x) +getunit(x) = Symbolics.getmetadata(x, VariableUnit, nothing) """ hasunit(x) From f5937aeaeb40a1e15be7d08c8640f9a9a6dc3a53 Mon Sep 17 00:00:00 2001 From: Herman Sletmoen Date: Fri, 17 Jan 2025 14:58:18 +0100 Subject: [PATCH 0599/1337] Use schedule to replace dummy derivatives --- src/systems/diffeqs/abstractodesystem.jl | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index c4da03e916..1417080587 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -1366,7 +1366,21 @@ function InitializationProblem{iip, specialize}(sys::AbstractSystem, end u0map = merge(ModelingToolkit.guesses(sys), todict(guesses), todict(u0map)) - u0map = Dict(diff2term(var) => val for (var, val) in u0map) # replace D(x) -> x_t etc. + + # Replace dummy derivatives in u0map: D(x) -> x_t etc. + if has_schedule(sys) + schedule = get_schedule(sys) + if !isnothing(schedule) + for (var, val) in u0map + dvar = get(schedule.dummy_sub, var, var) # with dummy derivatives + if dvar !== var # then replace it + delete!(u0map, var) + push!(u0map, dvar => val) + end + end + end + end + fullmap = merge(u0map, parammap) u0T = Union{} for sym in unknowns(isys) From 60e4723c5ea9102738e03527d2ac0109fee79ab3 Mon Sep 17 00:00:00 2001 From: Herman Sletmoen Date: Fri, 17 Jan 2025 15:01:31 +0100 Subject: [PATCH 0600/1337] Format --- test/extensions/ad.jl | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/test/extensions/ad.jl b/test/extensions/ad.jl index 75ec3ef753..7b5c939b0a 100644 --- a/test/extensions/ad.jl +++ b/test/extensions/ad.jl @@ -115,11 +115,12 @@ fwd, back = ChainRulesCore.rrule(remake_buffer, sys, ps, idxs, vals) # Find initial throw velocity that reaches exactly 10 m after 1 s dprob0 = ODEProblem(sys, [D(y) => NaN], (0.0, 1.0), []; guesses = [y => 0.0]) - nprob = NonlinearProblem((ics, _) -> begin + function f(ics, _) dprob = remake(dprob0, u0 = Dict(D(y) => ics[1])) dsol = solve(dprob, Tsit5()) return [dsol[y][end] - 10.0] - end, [1.0]) + end + nprob = NonlinearProblem(f, [1.0]) nsol = solve(nprob, NewtonRaphson()) - @test nsol[1] ≈ 10.0/1.0 + 9.81*1.0/2 # anal free fall solution is y = v0*t - g*t^2/2 -> v0 = y/t + g*t/2 + @test nsol[1] ≈ 10.0 / 1.0 + 9.81 * 1.0 / 2 # anal free fall solution is y = v0*t - g*t^2/2 -> v0 = y/t + g*t/2 end From 25f9ed84d0480298a8c9e60bff06922141bf661a Mon Sep 17 00:00:00 2001 From: vyudu Date: Fri, 17 Jan 2025 14:28:19 -0500 Subject: [PATCH 0601/1337] add test, fix --- src/systems/diffeqs/sdesystem.jl | 6 +----- test/sdesystem.jl | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/src/systems/diffeqs/sdesystem.jl b/src/systems/diffeqs/sdesystem.jl index 26f26b4263..fb2eb1d804 100644 --- a/src/systems/diffeqs/sdesystem.jl +++ b/src/systems/diffeqs/sdesystem.jl @@ -768,11 +768,7 @@ function DiffEqBase.SDEProblem{iip, specialize}( noise_rate_prototype = noise_rate_prototype, kwargs...) end -function DiffEqBase.SDEProblem{iip, specialize}( - sys::ODESystem, u0map = [], tspan = get_tspan(sys), - parammap = DiffEqBase.NullParameters(); - sparsenoise = nothing, check_length = true, - callback = nothing, kwargs...) where {iip, specialize} +function DiffEqBase.SDEProblem(sys::ODESystem, args...; kwargs...) if any(ModelingToolkit.isbrownian, unknowns(sys)) error("SDESystem constructed by defining Brownian variables with @brownian must be simplified by calling `structural_simplify` before a SDEProblem can be constructed.") diff --git a/test/sdesystem.jl b/test/sdesystem.jl index 8069581dcc..1a22a3501d 100644 --- a/test/sdesystem.jl +++ b/test/sdesystem.jl @@ -868,3 +868,24 @@ end @test length(ModelingToolkit.get_noiseeqs(sys)) == 1 @test length(observed(sys)) == 1 end +using ModelingToolkit, StochasticDiffEq +using ModelingToolkit: t_nounits as t, D_nounits as D + +@testset "Error when constructing SDESystem without `structural_simplify`" begin + @parameters σ ρ β + @variables x(t) y(t) z(t) + @brownian a + eqs = [D(x) ~ σ * (y - x) + 0.1a * x, + D(y) ~ x * (ρ - z) - y + 0.1a * y, + D(z) ~ x * y - β * z + 0.1a * z] + + @named de = System(eqs, t) + de = complete(de) + + u0map = [x => 1.0, y => 0.0, z => 0.0] + parammap = [σ => 10.0, β => 26.0, ρ => 2.33] + + @test_throws ErrorException("SDESystem constructed by defining Brownian variables with @brownian must be simplified by calling `structural_simplify` before a SDEProblem can be constructed.") SDEProblem(de, u0map, (0.0, 100.0), parammap) + de = structural_simplify(de) + @test SDEProblem(de, u0map, (0.0, 100.0), parammap) isa SDEProblem +end From 557b01fc6539b0ca09c8a3ffcad271ba74760287 Mon Sep 17 00:00:00 2001 From: vyudu Date: Fri, 17 Jan 2025 14:50:56 -0500 Subject: [PATCH 0602/1337] fix --- src/utils.jl | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/utils.jl b/src/utils.jl index f66055a65c..4742ed819f 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -156,9 +156,12 @@ function check_variables(dvs, iv) end function check_var_types(sys_type::Type{T}, dvs) where T <: AbstractSystem - any(u -> !(symtype(u) <: Number), dvs) && error("The type of unknown variables must be a numeric type.") - if sys_type == ODESystem || sys_type == SDESystem || sys_type == PDESystem - any(u -> !(symtype(u) <: AbstractFloat), dvs) && error("The type of unknown variables for an SDESystem, PDESystem, or ODESystem must be a float.") + if any(u -> !(symtype(u) <: Number), dvs) + error("The type of unknown variables must be a numeric type.") + elseif any(u -> (symtype(u) !== symtype(dvs[1])), dvs) + error("The type of all the unknown variables in a system must all be the same.") + elseif sys_type == ODESystem || sys_type == SDESystem || sys_type == PDESystem + any(u -> !(symtype(u) == Real), dvs) && error("The type of unknown variables for an SDESystem, PDESystem, or ODESystem should not be a concrete numeric type.") end nothing end From e12313e4201f878aa09b502d1dee2c79a9bb8130 Mon Sep 17 00:00:00 2001 From: vyudu Date: Fri, 17 Jan 2025 15:13:46 -0500 Subject: [PATCH 0603/1337] handle arr --- src/utils.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/utils.jl b/src/utils.jl index 4742ed819f..736a4cc03e 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -156,12 +156,12 @@ function check_variables(dvs, iv) end function check_var_types(sys_type::Type{T}, dvs) where T <: AbstractSystem - if any(u -> !(symtype(u) <: Number), dvs) + if any(u -> !(symtype(u) <: Number || eltype(symtype(u)) <: Number), dvs) error("The type of unknown variables must be a numeric type.") - elseif any(u -> (symtype(u) !== symtype(dvs[1])), dvs) + elseif any(u -> (symtype(u) !== symtype(dvs[1])), dvs) error("The type of all the unknown variables in a system must all be the same.") elseif sys_type == ODESystem || sys_type == SDESystem || sys_type == PDESystem - any(u -> !(symtype(u) == Real), dvs) && error("The type of unknown variables for an SDESystem, PDESystem, or ODESystem should not be a concrete numeric type.") + any(u -> !(symtype(u) == Real || eltype(symtype(u)) == Real), dvs) && error("The type of unknown variables for an SDESystem, PDESystem, or ODESystem should not be a concrete numeric type.") end nothing end From 483c97267dbfa89165bec31982df66c3146c538d Mon Sep 17 00:00:00 2001 From: vyudu Date: Fri, 17 Jan 2025 15:18:40 -0500 Subject: [PATCH 0604/1337] fix test --- test/test_variable_metadata.jl | 6 ------ 1 file changed, 6 deletions(-) diff --git a/test/test_variable_metadata.jl b/test/test_variable_metadata.jl index b6e05ce4e0..8fca96a23d 100644 --- a/test/test_variable_metadata.jl +++ b/test/test_variable_metadata.jl @@ -187,12 +187,6 @@ params_meta = Dict([ModelingToolkit.getname(meta.var) => meta for meta in params @test params_meta[:p].default == 3.0 @test isequal(params_meta[:q].dependency, 2p) -# Noise -@variables x [noise = 1] -@test hasnoise(x) -@test getnoise(x) == 1 -@test ModelingToolkit.dump_variable_metadata(x).noise == 1 - # Connect @variables x [connect = Flow] @test hasconnect(x) From 86d4144d910c0ba56ec5057f7a380ccca5caf879 Mon Sep 17 00:00:00 2001 From: vyudu Date: Fri, 17 Jan 2025 16:49:58 -0500 Subject: [PATCH 0605/1337] test more solvers: --- src/systems/diffeqs/abstractodesystem.jl | 2 +- test/bvproblem.jl | 333 ++++++++++++----------- 2 files changed, 172 insertions(+), 163 deletions(-) diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index d935a94eb8..cf6e0962fd 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -969,7 +969,7 @@ function validate_constraint_syms(eq, constraintsts, constraintps, sts, ps, iv) for var in constraintsts if length(arguments(var)) > 1 error("Too many arguments for variable $var.") - elseif arguments(var) == iv + elseif isequal(arguments(var)[1], iv) var ∈ sts || error("Constraint equation $eq contains a variable $var that is not a variable of the ODESystem.") error("Constraint equation $eq contains a variable $var that does not have a specified argument. Such equations should be specified as algebraic equations to the ODESystem rather than a boundary constraints.") else diff --git a/test/bvproblem.jl b/test/bvproblem.jl index a8de6af44d..2f278e6135 100644 --- a/test/bvproblem.jl +++ b/test/bvproblem.jl @@ -1,183 +1,186 @@ ### TODO: update when BoundaryValueDiffEqAscher is updated to use the normal boundary condition conventions using BoundaryValueDiffEq, OrdinaryDiffEq, BoundaryValueDiffEqAscher +using BenchmarkTools using ModelingToolkit using SciMLBase using ModelingToolkit: t_nounits as t, D_nounits as D import ModelingToolkit: process_constraints ### Test Collocation solvers on simple problems -solvers = [MIRK4, RadauIIa5] +solvers = [MIRK4, RadauIIa5, LobattoIIIa3] daesolvers = [Ascher2, Ascher4, Ascher6] -let - @parameters α=7.5 β=4.0 γ=8.0 δ=5.0 - @variables x(t)=1.0 y(t)=2.0 - - eqs = [D(x) ~ α * x - β * x * y, - D(y) ~ -γ * y + δ * x * y] - - u0map = [x => 1.0, y => 2.0] - parammap = [α => 7.5, β => 4, γ => 8.0, δ => 5.0] - tspan = (0.0, 10.0) - - @mtkbuild lotkavolterra = ODESystem(eqs, t) - op = ODEProblem(lotkavolterra, u0map, tspan, parammap) - osol = solve(op, Vern9()) - - bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}( - lotkavolterra, u0map, tspan, parammap; eval_expression = true) - - for solver in solvers - sol = solve(bvp, solver(), dt = 0.01) - @test isapprox(sol.u[end], osol.u[end]; atol = 0.01) - @test sol.u[1] == [1.0, 2.0] - end - - # Test out of place - bvp2 = SciMLBase.BVProblem{false, SciMLBase.AutoSpecialize}( - lotkavolterra, u0map, tspan, parammap; eval_expression = true) - - for solver in solvers - sol = solve(bvp2, solver(), dt = 0.01) - @test isapprox(sol.u[end], osol.u[end]; atol = 0.01) - @test sol.u[1] == [1.0, 2.0] - end -end - -### Testing on pendulum -let - @parameters g=9.81 L=1.0 - @variables θ(t) = π / 2 θ_t(t) - - eqs = [D(θ) ~ θ_t - D(θ_t) ~ -(g / L) * sin(θ)] - - @mtkbuild pend = ODESystem(eqs, t) - - u0map = [θ => π / 2, θ_t => π / 2] - parammap = [:L => 1.0, :g => 9.81] - tspan = (0.0, 6.0) - - op = ODEProblem(pend, u0map, tspan, parammap) - osol = solve(op, Vern9()) - - bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(pend, u0map, tspan, parammap) - for solver in solvers - sol = solve(bvp, solver(), dt = 0.01) - @test isapprox(sol.u[end], osol.u[end]; atol = 0.01) - @test sol.u[1] == [π / 2, π / 2] - end - - # Test out-of-place - bvp2 = SciMLBase.BVProblem{false, SciMLBase.FullSpecialize}(pend, u0map, tspan, parammap) - - for solver in solvers - sol = solve(bvp2, solver(), dt = 0.01) - @test isapprox(sol.u[end], osol.u[end]; atol = 0.01) - @test sol.u[1] == [π / 2, π / 2] - end -end - -################################################################## -### ODESystem with constraint equations, DAEs with constraints ### -################################################################## - -# Test generation of boundary condition function using `process_constraints`. Compare solutions to manually written boundary conditions -let - @parameters α=1.5 β=1.0 γ=3.0 δ=1.0 - @variables x(..) y(..) - eqs = [D(x(t)) ~ α * x(t) - β * x(t) * y(t), - D(y(t)) ~ -γ * y(t) + δ * x(t) * y(t)] - - tspan = (0., 1.) - @mtkbuild lksys = ODESystem(eqs, t) - - function lotkavolterra!(du, u, p, t) - du[1] = p[1]*u[1] - p[2]*u[1]*u[2] - du[2] = -p[4]*u[2] + p[3]*u[1]*u[2] - end - - function lotkavolterra(u, p, t) - [p[1]*u[1] - p[2]*u[1]*u[2], -p[4]*u[2] + p[3]*u[1]*u[2]] - end - # Compare the built bc function to the actual constructed one. - function bc!(resid, u, p, t) - resid[1] = u[1][1] - 1. - resid[2] = u[1][2] - 2. - nothing - end - function bc(u, p, t) - [u[1][1] - 1., u[1][2] - 2.] - end - - u0 = [1., 2.]; p = [1.5, 1., 1., 3.] - genbc_iip = ModelingToolkit.process_constraints(lksys, nothing, u0, [1, 2], tspan, true) - genbc_oop = ModelingToolkit.process_constraints(lksys, nothing, u0, [1, 2], tspan, false) - - bvpi1 = SciMLBase.BVProblem(lotkavolterra!, bc!, [1.,2.], tspan, p) - bvpi2 = SciMLBase.BVProblem(lotkavolterra!, genbc_iip, [1.,2.], tspan, p) - - sol1 = solve(bvpi1, MIRK4(), dt = 0.05) - sol2 = solve(bvpi2, MIRK4(), dt = 0.05) - @test sol1 ≈ sol2 - - bvpo1 = BVProblem(lotkavolterra, bc, [1,2], tspan, p) - bvpo2 = BVProblem(lotkavolterra, genbc_oop, [1,2], tspan, p) - - sol1 = solve(bvpo1, MIRK4(), dt = 0.05) - sol2 = solve(bvpo2, MIRK4(), dt = 0.05) - @test sol1 ≈ sol2 - - # Test with a constraint. - constraints = [y(0.5) ~ 2.] - - function bc!(resid, u, p, t) - resid[1] = u(0.0)[1] - 1. - resid[2] = u(0.5)[2] - 2. - end - function bc(u, p, t) - [u(0.0)[1] - 1., u(0.5)[2] - 2.] - end - - u0 = [1, 1.] - genbc_iip = ModelingToolkit.process_constraints(lksys, constraints, u0, [1], tspan, true) - genbc_oop = ModelingToolkit.process_constraints(lksys, constraints, u0, [1], tspan, false) - - bvpi1 = SciMLBase.BVProblem(lotkavolterra!, bc!, u0, tspan, p) - bvpi2 = SciMLBase.BVProblem(lotkavolterra!, genbc_iip, u0, tspan, p) - bvpi3 = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(lksys, [x(t) => 1.], tspan; guesses = [y(t) => 1.], constraints) - bvpi4 = SciMLBase.BVProblem{true, SciMLBase.FullSpecialize}(lksys, [x(t) => 1.], tspan; guesses = [y(t) => 1.], constraints) - - sol1 = @btime solve($bvpi1, MIRK4(), dt = 0.01) - sol2 = @btime solve($bvpi2, MIRK4(), dt = 0.01) - sol3 = @btime solve($bvpi3, MIRK4(), dt = 0.01) - sol4 = @btime solve($bvpi4, MIRK4(), dt = 0.01) - @test sol1 ≈ sol2 ≈ sol3 ≈ sol4 # don't get true equality here, not sure why - - bvpo1 = BVProblem(lotkavolterra, bc, [1,2], tspan, p) - bvpo2 = BVProblem(lotkavolterra, genbc_oop, [1,2], tspan, p) - bvpo3 = SciMLBase.BVProblem{false, SciMLBase.FullSpecialize}(lksys, [x(t) => 1.], tspan; guesses = [y(t) => 1.], constraints) - - sol1 = @btime solve($bvpo1, MIRK4(), dt = 0.05) - sol2 = @btime solve($bvpo2, MIRK4(), dt = 0.05) - sol3 = @btime solve($bvpo3, MIRK4(), dt = 0.05) - @test sol1 ≈ sol2 ≈ sol3 -end +# let +# @parameters α=7.5 β=4.0 γ=8.0 δ=5.0 +# @variables x(t)=1.0 y(t)=2.0 +# +# eqs = [D(x) ~ α * x - β * x * y, +# D(y) ~ -γ * y + δ * x * y] +# +# u0map = [x => 1.0, y => 2.0] +# parammap = [α => 7.5, β => 4, γ => 8.0, δ => 5.0] +# tspan = (0.0, 10.0) +# +# @mtkbuild lotkavolterra = ODESystem(eqs, t) +# op = ODEProblem(lotkavolterra, u0map, tspan, parammap) +# osol = solve(op, Vern9()) +# +# bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}( +# lotkavolterra, u0map, tspan, parammap; eval_expression = true) +# +# for solver in solvers +# sol = solve(bvp, solver(), dt = 0.01) +# @test isapprox(sol.u[end], osol.u[end]; atol = 0.01) +# @test sol.u[1] == [1.0, 2.0] +# end +# +# # Test out of place +# bvp2 = SciMLBase.BVProblem{false, SciMLBase.AutoSpecialize}( +# lotkavolterra, u0map, tspan, parammap; eval_expression = true) +# +# for solver in solvers +# sol = solve(bvp2, solver(), dt = 0.01) +# @test isapprox(sol.u[end], osol.u[end]; atol = 0.01) +# @test sol.u[1] == [1.0, 2.0] +# end +# end +# +# ### Testing on pendulum +# let +# @parameters g=9.81 L=1.0 +# @variables θ(t) = π / 2 θ_t(t) +# +# eqs = [D(θ) ~ θ_t +# D(θ_t) ~ -(g / L) * sin(θ)] +# +# @mtkbuild pend = ODESystem(eqs, t) +# +# u0map = [θ => π / 2, θ_t => π / 2] +# parammap = [:L => 1.0, :g => 9.81] +# tspan = (0.0, 6.0) +# +# op = ODEProblem(pend, u0map, tspan, parammap) +# osol = solve(op, Vern9()) +# +# bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(pend, u0map, tspan, parammap) +# for solver in solvers +# sol = solve(bvp, solver(), dt = 0.01) +# @test isapprox(sol.u[end], osol.u[end]; atol = 0.01) +# @test sol.u[1] == [π / 2, π / 2] +# end +# +# # Test out-of-place +# bvp2 = SciMLBase.BVProblem{false, SciMLBase.FullSpecialize}(pend, u0map, tspan, parammap) +# +# for solver in solvers +# sol = solve(bvp2, solver(), dt = 0.01) +# @test isapprox(sol.u[end], osol.u[end]; atol = 0.01) +# @test sol.u[1] == [π / 2, π / 2] +# end +# end +# +# ################################################################## +# ### ODESystem with constraint equations, DAEs with constraints ### +# ################################################################## +# +# # Test generation of boundary condition function using `process_constraints`. Compare solutions to manually written boundary conditions +# let +# @parameters α=1.5 β=1.0 γ=3.0 δ=1.0 +# @variables x(..) y(..) +# eqs = [D(x(t)) ~ α * x(t) - β * x(t) * y(t), +# D(y(t)) ~ -γ * y(t) + δ * x(t) * y(t)] +# +# tspan = (0., 1.) +# @mtkbuild lksys = ODESystem(eqs, t) +# +# function lotkavolterra!(du, u, p, t) +# du[1] = p[1]*u[1] - p[2]*u[1]*u[2] +# du[2] = -p[4]*u[2] + p[3]*u[1]*u[2] +# end +# +# function lotkavolterra(u, p, t) +# [p[1]*u[1] - p[2]*u[1]*u[2], -p[4]*u[2] + p[3]*u[1]*u[2]] +# end +# # Compare the built bc function to the actual constructed one. +# function bc!(resid, u, p, t) +# resid[1] = u[1][1] - 1. +# resid[2] = u[1][2] - 2. +# nothing +# end +# function bc(u, p, t) +# [u[1][1] - 1., u[1][2] - 2.] +# end +# +# u0 = [1., 2.]; p = [1.5, 1., 1., 3.] +# genbc_iip = ModelingToolkit.process_constraints(lksys, nothing, u0, [1, 2], tspan, true) +# genbc_oop = ModelingToolkit.process_constraints(lksys, nothing, u0, [1, 2], tspan, false) +# +# bvpi1 = SciMLBase.BVProblem(lotkavolterra!, bc!, [1.,2.], tspan, p) +# bvpi2 = SciMLBase.BVProblem(lotkavolterra!, genbc_iip, [1.,2.], tspan, p) +# +# sol1 = solve(bvpi1, MIRK4(), dt = 0.05) +# sol2 = solve(bvpi2, MIRK4(), dt = 0.05) +# @test sol1 ≈ sol2 +# +# bvpo1 = BVProblem(lotkavolterra, bc, [1,2], tspan, p) +# bvpo2 = BVProblem(lotkavolterra, genbc_oop, [1,2], tspan, p) +# +# sol1 = solve(bvpo1, MIRK4(), dt = 0.05) +# sol2 = solve(bvpo2, MIRK4(), dt = 0.05) +# @test sol1 ≈ sol2 +# +# # Test with a constraint. +# constraints = [y(0.5) ~ 2.] +# +# function bc!(resid, u, p, t) +# resid[1] = u(0.0)[1] - 1. +# resid[2] = u(0.5)[2] - 2. +# end +# function bc(u, p, t) +# [u(0.0)[1] - 1., u(0.5)[2] - 2.] +# end +# +# u0 = [1, 1.] +# genbc_iip = ModelingToolkit.process_constraints(lksys, constraints, u0, [1], tspan, true) +# genbc_oop = ModelingToolkit.process_constraints(lksys, constraints, u0, [1], tspan, false) +# +# bvpi1 = SciMLBase.BVProblem(lotkavolterra!, bc!, u0, tspan, p) +# bvpi2 = SciMLBase.BVProblem(lotkavolterra!, genbc_iip, u0, tspan, p) +# bvpi3 = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(lksys, [x(t) => 1.], tspan; guesses = [y(t) => 1.], constraints) +# bvpi4 = SciMLBase.BVProblem{true, SciMLBase.FullSpecialize}(lksys, [x(t) => 1.], tspan; guesses = [y(t) => 1.], constraints) +# +# sol1 = @btime solve($bvpi1, MIRK4(), dt = 0.01) +# sol2 = @btime solve($bvpi2, MIRK4(), dt = 0.01) +# sol3 = @btime solve($bvpi3, MIRK4(), dt = 0.01) +# sol4 = @btime solve($bvpi4, MIRK4(), dt = 0.01) +# @test sol1 ≈ sol2 ≈ sol3 ≈ sol4 # don't get true equality here, not sure why +# +# bvpo1 = BVProblem(lotkavolterra, bc, u0, tspan, p) +# bvpo2 = BVProblem(lotkavolterra, genbc_oop, u0, tspan, p) +# bvpo3 = SciMLBase.BVProblem{false, SciMLBase.FullSpecialize}(lksys, [x(t) => 1.], tspan; guesses = [y(t) => 1.], constraints) +# +# sol1 = @btime solve($bvpo1, MIRK4(), dt = 0.05) +# sol2 = @btime solve($bvpo2, MIRK4(), dt = 0.05) +# sol3 = @btime solve($bvpo3, MIRK4(), dt = 0.05) +# @test sol1 ≈ sol2 ≈ sol3 +# end -function test_solvers(solvers, prob, u0map, constraints, equations = []; dt = 0.05, atol = 1e-4) +function test_solvers(solvers, prob, u0map, constraints, equations = []; dt = 0.05, atol = 1e-3) for solver in solvers println("Solver: $solver") sol = @btime solve($prob, $solver(), dt = $dt, abstol = $atol) @test SciMLBase.successful_retcode(sol.retcode) p = prob.p; t = sol.t; bc = prob.f.bc ns = length(prob.u0) - if isinplace(bvp.f) + if isinplace(prob.f) resid = zeros(ns) bc(resid, sol, p, t) @test isapprox(zeros(ns), resid; atol) + @show resid else @test isapprox(zeros(ns), bc(sol, p, t); atol) + @show bc(sol, p, t) end for (k, v) in u0map @@ -194,6 +197,12 @@ function test_solvers(solvers, prob, u0map, constraints, equations = []; dt = 0. end end +solvers = [RadauIIa3, RadauIIa5, RadauIIa7, + LobattoIIIa2, LobattoIIIa4, LobattoIIIa5, + LobattoIIIb2, LobattoIIIb3, LobattoIIIb4, LobattoIIIb5, + LobattoIIIc2, LobattoIIIc3, LobattoIIIc4, LobattoIIIc5] +weird = [MIRK2, MIRK5, RadauIIa2] +daesolvers = [] # Simple ODESystem with BVP constraints. let @parameters α=1.5 β=1.0 γ=3.0 δ=1.0 @@ -213,8 +222,8 @@ let test_solvers(solvers, bvp, u0map, constraints; dt = 0.05) # Testing that more complicated constraints give correct solutions. - constraints = [y(.2) + x(.8) ~ 3., y(.3) + x(.5) ~ 5.] - bvp = SciMLBase.BVProblem{false, SciMLBase.FullSpecialize}(lksys, u0map, tspan; guesses, constraints, jac = true) + constraints = [y(.2) + x(.8) ~ 3., y(.3) ~ 2.] + bvp = SciMLBase.BVProblem{false, SciMLBase.FullSpecialize}(lksys, u0map, tspan; guesses, constraints) test_solvers(solvers, bvp, u0map, constraints; dt = 0.05) constraints = [α * β - x(.6) ~ 0.0, y(.2) ~ 3.] @@ -224,14 +233,14 @@ let # Testing that errors are properly thrown when malformed constraints are given. @variables bad(..) constraints = [x(1.) + bad(3.) ~ 10] - @test_throws Exception bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(lksys, u0map, tspan; guesses, constraints) + @test_throws ErrorException bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(lksys, u0map, tspan; guesses, constraints) constraints = [x(t) + y(t) ~ 3] - @test_throws Exception bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(lksys, u0map, tspan; guesses, constraints) + @test_throws ErrorException bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(lksys, u0map, tspan; guesses, constraints) @parameters bad2 constraints = [bad2 + x(0.) ~ 3] - @test_throws Exception bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(lksys, u0map, tspan; guesses, constraints) + @test_throws ErrorException bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(lksys, u0map, tspan; guesses, constraints) end # Cartesian pendulum from the docs. From df0ee8ae6804ad85b85885e3e5de8540bdd7e097 Mon Sep 17 00:00:00 2001 From: vyudu Date: Fri, 17 Jan 2025 16:51:19 -0500 Subject: [PATCH 0606/1337] fix unit --- src/variables.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/variables.jl b/src/variables.jl index 2f33e2eb29..049dac790b 100644 --- a/src/variables.jl +++ b/src/variables.jl @@ -589,7 +589,8 @@ setmisc(x, miscdata) = setmetadata(x, VariableMisc, miscdata) Fetch the unit associated with variable `x`. This function is a metadata getter for an individual variable, while `get_unit` is used for unit inference on more complicated sdymbolic expressions. """ -getunit(x) = Symbolics.getmetadata(x, VariableUnit, nothing) +getunit(x) = getunit(unwrap(x)) +getunit(x::Symbolic) = Symbolics.getmetadata(x, VariableUnit, nothing) """ hasunit(x) From b4d9a627b333236e80d238dea9389f469e76acc8 Mon Sep 17 00:00:00 2001 From: vyudu Date: Fri, 17 Jan 2025 16:55:47 -0500 Subject: [PATCH 0607/1337] fix SDE test --- test/sdesystem.jl | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/test/sdesystem.jl b/test/sdesystem.jl index 7ff7d5e7c8..53466768f0 100644 --- a/test/sdesystem.jl +++ b/test/sdesystem.jl @@ -875,22 +875,22 @@ let @parameters p d @brownian a seq = D(X) ~ p - d*X + a - @mtkbuild ssys1 = System([eq], t; name = :ssys) - @mtkbuild ssys2 = System([eq], t; name = :ssys) + @mtkbuild ssys1 = System([seq], t; name = :ssys) + @mtkbuild ssys2 = System([seq], t; name = :ssys) @test ssys1 == ssys2 # true continuous_events = [[X ~ 1.0] => [X ~ X + 5.0]] discrete_events = [5.0 => [d ~ d / 2.0]] - @mtkbuild ssys1 = System([eq], t; name = :ssys, continuous_events) - @mtkbuild ssys2 = System([eq], t; name = :ssys) + @mtkbuild ssys1 = System([seq], t; name = :ssys, continuous_events) + @mtkbuild ssys2 = System([seq], t; name = :ssys) @test ssys1 !== ssys2 - @mtkbuild ssys1 = System([eq], t; name = :ssys, discrete_events) - @mtkbuild ssys2 = System([eq], t; name = :ssys) + @mtkbuild ssys1 = System([seq], t; name = :ssys, discrete_events) + @mtkbuild ssys2 = System([seq], t; name = :ssys) @test ssys1 !== ssys2 - @mtkbuild ssys1 = System([eq], t; name = :ssys, continuous_events) - @mtkbuild ssys2 = System([eq], t; name = :ssys, discrete_events) + @mtkbuild ssys1 = System([seq], t; name = :ssys, continuous_events) + @mtkbuild ssys2 = System([seq], t; name = :ssys, discrete_events) @test ssys1 !== ssys2 end From ea95965838ae66eda3623d4e7a83dd511db5cc74 Mon Sep 17 00:00:00 2001 From: Christopher Rackauckas Date: Sat, 18 Jan 2025 09:20:18 +0100 Subject: [PATCH 0608/1337] Update test/sdesystem.jl --- test/sdesystem.jl | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/test/sdesystem.jl b/test/sdesystem.jl index 1a22a3501d..94f3e9cbfc 100644 --- a/test/sdesystem.jl +++ b/test/sdesystem.jl @@ -868,12 +868,10 @@ end @test length(ModelingToolkit.get_noiseeqs(sys)) == 1 @test length(observed(sys)) == 1 end -using ModelingToolkit, StochasticDiffEq -using ModelingToolkit: t_nounits as t, D_nounits as D @testset "Error when constructing SDESystem without `structural_simplify`" begin @parameters σ ρ β - @variables x(t) y(t) z(t) + @variables x(tt) y(tt) z(tt) @brownian a eqs = [D(x) ~ σ * (y - x) + 0.1a * x, D(y) ~ x * (ρ - z) - y + 0.1a * y, From 39f8271ea334c3c8659499fd788c3e3c54c60dac Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Sun, 19 Jan 2025 14:00:36 +0530 Subject: [PATCH 0609/1337] fix: only solve parameter initialization for `NonlinearSystem` --- src/systems/diffeqs/abstractodesystem.jl | 2 +- src/systems/nonlinear/initializesystem.jl | 106 +++++----- src/systems/problem_utils.jl | 38 +++- test/initializationsystem.jl | 239 +++++++++------------- 4 files changed, 180 insertions(+), 205 deletions(-) diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index 00b3c11ec3..ac28d41dd4 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -1335,7 +1335,7 @@ function InitializationProblem{iip, specialize}(sys::AbstractSystem, # TODO: throw on uninitialized arrays filter!(x -> !(x isa Symbolics.Arr), uninit) - if !isempty(uninit) + if is_time_dependent(sys) && !isempty(uninit) allow_incomplete || throw(IncompleteInitializationError(uninit)) # for incomplete initialization, we will add the missing variables as parameters. # they will be updated by `update_initializeprob!` and `initializeprobmap` will diff --git a/src/systems/nonlinear/initializesystem.jl b/src/systems/nonlinear/initializesystem.jl index 0b556eef68..a7f98d79b1 100644 --- a/src/systems/nonlinear/initializesystem.jl +++ b/src/systems/nonlinear/initializesystem.jl @@ -42,51 +42,53 @@ function generate_initializesystem(sys::AbstractSystem; diffmap = Dict() end - if has_schedule(sys) && (schedule = get_schedule(sys); !isnothing(schedule)) - # 2) process dummy derivatives and u0map into initialization system - # prepare map for dummy derivative substitution - for x in filter(x -> !isnothing(x[1]), schedule.dummy_sub) - # set dummy derivatives to default_dd_guess unless specified - push!(defs, x[1] => get(guesses, x[1], default_dd_guess)) - end - function process_u0map_with_dummysubs(y, x) - y = get(schedule.dummy_sub, y, y) - y = fixpoint_sub(y, diffmap) - if y ∈ vars_set - # variables specified in u0 overrides defaults - push!(defs, y => x) - elseif y isa Symbolics.Arr - # TODO: don't scalarize arrays - merge!(defs, Dict(scalarize(y .=> x))) - elseif y isa Symbolics.BasicSymbolic - # y is a derivative expression expanded; add it to the initialization equations - push!(eqs_ics, y ~ x) - else - error("Initialization expression $y is currently not supported. If its a higher order derivative expression, then only the dummy derivative expressions are supported.") + if is_time_dependent(sys) + if has_schedule(sys) && (schedule = get_schedule(sys); !isnothing(schedule)) + # 2) process dummy derivatives and u0map into initialization system + # prepare map for dummy derivative substitution + for x in filter(x -> !isnothing(x[1]), schedule.dummy_sub) + # set dummy derivatives to default_dd_guess unless specified + push!(defs, x[1] => get(guesses, x[1], default_dd_guess)) end - end - for (y, x) in u0map - if Symbolics.isarraysymbolic(y) - process_u0map_with_dummysubs.(collect(y), collect(x)) - else - process_u0map_with_dummysubs(y, x) + function process_u0map_with_dummysubs(y, x) + y = get(schedule.dummy_sub, y, y) + y = fixpoint_sub(y, diffmap) + if y ∈ vars_set + # variables specified in u0 overrides defaults + push!(defs, y => x) + elseif y isa Symbolics.Arr + # TODO: don't scalarize arrays + merge!(defs, Dict(scalarize(y .=> x))) + elseif y isa Symbolics.BasicSymbolic + # y is a derivative expression expanded; add it to the initialization equations + push!(eqs_ics, y ~ x) + else + error("Initialization expression $y is currently not supported. If its a higher order derivative expression, then only the dummy derivative expressions are supported.") + end + end + for (y, x) in u0map + if Symbolics.isarraysymbolic(y) + process_u0map_with_dummysubs.(collect(y), collect(x)) + else + process_u0map_with_dummysubs(y, x) + end + end + else + # 2) System doesn't have a schedule, so dummy derivatives don't exist/aren't handled (SDESystem) + for (k, v) in u0map + defs[k] = v end end - else - # 2) System doesn't have a schedule, so dummy derivatives don't exist/aren't handled (SDESystem) - for (k, v) in u0map - defs[k] = v - end - end - # 3) process other variables - for var in vars - if var ∈ keys(defs) - push!(eqs_ics, var ~ defs[var]) - elseif var ∈ keys(guesses) - push!(defs, var => guesses[var]) - elseif check_defguess - error("Invalid setup: variable $(var) has no default value or initial guess") + # 3) process other variables + for var in vars + if var ∈ keys(defs) + push!(eqs_ics, var ~ defs[var]) + elseif var ∈ keys(guesses) + push!(defs, var => guesses[var]) + elseif check_defguess + error("Invalid setup: variable $(var) has no default value or initial guess") + end end end @@ -180,16 +182,24 @@ function generate_initializesystem(sys::AbstractSystem; pars = Vector{SymbolicParam}(filter(p -> !haskey(paramsubs, p), parameters(sys))) is_time_dependent(sys) && push!(pars, get_iv(sys)) - # 8) use observed equations for guesses of observed variables if not provided - for eq in trueobs - haskey(defs, eq.lhs) && continue - any(x -> isequal(default_toterm(x), eq.lhs), keys(defs)) && continue + if is_time_dependent(sys) + # 8) use observed equations for guesses of observed variables if not provided + for eq in trueobs + haskey(defs, eq.lhs) && continue + any(x -> isequal(default_toterm(x), eq.lhs), keys(defs)) && continue - defs[eq.lhs] = eq.rhs + defs[eq.lhs] = eq.rhs + end + append!(eqs_ics, trueobs) + end + + eqs_ics = Symbolics.substitute.(eqs_ics, (paramsubs,)) + if is_time_dependent(sys) + vars = [vars; collect(values(paramsubs))] + else + vars = collect(values(paramsubs)) end - eqs_ics = Symbolics.substitute.([eqs_ics; trueobs], (paramsubs,)) - vars = [vars; collect(values(paramsubs))] for k in keys(defs) defs[k] = substitute(defs[k], paramsubs) end diff --git a/src/systems/problem_utils.jl b/src/systems/problem_utils.jl index 4cbf495d87..a8d443349b 100644 --- a/src/systems/problem_utils.jl +++ b/src/systems/problem_utils.jl @@ -546,30 +546,46 @@ function maybe_build_initialization_problem( initializeprob = ModelingToolkit.InitializationProblem( sys, t, u0map, pmap; guesses, kwargs...) - all_init_syms = Set(all_symbols(initializeprob)) - solved_unknowns = filter(var -> var in all_init_syms, unknowns(sys)) - initializeprobmap = getu(initializeprob, solved_unknowns) + if is_time_dependent(sys) + all_init_syms = Set(all_symbols(initializeprob)) + solved_unknowns = filter(var -> var in all_init_syms, unknowns(sys)) + initializeprobmap = getu(initializeprob, solved_unknowns) + else + initializeprobmap = nothing + end punknowns = [p for p in all_variable_symbols(initializeprob) if is_parameter(sys, p)] - getpunknowns = getu(initializeprob, punknowns) - setpunknowns = setp(sys, punknowns) - initializeprobpmap = GetUpdatedMTKParameters(getpunknowns, setpunknowns) + if isempty(punknowns) + initializeprobpmap = nothing + else + getpunknowns = getu(initializeprob, punknowns) + setpunknowns = setp(sys, punknowns) + initializeprobpmap = GetUpdatedMTKParameters(getpunknowns, setpunknowns) + end reqd_syms = parameter_symbols(initializeprob) - update_initializeprob! = UpdateInitializeprob( - getu(sys, reqd_syms), setu(initializeprob, reqd_syms)) + # we still want the `initialization_data` because it helps with `remake` + if initializeprobmap === nothing && initializeprobpmap === nothing + update_initializeprob! = nothing + else + update_initializeprob! = UpdateInitializeprob( + getu(sys, reqd_syms), setu(initializeprob, reqd_syms)) + end + for p in punknowns p = unwrap(p) stype = symtype(p) op[p] = get_temporary_value(p) end - for v in missing_unknowns - op[v] = zero_var(v) + if is_time_dependent(sys) + for v in missing_unknowns + op[v] = zero_var(v) + end + empty!(missing_unknowns) end - empty!(missing_unknowns) return (; initialization_data = SciMLBase.OverrideInitData( initializeprob, update_initializeprob!, initializeprobmap, diff --git a/test/initializationsystem.jl b/test/initializationsystem.jl index 62b6bba707..0a4ef18038 100644 --- a/test/initializationsystem.jl +++ b/test/initializationsystem.jl @@ -583,14 +583,6 @@ sol = solve(oprob_2nd_order_2, Rosenbrock23()) # retcode: Success @test all(sol(1.0, idxs = sys.x) .≈ +exp(1)) && all(sol(1.0, idxs = sys.y) .≈ -exp(1)) end -NonlinearSystemWrapper(eqs, t; kws...) = NonlinearSystem(eqs; kws...) -function NonlinearProblemWrapper(sys, u0, tspan, args...; kwargs...) - NonlinearProblem(sys, u0, args...; kwargs...) -end -function NLLSProblemWrapper(sys, u0, tspan, args...; kwargs...) - NonlinearLeastSquaresProblem(sys, u0, args...; kwargs...) -end - @testset "Initialization of parameters" begin @variables _x(..) y(t) @parameters p q @@ -606,31 +598,11 @@ end (ModelingToolkit.System, ODEProblem, Tsit5(), zeros(2)), (ModelingToolkit.System, SDEProblem, ImplicitEM(), [a, b]), (ModelingToolkit.System, DDEProblem, MethodOfSteps(Tsit5()), [_x(t - 0.1), 0.0]), - (ModelingToolkit.System, SDDEProblem, ImplicitEM(), [_x(t - 0.1) + a, b]), - # polyalg cache - (NonlinearSystemWrapper, NonlinearProblemWrapper, - FastShortcutNonlinearPolyalg(), zeros(2)), - # generalized first order cache - (NonlinearSystemWrapper, NonlinearProblemWrapper, NewtonRaphson(), zeros(2)), - # quasi newton cache - (NonlinearSystemWrapper, NonlinearProblemWrapper, Klement(), zeros(2)), - # noinit cache - (NonlinearSystemWrapper, NonlinearProblemWrapper, SimpleNewtonRaphson(), zeros(2)), - # DFSane cache - (NonlinearSystemWrapper, NonlinearProblemWrapper, DFSane(), zeros(2)), - # Least squares - # polyalg cache - (NonlinearSystemWrapper, NLLSProblemWrapper, FastShortcutNLLSPolyalg(), zeros(2)), - # generalized first order cache - (NonlinearSystemWrapper, NLLSProblemWrapper, LevenbergMarquardt(), zeros(2)), - # noinit cache - (NonlinearSystemWrapper, NLLSProblemWrapper, SimpleGaussNewton(), zeros(2)) + (ModelingToolkit.System, SDDEProblem, ImplicitEM(), [_x(t - 0.1) + a, b]) ] - is_nlsolve = alg isa SciMLBase.AbstractNonlinearAlgorithm - function test_parameter(prob, sym, val, initialval = zero(val)) @test prob.ps[sym] ≈ initialval - if !is_nlsolve || prob.u0 !== nothing + if prob.u0 !== nothing @test init(prob, alg).ps[sym] ≈ val end @test solve(prob, alg).ps[sym] ≈ val @@ -641,7 +613,6 @@ end @test is_variable(isys, p) @test equation in equations(isys) || (0 ~ -equation.rhs) in equations(isys) end - D = is_nlsolve ? v -> v^3 : Differential(t) u0map = Dict(x => 1.0, y => 1.0) pmap = Dict() @@ -777,6 +748,90 @@ end @test solve(prob, Tsit5()).ps[spring.s_rel0] ≈ -3.905 end +@testset "NonlinearSystem initialization" begin + nl_algs = [FastShortcutNonlinearPolyalg(), NewtonRaphson(), + Klement(), SimpleNewtonRaphson(), DFSane()] + nlls_algs = [FastShortcutNLLSPolyalg(), LevenbergMarquardt(), SimpleGaussNewton()] + + @testset "No initialization for variables" begin + @variables x=1.0 y=0.0 z=0.0 + @parameters σ=10.0 ρ=26.0 β=8 / 3 + + eqs = [0 ~ σ * (y - x), + 0 ~ x * (ρ - z) - y, + 0 ~ x * y - β * z] + @mtkbuild ns = NonlinearSystem(eqs, [x, y, z], [σ, ρ, β]) + + prob = NonlinearProblem(ns, []) + @test prob.f.initialization_data.update_initializeprob! === nothing + @test prob.f.initialization_data.initializeprobmap === nothing + @test prob.f.initialization_data.initializeprobpmap === nothing + for alg in nl_algs + @test SciMLBase.successful_retcode(solve(prob, alg)) + end + + prob = NonlinearLeastSquaresProblem(ns, []) + @test prob.f.initialization_data.update_initializeprob! === nothing + @test prob.f.initialization_data.initializeprobmap === nothing + @test prob.f.initialization_data.initializeprobpmap === nothing + for alg in nlls_algs + @test SciMLBase.successful_retcode(solve(prob, alg)) + end + end + + prob_alg_combinations = zip( + [NonlinearProblem, NonlinearLeastSquaresProblem], [nl_algs, nlls_algs]) + @testset "Parameter initialization" begin + function test_parameter(prob, alg, param, val) + integ = init(prob, alg) + @test integ.ps[param]≈val rtol=1e-6 + sol = solve(prob, alg) + @test sol.ps[param]≈val rtol=1e-6 + @test SciMLBase.successful_retcode(sol) + end + @variables x=1.0 y=3.0 + @parameters p q + + @mtkbuild sys = NonlinearSystem( + [(x - p)^2 + (y - q)^3 ~ 0, x - q ~ 0]; defaults = [q => missing], + guesses = [q => 1.0], initialization_eqs = [p^2 + q^2 + 2p * q ~ 0]) + + for (probT, algs) in prob_alg_combinations + prob = probT(sys, [], [p => 2.0]) + @test prob.f.initialization_data !== nothing + @test prob.f.initialization_data.initializeprobmap === nothing + for alg in algs + test_parameter(prob, alg, q, -2.0) + end + + # `update_initializeprob!` works + prob.ps[p] = -2.0 + for alg in algs + test_parameter(prob, alg, q, 2.0) + end + prob.ps[p] = 2.0 + + # `remake` works + prob2 = remake(prob; p = [p => -2.0]) + @test prob2.f.initialization_data !== nothing + @test prob2.f.initialization_data.initializeprobmap === nothing + for alg in algs + test_parameter(prob2, alg, q, 2.0) + end + + # changing types works + ps = parameter_values(prob) + newps = SciMLStructures.replace(Tunable(), ps, ForwardDiff.Dual.(ps.tunable)) + prob3 = remake(prob; p = newps) + @test prob3.f.initialization_data !== nothing + @test eltype(state_values(prob3.f.initialization_data.initializeprob)) <: + ForwardDiff.Dual + @test eltype(prob3.f.initialization_data.initializeprob.p.tunable) <: + ForwardDiff.Dual + end + end +end + @testset "Update initializeprob parameters" begin @variables _x(..) y(t) @parameters p q @@ -787,29 +842,8 @@ end (ModelingToolkit.System, ODEProblem, Tsit5(), zeros(2)), (ModelingToolkit.System, SDEProblem, ImplicitEM(), [a, b]), (ModelingToolkit.System, DDEProblem, MethodOfSteps(Tsit5()), [_x(t - 0.1), 0.0]), - (ModelingToolkit.System, SDDEProblem, ImplicitEM(), [_x(t - 0.1) + a, b]), - # polyalg cache - (NonlinearSystemWrapper, NonlinearProblemWrapper, - FastShortcutNonlinearPolyalg(), zeros(2)), - # generalized first order cache - (NonlinearSystemWrapper, NonlinearProblemWrapper, NewtonRaphson(), zeros(2)), - # quasi newton cache - (NonlinearSystemWrapper, NonlinearProblemWrapper, Klement(), zeros(2)), - # noinit cache - (NonlinearSystemWrapper, NonlinearProblemWrapper, SimpleNewtonRaphson(), zeros(2)), - # DFSane cache - (NonlinearSystemWrapper, NonlinearProblemWrapper, DFSane(), zeros(2)), - # Least squares - # polyalg cache - (NonlinearSystemWrapper, NLLSProblemWrapper, FastShortcutNLLSPolyalg(), zeros(2)), - # generalized first order cache - (NonlinearSystemWrapper, NLLSProblemWrapper, LevenbergMarquardt(), zeros(2)), - # noinit cache - (NonlinearSystemWrapper, NLLSProblemWrapper, SimpleGaussNewton(), zeros(2)) + (ModelingToolkit.System, SDDEProblem, ImplicitEM(), [_x(t - 0.1) + a, b]) ] - is_nlsolve = alg isa SciMLBase.AbstractNonlinearAlgorithm - D = is_nlsolve ? v -> v^3 : Differential(t) - @mtkbuild sys = System( [D(x) ~ x + rhss[1], p ~ x + y + rhss[2]], t; guesses = [x => 0.0, p => 0.0]) prob = Problem(sys, [y => 1.0], (0.0, 1.0), [p => 3.0]) @@ -837,29 +871,8 @@ end (ModelingToolkit.System, ODEProblem, Tsit5(), 0), (ModelingToolkit.System, SDEProblem, ImplicitEM(), a), (ModelingToolkit.System, DDEProblem, MethodOfSteps(Tsit5()), _x(t - 0.1)), - (ModelingToolkit.System, SDDEProblem, ImplicitEM(), _x(t - 0.1) + a), - # polyalg cache - (NonlinearSystemWrapper, NonlinearProblemWrapper, - FastShortcutNonlinearPolyalg(), 0), - # generalized first order cache - (NonlinearSystemWrapper, NonlinearProblemWrapper, NewtonRaphson(), 0), - # quasi newton cache - (NonlinearSystemWrapper, NonlinearProblemWrapper, Klement(), 0), - # noinit cache - (NonlinearSystemWrapper, NonlinearProblemWrapper, SimpleNewtonRaphson(), 0), - # DFSane cache - (NonlinearSystemWrapper, NonlinearProblemWrapper, DFSane(), 0), - # Least squares - # polyalg cache - (NonlinearSystemWrapper, NLLSProblemWrapper, FastShortcutNLLSPolyalg(), 0), - # generalized first order cache - (NonlinearSystemWrapper, NLLSProblemWrapper, LevenbergMarquardt(), 0), - # noinit cache - (NonlinearSystemWrapper, NLLSProblemWrapper, SimpleGaussNewton(), 0) + (ModelingToolkit.System, SDDEProblem, ImplicitEM(), _x(t - 0.1) + a) ] - is_nlsolve = alg isa SciMLBase.AbstractNonlinearAlgorithm - D = is_nlsolve ? v -> v^3 : Differential(t) - @mtkbuild sys = System( [D(x) ~ 2x + r + rhss], t; parameter_dependencies = [r ~ p + 2q, q ~ p + 3], guesses = [p => 1.0]) @@ -881,29 +894,8 @@ end (ModelingToolkit.System, ODEProblem, Tsit5(), zeros(2)), (ModelingToolkit.System, SDEProblem, ImplicitEM(), [a, b]), (ModelingToolkit.System, DDEProblem, MethodOfSteps(Tsit5()), [_x(t - 0.1), 0.0]), - (ModelingToolkit.System, SDDEProblem, ImplicitEM(), [_x(t - 0.1) + a, b]), - # polyalg cache - (NonlinearSystemWrapper, NonlinearProblemWrapper, - FastShortcutNonlinearPolyalg(), zeros(2)), - # generalized first order cache - (NonlinearSystemWrapper, NonlinearProblemWrapper, NewtonRaphson(), zeros(2)), - # quasi newton cache - (NonlinearSystemWrapper, NonlinearProblemWrapper, Klement(), zeros(2)), - # noinit cache - (NonlinearSystemWrapper, NonlinearProblemWrapper, SimpleNewtonRaphson(), zeros(2)), - # DFSane cache - (NonlinearSystemWrapper, NonlinearProblemWrapper, DFSane(), zeros(2)), - # Least squares - # polyalg cache - (NonlinearSystemWrapper, NLLSProblemWrapper, FastShortcutNLLSPolyalg(), zeros(2)), - # generalized first order cache - (NonlinearSystemWrapper, NLLSProblemWrapper, LevenbergMarquardt(), zeros(2)), - # noinit cache - (NonlinearSystemWrapper, NLLSProblemWrapper, SimpleGaussNewton(), zeros(2)) + (ModelingToolkit.System, SDDEProblem, ImplicitEM(), [_x(t - 0.1) + a, b]) ] - is_nlsolve = alg isa SciMLBase.AbstractNonlinearAlgorithm - D = is_nlsolve ? v -> v^3 : Differential(t) - @mtkbuild sys = System( [D(x) ~ x + rhss[1], p ~ x + y + rhss[2]], t; defaults = [p => missing], guesses = [ x => 0.0, p => 0.0]) @@ -934,35 +926,14 @@ end (ModelingToolkit.System, ODEProblem, Tsit5(), 0), (ModelingToolkit.System, SDEProblem, ImplicitEM(), a), (ModelingToolkit.System, DDEProblem, MethodOfSteps(Tsit5()), _x(t - 0.1)), - (ModelingToolkit.System, SDDEProblem, ImplicitEM(), _x(t - 0.1) + a), - # polyalg cache - (NonlinearSystemWrapper, NonlinearProblemWrapper, - FastShortcutNonlinearPolyalg(), 0), - # generalized first order cache - (NonlinearSystemWrapper, NonlinearProblemWrapper, NewtonRaphson(), 0), - # quasi newton cache - (NonlinearSystemWrapper, NonlinearProblemWrapper, Klement(), 0), - # noinit cache - (NonlinearSystemWrapper, NonlinearProblemWrapper, SimpleNewtonRaphson(), 0), - # DFSane cache - (NonlinearSystemWrapper, NonlinearProblemWrapper, DFSane(), 0), - # Least squares - # polyalg cache - (NonlinearSystemWrapper, NLLSProblemWrapper, FastShortcutNLLSPolyalg(), 0), - # generalized first order cache - (NonlinearSystemWrapper, NLLSProblemWrapper, LevenbergMarquardt(), 0), - # noinit cache - (NonlinearSystemWrapper, NLLSProblemWrapper, SimpleGaussNewton(), 0) + (ModelingToolkit.System, SDDEProblem, ImplicitEM(), _x(t - 0.1) + a) ] - is_nlsolve = alg isa SciMLBase.AbstractNonlinearAlgorithm - D = is_nlsolve ? v -> v^3 : Differential(t) alge_eqs = [y^2 * q + q^2 * x ~ 0, z * p - p^2 * x * z ~ 0] @mtkbuild sys = System( [D(x) ~ x * p + y^2 * q + rhss; alge_eqs], t; guesses = [x => 0.0, y => 0.0, z => 0.0, p => 0.0, q => 0.0]) - prob = Problem(sys, [x => 1.0], (0.0, 1.0), [p => 1.0, q => missing]; - initialization_eqs = is_nlsolve ? alge_eqs : []) + prob = Problem(sys, [x => 1.0], (0.0, 1.0), [p => 1.0, q => missing]) @test is_variable(prob.f.initialization_data.initializeprob, q) ps = prob.p newps = SciMLStructures.replace(Tunable(), ps, ForwardDiff.Dual.(ps.tunable)) @@ -1010,40 +981,18 @@ end (ModelingToolkit.System, ODEProblem, Tsit5(), 0), (ModelingToolkit.System, SDEProblem, ImplicitEM(), a), (ModelingToolkit.System, DDEProblem, MethodOfSteps(Tsit5()), _x(t - 0.1)), - (ModelingToolkit.System, SDDEProblem, ImplicitEM(), _x(t - 0.1) + a), - # polyalg cache - (NonlinearSystemWrapper, NonlinearProblemWrapper, - FastShortcutNonlinearPolyalg(), 0), - # generalized first order cache - (NonlinearSystemWrapper, NonlinearProblemWrapper, NewtonRaphson(), 0), - # quasi newton cache - (NonlinearSystemWrapper, NonlinearProblemWrapper, Klement(), 0), - # noinit cache - (NonlinearSystemWrapper, NonlinearProblemWrapper, SimpleNewtonRaphson(), 0), - # DFSane cache - (NonlinearSystemWrapper, NonlinearProblemWrapper, DFSane(), 0), - # Least squares - # polyalg cache - (NonlinearSystemWrapper, NLLSProblemWrapper, FastShortcutNLLSPolyalg(), 0), - # generalized first order cache - (NonlinearSystemWrapper, NLLSProblemWrapper, LevenbergMarquardt(), 0), - # noinit cache - (NonlinearSystemWrapper, NLLSProblemWrapper, SimpleGaussNewton(), 0) + (ModelingToolkit.System, SDDEProblem, ImplicitEM(), _x(t - 0.1) + a) ] - is_nlsolve = alg isa SciMLBase.AbstractNonlinearAlgorithm - D = is_nlsolve ? v -> v^3 : Differential(t) alge_eqs = [y^2 + 4y * p^2 ~ x^3] @mtkbuild sys = System( [D(x) ~ x + p * y^2 + rhss; alge_eqs], t; guesses = [ y => 1.0, p => 1.0]) - prob = Problem(sys, [x => 1.0], (0.0, 1.0), [p => 1.0]; - initialization_eqs = is_nlsolve ? alge_eqs : []) + prob = Problem(sys, [x => 1.0], (0.0, 1.0), [p => 1.0]) @test is_variable(prob.f.initialization_data.initializeprob, y) prob2 = @test_nowarn remake(prob; p = [p => 3.0]) # ensure no over/under-determined warning @test is_variable(prob.f.initialization_data.initializeprob, y) - prob = Problem(sys, [y => 1.0, x => 2.0], (0.0, 1.0), [p => missing]; - initialization_eqs = is_nlsolve ? alge_eqs : []) + prob = Problem(sys, [y => 1.0, x => 2.0], (0.0, 1.0), [p => missing]) @test is_variable(prob.f.initialization_data.initializeprob, p) prob2 = @test_nowarn remake(prob; u0 = [y => 0.5]) @test is_variable(prob.f.initialization_data.initializeprob, p) From ccfc13b12ea766e5e99e0e12d59a97f07dce221c Mon Sep 17 00:00:00 2001 From: Christopher Rackauckas Date: Sun, 19 Jan 2025 11:25:21 +0100 Subject: [PATCH 0610/1337] Update Project.toml --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 7ccb4d2110..1f46a4771c 100644 --- a/Project.toml +++ b/Project.toml @@ -132,7 +132,7 @@ RecursiveArrayTools = "3.26" Reexport = "0.2, 1" RuntimeGeneratedFunctions = "0.5.9" SCCNonlinearSolve = "1.0.0" -SciMLBase = "2.71" +SciMLBase = "2.71.1" SciMLStructures = "1.0" Serialization = "1" Setfield = "0.7, 0.8, 1" From 40a85df0d809bbae7ac2707abf4518ba40b89c7d Mon Sep 17 00:00:00 2001 From: Christopher Rackauckas Date: Sun, 19 Jan 2025 15:31:14 +0100 Subject: [PATCH 0611/1337] Update Project.toml --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 1f46a4771c..0a6238685a 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "ModelingToolkit" uuid = "961ee093-0014-501f-94e3-6117800e7a78" authors = ["Yingbo Ma ", "Chris Rackauckas and contributors"] -version = "9.60.0" +version = "9.61.0" [deps] AbstractTrees = "1520ce14-60c1-5f80-bbc7-55ef81b5835c" From 3993abd12483b7387d3dc99d5f701a1a4c626532 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 24 Dec 2024 20:26:52 +0530 Subject: [PATCH 0612/1337] fix: fix `add_fallbacks!` --- src/systems/problem_utils.jl | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/systems/problem_utils.jl b/src/systems/problem_utils.jl index a8d443349b..bf3d72e1e5 100644 --- a/src/systems/problem_utils.jl +++ b/src/systems/problem_utils.jl @@ -88,6 +88,18 @@ function symbols_to_symbolics!(sys::AbstractSystem, varmap::AbstractDict) end end +""" + $(TYPEDSIGNATURES) + +Utility function to get the value `val` corresponding to key `var` in `varmap`, and +return `getindex(val, idx)` if it exists or `nothing` otherwise. +""" +function get_and_getindex(varmap, var, idx) + val = get(varmap, var, nothing) + val === nothing && return nothing + return val[idx] +end + """ $(TYPEDSIGNATURES) @@ -115,8 +127,9 @@ function add_fallbacks!( val = map(eachindex(var)) do idx # @something is lazy and saves from writing a massive if-elseif-else @something(get(varmap, var[idx], nothing), - get(varmap, ttvar[idx], nothing), get(fallbacks, var, nothing)[idx], - get(fallbacks, ttvar, nothing)[idx], get(fallbacks, var[idx], nothing), + get(varmap, ttvar[idx], nothing), get_and_getindex(fallbacks, var, idx), + get_and_getindex(fallbacks, ttvar, idx), get( + fallbacks, var[idx], nothing), get(fallbacks, ttvar[idx], nothing), Some(nothing)) end # only push the missing entries From d7f908eea35a2658397328afe25d52342fd23dc0 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 20 Jan 2025 11:29:04 +0530 Subject: [PATCH 0613/1337] test: test `add_fallbacks!` bug --- test/initial_values.jl | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/initial_values.jl b/test/initial_values.jl index a2931e22b6..c31eca33f8 100644 --- a/test/initial_values.jl +++ b/test/initial_values.jl @@ -174,3 +174,11 @@ end @test_throws ModelingToolkit.UnexpectedSymbolicValueInVarmap ODEProblem( sys, [x => 1, y => 2], (0.0, 1.0), [p => 2q, q => 3p]) end + +@testset "`add_fallbacks!` checks scalarized array parameters correctly" begin + @variables x(t)[1:2] + @parameters p[1:2, 1:2] + @mtkbuild sys = ODESystem(D(x) ~ p * x, t) + # used to throw a `MethodError` complaining about `getindex(::Nothing, ::CartesianIndex{2})` + @test_throws ModelingToolkit.MissingParametersError ODEProblem(sys, [x => ones(2)], (0.0, 1.0)) +end From 76e515c4c55af8b9ca3bf8a3318d1dd3339702a3 Mon Sep 17 00:00:00 2001 From: vyudu Date: Thu, 23 Jan 2025 12:33:15 -0500 Subject: [PATCH 0614/1337] Init --- .../discrete_system/implicitdiscretesystem.jl | 406 ++++++++++++++++++ 1 file changed, 406 insertions(+) create mode 100644 src/systems/discrete_system/implicitdiscretesystem.jl diff --git a/src/systems/discrete_system/implicitdiscretesystem.jl b/src/systems/discrete_system/implicitdiscretesystem.jl new file mode 100644 index 0000000000..404fa7ff49 --- /dev/null +++ b/src/systems/discrete_system/implicitdiscretesystem.jl @@ -0,0 +1,406 @@ +""" +$(TYPEDEF) +An implicit system of difference equations. +# Fields +$(FIELDS) +# Example +``` +using ModelingToolkit +using ModelingToolkit: t_nounits as t +@parameters σ=28.0 ρ=10.0 β=8/3 δt=0.1 +@variables x(t)=1.0 y(t)=0.0 z(t)=0.0 +k = ShiftIndex(t) +eqs = [x(k+1) ~ σ*(y-x), + y(k+1) ~ x*(ρ-z)-y, + z(k+1) ~ x*y - β*z] +@named de = ImplicitDiscreteSystem(eqs,t,[x,y,z],[σ,ρ,β]; tspan = (0, 1000.0)) # or +@named de = ImplicitDiscreteSystem(eqs) +``` +""" +struct ImplicitDiscreteSystem <: AbstractTimeDependentSystem + """ + A tag for the system. If two systems have the same tag, then they are + structurally identical. + """ + tag::UInt + """The differential equations defining the discrete system.""" + eqs::Vector{Equation} + """Independent variable.""" + iv::BasicSymbolic{Real} + """Dependent (state) variables. Must not contain the independent variable.""" + unknowns::Vector + """Parameter variables. Must not contain the independent variable.""" + ps::Vector + """Time span.""" + tspan::Union{NTuple{2, Any}, Nothing} + """Array variables.""" + var_to_name::Any + """Observed states.""" + observed::Vector{Equation} + """ + The name of the system + """ + name::Symbol + """ + A description of the system. + """ + description::String + """ + The internal systems. These are required to have unique names. + """ + systems::Vector{DiscreteSystem} + """ + The default values to use when initial conditions and/or + parameters are not supplied in `DiscreteProblem`. + """ + defaults::Dict + """ + The guesses to use as the initial conditions for the + initialization system. + """ + guesses::Dict + """ + The system for performing the initialization. + """ + initializesystem::Union{Nothing, NonlinearSystem} + """ + Extra equations to be enforced during the initialization sequence. + """ + initialization_eqs::Vector{Equation} + """ + Inject assignment statements before the evaluation of the RHS function. + """ + preface::Any + """ + Type of the system. + """ + connector_type::Any + """ + Topologically sorted parameter dependency equations, where all symbols are parameters and + the LHS is a single parameter. + """ + parameter_dependencies::Vector{Equation} + """ + Metadata for the system, to be used by downstream packages. + """ + metadata::Any + """ + Metadata for MTK GUI. + """ + gui_metadata::Union{Nothing, GUIMetadata} + """ + Cache for intermediate tearing state. + """ + tearing_state::Any + """ + Substitutions generated by tearing. + """ + substitutions::Any + """ + If a model `sys` is complete, then `sys.x` no longer performs namespacing. + """ + complete::Bool + """ + Cached data for fast symbolic indexing. + """ + index_cache::Union{Nothing, IndexCache} + """ + The hierarchical parent system before simplification. + """ + parent::Any + isscheduled::Bool + + function ImplicitDiscreteSystem(tag, discreteEqs, iv, dvs, ps, tspan, var_to_name, + observed, name, description, systems, defaults, guesses, initializesystem, + initialization_eqs, preface, connector_type, parameter_dependencies = Equation[], + metadata = nothing, gui_metadata = nothing, + tearing_state = nothing, substitutions = nothing, + complete = false, index_cache = nothing, parent = nothing, + isscheduled = false; + checks::Union{Bool, Int} = true) + if checks == true || (checks & CheckComponents) > 0 + check_independent_variables([iv]) + check_variables(dvs, iv) + check_parameters(ps, iv) + end + if checks == true || (checks & CheckUnits) > 0 + u = __get_unit_type(dvs, ps, iv) + check_units(u, discreteEqs) + end + new(tag, discreteEqs, iv, dvs, ps, tspan, var_to_name, observed, name, description, + systems, defaults, guesses, initializesystem, initialization_eqs, + preface, connector_type, parameter_dependencies, metadata, gui_metadata, + tearing_state, substitutions, complete, index_cache, parent, isscheduled) + end +end + +""" + $(TYPEDSIGNATURES) +Constructs a DiscreteSystem. +""" +function ImplicitDiscreteSystem(eqs::AbstractVector{<:Equation}, iv, dvs, ps; + observed = Num[], + systems = DiscreteSystem[], + tspan = nothing, + name = nothing, + description = "", + default_u0 = Dict(), + default_p = Dict(), + guesses = Dict(), + initializesystem = nothing, + initialization_eqs = Equation[], + defaults = _merge(Dict(default_u0), Dict(default_p)), + preface = nothing, + connector_type = nothing, + parameter_dependencies = Equation[], + metadata = nothing, + gui_metadata = nothing, + kwargs...) + name === nothing && + throw(ArgumentError("The `name` keyword must be provided. Please consider using the `@named` macro")) + iv′ = value(iv) + dvs′ = value.(dvs) + ps′ = value.(ps) + if any(hasderiv, eqs) || any(hashold, eqs) || any(hassample, eqs) || any(hasdiff, eqs) + error("Equations in a `DiscreteSystem` can only have `Shift` operators.") + end + if !(isempty(default_u0) && isempty(default_p)) + Base.depwarn( + "`default_u0` and `default_p` are deprecated. Use `defaults` instead.", + :DiscreteSystem, force = true) + end + + defaults = Dict{Any, Any}(todict(defaults)) + guesses = Dict{Any, Any}(todict(guesses)) + var_to_name = Dict() + process_variables!(var_to_name, defaults, guesses, dvs′) + process_variables!(var_to_name, defaults, guesses, ps′) + process_variables!( + var_to_name, defaults, guesses, [eq.lhs for eq in parameter_dependencies]) + process_variables!( + var_to_name, defaults, guesses, [eq.rhs for eq in parameter_dependencies]) + defaults = Dict{Any, Any}(value(k) => value(v) + for (k, v) in pairs(defaults) if v !== nothing) + guesses = Dict{Any, Any}(value(k) => value(v) + for (k, v) in pairs(guesses) if v !== nothing) + + isempty(observed) || collect_var_to_name!(var_to_name, (eq.lhs for eq in observed)) + + sysnames = nameof.(systems) + if length(unique(sysnames)) != length(sysnames) + throw(ArgumentError("System names must be unique.")) + end + DiscreteSystem(Threads.atomic_add!(SYSTEM_COUNT, UInt(1)), + eqs, iv′, dvs′, ps′, tspan, var_to_name, observed, name, description, systems, + defaults, guesses, initializesystem, initialization_eqs, preface, connector_type, + parameter_dependencies, metadata, gui_metadata, kwargs...) +end + +function ImplicitDiscreteSystem(eqs, iv; kwargs...) + eqs = collect(eqs) + diffvars = OrderedSet() + allunknowns = OrderedSet() + ps = OrderedSet() + iv = value(iv) + for eq in eqs + collect_vars!(allunknowns, ps, eq, iv; op = Shift) + if iscall(eq.lhs) && operation(eq.lhs) isa Shift + isequal(iv, operation(eq.lhs).t) || + throw(ArgumentError("A DiscreteSystem can only have one independent variable.")) + eq.lhs in diffvars && + throw(ArgumentError("The shift variable $(eq.lhs) is not unique in the system of equations.")) + push!(diffvars, eq.lhs) + end + end + for eq in get(kwargs, :parameter_dependencies, Equation[]) + if eq isa Pair + collect_vars!(allunknowns, ps, eq, iv) + else + collect_vars!(allunknowns, ps, eq, iv) + end + end + new_ps = OrderedSet() + for p in ps + if iscall(p) && operation(p) === getindex + par = arguments(p)[begin] + if Symbolics.shape(Symbolics.unwrap(par)) !== Symbolics.Unknown() && + all(par[i] in ps for i in eachindex(par)) + push!(new_ps, par) + else + push!(new_ps, p) + end + else + push!(new_ps, p) + end + end + return DiscreteSystem(eqs, iv, + collect(allunknowns), collect(new_ps); kwargs...) +end + +function flatten(sys::DiscreteSystem, noeqs = false) + systems = get_systems(sys) + if isempty(systems) + return sys + else + return DiscreteSystem(noeqs ? Equation[] : equations(sys), + get_iv(sys), + unknowns(sys), + parameters(sys), + observed = observed(sys), + defaults = defaults(sys), + guesses = guesses(sys), + initialization_eqs = initialization_equations(sys), + name = nameof(sys), + description = description(sys), + metadata = get_metadata(sys), + checks = false) + end +end + +function generate_function( + sys::DiscreteSystem, dvs = unknowns(sys), ps = parameters(sys); wrap_code = identity, kwargs...) + exprs = [eq.rhs for eq in equations(sys)] + wrap_code = wrap_code .∘ wrap_array_vars(sys, exprs) .∘ + wrap_parameter_dependencies(sys, false) + generate_custom_function(sys, exprs, dvs, ps; wrap_code, kwargs...) +end + +function shift_u0map_forward(sys::DiscreteSystem, u0map, defs) + iv = get_iv(sys) + updated = AnyDict() + for k in collect(keys(u0map)) + v = u0map[k] + if !((op = operation(k)) isa Shift) + 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)).") + end + updated[Shift(iv, op.steps + 1)(arguments(k)[1])] = v + end + for var in unknowns(sys) + op = operation(var) + op isa Shift || continue + haskey(updated, var) && continue + root = first(arguments(var)) + haskey(defs, root) || error("Initial condition for $var not provided.") + updated[var] = defs[root] + end + return updated +end + +""" + $(TYPEDSIGNATURES) +Generates an ImplicitDiscreteProblem from an ImplicitDiscreteSystem. +""" +function SciMLBase.ImplicitDiscreteProblem( + sys::DiscreteSystem, u0map = [], tspan = get_tspan(sys), + parammap = SciMLBase.NullParameters(); + eval_module = @__MODULE__, + eval_expression = false, + use_union = false, + kwargs... +) + if !iscomplete(sys) + error("A completed `ImplicitDiscreteSystem` is required. Call `complete` or `structural_simplify` on the system before creating a `ImplicitDiscreteProblem`") + end + dvs = unknowns(sys) + ps = parameters(sys) + eqs = equations(sys) + iv = get_iv(sys) + + u0map = to_varmap(u0map, dvs) + u0map = shift_u0map_forward(sys, u0map, defaults(sys)) + f, u0, p = process_SciMLProblem( + DiscreteFunction, sys, u0map, parammap; eval_expression, eval_module) + u0 = f(u0, p, tspan[1]) + ImplicitDiscreteProblem(f, u0, tspan, p; kwargs...) +end + +function SciMLBase.ImplicitDiscreteFunction(sys::ImplicitDiscreteSystem, args...; kwargs...) + ImplicitDiscreteFunction{true}(sys, args...; kwargs...) +end + +function SciMLBase.ImplicitDiscreteFunction{true}(sys::ImplicitDiscreteSystem, args...; kwargs...) + ImplicitDiscreteFunction{true, SciMLBase.AutoSpecialize}(sys, args...; kwargs...) +end + +function SciMLBase.ImplicitDiscreteFunction{false}(sys::ImplicitDiscreteSystem, args...; kwargs...) + ImplicitDiscreteFunction{false, SciMLBase.FullSpecialize}(sys, args...; kwargs...) +end +function SciMLBase.ImplicitDiscreteFunction{iip, specialize}( + sys::ImplicitDiscreteSystem, + dvs = unknowns(sys), + ps = parameters(sys), + u0 = nothing; + version = nothing, + p = nothing, + t = nothing, + eval_expression = false, + eval_module = @__MODULE__, + analytic = nothing, + kwargs...) where {iip, specialize} + if !iscomplete(sys) + error("A completed `ImplicitDiscreteSystem` is required. Call `complete` or `structural_simplify` on the system before creating a `ImplicitDiscreteProblem`") + end + f_gen = generate_function(sys, dvs, ps; expression = Val{true}, + expression_module = eval_module, kwargs...) + f_oop, f_iip = eval_or_rgf.(f_gen; eval_expression, eval_module) + f(u, p, t) = f_oop(u, p, t) + f(du, u, p, t) = f_iip(du, u, p, t) + + if specialize === SciMLBase.FunctionWrapperSpecialize && iip + if u0 === nothing || p === nothing || t === nothing + error("u0, p, and t must be specified for FunctionWrapperSpecialize on DiscreteFunction.") + end + f = SciMLBase.wrapfun_iip(f, (u0, u0, p, t)) + end + + observedfun = ObservedFunctionCache( + sys; eval_expression, eval_module, checkbounds = get(kwargs, :checkbounds, false)) + + ImplicitDiscreteFunction{iip, specialize}(f; + sys = sys, + observed = observedfun, + analytic = analytic) +end + +""" +```julia +ImplicitDiscreteFunctionExpr{iip}(sys::ImplicitDiscreteSystem, dvs = states(sys), + ps = parameters(sys); + version = nothing, + kwargs...) where {iip} +``` + +Create a Julia expression for an `ImplicitDiscreteFunction` from the [`ImplicitDiscreteSystem`](@ref). +The arguments `dvs` and `ps` are used to set the order of the dependent +variable and parameter vectors, respectively. +""" +struct ImplicitDiscreteFunctionExpr{iip} end +struct ImplicitDiscreteFunctionClosure{O, I} <: Function + f_oop::O + f_iip::I +end +(f::ImplicitDiscreteFunctionClosure)(u, p, t) = f.f_oop(u, p, t) +(f::ImplicitDiscreteFunctionClosure)(du, u, p, t) = f.f_iip(du, u, p, t) + +function ImplicitDiscreteFunctionExpr{iip}(sys::ImplicitDiscreteSystem, dvs = unknowns(sys), + ps = parameters(sys), u0 = nothing; + version = nothing, p = nothing, + linenumbers = false, + simplify = false, + kwargs...) where {iip} + f_oop, f_iip = generate_function(sys, dvs, ps; expression = Val{true}, kwargs...) + + fsym = gensym(:f) + _f = :($fsym = $ImplicitDiscreteFunctionClosure($f_oop, $f_iip)) + + ex = quote + $_f + DiscreteFunction{$iip}($fsym) + end + !linenumbers ? Base.remove_linenums!(ex) : ex +end + +function ImplicitDiscreteFunctionExpr(sys::ImplicitDiscreteSystem, args...; kwargs...) + ImplicitDiscreteFunctionExpr{true}(sys, args...; kwargs...) +end + From 45c2588e6e65025283b97dd473e0052c7ed6bb66 Mon Sep 17 00:00:00 2001 From: vyudu Date: Thu, 23 Jan 2025 13:40:11 -0500 Subject: [PATCH 0615/1337] fix pdesystem typecheck --- src/systems/pde/pdesystem.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/systems/pde/pdesystem.jl b/src/systems/pde/pdesystem.jl index e067f0e149..e2a5bb5734 100644 --- a/src/systems/pde/pdesystem.jl +++ b/src/systems/pde/pdesystem.jl @@ -103,7 +103,7 @@ struct PDESystem <: ModelingToolkit.AbstractMultivariateSystem description = "", name) if checks == true || (checks & CheckComponents) > 0 - check_var_types(PDESystem, dvs) + check_var_types(PDESystem, [dv(ivs...) for dv in dvs]) end if checks == true || (checks & CheckUnits) > 0 u = __get_unit_type(dvs, ivs, ps) From 9530a2e6bce78f9fdd49ed1841b9ed6120e04ad2 Mon Sep 17 00:00:00 2001 From: vyudu Date: Thu, 23 Jan 2025 16:31:25 -0500 Subject: [PATCH 0616/1337] init --- src/systems/parameter_buffer.jl | 6 +++-- src/systems/problem_utils.jl | 42 +++++++++++++++++++++++++++++++-- test/problem_validation.jl | 24 +++++++++++++++++++ 3 files changed, 68 insertions(+), 4 deletions(-) create mode 100644 test/problem_validation.jl diff --git a/src/systems/parameter_buffer.jl b/src/systems/parameter_buffer.jl index bc4e62a773..7a88489b48 100644 --- a/src/systems/parameter_buffer.jl +++ b/src/systems/parameter_buffer.jl @@ -33,17 +33,19 @@ function MTKParameters( else error("Cannot create MTKParameters if system does not have index_cache") end + all_ps = Set(unwrap.(parameters(sys))) union!(all_ps, default_toterm.(unwrap.(parameters(sys)))) if p isa Vector && !(eltype(p) <: Pair) && !isempty(p) ps = parameters(sys) - length(p) == length(ps) || error("Invalid parameters") + length(p) == length(ps) || error("The number of parameter values is not equal to the number of parameters.") p = ps .=> p end if p isa SciMLBase.NullParameters || isempty(p) p = Dict() end p = todict(p) + defs = Dict(default_toterm(unwrap(k)) => v for (k, v) in defaults(sys)) if eltype(u0) <: Pair u0 = todict(u0) @@ -761,7 +763,7 @@ end function Base.showerror(io::IO, e::MissingParametersError) println(io, MISSING_PARAMETERS_MESSAGE) - println(io, e.vars) + println(io, join(e.vars, ", ")) end function InvalidParameterSizeException(param, val) diff --git a/src/systems/problem_utils.jl b/src/systems/problem_utils.jl index bf3d72e1e5..e2af471a15 100644 --- a/src/systems/problem_utils.jl +++ b/src/systems/problem_utils.jl @@ -684,12 +684,17 @@ function process_SciMLProblem( u0Type = typeof(u0map) pType = typeof(pmap) - _u0map = u0map + u0map = to_varmap(u0map, dvs) symbols_to_symbolics!(sys, u0map) - _pmap = pmap + check_keys(sys, u0map) + pmap = to_varmap(pmap, ps) symbols_to_symbolics!(sys, pmap) + check_keys(sys, pmap) + badkeys = filter(k -> symbolic_type(k) === NotSymbolic(), keys(pmap)) + isempty(badkeys) || throw(BadKeyError(collect(badkeys))) + defs = add_toterms(recursive_unwrap(defaults(sys))) cmap, cs = get_cmap(sys) kwargs = NamedTuple(kwargs) @@ -778,6 +783,39 @@ function process_SciMLProblem( implicit_dae ? (f, du0, u0, p) : (f, u0, p) end +# Check that the keys of a u0map or pmap are valid +# (i.e. are symbolic keys, and are defined for the system.) +function check_keys(sys, map) + badkeys = Any[] + for k in keys(map) + if symbolic_type(k) === NotSymbolic() + push!(badkeys, k) + elseif k isa Symbol + !hasproperty(sys, k) && push!(badkeys, k) + elseif k ∉ Set(parameters(sys)) && k ∉ Set(unknowns(sys)) + push!(badkeys, k) + end + end + + isempty(badkeys) || throw(BadKeyError(collect(badkeys))) +end + +const BAD_KEY_MESSAGE = """ + Undefined keys found in the parameter or initial condition maps. + The following keys are either invalid or not parameters/states of the system: + """ + +struct BadKeyError <: Exception + vars::Any +end + +function Base.showerror(io::IO, e::BadKeyError) + println(io, BAD_KEY_MESSAGE) + println(io, join(e.vars, ", ")) +end + + + ############## # Legacy functions for backward compatibility ############## diff --git a/test/problem_validation.jl b/test/problem_validation.jl new file mode 100644 index 0000000000..fb724c55bd --- /dev/null +++ b/test/problem_validation.jl @@ -0,0 +1,24 @@ +using ModelingToolkit +using ModelingToolkit: t_nounits as t, D_nounits as D + +@testset "Input map validation" begin + @variables X(t) + @parameters p d + eqs = [D(X) ~ p - d*X] + @mtkbuild osys = ODESystem(eqs, t) + + p = "I accidentally renamed p" + u0 = [X => 1.0] + ps = [p => 1.0, d => 0.5] + @test_throws ModelingToolkit.BadKeyError oprob = ODEProblem(osys, u0, (0.0, 1.0), ps) + + ps = [p => 1.0, d => 0.5, "Random stuff" => 3.0] + @test_throws ModelingToolkit.BadKeyError oprob = ODEProblem(osys, u0, (0.0, 1.0), ps) + + u0 = [:X => 1.0, "random" => 3.0] + @test_throws ModelingToolkit.BadKeyError oprob = ODEProblem(osys, u0, (0.0, 1.0), ps) + + @parameters k + ps = [p => 1., d => 0.5, k => 3.] + @test_throws ModelingToolkit.BadKeyError oprob = ODEProblem(osys, u0, (0.0, 1.0), ps) +end From a9dc112905ced2e1b1b16e1af0e179b0604f2563 Mon Sep 17 00:00:00 2001 From: vyudu Date: Thu, 23 Jan 2025 16:34:38 -0500 Subject: [PATCH 0617/1337] up --- test/problem_validation.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/test/problem_validation.jl b/test/problem_validation.jl index fb724c55bd..f871327ae8 100644 --- a/test/problem_validation.jl +++ b/test/problem_validation.jl @@ -12,6 +12,7 @@ using ModelingToolkit: t_nounits as t, D_nounits as D ps = [p => 1.0, d => 0.5] @test_throws ModelingToolkit.BadKeyError oprob = ODEProblem(osys, u0, (0.0, 1.0), ps) + @parameters p d ps = [p => 1.0, d => 0.5, "Random stuff" => 3.0] @test_throws ModelingToolkit.BadKeyError oprob = ODEProblem(osys, u0, (0.0, 1.0), ps) From 3c629ac227950886235d85307f3a82c7b3183ac7 Mon Sep 17 00:00:00 2001 From: vyudu Date: Thu, 23 Jan 2025 16:36:30 -0500 Subject: [PATCH 0618/1337] up --- src/systems/problem_utils.jl | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/systems/problem_utils.jl b/src/systems/problem_utils.jl index e2af471a15..8c6082e436 100644 --- a/src/systems/problem_utils.jl +++ b/src/systems/problem_utils.jl @@ -692,8 +692,6 @@ function process_SciMLProblem( pmap = to_varmap(pmap, ps) symbols_to_symbolics!(sys, pmap) check_keys(sys, pmap) - badkeys = filter(k -> symbolic_type(k) === NotSymbolic(), keys(pmap)) - isempty(badkeys) || throw(BadKeyError(collect(badkeys))) defs = add_toterms(recursive_unwrap(defaults(sys))) cmap, cs = get_cmap(sys) From 417b386a24a6c8c1ed8c49ee6a5c2581a562afae Mon Sep 17 00:00:00 2001 From: vyudu Date: Fri, 24 Jan 2025 08:48:29 -0500 Subject: [PATCH 0619/1337] just check not-symbolic --- src/systems/problem_utils.jl | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/systems/problem_utils.jl b/src/systems/problem_utils.jl index 8c6082e436..8541056272 100644 --- a/src/systems/problem_utils.jl +++ b/src/systems/problem_utils.jl @@ -788,10 +788,6 @@ function check_keys(sys, map) for k in keys(map) if symbolic_type(k) === NotSymbolic() push!(badkeys, k) - elseif k isa Symbol - !hasproperty(sys, k) && push!(badkeys, k) - elseif k ∉ Set(parameters(sys)) && k ∉ Set(unknowns(sys)) - push!(badkeys, k) end end From f1d2a0754ae33524a063d028b6d2515a9cabc06c Mon Sep 17 00:00:00 2001 From: vyudu Date: Fri, 24 Jan 2025 11:20:11 -0500 Subject: [PATCH 0620/1337] rename --- .../{implicitdiscretesystem.jl => implicit_discrete_system.jl} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/systems/discrete_system/{implicitdiscretesystem.jl => implicit_discrete_system.jl} (100%) diff --git a/src/systems/discrete_system/implicitdiscretesystem.jl b/src/systems/discrete_system/implicit_discrete_system.jl similarity index 100% rename from src/systems/discrete_system/implicitdiscretesystem.jl rename to src/systems/discrete_system/implicit_discrete_system.jl From 494df8f49bfabb5d495d50cd0d7f5b4ed39d634d Mon Sep 17 00:00:00 2001 From: Martijn Visser Date: Fri, 24 Jan 2025 21:18:01 +0100 Subject: [PATCH 0621/1337] Start using Moshi compatible syntax for singleton ADT variants In preparation for migrating from Expronicon to Moshi. --- src/clock.jl | 6 +++--- src/discretedomain.jl | 4 ++-- src/systems/clock_inference.jl | 6 +++--- src/systems/systemstructure.jl | 2 +- test/clock.jl | 30 +++++++++++++++--------------- 5 files changed, 24 insertions(+), 24 deletions(-) diff --git a/src/clock.jl b/src/clock.jl index 86b7296a6d..2cd00a24bc 100644 --- a/src/clock.jl +++ b/src/clock.jl @@ -29,7 +29,7 @@ true if `x` contains only continuous-domain signals. See also [`has_continuous_domain`](@ref) """ function is_continuous_domain(x) - issym(x) && return getmetadata(x, VariableTimeDomain, false) == Continuous + issym(x) && return getmetadata(x, VariableTimeDomain, false) == Continuous() !has_discrete_domain(x) && has_continuous_domain(x) end @@ -58,8 +58,8 @@ has_time_domain(x::Num) = has_time_domain(value(x)) has_time_domain(x) = false for op in [Differential] - @eval input_timedomain(::$op, arg = nothing) = Continuous - @eval output_timedomain(::$op, arg = nothing) = Continuous + @eval input_timedomain(::$op, arg = nothing) = Continuous() + @eval output_timedomain(::$op, arg = nothing) = Continuous() end """ diff --git a/src/discretedomain.jl b/src/discretedomain.jl index 95abe02a7a..c7f90007c5 100644 --- a/src/discretedomain.jl +++ b/src/discretedomain.jl @@ -247,7 +247,7 @@ function output_timedomain(s::Shift, arg = nothing) InferredDiscrete end -input_timedomain(::Sample, _ = nothing) = Continuous +input_timedomain(::Sample, _ = nothing) = Continuous() output_timedomain(s::Sample, _ = nothing) = s.clock function input_timedomain(h::Hold, arg = nothing) @@ -256,7 +256,7 @@ function input_timedomain(h::Hold, arg = nothing) end InferredDiscrete # the Hold accepts any discrete end -output_timedomain(::Hold, _ = nothing) = Continuous +output_timedomain(::Hold, _ = nothing) = Continuous() sampletime(op::Sample, _ = nothing) = sampletime(op.clock) sampletime(op::ShiftIndex, _ = nothing) = sampletime(op.clock) diff --git a/src/systems/clock_inference.jl b/src/systems/clock_inference.jl index a92b2aa67c..611f8e2fae 100644 --- a/src/systems/clock_inference.jl +++ b/src/systems/clock_inference.jl @@ -8,8 +8,8 @@ end function ClockInference(ts::TransformationState) @unpack structure = ts @unpack graph = structure - eq_domain = TimeDomain[Continuous for _ in 1:nsrcs(graph)] - var_domain = TimeDomain[Continuous for _ in 1:ndsts(graph)] + eq_domain = TimeDomain[Continuous() for _ in 1:nsrcs(graph)] + var_domain = TimeDomain[Continuous() for _ in 1:ndsts(graph)] inferred = BitSet() for (i, v) in enumerate(get_fullvars(ts)) d = get_time_domain(ts, v) @@ -151,7 +151,7 @@ function split_system(ci::ClockInference{S}) where {S} get!(clock_to_id, d) do cid = (cid_counter[] += 1) push!(id_to_clock, d) - if d == Continuous + if d == Continuous() continuous_id[] = cid end cid diff --git a/src/systems/systemstructure.jl b/src/systems/systemstructure.jl index 1c0ad8b8ae..1bdc11f06a 100644 --- a/src/systems/systemstructure.jl +++ b/src/systems/systemstructure.jl @@ -662,7 +662,7 @@ function structural_simplify!(state::TearingState, io = nothing; simplify = fals Dict(v => 0.0 for v in Iterators.flatten(inputs))) end ps = [sym isa CallWithMetadata ? sym : - setmetadata(sym, VariableTimeDomain, get(time_domains, sym, Continuous)) + setmetadata(sym, VariableTimeDomain, get(time_domains, sym, Continuous())) for sym in get_ps(sys)] @set! sys.ps = ps else diff --git a/test/clock.jl b/test/clock.jl index 91aaa8248e..67f469e1a6 100644 --- a/test/clock.jl +++ b/test/clock.jl @@ -77,19 +77,19 @@ k = ShiftIndex(d) d = Clock(dt) # Note that TearingState reorders the equations -@test eqmap[1] == Continuous +@test eqmap[1] == Continuous() @test eqmap[2] == d @test eqmap[3] == d @test eqmap[4] == d -@test eqmap[5] == Continuous -@test eqmap[6] == Continuous +@test eqmap[5] == Continuous() +@test eqmap[6] == Continuous() @test varmap[yd] == d @test varmap[ud] == d @test varmap[r] == d -@test varmap[x] == Continuous -@test varmap[y] == Continuous -@test varmap[u] == Continuous +@test varmap[x] == Continuous() +@test varmap[y] == Continuous() +@test varmap[u] == Continuous() @info "Testing shift normalization" dt = 0.1 @@ -192,10 +192,10 @@ eqs = [yd ~ Sample(dt)(y) @test varmap[ud1] == d @test varmap[yd2] == d2 @test varmap[ud2] == d2 - @test varmap[r] == Continuous - @test varmap[x] == Continuous - @test varmap[y] == Continuous - @test varmap[u] == Continuous + @test varmap[r] == Continuous() + @test varmap[x] == Continuous() + @test varmap[y] == Continuous() + @test varmap[u] == Continuous() @info "test composed systems" @@ -241,14 +241,14 @@ eqs = [yd ~ Sample(dt)(y) ci, varmap = infer_clocks(cl) @test varmap[f.x] == Clock(0.5) - @test varmap[p.x] == Continuous - @test varmap[p.y] == Continuous + @test varmap[p.x] == Continuous() + @test varmap[p.y] == Continuous() @test varmap[c.ud] == Clock(0.5) @test varmap[c.yd] == Clock(0.5) - @test varmap[c.y] == Continuous + @test varmap[c.y] == Continuous() @test varmap[f.y] == Clock(0.5) @test varmap[f.u] == Clock(0.5) - @test varmap[p.u] == Continuous + @test varmap[p.u] == Continuous() @test varmap[c.r] == Clock(0.5) ## Multiple clock rates @@ -474,7 +474,7 @@ eqs = [yd ~ Sample(dt)(y) ## Test continuous clock - c = ModelingToolkit.SolverStepClock + c = ModelingToolkit.SolverStepClock() k = ShiftIndex(c) @mtkmodel CounterSys begin From 2bd65e2702544b1ed561449665115247b65de121 Mon Sep 17 00:00:00 2001 From: vyudu Date: Fri, 24 Jan 2025 16:06:16 -0500 Subject: [PATCH 0622/1337] up --- src/systems/systems.jl | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/systems/systems.jl b/src/systems/systems.jl index 04c50bc766..ffbee75b33 100644 --- a/src/systems/systems.jl +++ b/src/systems/systems.jl @@ -39,13 +39,13 @@ function structural_simplify( else newsys = newsys′ end - if newsys isa DiscreteSystem && - any(eq -> symbolic_type(eq.lhs) == NotSymbolic(), equations(newsys)) - error(""" - Encountered algebraic equations when simplifying discrete system. This is \ - not yet supported. - """) - end + # if newsys isa DiscreteSystem && + # any(eq -> symbolic_type(eq.lhs) == NotSymbolic(), equations(newsys)) + # error(""" + # Encountered algebraic equations when simplifying discrete system. This is \ + # not yet supported. + # """) + # end for pass in additional_passes newsys = pass(newsys) end From 80d9bafca24006a2bb42ecd849b2b8e465f28ced Mon Sep 17 00:00:00 2001 From: Martijn Visser Date: Sat, 25 Jan 2025 20:51:57 +0100 Subject: [PATCH 0623/1337] Require latest SciMLBase --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 0a6238685a..164012d677 100644 --- a/Project.toml +++ b/Project.toml @@ -132,7 +132,7 @@ RecursiveArrayTools = "3.26" Reexport = "0.2, 1" RuntimeGeneratedFunctions = "0.5.9" SCCNonlinearSolve = "1.0.0" -SciMLBase = "2.71.1" +SciMLBase = "2.72.1" SciMLStructures = "1.0" Serialization = "1" Setfield = "0.7, 0.8, 1" From d2e31ac8553b91626b72e52dbdcf2ff083083bf3 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 20 Dec 2024 16:06:30 +0530 Subject: [PATCH 0624/1337] feat: add `FMUComponent` with support for v2 ME FMUs --- Project.toml | 3 + ext/MTKFMIExt.jl | 216 +++++++++++++++++++++++++++++++++++++++++ src/ModelingToolkit.jl | 2 + 3 files changed, 221 insertions(+) create mode 100644 ext/MTKFMIExt.jl diff --git a/Project.toml b/Project.toml index 0a6238685a..b25d2a0562 100644 --- a/Project.toml +++ b/Project.toml @@ -64,6 +64,7 @@ Unitful = "1986cc42-f94f-5a68-af5c-568840ba703d" BifurcationKit = "0f109fa4-8a5d-4b75-95aa-f515264e7665" ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4" DeepDiffs = "ab62b9b5-e342-54a8-a765-a90f495de1a6" +FMI = "14a09403-18e3-468f-ad8a-74f8dda2d9ac" HomotopyContinuation = "f213a82b-91d6-5c5d-acf7-10f1c761b327" InfiniteOpt = "20393b10-9daf-11e9-18c9-8db751c92c57" LabelledArrays = "2ee39098-c373-598a-b85f-a56591580800" @@ -72,6 +73,7 @@ LabelledArrays = "2ee39098-c373-598a-b85f-a56591580800" MTKBifurcationKitExt = "BifurcationKit" MTKChainRulesCoreExt = "ChainRulesCore" MTKDeepDiffsExt = "DeepDiffs" +MTKFMIExt = "FMI" MTKHomotopyContinuationExt = "HomotopyContinuation" MTKInfiniteOptExt = "InfiniteOpt" MTKLabelledArraysExt = "LabelledArrays" @@ -106,6 +108,7 @@ FindFirstFunctions = "1" ForwardDiff = "0.10.3" FunctionWrappers = "1.1" FunctionWrappersWrappers = "0.1" +FMI = "0.14" Graphs = "1.5.2" HomotopyContinuation = "2.11" InfiniteOpt = "0.5" diff --git a/ext/MTKFMIExt.jl b/ext/MTKFMIExt.jl new file mode 100644 index 0000000000..0812506df7 --- /dev/null +++ b/ext/MTKFMIExt.jl @@ -0,0 +1,216 @@ +module MTKFMIExt + +using ModelingToolkit +using ModelingToolkit: t_nounits as t, D_nounits as D +import ModelingToolkit as MTK +import FMI + +macro statuscheck(expr) + @assert Meta.isexpr(expr, :call) + fn = expr.args[1] + @assert Meta.isexpr(fn, :.) + @assert fn.args[1] == :FMI + fnname = fn.args[2] + + instance = expr.args[2] + + return quote + status = $expr + fnname = $fnname + if (status isa Tuple && status[1] == FMI.fmi2True) || + (!(status isa Tuple) && status != FMI.fmi2StatusOK && + status != FMI.fmi2StatusWarning) + if status != FMI.fmi2StatusFatal + FMI.fmi2Terminate(wrapper.instance) + end + FMI.fmi2FreeInstance!(wrapper.instance) + wrapper.instance = nothing + error("FMU Error: status $status") + end + end |> esc +end + +function MTK.FMIComponent(::Val{2}, ::Val{:ME}; fmu = nothing, tolerance = 1e-6, name) + value_references = Dict() + defs = Dict() + states = [] + diffvars = [] + observed = Equation[] + stateT = Float64 + for (valRef, snames) in FMI.getStateValueReferencesAndNames(fmu) + stateT = FMI.dataTypeForValueReference(fmu, valRef) + snames = map(parseFMIVariableName, snames) + vars = [MTK.unwrap(only(@variables $sname(t)::stateT)) for sname in snames] + for i in eachindex(vars) + if i == 1 + push!(diffvars, vars[i]) + else + push!(observed, vars[i] ~ vars[1]) + end + value_references[vars[i]] = valRef + end + append!(states, vars) + end + + inputs = [] + for (valRef, snames) in FMI.getInputValueReferencesAndNames(fmu) + snames = map(parseFMIVariableName, snames) + vars = [MTK.unwrap(only(@variables $sname(t)::stateT)) for sname in snames] + for i in eachindex(vars) + if i == 1 + push!(inputs, vars[i]) + else + push!(observed, vars[i] ~ vars[1]) + end + value_references[vars[i]] = valRef + end + append!(states, vars) + end + + outputs = [] + for (valRef, snames) in FMI.getOutputValueReferencesAndNames(fmu) + snames = map(parseFMIVariableName, snames) + vars = [MTK.unwrap(only(@variables $sname(t)::stateT)) for sname in snames] + for i in eachindex(vars) + if i == 1 + push!(outputs, vars[i]) + else + push!(observed, vars[i] ~ vars[1]) + end + value_references[vars[i]] = valRef + end + append!(states, vars) + end + + params = [] + parameter_dependencies = Equation[] + for (valRef, pnames) in FMI.getParameterValueReferencesAndNames(fmu) + defval = FMI.getStartValue(fmu, valRef) + T = FMI.dataTypeForValueReference(fmu, valRef) + pnames = map(parseFMIVariableName, pnames) + vars = [MTK.unwrap(only(@parameters $pname::T)) for pname in pnames] + for i in eachindex(vars) + if i == 1 + push!(params, vars[i]) + else + push!(parameter_dependencies, vars[i] ~ vars[1]) + end + value_references[vars[i]] = valRef + end + defs[vars[1]] = defval + end + + input_value_references = UInt32[value_references[var] for var in inputs] + param_value_references = UInt32[value_references[var] for var in params] + @parameters wrapper::FMI2InstanceWrapper = FMI2InstanceWrapper( + fmu, param_value_references, input_value_references, tolerance) + + output_value_references = UInt32[value_references[var] for var in outputs] + buffer_length = length(diffvars) + length(outputs) + _functor = FMI2MEFunctor(zeros(buffer_length), output_value_references) + @parameters (functor::(typeof(_functor)))(..)[1:buffer_length] = _functor + call_expr = functor(wrapper, copy(diffvars), copy(inputs), copy(params), t) + + diffeqs = Equation[] + for (i, var) in enumerate([D.(diffvars); outputs]) + push!(diffeqs, var ~ call_expr[i]) + end + + finalize_affect = MTK.FunctionalAffect(fmi2MEFinalize!, [], [wrapper], []) + step_affect = MTK.FunctionalAffect(fmi2MEStep!, [], [wrapper], []) + instance_management_callback = MTK.SymbolicDiscreteCallback( + (t != t - 1), step_affect; finalize = finalize_affect) + + push!(params, wrapper, functor) + eqs = [observed; diffeqs] + return ODESystem(eqs, t, states, params; parameter_dependencies, defaults = defs, + discrete_events = [instance_management_callback], name) +end + +function parseFMIVariableName(name::AbstractString) + return Symbol(replace(name, "." => "__")) +end + +mutable struct FMI2InstanceWrapper + const fmu::FMI.FMU2 + const param_value_references::Vector{UInt32} + const input_value_references::Vector{UInt32} + const tolerance::FMI.fmi2Real + instance::Union{FMI.FMU2Component, Nothing} +end + +function FMI2InstanceWrapper(fmu, params, inputs, tolerance) + FMI2InstanceWrapper(fmu, params, inputs, tolerance, nothing) +end + +function get_instance!(wrapper::FMI2InstanceWrapper, states, inputs, params, t) + if wrapper.instance === nothing + wrapper.instance = FMI.fmi2Instantiate!(wrapper.fmu)::FMI.FMU2Component + if !isempty(params) + @statuscheck FMI.fmi2SetReal(wrapper.instance, wrapper.param_value_references, + Csize_t(length(wrapper.param_value_references)), params) + end + @statuscheck FMI.fmi2SetupExperiment( + wrapper.instance, FMI.fmi2True, wrapper.tolerance, t, FMI.fmi2False, t) + @statuscheck FMI.fmi2EnterInitializationMode(wrapper.instance) + if !isempty(inputs) + @statuscheck FMI.fmi2SetReal(wrapper.instance, wrapper.input_value_references, + Csize_t(length(wrapper.param_value_references)), inputs) + end + @statuscheck FMI.fmi2ExitInitializationMode(wrapper.instance) + eventInfo = FMI.fmi2NewDiscreteStates(wrapper.instance) + @assert eventInfo.newDiscreteStatesNeeded == FMI.fmi2False + # TODO: Support FMU events + @statuscheck FMI.fmi2EnterContinuousTimeMode(wrapper.instance) + end + instance = wrapper.instance + @statuscheck FMI.fmi2SetTime(instance, t) + @statuscheck FMI.fmi2SetContinuousStates(instance, states) + if !isempty(inputs) + @statuscheck FMI.fmi2SetReal(instance, wrapper.input_value_references, + Csize_t(length(wrapper.param_value_references)), inputs) + end + + return instance +end + +function complete_step!(wrapper::FMI2InstanceWrapper) + wrapper.instance === nothing && return + @statuscheck FMI.fmi2CompletedIntegratorStep(wrapper.instance, FMI.fmi2True) +end + +function reset_instance!(wrapper::FMI2InstanceWrapper) + wrapper.instance === nothing && return + FMI.fmi2Terminate(wrapper.instance) + FMI.fmi2FreeInstance!(wrapper.instance) + wrapper.instance = nothing +end + +struct FMI2MEFunctor{T} + return_buffer::Vector{T} + output_value_references::Vector{UInt32} +end + +function (fn::FMI2MEFunctor)(wrapper::FMI2InstanceWrapper, states, inputs, params, t) + instance = get_instance!(wrapper, states, inputs, params, t) + + states_buffer = zeros(length(states)) + @statuscheck FMI.fmi2GetDerivatives!(instance, states_buffer) + outputs_buffer = zeros(length(fn.output_value_references)) + FMI.fmi2GetReal!(instance, fn.output_value_references, outputs_buffer) + return [states_buffer; outputs_buffer] +end + +function fmi2MEStep!(integrator, u, p, ctx) + wrapper_idx = p[1] + wrapper = integrator.ps[wrapper_idx] + complete_step!(wrapper) +end + +function fmi2MEFinalize!(integrator, u, p, ctx) + wrapper_idx = p[1] + wrapper = integrator.ps[wrapper_idx] + reset_instance!(wrapper) +end + +end # module diff --git a/src/ModelingToolkit.jl b/src/ModelingToolkit.jl index 743dc8ce76..2710d7d1e4 100644 --- a/src/ModelingToolkit.jl +++ b/src/ModelingToolkit.jl @@ -297,4 +297,6 @@ export HomotopyContinuationProblem export AnalysisPoint, get_sensitivity_function, get_comp_sensitivity_function, get_looptransfer_function, get_sensitivity, get_comp_sensitivity, get_looptransfer, open_loop +function FMIComponent end + end # module From 310c5f29d3d97257ec64b736ba8015caeb9ef7b7 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 20 Dec 2024 16:32:39 +0530 Subject: [PATCH 0625/1337] test: add tests for FMIComponent --- test/extensions/Project.toml | 2 ++ test/extensions/fmi.jl | 19 +++++++++++++++++++ test/runtests.jl | 1 + 3 files changed, 22 insertions(+) create mode 100644 test/extensions/fmi.jl diff --git a/test/extensions/Project.toml b/test/extensions/Project.toml index 5f7afe222a..72501554b5 100644 --- a/test/extensions/Project.toml +++ b/test/extensions/Project.toml @@ -2,6 +2,8 @@ BifurcationKit = "0f109fa4-8a5d-4b75-95aa-f515264e7665" ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4" ChainRulesTestUtils = "cdddcdb0-9152-4a09-a978-84456f9df70a" +FMI = "14a09403-18e3-468f-ad8a-74f8dda2d9ac" +FMIZoo = "724179cf-c260-40a9-bd27-cccc6fe2f195" ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210" HomotopyContinuation = "f213a82b-91d6-5c5d-acf7-10f1c761b327" InfiniteOpt = "20393b10-9daf-11e9-18c9-8db751c92c57" diff --git a/test/extensions/fmi.jl b/test/extensions/fmi.jl new file mode 100644 index 0000000000..b11c5f9a31 --- /dev/null +++ b/test/extensions/fmi.jl @@ -0,0 +1,19 @@ +using ModelingToolkit, FMI, FMIZoo, OrdinaryDiffEq +using ModelingToolkit: t_nounits as t, D_nounits as D +import ModelingToolkit as MTK + +@testset "Standalone pendulum model" begin + @testset "v2, ME" begin + fmu = loadFMU("SpringPendulum1D", "Dymola", "2022x"; type = :ME) + @mtkbuild sys = MTK.FMIComponent(Val(2), Val(:ME); fmu) + prob = ODEProblem{true, SciMLBase.FullSpecialize}( + sys, [sys.mass__s => 0.5, sys.mass__v => 0.0], (0.0, 8.0)) + sol = solve(prob, Tsit5(); reltol = 1e-8, abstol = 1e-8) + @test SciMLBase.successful_retcode(sol) + + truesol = FMI.simulate(fmu, (0.0, 8.0); saveat = 0.0:0.1:8.0) + @test sol(0.0:0.1:8.0).u≈truesol.states.u atol=1e-4 + # repeated solve works + @test_nowarn solve(prob, Tsit5()) + end +end diff --git a/test/runtests.jl b/test/runtests.jl index 95154e550e..bfb9e0b6f9 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -117,6 +117,7 @@ end if GROUP == "All" || GROUP == "Extensions" activate_extensions_env() + @safetestset "FMI Extension Test" include("extensions/fmi.jl") @safetestset "HomotopyContinuation Extension Test" include("extensions/homotopy_continuation.jl") @safetestset "Auto Differentiation Test" include("extensions/ad.jl") @safetestset "LabelledArrays Test" include("labelledarrays.jl") From 9bbcf7c7a2b5d02c96ba9ec8c22075f783a7b564 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 23 Dec 2024 17:26:23 +0530 Subject: [PATCH 0626/1337] fix: fix `getcalledparameter` namespacing issues --- src/parameters.jl | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/parameters.jl b/src/parameters.jl index ced7767718..91121b7cbb 100644 --- a/src/parameters.jl +++ b/src/parameters.jl @@ -33,7 +33,12 @@ end function getcalledparameter(x) x = unwrap(x) - return getmetadata(x, CallWithParent) + # `parent` is a `CallWithMetadata` with the correct metadata, + # but no namespacing. `operation(x)` has the correct namespacing, + # but is not a `CallWithMetadata` and doesn't have any metadata. + # This approach combines both. + parent = getmetadata(x, CallWithParent) + return CallWithMetadata(operation(x), metadata(parent)) end """ From 721430c913cdbf1f98ca913ce9d5f61334f94d19 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 23 Dec 2024 17:26:42 +0530 Subject: [PATCH 0627/1337] fix: handle scalarized called parameter in `vars!` --- src/utils.jl | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/utils.jl b/src/utils.jl index 5e4f0b52d2..b6544972b9 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -403,6 +403,9 @@ function vars!(vars, eq::Equation; op = Differential) end function vars!(vars, O; op = Differential) if isvariable(O) + if iscall(O) && operation(O) === getindex && iscalledparameter(first(arguments(O))) + O = first(arguments(O)) + end if iscalledparameter(O) f = getcalledparameter(O) push!(vars, f) From a20ba60f9daf536db41ba0d0a987a067d07df8f0 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 23 Dec 2024 17:27:08 +0530 Subject: [PATCH 0628/1337] fix: run array variables hack on equations added by CSE hack --- src/structural_transformation/symbolics_tearing.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/src/structural_transformation/symbolics_tearing.jl b/src/structural_transformation/symbolics_tearing.jl index 8161a9572b..bd4868125e 100644 --- a/src/structural_transformation/symbolics_tearing.jl +++ b/src/structural_transformation/symbolics_tearing.jl @@ -696,6 +696,7 @@ function cse_and_array_hacks(obs, subeqs, unknowns, neweqs; cse = true, array = tempvar; T = Symbolics.symtype(rhs_arr))) tempvar = setmetadata( tempvar, Symbolics.ArrayShapeCtx, Symbolics.shape(rhs_arr)) + vars!(all_vars, rhs_arr) tempeq = tempvar ~ rhs_arr rhs_to_tempvar[rhs_arr] = tempvar push!(obs, tempeq) From af92fe8cbc718c66ed9d601b9b66d93cbce34efe Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 23 Dec 2024 17:27:31 +0530 Subject: [PATCH 0629/1337] fix: only use `OffsetArray` in array hack for non-standard `firstindex` --- src/structural_transformation/symbolics_tearing.jl | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/structural_transformation/symbolics_tearing.jl b/src/structural_transformation/symbolics_tearing.jl index bd4868125e..4382ad6284 100644 --- a/src/structural_transformation/symbolics_tearing.jl +++ b/src/structural_transformation/symbolics_tearing.jl @@ -738,7 +738,9 @@ function cse_and_array_hacks(obs, subeqs, unknowns, neweqs; cse = true, array = # try to `create_array(OffsetArray{...}, ...)` which errors. # `term(Origin(firstind), scal)` doesn't retain the `symtype` and `size` # of `scal`. - push!(obs_arr_eqs, arrvar ~ change_origin(Origin(firstind), scal)) + rhs = scal + rhs = change_origin(firstind, rhs) + push!(obs_arr_eqs, arrvar ~ rhs) end append!(obs, obs_arr_eqs) append!(subeqs, obs_arr_eqs) @@ -765,10 +767,10 @@ getindex_wrapper(x, i) = x[i...] # PART OF HACK 2 function change_origin(origin, arr) - return origin(arr) + return Origin(origin)(arr) end -@register_array_symbolic change_origin(origin::Origin, arr::AbstractArray) begin +@register_array_symbolic change_origin(origin::Any, arr::AbstractArray) begin size = size(arr) eltype = eltype(arr) ndims = ndims(arr) From 3a8a0e1c728f2172443dcc7f6fe949cf4d49f68e Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 23 Dec 2024 17:28:22 +0530 Subject: [PATCH 0630/1337] fix: handle usage of FMU in initialization --- ext/MTKFMIExt.jl | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/ext/MTKFMIExt.jl b/ext/MTKFMIExt.jl index 0812506df7..fc9171fab8 100644 --- a/ext/MTKFMIExt.jl +++ b/ext/MTKFMIExt.jl @@ -191,6 +191,14 @@ struct FMI2MEFunctor{T} output_value_references::Vector{UInt32} end +@register_array_symbolic (fn::FMI2MEFunctor)( + wrapper::FMI2InstanceWrapper, states::Vector{<:Real}, + inputs::Vector{<:Real}, params::Vector{<:Real}, t::Real) begin + size = (length(states) + length(fn.output_value_references),) + eltype = eltype(states) + ndims = 1 +end + function (fn::FMI2MEFunctor)(wrapper::FMI2InstanceWrapper, states, inputs, params, t) instance = get_instance!(wrapper, states, inputs, params, t) From b75a49fa8669663780c6f2bede1de6492ba753f0 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 23 Dec 2024 17:28:52 +0530 Subject: [PATCH 0631/1337] refactor: modularize code and enable array hacks --- ext/MTKFMIExt.jl | 107 +++++++++++++++++++++++------------------------ 1 file changed, 53 insertions(+), 54 deletions(-) diff --git a/ext/MTKFMIExt.jl b/ext/MTKFMIExt.jl index fc9171fab8..8b6f294601 100644 --- a/ext/MTKFMIExt.jl +++ b/ext/MTKFMIExt.jl @@ -36,68 +36,43 @@ function MTK.FMIComponent(::Val{2}, ::Val{:ME}; fmu = nothing, tolerance = 1e-6, states = [] diffvars = [] observed = Equation[] - stateT = Float64 - for (valRef, snames) in FMI.getStateValueReferencesAndNames(fmu) - stateT = FMI.dataTypeForValueReference(fmu, valRef) - snames = map(parseFMIVariableName, snames) - vars = [MTK.unwrap(only(@variables $sname(t)::stateT)) for sname in snames] - for i in eachindex(vars) - if i == 1 - push!(diffvars, vars[i]) - else - push!(observed, vars[i] ~ vars[1]) - end - value_references[vars[i]] = valRef - end - append!(states, vars) + fmi_variables_to_mtk_variables!(fmu, FMI.getStateValueReferencesAndNames(fmu), + value_references, diffvars, states, observed) + if isempty(diffvars) + __mtk_internal_u = [] + else + @variables __mtk_internal_u(t)[1:length(diffvars)] + push!(observed, __mtk_internal_u ~ copy(diffvars)) + push!(states, __mtk_internal_u) end inputs = [] - for (valRef, snames) in FMI.getInputValueReferencesAndNames(fmu) - snames = map(parseFMIVariableName, snames) - vars = [MTK.unwrap(only(@variables $sname(t)::stateT)) for sname in snames] - for i in eachindex(vars) - if i == 1 - push!(inputs, vars[i]) - else - push!(observed, vars[i] ~ vars[1]) - end - value_references[vars[i]] = valRef - end - append!(states, vars) + fmi_variables_to_mtk_variables!(fmu, FMI.getInputValueReferencesAndNames(fmu), + value_references, inputs, states, observed) + if isempty(inputs) + __mtk_internal_x = [] + else + @variables __mtk_internal_x(t)[1:length(inputs)] + push!(observed, __mtk_internal_x ~ copy(inputs)) + push!(states, __mtk_internal_x) end outputs = [] - for (valRef, snames) in FMI.getOutputValueReferencesAndNames(fmu) - snames = map(parseFMIVariableName, snames) - vars = [MTK.unwrap(only(@variables $sname(t)::stateT)) for sname in snames] - for i in eachindex(vars) - if i == 1 - push!(outputs, vars[i]) - else - push!(observed, vars[i] ~ vars[1]) - end - value_references[vars[i]] = valRef - end - append!(states, vars) - end + fmi_variables_to_mtk_variables!(fmu, FMI.getOutputValueReferencesAndNames(fmu), + value_references, outputs, states, observed) + # @variables __mtk_internal_o(t)[1:length(outputs)] + # push!(observed, __mtk_internal_o ~ outputs) params = [] parameter_dependencies = Equation[] - for (valRef, pnames) in FMI.getParameterValueReferencesAndNames(fmu) - defval = FMI.getStartValue(fmu, valRef) - T = FMI.dataTypeForValueReference(fmu, valRef) - pnames = map(parseFMIVariableName, pnames) - vars = [MTK.unwrap(only(@parameters $pname::T)) for pname in pnames] - for i in eachindex(vars) - if i == 1 - push!(params, vars[i]) - else - push!(parameter_dependencies, vars[i] ~ vars[1]) - end - value_references[vars[i]] = valRef - end - defs[vars[1]] = defval + fmi_variables_to_mtk_variables!( + fmu, FMI.getParameterValueReferencesAndNames(fmu), value_references, + params, [], parameter_dependencies, defs; parameters = true) + if isempty(params) + __mtk_internal_p = [] + else + @parameters __mtk_internal_p[1:length(params)] + push!(parameter_dependencies, __mtk_internal_p ~ copy(params)) end input_value_references = UInt32[value_references[var] for var in inputs] @@ -109,7 +84,7 @@ function MTK.FMIComponent(::Val{2}, ::Val{:ME}; fmu = nothing, tolerance = 1e-6, buffer_length = length(diffvars) + length(outputs) _functor = FMI2MEFunctor(zeros(buffer_length), output_value_references) @parameters (functor::(typeof(_functor)))(..)[1:buffer_length] = _functor - call_expr = functor(wrapper, copy(diffvars), copy(inputs), copy(params), t) + call_expr = functor(wrapper, __mtk_internal_u, __mtk_internal_x, __mtk_internal_p, t) diffeqs = Equation[] for (i, var) in enumerate([D.(diffvars); outputs]) @@ -127,6 +102,30 @@ function MTK.FMIComponent(::Val{2}, ::Val{:ME}; fmu = nothing, tolerance = 1e-6, discrete_events = [instance_management_callback], name) end +function fmi_variables_to_mtk_variables!(fmu, varmap, value_references, truevars, allvars, + obseqs, defs = Dict(); parameters = false) + for (valRef, snames) in varmap + stateT = FMI.dataTypeForValueReference(fmu, valRef) + snames = map(parseFMIVariableName, snames) + if parameters + vars = [MTK.unwrap(only(@parameters $sname::stateT)) for sname in snames] + else + vars = [MTK.unwrap(only(@variables $sname(t)::stateT)) for sname in snames] + end + for i in eachindex(vars) + if i == 1 + push!(truevars, vars[i]) + else + push!(obseqs, vars[i] ~ vars[1]) + end + value_references[vars[i]] = valRef + end + append!(allvars, vars) + defval = FMI.getStartValue(fmu, valRef) + defs[vars[1]] = defval + end +end + function parseFMIVariableName(name::AbstractString) return Symbol(replace(name, "." => "__")) end From 537b561a3b32b9f6c1ffa761ae92eebd7830c9ea Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 23 Dec 2024 17:29:19 +0530 Subject: [PATCH 0632/1337] test: add test for component FMU hooked up to MTK model, initialization --- test/extensions/fmi.jl | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/test/extensions/fmi.jl b/test/extensions/fmi.jl index b11c5f9a31..7e5e01f76a 100644 --- a/test/extensions/fmi.jl +++ b/test/extensions/fmi.jl @@ -17,3 +17,20 @@ import ModelingToolkit as MTK @test_nowarn solve(prob, Tsit5()) end end + +@testset "IO Model" begin + fmu = loadFMU("./fmus/SimpleAdder.fmu"; type = :ME) + @named adder = MTK.FMIComponent(Val(2), Val(:ME); fmu) + @variables a(t) b(t) c(t) [guess = 1.0] + @mtkbuild sys = ODESystem( + [adder.a ~ a, adder.b ~ b, D(a) ~ t, + D(b) ~ adder.out + adder.c, c^2 ~ adder.out + adder.value], + t; + systems = [adder]) + + # c will be solved for by initialization + # this tests that initialization also works with FMUs + prob = ODEProblem(sys, [sys.adder.c => 1.0, sys.a => 1.0, sys.b => 1.0], (0.0, 1.0)) + sol = solve(prob, Rodas5P(autodiff = false)) + @test SciMLBase.successful_retcode(sol) +end From 71762a8e30e4c3e81d8a37802f866894e9224dbe Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 23 Dec 2024 17:31:28 +0530 Subject: [PATCH 0633/1337] test: add `SimpleAdder.fmu` for testing --- test/extensions/fmus/SimpleAdder.fmu | Bin 0 -> 658043 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 test/extensions/fmus/SimpleAdder.fmu diff --git a/test/extensions/fmus/SimpleAdder.fmu b/test/extensions/fmus/SimpleAdder.fmu new file mode 100644 index 0000000000000000000000000000000000000000..87248ce050479793a8566b3c61b097de464a48f1 GIT binary patch literal 658043 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≤?s+h>M%vru5f(A%zmmr4XwvCrVKoAeQ+ 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�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�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

j3|9>Dn9 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≻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
L^0e=q3`gz=-koYyd~O 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%0A8pd=-b@uY&998yUeFriA4)(-JrD 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

`Z^JU0{v#mxl(@0am$P`6T{L|gb zgBvr~MV)t6#}Vt{T2i{dk0km#jx;La&gVHXA^)d@;cu)3=A}yde$Ji990w9F4YES9 z{RZ$RG-u?AvtM&d^&9wnCqkTq3;PiwiBX~PQbM;VtR7@td19e=Q3A^S)_1l&}F>oMljXk{0{r7csi~a}mN1 z2-fe$It_uEz4nm`QB{VicaJyaKqL)K=gRj10M^^LYG6vg zGee&aiSi0dWK+5(8uVr6U>fz(O!l$(3>(Ty*L+YlG#p~v0c8f5DP-CSnOzc&I7f-` zi$IETc8D56nfq%J3LBizTX0#C_I2Tlrl-bVKS}nYCw=Ti86EoVnNmT}5Wl9W$2vl$ z5vmNQYDqXMEUU13rf5z6*%?Ymdhe9&Bs9(M+eJE711W0O&}ugRAX_$46yuIbPE z_i!t-LMn%`(-G!fR_ivjV$u($#Me`okraMJ`72}sRvL!nme84vW{_J}1QvmP$WrWQ zV$krDJNTXt^m)mI>*4GknW~bgF%6?M27UHCrQRWbi5x83%fLUMy?T7|bd;+;D zA<%$RwqDH!s>P)+#5iW)*-oi*9LiWm1W@)RrQY>gxsidUtsL&8tKjr#IkFY6VU^7B z`~X@dWoY?=l&~f4hHZwMf=u)l^Y|xFRs%7N!{Z@kt8fdm^VK7$M@vv9xhfQjatfK| zFm~GPcIa%odj9>ESf@XOxlUObOY-uOId~Que>_ltU@q=uC7$PLRw<>7GacQjig~i#bH@- z@GgFb-#J(p84T6^2*D_vWZ~giRurDBix0sZPQfx%Vws!1uhwCi$HBHpbFl8e=KtnJ zsK3U{GUyp9#jFT4?hOnZ`8i`(LYR{~pbQ)WC-uctxe6I4NuZ_TH11H6L8mqEJnmXD zMyI9X`uqC&``PXHm5N?VLT=?KgKOT6RzUMj8GH?wh}l?GKx|YJH8)dF%|RL`mf3H4g^$2dy#XQ+rvq zRj^AejZvaW1*jwRZwGgQ4XKu+m4&LntGFmzn0M3IBMR}PdB}UZc_-OH>GaMF=ZBAIX;oGy@q?IDIXU|Kx!gU`d5{B3URq4yn}?DDCb`#Y|UNi;#Z|hS>iJG&oDbh;FYM-XP*DYpCu$ zX_La1;xSh3s)t^EOISHl%KTHlB==%Ng;(a_k?QH9?BiTC|CW;mdw;z9<8r*BKd-K% z-QsKuSZ96lC$m$4>D*OfROx&3&8wWBk+@#nub&DH-$g|nqM8)GQ2jGb?ptz;jX#%& z&^JtComuDM@$bg>KTM{2&Ly3Kde{u;QJ+S5AVBF|YvS^#y#_wVN;b~1Efjz#1($zS ze>D!d*u1qc8pP$TEDmy|%M7vWUD9>ucz5B-x25t0hi@IJ%L#zz&PH=gVRm; zw0-={p7iVA+8Wu5GORo4x^Vdf`!i9hl_l9Eh5!KgGs^h);}6^amzndg3Ad4*lZB0o zwZZ>z@&3nQ2s@+$(Q1>pfu6(_?g>ny7~qMsuK1X#zZ<$hl$}#%gK5$iV z_CG!_72Uw-|1Ra9lKy)iw(uqFILER{%o>67bZCY^$&{O4O;As`J8nK|!H1*llH|o1zVLxqt`c?# zVea3t55ZC5o~gA@sb)>Rev!wa6&aeDO{{IL4Vvy&=i4wYde5m(G(-R7(S;Zs2VIj&}lUm_P-%i(Mk*hcF z$_n;K8n`w+J5iqZ;UPnXq%VZg2~o$8O})XOGNyn#;t-BRkuSvN5nNLA4d29nc)j`s z*7><(Z_jX?o5c%A!mC6&RLTzV=;yV>&&_v&55t$oBX~4!&L&966}!vO8LXCr;8}d! z8zdy7$t8i!>rJtR_|nC}je+YLD$>_6vAWI< zsHnO!&Z@BodUlfsz4VIQ(M%*T9T$~$j2Az|D|vrIKk@C>@kaY%LGKE=)XUcOL_#Fr@i*#a(Y3deLC2UqoR1_%-bmzftM3=1k92K>t0J|{PbeV0d+Zu` zhZvn=!dbt3O+ z!d?-}G0>9Y$x6JS>@Yn8nHB?vh^dL2n44q)RXcp5Jr&vD=EC8;;}I^f9_2UT;hsW$ z!U3k3tb3H3osXqXSOokK4tFLZb0A13pY|mAtAq98Usy_5#`FXfE_%VCzAvJ{+ z3btOm;o*W;n|+5lOTXf%fDkZqLmEi=dX=+w-J>X5r&eQzWKlb3cVL31^FgAKB^F7A z!s>OC4qIR(BEl@EIkKXAQ4AiNQ6)ZD2?g^7FuYWF{A^8T(7jU%DRQ>4Z!a;EDhyxQMg{&DCH9bF?3B&wE@!b&joLYKheR+zK7+$+k*O_-mtPwNU zt%TeC8hVThygIP*-%IGF4cVNWaZRtW!8k<8uuvQe=9w=JWuM@^l<-jwAgPkcf2tp7 zIHj6A&6uF|tBm80rIko`JOw^Dv5?j(v%uSlxXFt65`?^s~V;mfF`o6!XeATesX@ws=Og&@2(C2lU4#a!c(4wweKtGX%BNi-~3pptT~tpyuQEd z_^$zji?}Mw9Eg@>=0X#Bb^@)H)@P$DW9aERLt2&`*Y&+a=L=9DYjzm^4IqazZTD4>r5w(oaS z{M8AgNk2G!N2jQy4GarBf=C$L9u6GEavmV$nqM;ciR!=rO_1Ne5!8r5pJ>7sb=W98 zqrN+{4f<}qmKH>B5*?x>GdR^ZMQrpzov`mMx}m}J*&%;_Bjxyrnyrs^Pou1Ons*!| z)uzVon)s4+pL)HM@0XVa(~T1%&!~TLRlyM5=d}ByE_{&P8s1bgt@}BY5~hhBk~}7c zSSE9#RfA1bmC&?>0z41Ei3c!1EK-uubrkvpn_Fc3#p|U(y0Hx;Yr1IV>(*HY_BbB7 zLvU?MZ?nt>`}a+X9E;T&nB8>qg5Kn6bMKkZR|*L;4i|YH)-<3FqeiPu^Jgs)3oe^& zndI-`q~Di1_V>J501#3`SZ}bNBsm{Jj)PLE5+wxHcQuUc0CM~W6SNYT#=doxMom+z za!p!B7SyT~jr4LgX&D(z^I#uJ;3@{XA5|$@DNNIvrlL0ja;-}<{m@9h@dOVk$2^cB zhZ76;?u4N%yBryg_JFvEQ$NN?L$>=N)n%(s84gm@IEDts26m#vP-wF5USzMtV@Ark z)-@pEvV6()O}r>s+1&f`fp5iuIT~i5e&RWpWXFBteG8(*5J8&xG5<^FMP~soT(N8m z`7p$wR=_8h_EZbq>zCWYkWOF%XD^B%p| zP@NfRF3h>bU@v=+RkfITmWo-uku>y#Z;`v0MMk;*_*{v(`#iU2aPbVR7Rya*AV()q zZ%iSlVv=V@DF9y44o3~H9(YAFM&;@*VOK~k)1a}l5Uj<|b46le{%?8(ihGOO!0P5&PdWk3~mlEcIxUr z<78$yaP(f$Qp1e%5v|Y+9{qkqx2Hwh!`ESB)4P)E$k3$!7uiMd`mYGSBCbJt^DyB6nJde-|LU-Scw(Cv0qou zbc`R@$5IiYT@7n1iZ>8xQhkadu8P~4Op|~}h(qfHeyBF;`K?Uz5oyr96kgms5Jee9 ztB4njJ-@ar26AFSpI^qTh1?oi!(-jQXl+M=1ut^aNbrW&ng&PN!N@t>4EmeDo~cOV zk$Z}^a4`_0AR+?Ip&|;}+T}`{1>)^@+J@e@J-3gikA8#hk`mDAYQ&0}Y{qn^X{&jr zh&68u->G$1pzk`3w9U!|QHQW140ixp?zhHT){Lkt0M0NsGyq4J-E~&(0Nml;d%0If z_?-vsM(`XiHfGRZ^n$2xguOqqq>%6-R$=CY<0a$@yhsVp*SM z9)MFI(8BdO!Qx>>H>qk1QImME7#){&smdmD2QkkxNYgxB+Nf3{cW7uFG$+O{xGAJ+dHf&`PS~+-rYX#A-CVm=SHw zuw6(j2R!fjR-|sh4MklD)oafo)%i>`A)&S%Dd^^xdtUy`x1P_lLIqB*Gra@EA(;7H z?hC9h;~q&cmf57@Rh(eH{UGmPfM_3XoAKJFLDD1LY{Xf963aHN8m2}eF3&XYAqP^4 zhz8^Im>Y*Qy|J-NRWfH(6U3W7>NCvF8mU5jJy8p!IT`w3-XGuk;x64|8)!OubeJrs z`VDa{$Rb4bV&TLEyO*er3mZ?OXu0kQ#E91C+dvuL0X$P%AUH z@NuF5;n`p`KDk$m4sI(xO;QiXWCm&iL=VMfUM1WB%5T)|Bj}0ROyGVZ-h7-2N{GAV zPQJEFjS*+_G*%M!GEcsymBKOViIb+J?NFSzUxmw$#i^{qyQ|d^2CFpH^=2^ocmbkm z)Vv>xQ_`R&=r(Dim8%-HN)_(m-JAur6g{+`qW4N~m}8Bpb~vn*>6}a;*+mow*$J1U zHx8;{K&NtR(VxSZ_-jGa%0E@>XdjN70A$|d_R)n}x99ZkN6OyKDt zFrMq&L>*A0emvhfMBI1-?lN~UKO}=-4l?6U&$_GjS3$S=-Fp?feQatx9n)ZR;CHTp zKWPEK8Q}8#(2lQ$%##%HG8?T}H|rpudo}tQ-`ts3bGkNF$RQwH?a^ax2SR`T z^45t_eoBD`zd{}0j?$j5cwbT!MfEIY3iL!FJ%hnQ!2|nyue(9@FZ(g}GI7+#QW(#(5IkcH z-qzV z9mBU%CZ@CYEE3RO#p1R(w2<^2%FX*6LW+|70miHhUU7yKi`q}P^GIweuEG*5L+j5d z@bzYk9n+|i(1r{`9yq>|iyx$(!ez^OtA;q!^Z2e#AXM2`BIy}A*YuKq^DG#G7q_u` z`Q9|2EUVt`eic{pjSYZL?CYu9H6=ZUyq5Q#3U8dpW$cG-w3{YDFn}YD)eW|(Olv=z zq9`EP@TTGEn9rj#wKTtE9qE`b!h878^!F{zMBnL{5Exo|+{E_9v6u@jBYljR`q!vP z(EHILD=9l9F;)M{;ficZ_jqZD#T@ZiryyrxY~jQPINUR6bRZL>c4VY73M7NKYopKU z)~;gC>83f0hP`9dOpaHiBxkAdoWnI-sms9KZBZ|r{6EPD-gCJ12@5~$O>pZC_{1uh zn5YhlAYj^}HZL zG*y|JLs7ktrv-N;Z1U&R_h71XJ_L=jTPhINzB;=3(w;qlEfEB(&GnuLY~9T~Wfwx{ zve6G%6>te3((aPE$F1``CPiwge8E$(VM^)kBQEWNnnpt}*#a}~F_Bs>-0NBbrdJA5 zE%{o(s%yKdg5;5P!mKqUu%^H!{kz_bUZqLSrQQUySy(oldP(CZ;ha3(Z8U2x`^1sc zP&>iNYuaV!=i&$H$>rfw4EbYZYo4&Vc2iPUDWO*Vx%{j}PHGKSKMWLWp1e73v){>m zZTfQt*}s!l%={#NC;$9MvQUTq9bt~hBFQwgCAeqr5HK(GLOZ80>5gvgjeRWP9hi?4 zcs1TD=o$0}hPO+Po1LIab2ULkc(X0VK{uuR>`^=O)WF_P9{KHWTMr&WhVLSALZVgA zCJp}iB5L$JFpDU7LZKrRQzJn?ZlNQE11EtcTo!h;_If(bfg-sNXRb@?uds}ad8Y;I zq-fEpr#9yfip_Ye>_xY*z`=NLEvd-C?{>Xgi)+)eaf*gr9b8!sVE0eQviv1ED{N1@ z!)>=qp)d|y$nA(d`SV{(SW9I*Duhs{l16~ z4ko~eRi7*le0Na24Z@pQF}?<5JJNkURozZ}%Fa7{ceC4U5+={IW0mKs&Rwljegw66 zuBIhk4&E9_##^zrfC`uA;s(Z2orY?vu42A$*yC_#yzY5w;_^+ph7 zN}@o3h&>38AR>;0#2|M<&W({m?l*L*LBnqr#jUl&6%ml$<|ZC}x8c#G1X3TxKRoJU z?IBrM&;hg#`@RTCrBuh5lZF7OHl;O|;edv?Sn$3GhEOx1;ZMYjL*CTprFku>Dk);h zZW>L`7~2*mcTFHx)6X|f^aDalGOI-c4^P%39ecM*G513Uuhk(+ZKQaN{&fTm?U@^O zg*@aOnZQdc@3p8fz$}4g$F+BNk>fy$4VIB-9g~NxqHU2V2&#`L=%wEM0rC-}Dr9X4 zf%VzvepS#a^kl0f)-9Z)O6%m)?|j4ITmz|!`%+4LXzd{ynscP9f8==|A9Dttg=xKo zxH?YPZpYsEXUo3b2LzV)a9~|M?QQm1iD<~$qv3I&*pn?D} z8`c=)$0sMYRcTTenW618tPrCA+x;;$XfC}?Kr`|Q~4<1@Cs|0g=Y5LMRJqgQ< zsuHl&8J4dcBYI$s09<9qO0i{7VtixfCc+O;7Tn~TS)p99!uPgSW~Rf@p0 z?UF@sN+#mLq?RtzBEZ}vJ2f6Uv87a9(`12|;#JFDKU`CX)E<90W)t}rLV{@D3IH>n zslk}AU@myaw73BKW^SPFcTh)rj@-Kfkdbq{5^lb7XsnOLdg!~xKe6PqPmBK6D4 zaK2=2I#y{H!dxRjym^Ygh&c_bi>rA|R`)af49wtB32DaJC}-(kb)07`pKiukFy>B< zm=Vh!;{ugeT`;adEeFkVr8>30q$;P(;!2uBuozYyCR9{sl@~TGX~l9>tHZvfhpKXQ za9D98R_hvAZn!n|uGMe)!G1>#_qdv=Q}!s8aWsdtVypt5VXK<}xWoXX)`65CZ^r-Wx;!f5B;!t~Ort zX7$n&c46ZWmVDTJja@25RtFSaP&NoE>2c!T9L`v!#d{T-$ibE;+dVa)n^JF~mfTS9 zbffr9R4*qu7?JIXHey@Nt-%^y=b8T$5Klm>GH=CeUk;Y-py(jZBu1COI*kEND4o9V z>T+)hWYwmuf0fXTofs9m(WP6-s3z~3zeXvYMWr&0?v`9@!wQ)h+ zCri5gl@XasT&=6sqtPmZR5I<5jTc#!UH>QeukmK7*KSWWbxsceU+^FI$PR7~H1=DG zDG>y>a$}leg>iHU>hFphX?Al<)_toFs24Lbc}E$YYO7qHX9|Xbn?p%U%#g|E)g0z3 zev1!{yOEl-6Rf5uF|6F3beYDBzd4&1*tNJAS-UnfdJAv~M8A7ag&x`UU)Q!%D%xlq$XC+`?CREbU^9V>zE9poxkE6@T800fUebR zcu32=z# zm+a*7_<0ac_4n(^H$O0sZx*|Kq$SfBZ9#f&ua{9AqwisXMpIYOCiOm9os4$8Bw*Ay z!PHd_#%K{*O`F&3d)CZ6LBGVedn};468jYTKyL2{lb{SP;Y;~+>z*HR37QLP8D$_) z5y=*d5{@8swFWaQkjK$A3N20VD67gP_ihV!yP@H9a$n>k$XEMykjE$A z%GGixbl;TKlxcCTSA$npDeS2m$;~X)lpdTz&$@5F9;OucHjYn%Sle*{m5eOpgsV76w8KbNQgyH$*h3w~X>4D#<+~;nZMv zmKEqZj2UQ0`k}l1XpNgnu=s1<{ajIlEqs`R^QDNmtIty@s~J0}D7ZYU4@!FoGuh== zzFa4@m2g}&MEk0$tKVbS&IZLju=>y-!>KVs1Tck(hlZ_EWA^N{Y!&eUqXd)WbuRBG z{GUw?E6d4Bdkg@8KU)|7ep4e>>>o#_{o9S1{}{+NsP#KOgahd_N3XZoe9CpBapN&y zuz0jDIy*;kn9h*fE3}ybB~mVEEI~afE7j)f4OVXueKbXreo+8B69+CA6K*tg{@4Lw zvVUUum{#(G;V0KmPQl|r=*BQ)mZv_39QF*GMhOO6mM1*w9Z?J~%EaWp4YxpztG%E6 z7_h9I3RnXKW@sLxO*$zc@fywiv4x@lCU0VxkSQ9+KLl}Ruu3)a8ty*N>)jYHV?&Og zF*+Cb(Z{zRG|(o?cnr$z_rakTe+@&kaO3CBHhabHs3RDF2)tVdo4(i3`mSQaJmQ%t ze(Een9yZq2nG^Ag`4KXFFEY>av=9?ud~=FI6Ng}6AGi`9HowmgN6IplBngU{c60VZ z4#HqX)0MDyva`G5d30C?_t9k;E@hRkO*Jg6tphjm?|Xj5B2Ron6}zhde48;v6-?SS z4*vjHl~w0^p9)$xr0a?S#%~Kxt>EZWT#Z~5L=u;N9y@Ebxx=OxtP&u=@RZh8%LKEY zAiGh=X=Q>Wb4Z!EA;@87q;o(WCS-;XG9np!MY-zBv^qYhA$zro9bN7MBJR2BKhd`z z04@E(KPmmadF#9{jKg}rO9oq#szQ&1zpC__oU*p_gjN7=fls+caZj@P<qCpD9Q$q07W8W}wF2m?NzsoZ$2la?*9>SgT~-X=v;UC)Xo_FJzGQPB z%I!d!@d0}=Vj2Z|t%8lH7dQsJSfHV-3RxxiQt$QcMKYdsT2b*Pej%x5dgL94T6|^q z5@>Q@!>}dyWhu7ShF*O>+(W!I(Q$=_DuFJAb#YRgc8g*1V9wT({;r@*`MrJKbctog zE;wz#)bh>wKELOslNAPv5p;n}nM7WAExisWL#yzWC+wi2*IF$qn3{ebNsK z-8@p=p;6vUZzab$Z7<&hFijN{-=5dNRPiXbA*-x)GC?VJM-Y`NRu@Dj#fa-lS0q*R#yd1tBE1u6T7t%;Q7RMIn?bC%0(DXDP$Q}? z8|M_?k)&1Jjl;y?I4=d+Gj(nVc~9yeHV-89x-DvqePR#`LPKfjN4CsIqEcz3(hI*F z(w}BsNQuO(gA6bYDiokF8xqJQoN}6P7{!)u7rWd}1UhBJ&SFR&S$|{JkOr2G$#yjk z;Q@s+?E89W$s#4aRHrjlT-u)##nbwl!<_O=?c6b5#CLrw$sRp6tdu_hKmNih>9_U> zPMislJ>yfLWdLtxG3q)r0I8Cj5Ht?eBbNJ__$*@1ntSN5@vGfd67Fz#MHOOS`c6wU zT6G>WlsQ`8Dn42%60$ZOS$-q8gqUEb|HPat2aquL9wu}%9j-}zk12NZBTxKl)nl03 zzC=%S%k3a(HZGVIz63i^bt~>dcBxeUnXSJyP?kMQskaSfib=vy4mz^sM@R3Yj^r}6 zo!`I%x@8BKdwA|wd)cDGCHI&qm%4~MQq_S3w2757P?-OFwf_O1-kFvC=I45Q;zegO zO?N#DD-Xw^;Lg=Cl!}K8a=Q%Vhy@0+c7b4UVb4kaQRD7K9g7US>07&^Mul2kxKXyua4Iv^Jx?Q-ns0*>(kxrQ-%^<9XQ0RCPkOs%k6r*Dl ztfq{eHR@n+Yp3Q}lSfk#Ed<}lRhN=XZEZIT{Tf><@&Qd-=bS8xa$(HqNt{UaRw~Q& zM%9&fTl{+0zR5P=KqW>F@Y5v=BrVN0o?qpmgg!*CF~o74m45p6)X27CQp!f&rO2(Z zcpQd7#37=(_B!>De3BfCb{1wE$vP~M8Jd70DU0uYHHw`y% zX8HJx1oe)PK1he6I=RIw)#mR7A0NaE_INdy*p&vEf!n(s7a1ZIAS$$2)3vhDYZ8KH z(Lo#(K%X0NqGM3yhgF#UgU{0X_8qWry1GfqKc*CXb>yrvwe0V}z#gHdbVFqE#*nw6i@?Zgtu_cam>2WPZ(D;3&Y(_4fL6Q#QUC zv0hZQv-k*=Dw=|`v;Cq%FrMCb$_&KxVL&-%8u$@m3zr47GFJo9t)L z?6kNX{$E0$#pEKl^ymlO0uw04(ed!4%+ayMxdCxCvGRke- zbcimK@L>f8KOTRVrCFTa2@90{H{J6(Rt01zb*}X}kljiGQ!@uf*=R4vYy^!N_N-$9 zb8|XI+4um*bh)=R_AIQowe=mNQfe$^t{M?HYL@1Z%MctTag(IR8^5bLuQJ9S#%Sb;5ctPa$~0X>T zvI%y3QuKU@HI6;$ZZqt;*e;uE81@U~t{8pvazkToP)B%IH14aI7B`xxh#bZuXZAfkcjD%`GXvKLLG<0BMeU=Kw-j>}HI_NxED|#d*KtBUQnhst zV7WFpB)=DgoHlOCFGVYYV9^3MhZW2;arvz8Ib-gepjJ2UORTcw-|cAc%?^WmiF#PQ zD}FMo8>bw;mZz5l?0L^$z?$N7qbKtD1opxqn;weOO9E$*YRp-d1$VI^(457lE_A&1 zFgkGsYP7fsR-NlTHWC^yx*MRA%e=JhJEWMXqWMJcu8mNl|LEIZ98>FBFwbA6p`mbO zt&q0ayPt<0cADw<}73tOiY?aoE(cU^T(!{-I+sf_PDq{)txOjQCkW zkREew*Ks%qy@yJmA;x@t;KtpwB6nfL=_caKue8Gdexkk^`5_cpnLhWRiE?1`u%v2S z_JI|-iRxEdgns?RszMJMyG0}Hc%^ws??w4NJXOWki~oCUpqAnjDxi{J2humPpRE;4TKww2?4Bp%Q?ol))MMJ_0qU?9KvDJoqnIxy$9T(_{k zU-md8jKg-q%iw=?Qyi^Lx0%F2q1*HGL0{I&AB-{2YVs_^ffmu^Q4Td>;!(SKX3ZPyJ0cZ5AzzPnHxW8&>11fbE(p%FGOeB3k`e$WKY?tM z_gag3i{l)MrL#Nw3>-H^$PEX%Cdq|8quS^)N-zSIk(NBMT)YcdHhzyQ@N^V0(9$g+ zB+HeCG6rw^K8l6P6*0E?j1YW7ZANh^dFg>`=cgE29Kppb+hFOc#GyA;WfaYqm{Z&r z;vR6Dxzjc1OP{^59RbJ`H%2_e*W-aDe*07Zht(D@zof$~8(CFbnrf^2IZgSknQR%K z>gTMJvNzD5#fbHw&BEi~(UpJdO+gwE2n84g;4eVk-;an#`CX@fNu&RtyZ?dJ{g*ZS zmxVw1|J_mZf71WRhhWKXc;A0<@ccgt{wM3dJ9_?4`XBiay!^Y`|MDREf3BRr<5~Y4 zMgNz4_zV38{rsPz|C5dX>R|f6*VVr|^#>;VmwX7KO91>ApxJ+R&g4q|smd`~TYb|I-yKNP~j?(FXJTllc38Rqg-h>Hh&&2RSGJ literal 0 HcmV?d00001 From ba2a8aec9e2fc607621972507d074bea702fec5e Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 3 Jan 2025 14:06:14 +0530 Subject: [PATCH 0648/1337] test: update `SimpleAdder.fmu` --- test/extensions/fmus/SimpleAdder.fmu | Bin 658043 -> 658163 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/test/extensions/fmus/SimpleAdder.fmu b/test/extensions/fmus/SimpleAdder.fmu index 87248ce050479793a8566b3c61b097de464a48f1..a1c3fd2728376ebed8f8dfd81819b0be51c84bef 100644 GIT binary patch delta 199526 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�x(E7wlw{c|PmBa3pLR?P**T?TZ;rr(3k)JIxz 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

S}yxItNOSt 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%0AAA&)G2kq%8&5? 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�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

9V6U@5zF*mVDFu>xpjqOG1CnhAU>=j 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

}m;A{^foMasgt0j7JkEW7^;0bi$b^ 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 From be249252145c7eb05068f8117e4e2b4bd9a763cc Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 3 Jan 2025 16:54:08 +0530 Subject: [PATCH 0649/1337] refactor: remove redundant fields from structs --- ext/MTKFMIExt.jl | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/ext/MTKFMIExt.jl b/ext/MTKFMIExt.jl index e0d59c4032..00ac91fb46 100644 --- a/ext/MTKFMIExt.jl +++ b/ext/MTKFMIExt.jl @@ -128,7 +128,7 @@ function MTK.FMIComponent(::Val{Ver}; fmu = nothing, tolerance = 1e-6, if type == :ME FunctorT = Ver == 2 ? FMI2MEFunctor : FMI3MEFunctor - _functor = FunctorT(zeros(buffer_length), output_value_references) + _functor = FunctorT(output_value_references) @parameters (functor::(typeof(_functor)))(..)[1:buffer_length] = _functor call_expr = functor( wrapper, __mtk_internal_u, __mtk_internal_x, __mtk_internal_p, t) @@ -147,11 +147,8 @@ function MTK.FMIComponent(::Val{Ver}; fmu = nothing, tolerance = 1e-6, push!(states, __mtk_internal_u) elseif type == :CS state_value_references = UInt32[value_references[var] for var in diffvars] - state_and_output_value_references = vcat( - state_value_references, output_value_references) _functor = if Ver == 2 - FMI2CSFunctor(state_and_output_value_references, - state_value_references, output_value_references) + FMI2CSFunctor(state_value_references, output_value_references) else FMI3CSFunctor(state_value_references, output_value_references) end @@ -351,9 +348,8 @@ function reset_instance!(wrapper::FMI3InstanceWrapper) wrapper.instance = nothing end -struct FMI2MEFunctor{T} - return_buffer::Vector{T} - output_value_references::Vector{UInt32} +struct FMI2MEFunctor + output_value_references::Vector{FMI.fmi2ValueReference} end @register_array_symbolic (fn::FMI2MEFunctor)( @@ -385,9 +381,8 @@ function (fn::FMI2MEFunctor)(wrapper::FMI2InstanceWrapper, states, inputs, param return [states_buffer; outputs_buffer] end -struct FMI3MEFunctor{T} - return_buffer::Vector{T} - output_value_references::Vector{UInt32} +struct FMI3MEFunctor + output_value_references::Vector{FMI.fmi3ValueReference} end @register_array_symbolic (fn::FMI3MEFunctor)( @@ -431,7 +426,6 @@ function fmiFinalize!(integrator, u, p, ctx) end struct FMI2CSFunctor - state_and_output_value_references::Vector{UInt32} state_value_references::Vector{UInt32} output_value_references::Vector{UInt32} end From 7b5e543e2e427be0596853fcaa3492980487557b Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 3 Jan 2025 16:54:24 +0530 Subject: [PATCH 0650/1337] docs: add documentation for all functions in FMIExt --- ext/MTKFMIExt.jl | 421 +++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 392 insertions(+), 29 deletions(-) diff --git a/ext/MTKFMIExt.jl b/ext/MTKFMIExt.jl index 00ac91fb46..4fea249dd9 100644 --- a/ext/MTKFMIExt.jl +++ b/ext/MTKFMIExt.jl @@ -3,10 +3,19 @@ module MTKFMIExt using ModelingToolkit using SymbolicIndexingInterface using ModelingToolkit: t_nounits as t, D_nounits as D +using DocStringExtensions import ModelingToolkit as MTK import SciMLBase import FMI +""" + $(TYPEDSIGNATURES) + +A utility macro for FMI.jl functions that return a status. Will terminate on +fatal statuses. Must be used as `@statuscheck FMI.fmiXFunction(...)` where +`X` should be `2` or `3`. Has an edge case for handling tuples for +`FMI.fmi2CompletedIntegratorStep`. +""" macro statuscheck(expr) @assert Meta.isexpr(expr, :call) fn = expr.args[1] @@ -40,6 +49,12 @@ macro statuscheck(expr) end @static if !hasmethod(FMI.getValueReferencesAndNames, Tuple{FMI.fmi3ModelDescription}) + """ + $(TYPEDSIGNATURES) + + This is type piracy, but FMI.jl is missing this implementation. It allows + `FMI.getStateValueReferencesAndNames` to work. + """ function FMI.getValueReferencesAndNames( md::FMI.fmi3ModelDescription; vrs = md.valueReferences) dict = Dict{FMI.fmi3ValueReference, Array{String}}() @@ -50,6 +65,29 @@ end end end +""" + $(TYPEDSIGNATURES) + +A component that wraps an FMU loaded via FMI.jl. The FMI version (2 or 3) should be +provided as a `Val` to the function. Supports Model Exchange and CoSimulation FMUs. +All inputs, continuous variables and outputs must be `FMI.fmi2Real` or `FMI.fmi3Float64`. +Does not support events or discrete variables in the FMU. Does not support automatic +differentiation. Parameters of the FMU will have defaults corresponding to their initial +values in the FMU specification. All other variables will not have a default. Hierarchical +names in the FMU of the form `namespace.variable` are transformed into symbolic variables +with the name `namespace__variable`. + +# Keyword Arguments + +- `fmu`: The FMU loaded via `FMI.loadFMU`. +- `tolerance`: The tolerance to provide to the FMU. Not used for v3 FMUs since it is not + supported by FMI.jl. +- `communication_step_size`: The periodic interval at which communication with CoSimulation + FMUs will occur. Must be provided for CoSimulation FMU components. +- `type`: Either `:ME` or `:CS` depending on whether `fmu` is a Model Exchange or + CoSimulation FMU respectively. +- `name`: The name of the system. +""" function MTK.FMIComponent(::Val{Ver}; fmu = nothing, tolerance = 1e-6, communication_step_size = nothing, type, name) where {Ver} if Ver != 2 && Ver != 3 @@ -58,26 +96,44 @@ function MTK.FMIComponent(::Val{Ver}; fmu = nothing, tolerance = 1e-6, if type == :CS && communication_step_size === nothing throw(ArgumentError("`communication_step_size` must be specified for Co-Simulation FMUs.")) end + # mapping from MTK variable to value reference value_references = Dict() + # defaults defs = Dict() + # unknowns of the system states = [] + # differential variables of the system + # this is a subset of `states` in the case where the FMU has multiple names for + # the same value reference. diffvars = [] + # observed equations observed = Equation[] + # parse states fmi_variables_to_mtk_variables!(fmu, FMI.getStateValueReferencesAndNames(fmu), value_references, diffvars, states, observed) + # create a symbolic variable __mtk_internal_u to pass to the relevant registered + # functions as the state vector if isempty(diffvars) + # no differential variables __mtk_internal_u = [] elseif type == :ME + # ME FMUs perform integration using the Julia solver, so unknowns of the FMU + # are unknowns of the `ODESystem` @variables __mtk_internal_u(t)[1:length(diffvars)] [guess = diffvars] push!(observed, __mtk_internal_u ~ copy(diffvars)) elseif type == :CS + # CS FMUs do their own independent integration in a periodic callback, so their + # unknowns are discrete variables in the `ODESystem`. A default of `missing` allows + # them to be solved for during initialization. @parameters __mtk_internal_u(t)[1:length(diffvars)]=missing [guess = diffvars] push!(observed, __mtk_internal_u ~ copy(diffvars)) end + # parse the inputs to the FMU inputs = [] fmi_variables_to_mtk_variables!(fmu, FMI.getInputValueReferencesAndNames(fmu), value_references, inputs, states, observed) + # create a symbolic variable for the input buffer if isempty(inputs) __mtk_internal_x = [] else @@ -86,9 +142,12 @@ function MTK.FMIComponent(::Val{Ver}; fmu = nothing, tolerance = 1e-6, push!(states, __mtk_internal_x) end + # parse the outputs of the FMU outputs = [] fmi_variables_to_mtk_variables!(fmu, FMI.getOutputValueReferencesAndNames(fmu), value_references, outputs, states, observed) + # create the output buffer. This is only required for CoSimulation to pass it to + # the callback affect if type == :CS if isempty(outputs) __mtk_internal_o = [] @@ -98,11 +157,14 @@ function MTK.FMIComponent(::Val{Ver}; fmu = nothing, tolerance = 1e-6, end end + # parse the parameters params = [] + # multiple names for the same parameter are treated as parameter dependencies. parameter_dependencies = Equation[] fmi_variables_to_mtk_variables!( fmu, FMI.getParameterValueReferencesAndNames(fmu), value_references, params, [], parameter_dependencies, defs; parameters = true) + # create a symbolic variable for the parameter buffer if isempty(params) __mtk_internal_p = [] else @@ -113,6 +175,8 @@ function MTK.FMIComponent(::Val{Ver}; fmu = nothing, tolerance = 1e-6, input_value_references = UInt32[value_references[var] for var in inputs] param_value_references = UInt32[value_references[var] for var in params] + # create a parameter for the instance wrapper + # this manages the creation and deallocation of FMU instances if Ver == 2 @parameters wrapper::FMI2InstanceWrapper = FMI2InstanceWrapper( fmu, param_value_references, input_value_references, tolerance) @@ -124,20 +188,28 @@ function MTK.FMIComponent(::Val{Ver}; fmu = nothing, tolerance = 1e-6, output_value_references = UInt32[value_references[var] for var in outputs] buffer_length = length(diffvars) + length(outputs) + # any additional initialization equations for the system initialization_eqs = Equation[] if type == :ME + # the functor is a callable struct which returns the state derivative and + # output values FunctorT = Ver == 2 ? FMI2MEFunctor : FMI3MEFunctor _functor = FunctorT(output_value_references) @parameters (functor::(typeof(_functor)))(..)[1:buffer_length] = _functor + + # symbolic expression for calling the functor call_expr = functor( wrapper, __mtk_internal_u, __mtk_internal_x, __mtk_internal_p, t) + # differential and observed equations diffeqs = Equation[] for (i, var) in enumerate([D.(diffvars); outputs]) push!(diffeqs, var ~ call_expr[i]) end + # instance management callback which deallocates the instance when + # necessary and notifies the FMU of completed integrator steps finalize_affect = MTK.FunctionalAffect(fmiFinalize!, [], [wrapper], []) step_affect = MTK.FunctionalAffect(fmiMEStep!, [], [wrapper], []) instance_management_callback = MTK.SymbolicDiscreteCallback( @@ -153,6 +225,8 @@ function MTK.FMIComponent(::Val{Ver}; fmu = nothing, tolerance = 1e-6, FMI3CSFunctor(state_value_references, output_value_references) end @parameters (functor::(typeof(_functor)))(..)[1:(length(__mtk_internal_u) + length(__mtk_internal_o))] = _functor + # for co-simulation, we need to ensure the output buffer is solved for + # during initialization for (i, x) in enumerate(collect(__mtk_internal_o)) push!(initialization_eqs, x ~ functor( @@ -161,18 +235,22 @@ function MTK.FMIComponent(::Val{Ver}; fmu = nothing, tolerance = 1e-6, diffeqs = Equation[] + # use `ImperativeAffect` for instance management here cb_observed = (; inputs = __mtk_internal_x, params = copy(params), t, wrapper, dt = communication_step_size) cb_modified = (;) + # modify the outputs if present if symbolic_type(__mtk_internal_o) != NotSymbolic() cb_modified = (cb_modified..., outputs = __mtk_internal_o) end + # modify the continuous state if present if symbolic_type(__mtk_internal_u) != NotSymbolic() cb_modified = (cb_modified..., states = __mtk_internal_u) end initialize_affect = MTK.ImperativeAffect(fmiCSInitialize!; observed = cb_observed, modified = cb_modified, ctx = _functor) finalize_affect = MTK.FunctionalAffect(fmiFinalize!, [], [wrapper], []) + # the callback affect performs the stepping step_affect = MTK.ImperativeAffect( fmiCSStep!; observed = cb_observed, modified = cb_modified, ctx = _functor) instance_management_callback = MTK.SymbolicDiscreteCallback( @@ -180,6 +258,7 @@ function MTK.FMIComponent(::Val{Ver}; fmu = nothing, tolerance = 1e-6, finalize = finalize_affect, reinitializealg = SciMLBase.NoInit() ) + # guarded in case there are no outputs/states and the variable is `[]`. symbolic_type(__mtk_internal_o) == NotSymbolic() || push!(params, __mtk_internal_o) symbolic_type(__mtk_internal_u) == NotSymbolic() || push!(params, __mtk_internal_u) @@ -191,7 +270,24 @@ function MTK.FMIComponent(::Val{Ver}; fmu = nothing, tolerance = 1e-6, discrete_events = [instance_management_callback], name, initialization_eqs) end -function fmi_variables_to_mtk_variables!(fmu, varmap, value_references, truevars, allvars, +""" + $(TYPEDSIGNATURES) + +A utility function which accepts an FMU `fmu` and a mapping from value reference to a +list of associated names `varmap`. A symbolic variable is created for each name. The +associated value reference is kept track of in `value_references`. In case there are +multiple names for a value reference, the symbolic variable for the first name is pushed +to `truevars`. All of the created symbolic variables are pushed to `allvars`. Observed +equations equating identical variables are pushed to `obseqs`. `defs` is a dictionary of +defaults. + +# Keyword Arguments +- `parameters`: A boolean indicating whether to use `@parameters` for the symbolic + variables instead of `@variables`. +""" +function fmi_variables_to_mtk_variables!( + fmu::Union{FMI.FMU2, FMI.FMU3}, varmap::AbstractDict, + value_references::AbstractDict, truevars, allvars, obseqs, defs = Dict(); parameters = false) for (valRef, snames) in varmap stateT = FMI.dataTypeForValueReference(fmu, valRef) @@ -215,23 +311,69 @@ function fmi_variables_to_mtk_variables!(fmu, varmap, value_references, truevars end end +""" + $(TYPEDSIGNATURES) + +Parse the string name of an FMI variable into a `Symbol` name for the corresponding +MTK vriable. +""" function parseFMIVariableName(name::AbstractString) return Symbol(replace(name, "." => "__")) end +""" + $(TYPEDEF) + +A struct which manages instance creation and deallocation for v2 FMUs. + +# Fields + +$(TYPEDFIELDS) +""" mutable struct FMI2InstanceWrapper + """ + The FMU from `FMI.loadFMU`. + """ const fmu::FMI.FMU2 - const param_value_references::Vector{UInt32} - const input_value_references::Vector{UInt32} + """ + The parameter value references. These should be in the same order as the parameter + vector passed to functions involving this wrapper. + """ + const param_value_references::Vector{FMI.fmi2ValueReference} + """ + The input value references. These should be in the same order as the inputs passed + to functions involving this wrapper. + """ + const input_value_references::Vector{FMI.fmi2ValueReference} + """ + The tolerance with which to setup the FMU instance. + """ const tolerance::FMI.fmi2Real - instance::Union{FMI.FMU2Component, Nothing} + """ + The FMU instance, if present, and `nothing` otherwise. + """ + instance::Union{FMI.FMU2Component{FMI.FMU2}, Nothing} end +""" + $(TYPEDSIGNATURES) + +Create an `FMI2InstanceWrapper` with no instance. +""" function FMI2InstanceWrapper(fmu, params, inputs, tolerance) FMI2InstanceWrapper(fmu, params, inputs, tolerance, nothing) end -function get_instance_common!(wrapper::FMI2InstanceWrapper, states, inputs, params, t) +""" + $(TYPEDSIGNATURES) + +Common functionality for creating an instance of a v2 FMU. Does not check if +`wrapper.instance` is already present, and overwrites the existing value with +a new instance. `inputs` should be in the order of `wrapper.input_value_references`. +`params` should be in the order of `wrapper.param_value_references`. `t` is the current +time. Returns the created instance, which is also stored in `wrapper.instance`. +""" +function get_instance_common!(wrapper::FMI2InstanceWrapper, inputs, params, t) wrapper.instance = FMI.fmi2Instantiate!(wrapper.fmu)::FMI.FMU2Component if !isempty(params) @statuscheck FMI.fmi2SetReal(wrapper.instance, wrapper.param_value_references, @@ -248,9 +390,17 @@ function get_instance_common!(wrapper::FMI2InstanceWrapper, states, inputs, para return wrapper.instance end -function get_instance_ME!(wrapper::FMI2InstanceWrapper, states, inputs, params, t) +""" + $(TYPEDSIGNATURES) + +Create an instance of a Model Exchange FMU. Use the existing instance in `wrapper` if +present and create a new one otherwise. Return the instance. + +See `get_instance_common!` for a description of the arguments. +""" +function get_instance_ME!(wrapper::FMI2InstanceWrapper, inputs, params, t) if wrapper.instance === nothing - get_instance_common!(wrapper, states, inputs, params, t) + get_instance_common!(wrapper, inputs, params, t) @statuscheck FMI.fmi2ExitInitializationMode(wrapper.instance) eventInfo = FMI.fmi2NewDiscreteStates(wrapper.instance) @assert eventInfo.newDiscreteStatesNeeded == FMI.fmi2False @@ -261,19 +411,39 @@ function get_instance_ME!(wrapper::FMI2InstanceWrapper, states, inputs, params, return wrapper.instance end -function get_instance_CS!(wrapper::FMI2InstanceWrapper, states, inputs, params, t) +""" + $(TYPEDSIGNATURES) + +Create an instance of a CoSimulation FMU. Use the existing instance in `wrapper` if +present and create a new one otherwise. Return the instance. + +See `get_instance_common!` for a description of the arguments. +""" +function get_instance_CS!(wrapper::FMI2InstanceWrapper, inputs, params, t) if wrapper.instance === nothing - get_instance_common!(wrapper, states, inputs, params, t) + get_instance_common!(wrapper, inputs, params, t) @statuscheck FMI.fmi2ExitInitializationMode(wrapper.instance) end return wrapper.instance end +""" + $(TYPEDSIGNATURES) + +If `wrapper.instance !== nothing`, tell the FMU that an integrator step has been accepted. +This is relevant only for ModelExchange FMUs. +""" function complete_step!(wrapper::FMI2InstanceWrapper) wrapper.instance === nothing && return @statuscheck FMI.fmi2CompletedIntegratorStep(wrapper.instance, FMI.fmi2True) end +""" + $(TYPEDSIGNATURES) + +If `wrapper.instance !== nothing`, terminate and free the instance. Also set +`wrapper.instance` to `nothing`. +""" function reset_instance!(wrapper::FMI2InstanceWrapper) wrapper.instance === nothing && return FMI.fmi2Terminate(wrapper.instance) @@ -281,18 +451,55 @@ function reset_instance!(wrapper::FMI2InstanceWrapper) wrapper.instance = nothing end +""" + $(TYPEDEF) + +A struct which manages instance creation and deallocation for v3 FMUs. + +# Fields + +$(TYPEDFIELDS) +""" mutable struct FMI3InstanceWrapper + """ + The FMU from `FMI.loadFMU`. + """ const fmu::FMI.FMU3 - const param_value_references::Vector{UInt32} - const input_value_references::Vector{UInt32} + """ + The parameter value references. These should be in the same order as the parameter + vector passed to functions involving this wrapper. + """ + const param_value_references::Vector{FMI.fmi3ValueReference} + """ + The input value references. These should be in the same order as the inputs passed + to functions involving this wrapper. + """ + const input_value_references::Vector{FMI.fmi3ValueReference} + """ + The FMU instance, if present, and `nothing` otherwise. + """ instance::Union{FMI.FMU3Instance{FMI.FMU3}, Nothing} end +""" + $(TYPEDSIGNATURES) + +Create an `FMI3InstanceWrapper` with no instance. +""" function FMI3InstanceWrapper(fmu, params, inputs) FMI3InstanceWrapper(fmu, params, inputs, nothing) end -function get_instance_common!(wrapper::FMI3InstanceWrapper, states, inputs, params, t) +""" + $(TYPEDSIGNATURES) + +Common functionality for creating an instance of a v3 FMU. Since v3 FMUs need to be +instantiated differently depending on the type, this assumes `wrapper.instance` is a +freshly instantiated FMU which needs to be initialized. `inputs` should be in the order +of `wrapper.input_value_references`. `params` should be in the order of +`wrapper.param_value_references`. `t` is the current time. Returns `wrapper.instance`. +""" +function get_instance_common!(wrapper::FMI3InstanceWrapper, inputs, params, t) if !isempty(params) @statuscheck FMI.fmi3SetFloat64(wrapper.instance, wrapper.param_value_references, params) @@ -307,10 +514,18 @@ function get_instance_common!(wrapper::FMI3InstanceWrapper, states, inputs, para return wrapper.instance end -function get_instance_ME!(wrapper::FMI3InstanceWrapper, states, inputs, params, t) +""" + $(TYPEDSIGNATURES) + +Create an instance of a Model Exchange FMU. Use the existing instance in `wrapper` if +present and create a new one otherwise. Return the instance. + +See `get_instance_common!` for a description of the arguments. +""" +function get_instance_ME!(wrapper::FMI3InstanceWrapper, inputs, params, t) if wrapper.instance === nothing wrapper.instance = FMI.fmi3InstantiateModelExchange!(wrapper.fmu)::FMI.FMU3Instance - get_instance_common!(wrapper, states, inputs, params, t) + get_instance_common!(wrapper, inputs, params, t) @statuscheck FMI.fmi3ExitInitializationMode(wrapper.instance) eventInfo = FMI.fmi3UpdateDiscreteStates(wrapper.instance) @assert eventInfo[1] == FMI.fmi2False @@ -321,16 +536,31 @@ function get_instance_ME!(wrapper::FMI3InstanceWrapper, states, inputs, params, return wrapper.instance end -function get_instance_CS!(wrapper::FMI3InstanceWrapper, states, inputs, params, t) +""" + $(TYPEDSIGNATURES) + +Create an instance of a CoSimulation FMU. Use the existing instance in `wrapper` if +present and create a new one otherwise. Return the instance. + +See `get_instance_common!` for a description of the arguments. +""" +function get_instance_CS!(wrapper::FMI3InstanceWrapper, inputs, params, t) if wrapper.instance === nothing wrapper.instance = FMI.fmi3InstantiateCoSimulation!( wrapper.fmu; eventModeUsed = false)::FMI.FMU3Instance - get_instance_common!(wrapper, states, inputs, params, t) + get_instance_common!(wrapper, inputs, params, t) @statuscheck FMI.fmi3ExitInitializationMode(wrapper.instance) end return wrapper.instance end +""" + $(TYPEDSIGNATURES) + +If `wrapper.instance !== nothing`, tell the FMU that an integrator step has been accepted. +This is relevant only for ModelExchange FMUs. Asserts that the simulation does not need +to be terminated and does not require entering event mode. +""" function complete_step!(wrapper::FMI3InstanceWrapper) wrapper.instance === nothing && return enterEventMode = Ref(FMI.fmi3False) @@ -341,6 +571,9 @@ function complete_step!(wrapper::FMI3InstanceWrapper) @assert terminateSimulation[] == FMI.fmi3False end +""" + $(TYPEDSIGNATURES) +""" function reset_instance!(wrapper::FMI3InstanceWrapper) wrapper.instance === nothing && return FMI.fmi3Terminate(wrapper.instance) @@ -348,7 +581,22 @@ function reset_instance!(wrapper::FMI3InstanceWrapper) wrapper.instance = nothing end +""" + $(TYPEDEF) + +A callable struct useful for simulating v2 Model Exchange FMUs. When called, updates the +internal state of the FMU and gets updated values for continuous state derivatives and +output variables. + +# Fields + +$(TYPEDFIELDS) +""" struct FMI2MEFunctor + """ + The value references for outputs of the FMU, in the order that the caller expects + them to be returned when calling `FMI2MEFunctor`. + """ output_value_references::Vector{FMI.fmi2ValueReference} end @@ -360,6 +608,11 @@ end ndims = 1 end +""" + $(TYPEDSIGNATURES) + +Update `wrapper.instance` with the new values of state, input and independent variables. +""" function update_instance_ME!(wrapper::FMI2InstanceWrapper, states, inputs, t) instance = wrapper.instance @statuscheck FMI.fmi2SetTime(instance, t) @@ -370,10 +623,19 @@ function update_instance_ME!(wrapper::FMI2InstanceWrapper, states, inputs, t) end end +""" + $(TYPEDSIGNATURES) + +Get the FMU instance (creating and initializing it if not present), update it +with the current values of variables, and return a vector of the state derivatives +and output variables. +""" function (fn::FMI2MEFunctor)(wrapper::FMI2InstanceWrapper, states, inputs, params, t) - instance = get_instance_ME!(wrapper, states, inputs, params, t) + instance = get_instance_ME!(wrapper, inputs, params, t) update_instance_ME!(wrapper, states, inputs, t) + # TODO: Find a way to do this without allocating. We can't pass a view to these + # functions. states_buffer = zeros(length(states)) @statuscheck FMI.fmi2GetDerivatives!(instance, states_buffer) outputs_buffer = zeros(length(fn.output_value_references)) @@ -381,7 +643,22 @@ function (fn::FMI2MEFunctor)(wrapper::FMI2InstanceWrapper, states, inputs, param return [states_buffer; outputs_buffer] end +""" + $(TYPEDEF) + +A callable struct useful for simulating v3 Model Exchange FMUs. When called, updates the +internal state of the FMU and gets updated values for continuous state derivatives and +output variables. + +# Fields + +$(TYPEDFIELDS) +""" struct FMI3MEFunctor + """ + The value references for outputs of the FMU, in the order that the caller expects + them to be returned when calling `FMI3MEFunctor`. + """ output_value_references::Vector{FMI.fmi3ValueReference} end @@ -393,6 +670,11 @@ end ndims = 1 end +""" + $(TYPEDSIGNATURES) + +Update `wrapper.instance` with the new values of state, input and independent variables. +""" function update_instance_ME!(wrapper::FMI3InstanceWrapper, states, inputs, t) instance = wrapper.instance @statuscheck FMI.fmi3SetTime(instance, t) @@ -402,10 +684,18 @@ function update_instance_ME!(wrapper::FMI3InstanceWrapper, states, inputs, t) end end +""" + $(TYPEDSIGNATURES) + +Get the FMU instance (creating and initializing it if not present), update it +with the current values of variables, and return a vector of the state derivatives +and output variables. +""" function (fn::FMI3MEFunctor)(wrapper::FMI3InstanceWrapper, states, inputs, params, t) - instance = get_instance_ME!(wrapper, states, inputs, params, t) + instance = get_instance_ME!(wrapper, inputs, params, t) update_instance_ME!(wrapper, states, inputs, t) + # TODO: Don't allocate states_buffer = zeros(length(states)) @statuscheck FMI.fmi3GetContinuousStateDerivatives!(instance, states_buffer) outputs_buffer = zeros(length(fn.output_value_references)) @@ -413,28 +703,60 @@ function (fn::FMI3MEFunctor)(wrapper::FMI3InstanceWrapper, states, inputs, param return [states_buffer; outputs_buffer] end +""" + $(TYPEDSIGNATURES) + +An affect function for use inside a `FunctionalAffect`. This should be triggered every +time an integrator step is accepted. Expects `p` to be a 1-length array containing +the index of the instance wrapper (`FMI2InstanceWrapper` or `FMI3InstanceWrapper`) in +the parameter object. +""" function fmiMEStep!(integrator, u, p, ctx) wrapper_idx = p[1] wrapper = integrator.ps[wrapper_idx] complete_step!(wrapper) end +""" + $(TYPEDSIGNATURES) + +An affect function for use inside a `FunctionalAffect`. This should be triggered at the +end of the solve, regardless of whether it succeeded or failed. Expects `p` to be a +1-length array containing the index of the instance wrapper (`FMI2InstanceWrapper` or +`FMI3InstanceWrapper`) in the parameter object. +""" function fmiFinalize!(integrator, u, p, ctx) wrapper_idx = p[1] wrapper = integrator.ps[wrapper_idx] reset_instance!(wrapper) end +""" + $(TYPEDEF) + +A callable struct useful for initializing v2 CoSimulation FMUs. When called, updates the +internal state of the FMU and gets updated values for output variables. + +# Fields + +$(TYPEDFIELDS) +""" struct FMI2CSFunctor - state_value_references::Vector{UInt32} - output_value_references::Vector{UInt32} + """ + The value references of state variables in the FMU. + """ + state_value_references::Vector{FMI.fmi2ValueReference} + """ + The value references of output variables in the FMU. + """ + output_value_references::Vector{FMI.fmi2ValueReference} end function (fn::FMI2CSFunctor)(wrapper::FMI2InstanceWrapper, states, inputs, params, t) states = states isa SubArray ? copy(states) : states inputs = inputs isa SubArray ? copy(inputs) : inputs params = params isa SubArray ? copy(params) : params - instance = get_instance_CS!(wrapper, states, inputs, params, t) + instance = get_instance_CS!(wrapper, inputs, params, t) if isempty(fn.output_value_references) return eltype(states)[] else @@ -450,6 +772,17 @@ end ndims = 1 end +""" + $(TYPEDSIGNATURES) + +An affect function designed for use with `ImperativeAffect`. Should be triggered during +callback initialization. `m` should contain the key `:states` with the value being the +state vector if the FMU has continuous states. `m` should contain the key `:outputs` with +the value being the output vector if the FMU has output variables. `o` should contain the +`:inputs`, `:params`, `:t` and `:wrapper` where the latter contains the `FMI2InstanceWrapper`. + +Initializes the FMU. Only for use with CoSimulation FMUs. +""" function fmiCSInitialize!(m, o, ctx::FMI2CSFunctor, integrator) states = isdefined(m, :states) ? m.states : () inputs = o.inputs @@ -460,8 +793,7 @@ function fmiCSInitialize!(m, o, ctx::FMI2CSFunctor, integrator) reset_instance!(wrapper) end - instance = get_instance_common!(wrapper, states, inputs, params, t) - @statuscheck FMI.fmi2ExitInitializationMode(instance) + instance = get_instance_CS!(wrapper, inputs, params, t) if isdefined(m, :states) @statuscheck FMI.fmi2GetReal!(instance, ctx.state_value_references, m.states) end @@ -472,6 +804,14 @@ function fmiCSInitialize!(m, o, ctx::FMI2CSFunctor, integrator) return m end +""" + $(TYPEDSIGNATURES) + +An affect function designed for use with `ImperativeAffect`. Should be triggered +periodically to communicte with the CoSimulation FMU. Has the same requirements as +`fmiCSInitialize!` for `m` and `o`, with the addition that `o` should have a key +`:dt` with the value being the communication step size. +""" function fmiCSStep!(m, o, ctx::FMI2CSFunctor, integrator) wrapper = o.wrapper states = isdefined(m, :states) ? m.states : () @@ -480,7 +820,7 @@ function fmiCSStep!(m, o, ctx::FMI2CSFunctor, integrator) t = o.t dt = o.dt - instance = get_instance_CS!(wrapper, states, inputs, params, integrator.t) + instance = get_instance_CS!(wrapper, inputs, params, integrator.t) @statuscheck FMI.fmi2DoStep(instance, integrator.t - dt, dt, FMI.fmi2True) if isdefined(m, :states) @@ -493,16 +833,33 @@ function fmiCSStep!(m, o, ctx::FMI2CSFunctor, integrator) return m end +""" + $(TYPEDEF) + +A callable struct useful for initializing v3 CoSimulation FMUs. When called, updates the +internal state of the FMU and gets updated values for output variables. + +# Fields + +$(TYPEDFIELDS) +""" struct FMI3CSFunctor - state_value_references::Vector{UInt32} - output_value_references::Vector{UInt32} + """ + The value references of state variables in the FMU. + """ + state_value_references::Vector{FMI.fmi3ValueReference} + """ + The value references of output variables in the FMU. + """ + output_value_references::Vector{FMI.fmi3ValueReference} end function (fn::FMI3CSFunctor)(wrapper::FMI3InstanceWrapper, states, inputs, params, t) states = states isa SubArray ? copy(states) : states inputs = inputs isa SubArray ? copy(inputs) : inputs params = params isa SubArray ? copy(params) : params - instance = get_instance_CS!(wrapper, states, inputs, params, t) + instance = get_instance_CS!(wrapper, inputs, params, t) + if isempty(fn.output_value_references) return eltype(states)[] else @@ -518,6 +875,9 @@ end ndims = 1 end +""" + $(TYPEDSIGNATURES) +""" function fmiCSInitialize!(m, o, ctx::FMI3CSFunctor, integrator) states = isdefined(m, :states) ? m.states : () inputs = o.inputs @@ -527,7 +887,7 @@ function fmiCSInitialize!(m, o, ctx::FMI3CSFunctor, integrator) if wrapper.instance !== nothing reset_instance!(wrapper) end - instance = get_instance_CS!(wrapper, states, inputs, params, t) + instance = get_instance_CS!(wrapper, inputs, params, t) if isdefined(m, :states) @statuscheck FMI.fmi3GetFloat64!(instance, ctx.state_value_references, m.states) end @@ -538,6 +898,9 @@ function fmiCSInitialize!(m, o, ctx::FMI3CSFunctor, integrator) return m end +""" + $(TYPEDSIGNATURES) +""" function fmiCSStep!(m, o, ctx::FMI3CSFunctor, integrator) wrapper = o.wrapper states = isdefined(m, :states) ? m.states : () @@ -546,7 +909,7 @@ function fmiCSStep!(m, o, ctx::FMI3CSFunctor, integrator) t = o.t dt = o.dt - instance = get_instance_CS!(wrapper, states, inputs, params, integrator.t) + instance = get_instance_CS!(wrapper, inputs, params, integrator.t) eventEncountered = Ref(FMI.fmi3False) terminateSimulation = Ref(FMI.fmi3False) earlyReturn = Ref(FMI.fmi3False) From 9614c0333c8b85a44dcdbbcaf3c244c6d8fbe1fc Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 7 Jan 2025 13:00:58 +0530 Subject: [PATCH 0651/1337] build: bump SymbolicIndexingInterface compat --- Project.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Project.toml b/Project.toml index b25d2a0562..13684a6a52 100644 --- a/Project.toml +++ b/Project.toml @@ -145,9 +145,9 @@ SpecialFunctions = "0.7, 0.8, 0.9, 0.10, 1.0, 2" StaticArrays = "0.10, 0.11, 0.12, 1.0" StochasticDiffEq = "6.72.1" StochasticDelayDiffEq = "1.8.1" -SymbolicIndexingInterface = "0.3.36" +SymbolicIndexingInterface = "0.3.37" SymbolicUtils = "3.10" -Symbolics = "6.22.1" +Symbolics = "6.23" URIs = "1" UnPack = "0.1, 1.0" Unitful = "1.1" From 1359630a1681ae8ee5045357a7d8a19e9289f654 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 7 Jan 2025 13:10:20 +0530 Subject: [PATCH 0652/1337] feat: mark inputs and outputs of FMU with appropriate metadata --- ext/MTKFMIExt.jl | 16 +++++++++++----- test/extensions/fmi.jl | 25 +++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/ext/MTKFMIExt.jl b/ext/MTKFMIExt.jl index 4fea249dd9..95e1c0ef26 100644 --- a/ext/MTKFMIExt.jl +++ b/ext/MTKFMIExt.jl @@ -132,7 +132,8 @@ function MTK.FMIComponent(::Val{Ver}; fmu = nothing, tolerance = 1e-6, # parse the inputs to the FMU inputs = [] fmi_variables_to_mtk_variables!(fmu, FMI.getInputValueReferencesAndNames(fmu), - value_references, inputs, states, observed) + value_references, inputs, states, observed; postprocess_variable = v -> MTK.setinput( + v, true)) # create a symbolic variable for the input buffer if isempty(inputs) __mtk_internal_x = [] @@ -145,7 +146,8 @@ function MTK.FMIComponent(::Val{Ver}; fmu = nothing, tolerance = 1e-6, # parse the outputs of the FMU outputs = [] fmi_variables_to_mtk_variables!(fmu, FMI.getOutputValueReferencesAndNames(fmu), - value_references, outputs, states, observed) + value_references, outputs, states, observed; postprocess_variable = v -> MTK.setoutput( + v, true)) # create the output buffer. This is only required for CoSimulation to pass it to # the callback affect if type == :CS @@ -284,18 +286,22 @@ defaults. # Keyword Arguments - `parameters`: A boolean indicating whether to use `@parameters` for the symbolic variables instead of `@variables`. +- `postprocess_variable`: A function applied to each created variable that should + return the updated variable. This is useful to add metadata to variables. """ function fmi_variables_to_mtk_variables!( fmu::Union{FMI.FMU2, FMI.FMU3}, varmap::AbstractDict, value_references::AbstractDict, truevars, allvars, - obseqs, defs = Dict(); parameters = false) + obseqs, defs = Dict(); parameters = false, postprocess_variable = identity) for (valRef, snames) in varmap stateT = FMI.dataTypeForValueReference(fmu, valRef) snames = map(parseFMIVariableName, snames) if parameters - vars = [MTK.unwrap(only(@parameters $sname::stateT)) for sname in snames] + vars = [postprocess_variable(MTK.unwrap(only(@parameters $sname::stateT))) + for sname in snames] else - vars = [MTK.unwrap(only(@variables $sname(t)::stateT)) for sname in snames] + vars = [postprocess_variable(MTK.unwrap(only(@variables $sname(t)::stateT))) + for sname in snames] end for i in eachindex(vars) if i == 1 diff --git a/test/extensions/fmi.jl b/test/extensions/fmi.jl index 868d60a9bb..4dc0fc9a67 100644 --- a/test/extensions/fmi.jl +++ b/test/extensions/fmi.jl @@ -9,9 +9,16 @@ const FMU_DIR = joinpath(@__DIR__, "fmus") truesol = FMI.simulate( fmu, (0.0, 8.0); saveat = 0.0:0.1:8.0, recordValues = ["mass.s", "mass.v"]) + function test_no_inputs_outputs(sys) + for var in unknowns(sys) + @test !MTK.isinput(var) + @test !MTK.isoutput(var) + end + end @testset "v2, ME" begin fmu = loadFMU("SpringPendulum1D", "Dymola", "2022x"; type = :ME) @mtkbuild sys = MTK.FMIComponent(Val(2); fmu, type = :ME) + test_no_inputs_outputs(sys) prob = ODEProblem{true, SciMLBase.FullSpecialize}( sys, [sys.mass__s => 0.5, sys.mass__v => 0.0], (0.0, 8.0)) sol = solve(prob, Tsit5(); reltol = 1e-8, abstol = 1e-8) @@ -28,6 +35,7 @@ const FMU_DIR = joinpath(@__DIR__, "fmus") Val(2); fmu, communication_step_size = 0.001, type = :CS) @variables x(t) = 1.0 @mtkbuild sys = ODESystem([D(x) ~ x], t; systems = [inner]) + test_no_inputs_outputs(sys) prob = ODEProblem{true, SciMLBase.FullSpecialize}( sys, [sys.inner.mass__s => 0.5, sys.inner.mass__v => 0.0], (0.0, 8.0)) @@ -44,6 +52,7 @@ const FMU_DIR = joinpath(@__DIR__, "fmus") @testset "v3, ME" begin fmu = loadFMU("SpringPendulum1D", "Dymola", "2023x", "3.0"; type = :ME) @mtkbuild sys = MTK.FMIComponent(Val(3); fmu, type = :ME) + test_no_inputs_outputs(sys) prob = ODEProblem{true, SciMLBase.FullSpecialize}( sys, [sys.mass__s => 0.5, sys.mass__v => 0.0], (0.0, 8.0)) sol = solve(prob, Tsit5(); reltol = 1e-8, abstol = 1e-8) @@ -60,6 +69,7 @@ const FMU_DIR = joinpath(@__DIR__, "fmus") Val(3); fmu, communication_step_size = 0.001, type = :CS) @variables x(t) = 1.0 @mtkbuild sys = ODESystem([D(x) ~ x], t; systems = [inner]) + test_no_inputs_outputs(sys) prob = ODEProblem{true, SciMLBase.FullSpecialize}( sys, [sys.inner.mass__s => 0.5, sys.inner.mass__v => 0.0], (0.0, 8.0)) @@ -75,6 +85,11 @@ end @testset "v2, ME" begin fmu = loadFMU(joinpath(FMU_DIR, "SimpleAdder.fmu"); type = :ME) @named adder = MTK.FMIComponent(Val(2); fmu, type = :ME) + @test MTK.isinput(adder.a) + @test MTK.isinput(adder.b) + @test MTK.isoutput(adder.out) + @test !MTK.isinput(adder.c) && !MTK.isoutput(adder.c) + @variables a(t) b(t) c(t) [guess = 1.0] @mtkbuild sys = ODESystem( [adder.a ~ a, adder.b ~ b, D(a) ~ t, @@ -92,6 +107,10 @@ end fmu = loadFMU(joinpath(FMU_DIR, "SimpleAdder.fmu"); type = :CS) @named adder = MTK.FMIComponent( Val(2); fmu, type = :CS, communication_step_size = 0.001) + @test MTK.isinput(adder.a) + @test MTK.isinput(adder.b) + @test MTK.isoutput(adder.out) + @test !MTK.isinput(adder.c) && !MTK.isoutput(adder.c) @variables a(t) b(t) c(t) [guess = 1.0] @mtkbuild sys = ODESystem( [adder.a ~ a, adder.b ~ b, D(a) ~ t, @@ -110,6 +129,9 @@ end @testset "v3, ME" begin fmu = loadFMU(joinpath(FMU_DIR, "StateSpace.fmu"); type = :ME) @named sspace = MTK.FMIComponent(Val(3); fmu, type = :ME) + @test MTK.isinput(sspace.u) + @test MTK.isoutput(sspace.y) + @test !MTK.isinput(sspace.x) && !MTK.isoutput(sspace.x) @variables u(t)=1.0 x(t)=1.0 y(t) [guess = 1.0] @mtkbuild sys = ODESystem( [sspace.u ~ u, D(u) ~ t, D(x) ~ sspace.x + sspace.y, y^2 ~ sspace.y + x], t; @@ -125,6 +147,9 @@ end fmu = loadFMU(joinpath(FMU_DIR, "StateSpace.fmu"); type = :CS) @named sspace = MTK.FMIComponent( Val(3); fmu, communication_step_size = 1e-3, type = :CS) + @test MTK.isinput(sspace.u) + @test MTK.isoutput(sspace.y) + @test !MTK.isinput(sspace.x) && !MTK.isoutput(sspace.x) @variables u(t)=1.0 x(t)=1.0 y(t) [guess = 1.0] @mtkbuild sys = ODESystem( [sspace.u ~ u, D(u) ~ t, D(x) ~ sspace.x + sspace.y, y^2 ~ sspace.y + x], t; From 66f2d75516e36484b61ef4d7f7ade1c65f9b9afa Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 8 Jan 2025 16:24:33 +0530 Subject: [PATCH 0653/1337] fix: fix array hack when looking through unknowns --- src/structural_transformation/symbolics_tearing.jl | 1 - 1 file changed, 1 deletion(-) diff --git a/src/structural_transformation/symbolics_tearing.jl b/src/structural_transformation/symbolics_tearing.jl index 173f430c04..f1a152b8e7 100644 --- a/src/structural_transformation/symbolics_tearing.jl +++ b/src/structural_transformation/symbolics_tearing.jl @@ -719,7 +719,6 @@ function cse_and_array_hacks(sys, obs, subeqs, unknowns, neweqs; cse = true, arr Symbolics.shape(sym) != Symbolics.Unknown() || continue arg1 = arguments(sym)[1] cnt = get(arr_obs_occurrences, arg1, 0) - cnt == 0 && continue arr_obs_occurrences[arg1] = cnt + 1 end for eq in neweqs From 7f1a7a167c582ea5d51efe01a32d4bfaea65aef6 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 8 Jan 2025 16:25:22 +0530 Subject: [PATCH 0654/1337] fix: fix several bugs causing incorrect results in FMU components --- ext/MTKFMIExt.jl | 364 +++++++++++++++++++++++------------------------ 1 file changed, 177 insertions(+), 187 deletions(-) diff --git a/ext/MTKFMIExt.jl b/ext/MTKFMIExt.jl index 95e1c0ef26..603ec8906c 100644 --- a/ext/MTKFMIExt.jl +++ b/ext/MTKFMIExt.jl @@ -84,12 +84,16 @@ with the name `namespace__variable`. supported by FMI.jl. - `communication_step_size`: The periodic interval at which communication with CoSimulation FMUs will occur. Must be provided for CoSimulation FMU components. +- `reinitializealg`: The DAE initialization algorithm to use for the callback managing the + FMU. For CoSimulation FMUs whose states/outputs are used in algebraic equations of the + system, this needs to be an algorithm that will solve for the new algebraic variables. + For example, `OrdinaryDiffEqCore.BrownFullBasicInit()`. - `type`: Either `:ME` or `:CS` depending on whether `fmu` is a Model Exchange or CoSimulation FMU respectively. - `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, reinitializealg = SciMLBase.NoInit(), type, name) where {Ver} if Ver != 2 && Ver != 3 throw(ArgumentError("FMI Version must be `2` or `3`")) end @@ -106,8 +110,14 @@ function MTK.FMIComponent(::Val{Ver}; fmu = nothing, tolerance = 1e-6, # this is a subset of `states` in the case where the FMU has multiple names for # the same value reference. diffvars = [] + # variables that are derivatives of diffvars + dervars = [] # observed equations observed = Equation[] + # need to separate observed equations for duplicate derivative variables + # since they aren't included in CS FMUs + der_observed = Equation[] + # parse states fmi_variables_to_mtk_variables!(fmu, FMI.getStateValueReferencesAndNames(fmu), value_references, diffvars, states, observed) @@ -115,12 +125,14 @@ function MTK.FMIComponent(::Val{Ver}; fmu = nothing, tolerance = 1e-6, # functions as the state vector if isempty(diffvars) # no differential variables - __mtk_internal_u = [] + __mtk_internal_u = Float64[] elseif type == :ME - # ME FMUs perform integration using the Julia solver, so unknowns of the FMU - # are unknowns of the `ODESystem` - @variables __mtk_internal_u(t)[1:length(diffvars)] [guess = diffvars] - push!(observed, __mtk_internal_u ~ copy(diffvars)) + # to avoid running into `structural_simplify` warnings about array variables + # and some unfortunate circular dependency issues, ME FMUs use an array of + # symbolics instead. This is also not worse off in performance + # because the former approach would allocate anyway. + # TODO: Can we avoid an allocation here using static arrays? + __mtk_internal_u = copy(diffvars) elseif type == :CS # CS FMUs do their own independent integration in a periodic callback, so their # unknowns are discrete variables in the `ODESystem`. A default of `missing` allows @@ -129,18 +141,30 @@ function MTK.FMIComponent(::Val{Ver}; fmu = nothing, tolerance = 1e-6, push!(observed, __mtk_internal_u ~ copy(diffvars)) end + # parse derivatives of states + # the variables passed to `postprocess_variable` haven't been differentiated yet, so they + # should match one variable in states. That's the one this is the derivative of, and we + # keep track of this ordering + derivative_order = [] + function derivative_order_postprocess(var) + idx = findfirst(isequal(var), states) + idx === nothing || push!(derivative_order, states[idx]) + return var + end + fmi_variables_to_mtk_variables!( + fmu, FMI.getDerivateValueReferencesAndNames(fmu), value_references, dervars, + states, der_observed; postprocess_variable = derivative_order_postprocess) + @assert length(derivative_order) == length(dervars) + # parse the inputs to the FMU inputs = [] fmi_variables_to_mtk_variables!(fmu, FMI.getInputValueReferencesAndNames(fmu), value_references, inputs, states, observed; postprocess_variable = v -> MTK.setinput( v, true)) # create a symbolic variable for the input buffer - if isempty(inputs) - __mtk_internal_x = [] - else - @variables __mtk_internal_x(t)[1:length(inputs)] [guess = inputs] - push!(observed, __mtk_internal_x ~ copy(inputs)) - push!(states, __mtk_internal_x) + __mtk_internal_x = copy(inputs) + if isempty(__mtk_internal_x) + __mtk_internal_x = Float64[] end # parse the outputs of the FMU @@ -152,7 +176,7 @@ function MTK.FMIComponent(::Val{Ver}; fmu = nothing, tolerance = 1e-6, # the callback affect if type == :CS if isempty(outputs) - __mtk_internal_o = [] + __mtk_internal_o = Float64[] else @parameters __mtk_internal_o(t)[1:length(outputs)]=missing [guess = zeros(length(outputs))] push!(observed, __mtk_internal_o ~ outputs) @@ -167,60 +191,58 @@ function MTK.FMIComponent(::Val{Ver}; fmu = nothing, tolerance = 1e-6, fmu, FMI.getParameterValueReferencesAndNames(fmu), value_references, params, [], parameter_dependencies, defs; parameters = true) # create a symbolic variable for the parameter buffer - if isempty(params) - __mtk_internal_p = [] - else - @parameters __mtk_internal_p[1:length(params)] - push!(parameter_dependencies, __mtk_internal_p ~ copy(params)) + __mtk_internal_p = copy(params) + if isempty(__mtk_internal_p) + __mtk_internal_p = Float64[] end + derivative_value_references = UInt32[value_references[var] for var in dervars] + state_value_references = UInt32[value_references[var] for var in diffvars] + output_value_references = UInt32[value_references[var] for var in outputs] input_value_references = UInt32[value_references[var] for var in inputs] param_value_references = UInt32[value_references[var] for var in params] # create a parameter for the instance wrapper # this manages the creation and deallocation of FMU instances + buffer_length = length(diffvars) + length(outputs) if Ver == 2 - @parameters wrapper::FMI2InstanceWrapper = FMI2InstanceWrapper( - fmu, param_value_references, input_value_references, tolerance) + @parameters (wrapper::FMI2InstanceWrapper)(..)[1:buffer_length] = FMI2InstanceWrapper( + fmu, derivative_value_references, state_value_references, output_value_references, + param_value_references, input_value_references, tolerance) else - @parameters wrapper::FMI3InstanceWrapper = FMI3InstanceWrapper( - fmu, param_value_references, input_value_references) + @parameters (wrapper::FMI3InstanceWrapper)(..)[1:buffer_length] = FMI3InstanceWrapper( + fmu, derivative_value_references, state_value_references, + output_value_references, param_value_references, input_value_references) end - output_value_references = UInt32[value_references[var] for var in outputs] - buffer_length = length(diffvars) + length(outputs) - # any additional initialization equations for the system initialization_eqs = Equation[] if type == :ME - # the functor is a callable struct which returns the state derivative and + # the wrapper is a callable struct which returns the state derivative and # output values - FunctorT = Ver == 2 ? FMI2MEFunctor : FMI3MEFunctor - _functor = FunctorT(output_value_references) - @parameters (functor::(typeof(_functor)))(..)[1:buffer_length] = _functor - - # symbolic expression for calling the functor - call_expr = functor( - wrapper, __mtk_internal_u, __mtk_internal_x, __mtk_internal_p, t) + # symbolic expression for calling the wrapper + call_expr = wrapper(__mtk_internal_u, __mtk_internal_x, __mtk_internal_p, t) # differential and observed equations diffeqs = Equation[] - for (i, var) in enumerate([D.(diffvars); outputs]) + for (i, var) in enumerate([dervars; outputs]) push!(diffeqs, var ~ call_expr[i]) end + for (var, dervar) in zip(derivative_order, dervars) + push!(diffeqs, D(var) ~ dervar) + end # instance management callback which deallocates the instance when # necessary and notifies the FMU of completed integrator steps finalize_affect = MTK.FunctionalAffect(fmiFinalize!, [], [wrapper], []) - step_affect = MTK.FunctionalAffect(fmiMEStep!, [], [wrapper], []) + step_affect = MTK.FunctionalAffect(Returns(nothing), [], [], []) instance_management_callback = MTK.SymbolicDiscreteCallback( - (t != t - 1), step_affect; finalize = finalize_affect, reinitializealg = SciMLBase.NoInit()) + (t != t - 1), step_affect; finalize = finalize_affect, reinitializealg = reinitializealg) - push!(params, wrapper, functor) - push!(states, __mtk_internal_u) + push!(params, wrapper) + append!(observed, der_observed) elseif type == :CS - state_value_references = UInt32[value_references[var] for var in diffvars] _functor = if Ver == 2 FMI2CSFunctor(state_value_references, output_value_references) else @@ -257,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, reinitializealg = SciMLBase.NoInit() + finalize = finalize_affect, reinitializealg = reinitializealg ) # guarded in case there are no outputs/states and the variable is `[]`. @@ -293,9 +315,15 @@ function fmi_variables_to_mtk_variables!( fmu::Union{FMI.FMU2, FMI.FMU3}, varmap::AbstractDict, value_references::AbstractDict, truevars, allvars, obseqs, defs = Dict(); parameters = false, postprocess_variable = identity) - for (valRef, snames) in varmap + for (valRef, varnames) in varmap stateT = FMI.dataTypeForValueReference(fmu, valRef) - snames = map(parseFMIVariableName, snames) + snames = Symbol[] + ders = Int[] + for name in varnames + sname, der = parseFMIVariableName(name) + push!(snames, sname) + push!(ders, der) + end if parameters vars = [postprocess_variable(MTK.unwrap(only(@parameters $sname::stateT))) for sname in snames] @@ -303,6 +331,14 @@ function fmi_variables_to_mtk_variables!( vars = [postprocess_variable(MTK.unwrap(only(@variables $sname(t)::stateT))) for sname in snames] end + for i in eachindex(vars) + der = ders[i] + vars[i] = MTK.unwrap(vars[i]) + for j in 1:der + vars[i] = D(vars[i]) + end + vars[i] = MTK.default_toterm(vars[i]) + end for i in eachindex(vars) if i == 1 push!(truevars, vars[i]) @@ -321,10 +357,22 @@ end $(TYPEDSIGNATURES) Parse the string name of an FMI variable into a `Symbol` name for the corresponding -MTK vriable. +MTK variable. Return the `Symbol` name and the number of times it is differentiated. """ function parseFMIVariableName(name::AbstractString) - return Symbol(replace(name, "." => "__")) + name = replace(name, "." => "__") + der = 0 + if startswith(name, "der(") + idx = findfirst(',', name) + if idx === nothing + name = @view name[5:(end - 1)] + der = 1 + else + der = parse(Int, @view name[(idx + 1):(end - 1)]) + name = @view name[5:(idx - 1)] + end + end + return Symbol(name), der end """ @@ -342,6 +390,20 @@ mutable struct FMI2InstanceWrapper """ const fmu::FMI.FMU2 """ + The value references for derivatives of states of the FMU, in the order that the + caller expects them to be returned when calling this struct. + """ + const derivative_value_references::Vector{FMI.fmi2ValueReference} + """ + The value references for the states of the FMU. + """ + const state_value_references::Vector{FMI.fmi2ValueReference} + """ + The value references for outputs of the FMU, in the order that the caller expects + them to be returned when calling this struct. + """ + const output_value_references::Vector{FMI.fmi2ValueReference} + """ The parameter value references. These should be in the same order as the parameter vector passed to functions involving this wrapper. """ @@ -366,10 +428,12 @@ end Create an `FMI2InstanceWrapper` with no instance. """ -function FMI2InstanceWrapper(fmu, params, inputs, tolerance) - FMI2InstanceWrapper(fmu, params, inputs, tolerance, nothing) +function FMI2InstanceWrapper(fmu, ders, states, outputs, params, inputs, tolerance) + FMI2InstanceWrapper(fmu, ders, states, outputs, params, inputs, tolerance, nothing) end +Base.nameof(::FMI2InstanceWrapper) = :FMI2InstanceWrapper + """ $(TYPEDSIGNATURES) @@ -381,6 +445,10 @@ time. Returns the created instance, which is also stored in `wrapper.instance`. """ function get_instance_common!(wrapper::FMI2InstanceWrapper, inputs, params, t) wrapper.instance = FMI.fmi2Instantiate!(wrapper.fmu)::FMI.FMU2Component + if !isempty(inputs) + @statuscheck FMI.fmi2SetReal(wrapper.instance, wrapper.input_value_references, + Csize_t(length(wrapper.param_value_references)), inputs) + end if !isempty(params) @statuscheck FMI.fmi2SetReal(wrapper.instance, wrapper.param_value_references, Csize_t(length(wrapper.param_value_references)), params) @@ -388,11 +456,6 @@ function get_instance_common!(wrapper::FMI2InstanceWrapper, inputs, params, t) @statuscheck FMI.fmi2SetupExperiment( wrapper.instance, FMI.fmi2True, wrapper.tolerance, t, FMI.fmi2False, t) @statuscheck FMI.fmi2EnterInitializationMode(wrapper.instance) - if !isempty(inputs) - @statuscheck FMI.fmi2SetReal(wrapper.instance, wrapper.input_value_references, - Csize_t(length(wrapper.param_value_references)), inputs) - end - return wrapper.instance end @@ -425,9 +488,13 @@ present and create a new one otherwise. Return the instance. See `get_instance_common!` for a description of the arguments. """ -function get_instance_CS!(wrapper::FMI2InstanceWrapper, inputs, params, t) +function get_instance_CS!(wrapper::FMI2InstanceWrapper, states, inputs, params, t) if wrapper.instance === nothing get_instance_common!(wrapper, inputs, params, t) + if !isempty(states) + @statuscheck FMI.fmi2SetReal(wrapper.instance, wrapper.state_value_references, + Csize_t(length(wrapper.state_value_references)), states) + end @statuscheck FMI.fmi2ExitInitializationMode(wrapper.instance) end return wrapper.instance @@ -436,12 +503,10 @@ end """ $(TYPEDSIGNATURES) -If `wrapper.instance !== nothing`, tell the FMU that an integrator step has been accepted. -This is relevant only for ModelExchange FMUs. +Call `fmiXCompletedIntegratorStep` with `noSetFMUStatePriorToCurrentPoint` as false. """ -function complete_step!(wrapper::FMI2InstanceWrapper) - wrapper.instance === nothing && return - @statuscheck FMI.fmi2CompletedIntegratorStep(wrapper.instance, FMI.fmi2True) +function partiallyCompleteIntegratorStep(wrapper::FMI2InstanceWrapper) + @statuscheck FMI.fmi2CompletedIntegratorStep(wrapper.instance, FMI.fmi2False) end """ @@ -472,6 +537,17 @@ mutable struct FMI3InstanceWrapper """ const fmu::FMI.FMU3 """ + The value references for derivatives of states of the FMU, in the order that the + caller expects them to be returned when calling this struct. + """ + const derivative_value_references::Vector{FMI.fmi3ValueReference} + const state_value_references::Vector{FMI.fmi3ValueReference} + """ + The value references for outputs of the FMU, in the order that the caller expects + them to be returned when calling this struct. + """ + const output_value_references::Vector{FMI.fmi3ValueReference} + """ The parameter value references. These should be in the same order as the parameter vector passed to functions involving this wrapper. """ @@ -492,10 +568,12 @@ end Create an `FMI3InstanceWrapper` with no instance. """ -function FMI3InstanceWrapper(fmu, params, inputs) - FMI3InstanceWrapper(fmu, params, inputs, nothing) +function FMI3InstanceWrapper(fmu, ders, states, outputs, params, inputs) + FMI3InstanceWrapper(fmu, ders, states, outputs, params, inputs, nothing) end +Base.nameof(::FMI3InstanceWrapper) = :FMI3InstanceWrapper + """ $(TYPEDSIGNATURES) @@ -550,11 +628,15 @@ present and create a new one otherwise. Return the instance. See `get_instance_common!` for a description of the arguments. """ -function get_instance_CS!(wrapper::FMI3InstanceWrapper, inputs, params, t) +function get_instance_CS!(wrapper::FMI3InstanceWrapper, states, inputs, params, t) if wrapper.instance === nothing wrapper.instance = FMI.fmi3InstantiateCoSimulation!( wrapper.fmu; eventModeUsed = false)::FMI.FMU3Instance get_instance_common!(wrapper, inputs, params, t) + if !isempty(states) + @statuscheck FMI.fmi3SetFloat64( + wrapper.instance, wrapper.state_value_references, states) + end @statuscheck FMI.fmi3ExitInitializationMode(wrapper.instance) end return wrapper.instance @@ -562,17 +644,12 @@ end """ $(TYPEDSIGNATURES) - -If `wrapper.instance !== nothing`, tell the FMU that an integrator step has been accepted. -This is relevant only for ModelExchange FMUs. Asserts that the simulation does not need -to be terminated and does not require entering event mode. """ -function complete_step!(wrapper::FMI3InstanceWrapper) - wrapper.instance === nothing && return +function partiallyCompleteIntegratorStep(wrapper::FMI3InstanceWrapper) enterEventMode = Ref(FMI.fmi3False) terminateSimulation = Ref(FMI.fmi3False) @statuscheck FMI.fmi3CompletedIntegratorStep!( - wrapper.instance, FMI.fmi3True, enterEventMode, terminateSimulation) + wrapper.instance, FMI.fmi3False, enterEventMode, terminateSimulation) @assert enterEventMode[] == FMI.fmi3False @assert terminateSimulation[] == FMI.fmi3False end @@ -587,88 +664,14 @@ function reset_instance!(wrapper::FMI3InstanceWrapper) wrapper.instance = nothing end -""" - $(TYPEDEF) - -A callable struct useful for simulating v2 Model Exchange FMUs. When called, updates the -internal state of the FMU and gets updated values for continuous state derivatives and -output variables. - -# Fields - -$(TYPEDFIELDS) -""" -struct FMI2MEFunctor - """ - The value references for outputs of the FMU, in the order that the caller expects - them to be returned when calling `FMI2MEFunctor`. - """ - output_value_references::Vector{FMI.fmi2ValueReference} -end - -@register_array_symbolic (fn::FMI2MEFunctor)( - wrapper::FMI2InstanceWrapper, states::Vector{<:Real}, - inputs::Vector{<:Real}, params::Vector{<:Real}, t::Real) begin +@register_array_symbolic (fn::FMI2InstanceWrapper)( + states::Vector{<:Real}, inputs::Vector{<:Real}, params::Vector{<:Real}, t::Real) begin size = (length(states) + length(fn.output_value_references),) eltype = eltype(states) ndims = 1 end -""" - $(TYPEDSIGNATURES) - -Update `wrapper.instance` with the new values of state, input and independent variables. -""" -function update_instance_ME!(wrapper::FMI2InstanceWrapper, states, inputs, t) - instance = wrapper.instance - @statuscheck FMI.fmi2SetTime(instance, t) - @statuscheck FMI.fmi2SetContinuousStates(instance, states) - if !isempty(inputs) - @statuscheck FMI.fmi2SetReal(instance, wrapper.input_value_references, - Csize_t(length(wrapper.param_value_references)), inputs) - end -end - -""" - $(TYPEDSIGNATURES) - -Get the FMU instance (creating and initializing it if not present), update it -with the current values of variables, and return a vector of the state derivatives -and output variables. -""" -function (fn::FMI2MEFunctor)(wrapper::FMI2InstanceWrapper, states, inputs, params, t) - instance = get_instance_ME!(wrapper, inputs, params, t) - update_instance_ME!(wrapper, states, inputs, t) - - # TODO: Find a way to do this without allocating. We can't pass a view to these - # functions. - states_buffer = zeros(length(states)) - @statuscheck FMI.fmi2GetDerivatives!(instance, states_buffer) - outputs_buffer = zeros(length(fn.output_value_references)) - FMI.fmi2GetReal!(instance, fn.output_value_references, outputs_buffer) - return [states_buffer; outputs_buffer] -end - -""" - $(TYPEDEF) - -A callable struct useful for simulating v3 Model Exchange FMUs. When called, updates the -internal state of the FMU and gets updated values for continuous state derivatives and -output variables. - -# Fields - -$(TYPEDFIELDS) -""" -struct FMI3MEFunctor - """ - The value references for outputs of the FMU, in the order that the caller expects - them to be returned when calling `FMI3MEFunctor`. - """ - output_value_references::Vector{FMI.fmi3ValueReference} -end - -@register_array_symbolic (fn::FMI3MEFunctor)( +@register_array_symbolic (fn::FMI3InstanceWrapper)( wrapper::FMI3InstanceWrapper, states::Vector{<:Real}, inputs::Vector{<:Real}, params::Vector{<:Real}, t::Real) begin size = (length(states) + length(fn.output_value_references),) @@ -679,50 +682,30 @@ end """ $(TYPEDSIGNATURES) -Update `wrapper.instance` with the new values of state, input and independent variables. -""" -function update_instance_ME!(wrapper::FMI3InstanceWrapper, states, inputs, t) - instance = wrapper.instance - @statuscheck FMI.fmi3SetTime(instance, t) - @statuscheck FMI.fmi3SetContinuousStates(instance, states) - if !isempty(inputs) - @statuscheck FMI.fmi3SetFloat64(instance, wrapper.input_value_references, inputs) - end -end - -""" - $(TYPEDSIGNATURES) - -Get the FMU instance (creating and initializing it if not present), update it -with the current values of variables, and return a vector of the state derivatives -and output variables. +Update the internal state of the ME FMU and return a vector of updated values +for continuous state derivatives and output variables respectively. Needs to be a +callable struct to enable symbolic registration with an inferred return size. """ -function (fn::FMI3MEFunctor)(wrapper::FMI3InstanceWrapper, states, inputs, params, t) +function (wrapper::Union{FMI2InstanceWrapper, FMI3InstanceWrapper})( + states, inputs, params, t) instance = get_instance_ME!(wrapper, inputs, params, t) - update_instance_ME!(wrapper, states, inputs, t) - # TODO: Don't allocate + # TODO: Find a way to do this without allocating. We can't pass a view to these + # functions. states_buffer = zeros(length(states)) - @statuscheck FMI.fmi3GetContinuousStateDerivatives!(instance, states_buffer) - outputs_buffer = zeros(length(fn.output_value_references)) - FMI.fmi3GetFloat64!(instance, fn.output_value_references, outputs_buffer) + outputs_buffer = zeros(length(wrapper.output_value_references)) + # Defined in FMIBase.jl/src/eval.jl + # Doesn't seem to be documented, but somehow this is the only way to + # propagate inputs to the FMU consistently. I have no idea why. + instance(; x = states, u = inputs, u_refs = wrapper.input_value_references, + p = params, p_refs = wrapper.param_value_references, t = t) + # the spec requires completing the step before getting updated derivative/output values + partiallyCompleteIntegratorStep(wrapper) + instance(; dx = states_buffer, dx_refs = wrapper.derivative_value_references, + y = outputs_buffer, y_refs = wrapper.output_value_references) return [states_buffer; outputs_buffer] end -""" - $(TYPEDSIGNATURES) - -An affect function for use inside a `FunctionalAffect`. This should be triggered every -time an integrator step is accepted. Expects `p` to be a 1-length array containing -the index of the instance wrapper (`FMI2InstanceWrapper` or `FMI3InstanceWrapper`) in -the parameter object. -""" -function fmiMEStep!(integrator, u, p, ctx) - wrapper_idx = p[1] - wrapper = integrator.ps[wrapper_idx] - complete_step!(wrapper) -end - """ $(TYPEDSIGNATURES) @@ -762,7 +745,7 @@ function (fn::FMI2CSFunctor)(wrapper::FMI2InstanceWrapper, states, inputs, param states = states isa SubArray ? copy(states) : states inputs = inputs isa SubArray ? copy(inputs) : inputs params = params isa SubArray ? copy(params) : params - instance = get_instance_CS!(wrapper, inputs, params, t) + instance = get_instance_CS!(wrapper, states, inputs, params, t) if isempty(fn.output_value_references) return eltype(states)[] else @@ -799,7 +782,7 @@ function fmiCSInitialize!(m, o, ctx::FMI2CSFunctor, integrator) reset_instance!(wrapper) end - instance = get_instance_CS!(wrapper, inputs, params, t) + instance = get_instance_CS!(wrapper, states, inputs, params, t) if isdefined(m, :states) @statuscheck FMI.fmi2GetReal!(instance, ctx.state_value_references, m.states) end @@ -826,7 +809,11 @@ function fmiCSStep!(m, o, ctx::FMI2CSFunctor, integrator) t = o.t dt = o.dt - instance = get_instance_CS!(wrapper, inputs, params, integrator.t) + instance = get_instance_CS!(wrapper, states, inputs, params, integrator.t) + if !isempty(inputs) + FMI.fmi2SetReal( + instance, wrapper.input_value_references, Csize_t(length(inputs)), inputs) + end @statuscheck FMI.fmi2DoStep(instance, integrator.t - dt, dt, FMI.fmi2True) if isdefined(m, :states) @@ -864,7 +851,7 @@ function (fn::FMI3CSFunctor)(wrapper::FMI3InstanceWrapper, states, inputs, param states = states isa SubArray ? copy(states) : states inputs = inputs isa SubArray ? copy(inputs) : inputs params = params isa SubArray ? copy(params) : params - instance = get_instance_CS!(wrapper, inputs, params, t) + instance = get_instance_CS!(wrapper, states, inputs, params, t) if isempty(fn.output_value_references) return eltype(states)[] @@ -893,7 +880,7 @@ function fmiCSInitialize!(m, o, ctx::FMI3CSFunctor, integrator) if wrapper.instance !== nothing reset_instance!(wrapper) end - instance = get_instance_CS!(wrapper, inputs, params, t) + instance = get_instance_CS!(wrapper, states, inputs, params, t) if isdefined(m, :states) @statuscheck FMI.fmi3GetFloat64!(instance, ctx.state_value_references, m.states) end @@ -915,7 +902,10 @@ function fmiCSStep!(m, o, ctx::FMI3CSFunctor, integrator) t = o.t dt = o.dt - instance = get_instance_CS!(wrapper, inputs, params, integrator.t) + instance = get_instance_CS!(wrapper, states, inputs, params, integrator.t) + if !isempty(inputs) + FMI.fmi3SetFloat64(instance, wrapper.input_value_references, inputs) + end eventEncountered = Ref(FMI.fmi3False) terminateSimulation = Ref(FMI.fmi3False) earlyReturn = Ref(FMI.fmi3False) From 12d3454c8c848c0cf5f33eecda494296db6bdc1c Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 8 Jan 2025 16:25:41 +0530 Subject: [PATCH 0655/1337] test: update `SimpleAdder.fmu` --- test/extensions/fmus/SimpleAdder.fmu | Bin 658163 -> 659450 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/test/extensions/fmus/SimpleAdder.fmu b/test/extensions/fmus/SimpleAdder.fmu index a1c3fd2728376ebed8f8dfd81819b0be51c84bef..ed12961bfb0a2c03a9ccbe1c746028aa168fadb3 100644 GIT binary patch delta 210707 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

A9>}LD;e9WWP$oz^=~~UxU%-nX>%=%e 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

mIS+Z|y{sK^b zMruCeBo~8Uq3^#J0G7|RUow5{3AV(lc1AkTZKiUsekA#PB-r-SPvYInXWgTz5A}No zG3Yl%DX;~pW}m8)5``IoG)Oe^!ndH51AS%>=OvU?xm*M`8aXgbqX2Rj{esj(Wyvpo7jELCw_E#!P+82 z+%S(&kV)hDn6X_x-het)8>D%ENL`(Ws~R-2_H*R;7BSR=E{98*wXP3CQ4{(T`nc9a zHU@V^4K$%iqaO78{YkV+>NrlXjZLosNG=EBGyO+2F1lW&X?MtHa#w03_!@(yZ97d553)k;UBK}79Sgl_I3;24(Cx620GvP^f zFW9HyVGX7-?kIJ)g4iDBm%rA>djvLHwI^x(Ag|vfc}>&>10s|w{eWCcoHWE!e)*8P z2IT^~I(HDSR2XmDbRd0j9V@^}NxoH?gK$@>Kzci^iwF?h}z7!W*Xsaj4J;01>k z_Kn_Wq<=DEDmb)%Xl7z4+43&X;P&`r3`>hqV7FHN)!iCdA*Ay=v0Qh)%1$tDx`IwH za;~HkjJ-omLUhdfSkiSLt6Z$F+#4(RW0gw1QSuh&)K zSN_7(xYf)OXep4SvKq}COO=13MO3NjNNEO|PCoU zU(Ut;?YMw1b!B_ur;Az73W3R;8=Gag37xx!By4g+Bex7;chQ~?aAiTJn1QQDd0;Yi zBJy*(snza(WH+%p+1W9ElpeoVxhI!k-$6}e%^#vSHDG|;1gOGNHC)2za)>tcp6*9Z z#F$PB#&j|aHqyEmN&InUc1v83450aQ0`}O6oIXHjE9?nf6O%%{$xE^Olv?;NP@F;i zDk`hnm#Bz=52e7(d9JEwH8 z*XuMuWOk>4*~!&9aeNkmP&e|r`II>Y!*}^z6~O5(Qb5I8d|>NQR%!#<1>-4vOl}|2 zT+mE^LyB2pNTzM*dMIXrF>N(97ep4_saZ1rKlYw9JgO?oS24i=iIpHoP?kqXNYIdg zqb;Pv2L%Btng9wSC^o6(B`K-Rdar=Aw2-bKR+mxe8N1aPN2h06olyqt&kb88tU+8@ znnm=RcIg?c5S9oMkdD-x<-Mv`OIX_WANjt2%G>U__uO;Ox#!$_&pkJ^yx44m?jvhg zRWNH0#s(Q1F^@ms-_Y*=KE?XH$_w+qkK+ZFTx%(@!?-eHhhG#Kg!ZEs$YVf49<(wu zl=JwOF4q-_zs1tIkYgid1KR zrm^pb4{b?u-Yn=rp8AgF?rCIHo}hl%ubA7WEEQBEE_X_;>Nl1cEwob5$HQ#7_1%$( z1+hYS_7-xp&n7&yP!>H)_9({Wp%^B-qgI7%-?zIvg8i9VUx%)w_RW{7jz?!ogQJQo&Vp$$qjRsr(z&rSiO3*o_}OV1^sIbCY^@j;SrZKuF3*jW`(Y|1o)G z-h!#|RnN>(#uYs|BE}qQY3ui)na53wK{;0J?Xhv!o~FkA*@s%=;@CL+`Ht?mz#Q6M zxrF)6+3Xg^l6BJvTwvm!A82HMh)5R8ZY;dMT9WWeVn^;dE8*9WVD z-HUxL+0k8(^|$lpz^d(!Aqt?B>lJ~@M@}8JijQ$rN0R9K_4Z_smm+Zb4JOtTYou_iMDbSs1T!qxRBb0s|o_hJp z;_X>{r+7IX+Fho=*6${Vvi=?3q?v*r%AcD;Rl248s?eG^2a7M+!J-l!EYxrKGSR`p zpt;qp#AAy(@~c9@BnOLsSela*@u(?E>0|Bhkvy14!htI2ctgbDgB^)L^I66NM>DHi z2E02Q9m%rKkE90fS4=X#dbnQ3$v64|12orNv`JL#SP)-=N`6yv?pDtIdCvW6x@6xx zJiZ4}v(1UgN8;rF3X*b`j|0(F-6JB}y3Eg>cT&s8`Rd?FmX8U4+Y{s?NxxL#(Vl-b z!T83w@?&~FLtI!uo#V^YH)+ZO10-$vhLi<#-Vg2|MRD&|RZ&z0_f?11MiqrQ_%11m z8+Wpzz_>=$!L_QQxJ6R~wBENh>YuE2C<~~VaQ2RP1@V7EqJr=i5|tN-Ngeczw3G_s zcL`Z=o4q`Y+U#h50dM}{HrqNZ6-Ug~IHLWiA=MDBGCB9hIrqmo_tPUN_qt)JIiejq z0FF%( zK9SHLqJQil^pC9||Jdr40z1fl-EO53QYv3q9bPLNu)nw-okMTD))4n)27IYo%`URI zcGPa63~}u{xH_uSUim<$v2wzrHz#r8-s8lHfoDq`z$^uI_$~Gkfo}CK231C4~Dy z_JL&uS%OlbQ_(lZ8!x`$RHwFnRHuI(tdB{^wM)5w9V13!9zPu5wwgXT)>hZPnPU7v zt#wix6Rdx)I})eQq1C&&Cmm^s^`ws&RNwVR1IPj z1r)(eq{?n7sDj}*H7^OFS0oCYy@7=AS~|OL{Apuz7O_IcG9YGUDC?z*%HPCP@`wAH zQVF1c9xoQbA{4)&Kc8+?H*WGcYdUu|%c-kb zj?J@^*aGrhsPAg*c|^3|525TgFwp{oF04lHNhpD_b|&Y(U=Za#opb;1!IXR9pdQqJ zs9@m0dx0;y%`M(;JSBS5ku;o?3GNAed9^ukez2T7wyPcYM6UF((ZB?(k)$14P zm1Eg%he~cf>wf|U_|VC<^gjiUIp#BehX9p^tk`}BWA5ZmV=mT0G)Ff1JE1=r->Kx) z#n$60w(u<4cK@kZ6t~3jR*WAH4^;Pc0Y47=!^QZqrmbiF6gZk!gUcoClCEw|J~0r7 zmF%n1`syY5qaQOipA*y}P?0*w>1P(Q^ZF!|w*_{n1Sk-Z%6dSv+0(^F&~O{5m;M zvEE>Og17Kgzb^0|p0mt8f$wlW%+MNWji3|x2C8V~roh<+l^p7}22)%VM zYjY2-@-!AZLw)pz*At3`1EZUWwv&;x(j7^306(1C0en}ISNDs#=ozUxlCMt<^>h-{ z;@s#2hk=o50P*Q4@6qLZ?)g-IF5lY*5$r!x7TJBTOZFolN-i7v`Ttop!R`9ZUz4vR z&S=0H`xn;fc7821=y!hoT2Qs;8L}&G#lmp5{ZAX?cYcl1M$ICRnmc}yazB@eG5>0S zfkO41&WzjFbwZh|?dg(do7EGv&Mv16eutPr2-q5ZsaFBk??e`1KoI@PS%dvRS1rG=}N2u~X2`i3pU? z3`%z}W(C(ufgACO{&adOG(RpggkM}6=fw8Qbc|?~(R4L*d$?{7fMW;{ZpN_?C(OQ7 z7t}&onOY=*d7IR|&4fUIj*?SnEG2cexR>c2vgbl(?QAwkFe|#3o*w6!wbK&KEzSZl z$sY%8-)(P`aViQ*7F-Ghe@5W!RsOH0;<=QqSLrY`qg4l0Wz+x&pX`H1o*X7Q z`!j9{oX<3_x(cugYzG@~+3N!C(l{^wCG^ldTn}hQ3lAs$n-{`Ou#2{^m_uIlM?;ga z{Q-Ec41UD1TS;MmgbQBpjxeoJq}QgYGm~nV%4wTvPGIGexMhDCv@QplQ2G&>#+fKB zhPRfl#?acP;1M7w>etGUIS-^0ENo|5B9$TK4ybK!ut4V<;orP#&=$Q0Pjk{u%c+D0 z`ge>`X?;nQe(VQQdW3c!gGTSC4XNlI>>`R=HHyziJ6TG9ptqgqy#l0rC{jU~iygJ|!6P4gwQbQU;_wQs8q-J=;=y9qdB zJCC)CjJgm%{iyGrzvY7Ps~P?ghVQ!Oa^Yt&{4|E&dfD(f4Bw04e{A=i?}F4~evf&TIix z&n6sy4rY@PrLOlNTn}4#o)Y3rC}yTMBW3a|&SKb-=RfP?x$t~1+!T9h8+Q)&igK)# z3wi$)0^C7&56s6eLv{6q48NWb;NPvKJt%!QeuisN+TK=_;2X;!SR zO~_||y7S?O-=>ML@FVMC7-ecpPtS+pM8)y&Vt6*U%u1s!^KiI+Eq9r_1VRdO`8+nh z`)gI8&46fElq*ASXG53eKqDl_2I?V)9-h48+0j3Ie;g8lOfEiY!_NTw=qKG*+Z#{imJtqgke z^!^2iD527aQ0c5@uiVl9a>xL5nm6vC?<>AT+Q08V(RA)Gu;h}=W-x4bp_4|uzg6P=@kP&%1i330gv9t@ zIl=uVv^^H80aAk>R|hwTmxGe7L>HmJ_kEW4Guu8|ZwM?4fwIn|aZK9S)~PFh_bQX2 zE^dy`svA$(J~Foqcz8b&G;3#dC=-X+Sq2cgld+2Eg9W40v+BBx1+|> zEgrc!o>$5}=Z@A;q@E@%Mkajbn59i`(dVXC6s@)`o{MPJir7KPai`r; zE~2|HQ7X((om|9OE@A~|<)9DRo(CQ)(>YIBT> zc$SN(WvB+atnngn@qpe`#G0i9^JuJy)tvY#F5-DEVm%j8Jm4>V2EF;@EwQaOLmH-<7|AD}Pu1uKeMjNdF7v z@L%aTuVc-&%bp>TP8%$>s4iF7$dl&QhCzmxa<& zuf=T_#)uqUz!I^2rnwnLS~Sx(6!OQOvuspU9i z*)NGu^9Dun%H?Hc^$w-MZ!NWXTxB!8lDo!Zmz)lpCH{ebpybkgA+C1i;#8l{<0}`Z z5dq_>q~cs`qc@^YQv5zQq?vJdb#>_|VWwXZJ#}iT;nY46{#&kH)y7*9e?2*|>Amg< z{yu@96Mi4U?@jpO@D>dzG&ArXXN2&`DW6rC;_*A}L@y82N|e18pX^W?MXv=K!{-+3 z0DWMLQi1A!BQ9ycXCRp!>1p#g;a#C0@lAyM8z96gXqlyApYTWlVy z16s(k7^rEnIuS@#EQ%l6QwIK6_(T>156?I zl$FN~Oq^Wp+4>TQLck3_S^=ptzT4dZVu*}tLz!>@BS?z&xkgeNJa*x388e8M?4q~j z&}J8)NDnYGEw8DAcZ8Tvfu0V z$lM!Ru}|&T3*IImkzTj;r>#7e=mH0$ZyVN)bYpo zb;3_}xcp8FHuge?yUw%l0T~!7X3N<$g&S|ll?pC22CD#Kx};2f&`&w2rZX~Yy`+ek zA90qXeJ}jEzeF>1d=_`T1XlK4@D&7q62PL#Ky^2WUY`e*kq@#V3(09O5v>5aL}Gz# zuq>AFEy#$iL9_yTi{#7B%oa5T>m1lkD8mIn*kw_KIe?$c_mB(dNV{b9*Vj`Pbsho4 zS%&vQo!@OEdrF8|y=aS|0Vzyf3cUg%Q&eaxDiP~F3Miz~o5#p~gxhD&oPm0O4f`bS zt3co-HGPVt7N5`37)>A+l@y6U*rNLrvYm$3qX^bMQW$Go5`$6Sa-qTuY*aKctdfuN zh%1g!x&S^N>jUs98KGaKs%KK-@`O}TLScJqEhlpmQDfIpy$T(%kEMe2Alc3WkQ+KW zL6)c?gpz#4QlDR*FE;`oxbmZa&x*(A*51Jz!S0Yj7dHy3DJ?@ny98Vy$@%5?#W2!Q zD2ncMpccOu3+rMOhVONKg#2;=<4C0aPj}=7_~HHI@QV#Hf&R!xkhc|ER5c~1h#D(_ zWvb8s4RlEqD5^;8l0t=P{qi5*J$UHzWuHypr}Y)NM@Bb2{o4m8{Bm)BRm0}OiU~5* zTdI_xrji{h1-efr!$`B5r0Bb7mF$8Z=)OYx{bF%peMw>cC{PzN>5U1H$d2EuK+%*_ zCNmg%0>BeVe5?WCxxPeH94_saz~*Srf*ucD7Qzgv9vT`%oYI5JQU$v_F#{bMu`i$w ze=hkv$dZc3Bg!u5ze@Xm=m)W6*|>`oR7>FW;`Ol z2wG}AYb;BXu&jaMGm5>#4WxvYCs`{I{qz#@_S2YN?G!K>iL(?`iX$iC?nPeu((8l7H zR;*GqU#VCHH40*dDp4y`lXU)T?fvXC=j?MPO#y%J|M$M;LwRODd+oLNS!dsGM^rcU zt&NR=Snjam#rArCkssb@Xbv>27xg(6odz1hQ-7qHD{&f2CWOesDY<$1O~&Mgc)&1a z+I%s{iRl*E|Cu6&WmB3?Bo-1CD#3{arQ8X{;zBHLTzv7xo~jiUtE$VXdRkHDsa{g* zDPLY*U0zgCer=IpNyOj*H$5Kd3i4nxZmglCtn;i4&}*B263-&~N$dNTqlo zml1y7o~$U3R>(u3;ik**@|-4b3?*1S+gM)r1wfbJj;=5Tom%P)cbuG zkzUqMK)G*rlYF(+MJub_UT9-WkShFGpdr!}4&KNeY1z-s2?jZL7!9p4ZcMC`B=d~T zo+&wbv*MmLQ{wBWnF&x0KqHI|zF3$WQWWFhjk3Fcp+?!3Agy84G4}YVjX~Xy_K})5 zs<(JbjA{?65`C%Gp@9-32Ejc3qK?KD;xS8?m$*G-u`kT_G7}aMqaOdpu&*W9ASRJ$ zbQ1{4_L3()#DrszYBGALjk%sx)O*PZwRtL5&``UoicQW>%}P=eLrM*eq(qgW@K=9rk-j7C_PGLTTnb(cX^!n_mJXsp{RXs@U&9@aXDdU-y|Fror~ z6bghglU21*O3YbFHwN1SepSCUnHB0VI^613O%F9qs%jlZkxMPB>4~fQbr>zfRkhTi zm$RUO)O1;DnpC_>7RxoNbRo(=Wa%}Wb5+=1!Gg4eH#t1}x!);)Ym6^!SEVlAlXm2mZfQ)W8**rMrfq zH4TXZ4L+{lNVz}AKBE4hcSq^wt{ay(kFknQ9(_I9ALqVIQ z%E~U1aSwId+Y_jcRG+`mxF*sXQ%gWWtW+gp5qh{lIMUkGEOIBy@B_=|z0inQ*My9K zSXjX<1e+ybZ#?5hRe)E~XpGOZ$U-9=6L(EwKqr$Vd`0fy z>bNZ+8g&+iEDv!*QU&KgDp%6ip@Y3CJG*(;Mp_zbp<0MieiwtF08hfo)j+TT4H7Sm47%9Z(_a%I)BO{xLK+ISz{Ni%fhBJWZO;EjmW+v}KF`kh+ z^4yh3&-`~41LE=1sEylnF~>ceIuTfn70C__&HS3EZ;%M`gt)2JW++#U(l3xUg;`|U zY_jf!3-Mzrf~2vA^h{$7GqT~+PcdCcWBI9gTz6e-rp%yvYD&&buPEjDUesRs%)&*z z?>sM6DXT4cj`XNZDe|0u$eC#c0@7dp^_wSjF`w~FQL5;pCJ|XL$nA_6MpGA&9=X1i zUujl+YOYZh3j{=SIgCuX>Q9O;t&0?OLBy2d!YRS2DQTH+b0qk#InlnG#&*x!mL6463;<=bE>FN~W*GmON_rsOa+Y z^hR@D*MFgl=7mnpZk(}#r|3j&8kY}GDW&dvAV*GO@S*}UaB)|FV%@+-;TqKJz{ZU- z(j9N6$}D)I_hNl5#iULSNg}a;+Y_6z*=T+PQ;iaH4uh*PF)!*K+E_{1t&P$|nNAFX z!s20M8z*KJ;?6*Sxztw+L}f1LT0Piwp^=cSb7Qt!0GVINo6?ARIMXc5rXF&H&zD8* zr#GAam&ZLsmrz-p67wJR**qip3x>U^pLyzMyCmY3VtTMGusT4wih5l}yO>#y1yW;C z9z%(K4OIxzhN;#z*JGQZqd^SSS#p)0YY-Z0+Mf!KK;2D$w4QQH&bxk7m_?U^s<3GBUWYHTEMf(gf3W-G{)XfZ_f6em*SGD?Rl$O{y&$B`FlOi!c z=ZKhWF4QdSg+`HI48$-4$cf@<6MEKgf{e1kBYv6kX6MW72?ThU9cT+isJki`PQzsV zToIPn3-pkG^TQsGWMPHE|P8nZPt>>(-5Dspd5nzzI6*)|QjA}Uf4C=L5v zDg`yV=ba<*W^;w3N^{9tk8R0@jdS0ge|V-u4PPU73hD!t>;UzDxir3+I>}Q?1rSj_!`guS| z<1jH@lw#&#^fM(66#-L$q1GHxX;8sb`%`4$N;8vXsNGH&O0Eg|#gxK2F*P_V;F0F0 z>X#m?HROw>w#E#j)l$&U&?d%euPiE7Yn|MGQR>_iLg^4js4_M4h=uLsvyPSSoA#yK z?%Q&I>Oz;k1`Ru!scdn^mlH;DbS~z3>gfeHh$T>NkaI4Nx7HU6r|QgIXcSYa42D}d zw2%}#fTR{)KufKJ$Sni(%9?|XqFy6qIU6P)Auai5iwsf#>oHK(cPSbhHt5Ljv z#*|x3;Kr7zx$n3Urk2-ej)eTBstXgBYgMeaHt28n%oE*}jZra!lI`(HJ5pjXi$Q&u z)O)pumE9ytf66huBJ)v&p3;>oSFBW{~~to?)cI z@=gw^MAZYjFIQZt4yTkgQz1&WXMxA-2<8x!lNv1U8jU6|`&)7?QBm|flt}u&jL(;I zxqLbrU)SSzwmIGw{zTQXBC$j*#jKQ7S`No}KHTbVz!c(@eAaP2zY3WWX0&JX^L}la;WZ)+hvFpLCeE@I3k|v%&7$f zO`qItVE*;rB3nSYlAVZcWC4s^{lT`ND7p0;<$8db7GAFwE#vSa*`6%lLh);=XF9cl z_?=Ci6Y7&l{k&3V-OCeyjJqoiie)M>oK;(?Snv#ruv$zVG@g^gR4B-(+xrE3(ga421VCWNLSt#J6QA?2b6mW{zkLrz$VrjNg>ct?9(??@^3(1cu zxc8>@X$pk-5R2J=`qJfBRnYC0}xPlbf7-&YY3oOpeS-gY)8X>IYAZ`8kVRgI|FN|-oTV7yuEJBp>%a=!Y)Dv;J${aE&U>0Q1N00=UK zf^`z~FG1E4Yg6cISg7G?)8Q{2FDA}OnF;h3?FtizbMIPgz==iFMwq#N4d&xwG~Chn z{%bRR#eiUr1%%t&R7cZnqpY*IeulTG+eR?);H2tiRs$h&!7cafS^}J?I>2Ki4Krmr zn-UvM{yLR)PX$<)A2Jz>aNlM^(a*UYASW!U~k!H*c9B!JdiA?do|n0hbk2% zBx;i1i)S9dDfwdx&N~syjVM) zrf!LjUscKi5$7K>l2`$pJ3CtY**BC8As^zrBf}SpzpRjJo2AXFu$)3}HhF5fOPRtw z*>IbzxD}JDG~S0(?M|(RrITtNvW36Ll0EeQD8pgapg4{!yb{l{F~mYE_0f8RH%Bp< zVI~MqgWc>yrJU9&U!stddWu)*)t9#ZJSwS*n;>l$8KEDe=)eScady{eS-aE6Ot#70 zLR=!MkFd9;q=u2v{ymr_w|#;AGZDcb@H};S9GkVc>X1`m=8y9?93~__LHrljOYO7&H#XQe%J zuE(~Nr=V|7E7->!5Bi^;zg@UtJhHq^RplQzoH`M{{6Cuf`aJhdTU)G-f7Co=VPN&jlXr{ z^+}T{URcPJN+)=l7tK)%_EV?+r^7hi z7T;5f%b2N~va7R9&GbI)C{n@R_gwfgDeK@IxEReuPGgFWjIpP9QssU`!wPAccXP!E z>+rlgO#5(m=nEpb*W?JL7gV=&2zH%Vua-SpZrEmBRnb8v(IF559Bp4XHxe;+18l!X z*=eeuEbG7B_#}v`{z6U5+}0)LWP34}lqWw+mS)$aEf-6EFzd0Z?~~T%xV&1;WP!B| zj|edd+Dm~NG-wZQICK(Qqx)7>Cmfe&e!$!cga`{T^-@appsq)M27Q+PwkGQRttU|^ z>_Y*>kh?LuWLS{;&*`~IIY_`P3~-+lumAjdv5do#p4_vORL%ie2hcu)TQyic$9I$7 zPT0~EyZqJJx>Lf;Hkqwucv|zj1&iS>6|u`BE1!RkDN{v-QLU@hU5OJM0i&u?$9i>J z^yf@GhFvikD|}hRRTD$HD&N<9O^Tp`VybCNHYEigJ=5&|*(L{m6R78>*Z};;sZTSE z>wl*D=z6^O2Fd6a2~>x1-Z!1{q^=moy`ZKx73ntCwj&<(G7E2SmJ~?rCxbQHM@z^* z094fIJ7d>@bz$8RnSHEmUOGs}l)n4EIu$jxfwTEj`u*?0B}|Q~_!4|!MrJp)<%R_- z=i*$1KL2=m`KysL=kWooc3^}`|8wDZq8fT5qO-HqL~iG0 zew4h_GtSauo~jkR&IvQ85ExreE?)l?{+0-~Z2y*lNK@9+#z~n_e#DPhnN4HZY8|pi z>drcfR?qM&I$zkH)EdyOpdv5<}Z_0{B&J;vJMb>UPc& zurFA~;vt(%h27h{yoLCUCMchHj%j?sVI5-%u9w4>^w?NoVx9Q4G&MA%SfYJ^hu-m# zNbz+x>o(PY)c*GCtgrnG3|kJq73AO5LOox&#eGVM5mZ8fL*I==_njq$D=72XUug%; zw1kCn>iEnoSS_$^kpXpg4yi@_8KwTz{2P`YNlLQA+DB&nW3lXn*73aycBqzTCtSCoC_2If>T`+lU>)gE78Vk z_xZZfiers2Z3oIRDVM|t!=ukf}mt0aANi_597v{>}_BI7;3(U7bYV%*+c%-H?YyG^n2 z>HV=fqVd${)TAJ@hEu6@pP!#TE1D&O+l$w4LLsFc{TDZI^7=2yElJ9}D*l#HWb5Ig>+;LL5U>K)dM3*Sn;5n%Oe>TTZNc+tFH^v9)S@ z$H44H53L5U32aFX{Y$hE?$hbSx?YjEY9g%AJR@1tF8+zXq>ByL>mxg>OJHVm>V44` zv5LDzvhPWf3Um9p_+^WwUex}vLkRK+OjtC+g| zrVyG5fO7^>8G1Jsa+-|Ncf+XX+q){ZUWm=Fi?PvN^CtmISI}!MT<$dACiV@PU#}KZ z4I^fsQ`pNp$(Jq{)P|w*Z!aqJOr~Wc_X|?*eoW${d14ts$=HWc8O8%kQ@?Vn6;P~J z7?MUa;A$Ie#o~L_QlV<=QKGN!ekhZH?!VAa1`q=9yp~cOw4v0CXQ40_yc)}bFDN~9 zZ`RT5oWD(x8-)grP7$lQdlC(N3l?dUx)2->?aTZWAyJ= z2r*FzHykfTdXE@IOg4`xbkrD5-^{+hZQ;p+YiyQr=fb3V70ebCWh-rZ>iC445jvqw z3Iwj`w1njB)X266tPw9MWawdYY_dvtD2>{eyX65`Q3jD;`KF%B=n9^bSN5Lcj+W3) z^h5{{MzcoaU;1B8*KVfIzN{%ZkVqw=GW(z5|xSf zD{%9tJJ}QYmZjsDy(?sLbcd&IGI#_t0l;$dwyyRcj{~_V2P5R|S7~{VxG*6*nGXF< zC+YZp;_AwAJ!IiD)*S&=7@Iz6FxiIUJ01BEVN(CQ$V~o?&Kij6X>J?u)NPGM3H!1a#^@XGXw%N;vjN^6DbgD1NfRoNR^d;hfL@e^ zj*96g>8@9CaoypcKdD$m-wtm0dQ z&2qAs)kV~fV)muZxjfJ3NJ-O#*~z3fcalrWE5b76sJqb1!dodoA=!qo6T{fD63-|# zv)n*wjzr!1()^~M_2uoBeTXs`AomFqvaxX^OlCA(yo{`Cl@%KNI(HyBJ+LG2C+)z= zd1YCG>>MimkYQ5kbwa&Wdn5Zg#(PfXuPx*8U4eh2rZu9t0}1|2Z;?6q9SbWndf0Z4 zwwE1>x~Af$Y38GqG256aR~7$OUTle=@jjg@AJiDrNm5MU$TJyWs>Q}z0`?5KcGkDV zH~o~7jjS@+K9YMREG#LkJnYuJ`$_TVo2`UClm7Rtmx281<(NbFu7k0n1>pp)T(AYh z&!s`@6}v_S&`g_m2*pH(x@3K-fuA}C?UNxQT~D8+)L0Cd&u(-=Q{7F%a;#S6T`Rkf z4ByW!hR#erxzr z)}y)J`InSu!+vFrG@YTk4-chsdZ35b=;{eeikoq%jGF#u**9jm5gkj=i7mGJ@T~Gv zlYfRpVDa;fsSJA!>?5$S8_~>f{j5=slXM{~r8QQAXj1V3N{jWxNYpX8b!1)qFO zCE?>(>5A{Il6~J@9$9`<248LzmO-pf5l{3K*V^_tQX|0WoiKzXsjE^;m z%dl>^K<4^(i^D}#BiHm`PGkZk+~D#`ETD-;KT{izHtJj`>64Sf@*Uo}>?=XJ8z$=~ z0cni2kZD4LwaimkhlY2Ke|aii1#BmWXt^-l#K1$QXs#O9S?g)3YQgpNt;%0K)mhQPDK!{@Sx` zl!tU_dS+PpUn*WhfN;ZVwAahAc24G$lz*E?Kb%T6vf6{E1}dgGYq@8wq%u7}`vhOb z9xa;~|C8|s)I-VM{h)qRku^l9n`(faa+xYRV7DL^QAeSKC%a`9&GD!voJWU$=xLoZ z&2f2Na2GUwKz11_dYr?iTHQ#<6;dBx(fdiw^=ey@>HQ6XHrh=4Fb7-c;?LGjhE?H^ zoVG~5-&FSY??gyE#e{eGwx#OO`#$j1`FRv?!6?X+0eg39!i5T~_BxK~3tDrCoT`qt zhAee7$`T!+1O4S6KMsEHgF*h{3F|qUmfu3EMn=YD!tc4Hxfig=s%Ni9gf$bf15x4p zdILfOhpHu{K|;$Mu5%GRP6O{|+I`nds}?Z{-~NON$nE&0Bb-bE?Uc+G%C4a4f{#{* ziZrtoKuY59o)QG*@}I=6o7(dinOO@#8`;C#Sht zBalyb&5(;<)=iU^%dR{+G1sInHni)Lb8y)xQ)i*5=2bSXaRZjTe4l6|QaaLW=BQrO zqA$^ivzURkweiCHwT=F8Q16!bPs!G!w(1qKkS_$CJ^U~A`kphwXwj4mCTtj(Wx1S^Ue`Ae(wvOgdV!s>wHPkEu&13KL~15Gnrc34OK;MDi6(Jv!*@Z| zy@7^!J)w7IYdyQ|0j^g!jEoOp;|8>HiV!6$PYY20#B>z?&2ZDa#e6PL20N=R?j3ns zYFCnS0jLp1niYjo4M$qJ)%Mm2&yJQOh{tl%j~7Cc_ZlAywnNr>#Su>{%>9y4rVC&A zYg*_Q{CjPipY%!M2aLbwtNRBCPeRyjV>F8pkLPqFXO{Y*^JuOM50(xM9Aq;|Wy;+2jb-w#C~y`&68#7% zlZdyXKZ&nnjdy;{wuH><1?CyWTe@VfFf~lM`QkP5viD)9mH!km2?yLX!(Qm{jxA+h zXoqb4E>7qoTN+*avrgM3EZa@VCh%QdoYTCC$YEHKQsiy;b`-q}+3$c4(_d3jBz=0f zkJ-(g=#mau-5_FA=bJ8ucy$(kL}uxc$Biv#@9N1q=k3(-w5#oessG{LljEnlwVv}v zG7@wxE;H^vyYpu}p2`7gYHSYS1KMBmRV`qwd3Z+{p_fF(!>D)Rgu6O=y58T$EFD)Q zgKFQCxS}=Qy*eKk>XGi*N}1KsmD$THWf3qKF8R%)8+kfcUm0#+QDR~BN7d*aO|N>x%{xlc8awyhDKF#2Su?MaTodgWf>zv1f#B+FCv6BItTqa?p4*&UN=RO8 z?0=h@5V&bnT_R(%j$fA(D)Ea6$GK>2+S&X#v7gJ#)Y#0l6@TFF%;q}2TK*Fs#urV*7HwLlDbB|Uu42cFTyW}2kqy96)J^z$tT_bnM<*(v~5&G zac5eT`&FZw<-Psz5%cmVHf3gHZRbF&?FCCK(yrGY1}ju%gH~M*@vzMq%tU;1O~a#g z7Xms6^Wgzvn-|dssY$=ZGZrYQ`%JSQ<3d=k+vk}SaaSHvX3J4UN3m!%+1@swdN4B?$-1zA*EtHP9FIUx(^ufA-#PiX3@ z$(wR5VuGqWwiQvXV{P;0>>t(@oyS!5eO3!rU|h4nipWB4Rsf2F(Sz&No+pTXGsLAW zfmf}GxkMN@nrjP1YP1H%wiDrLkKn5xOYJD==vaQ65!WQ>n@Sd@(eauzJH zOB3gEFJsrhJyXhbphZq;lm6x8qEHa8ZO*X0W?svNUv^Y_fsWI}Zl)NCkwZ{b3;R~-HA(HJaSfy^yc6BVAh%8IO~rulFRw=oS%`o?qtD{ zuB_<5)CHW%l)5XYU6U%v+23GuBK>7Z%d8Ao5{1BK1+e)jvx_~QYWg3R+c^ynHp-kQ z0_xsnjd=2li*5Iy@>WgP7^J!BnwKVpGJ$l{G6@{8?rq|WJ1GSn0|(dsja-O=11AK6 zz2G%+%sna@KSldPKd8rkN;$$VboKeTE;6Wj^3~jM2=FO@ajxwQXWv8R9oDQ}Io*NC zx(CXC-16d@#=VcJ`$>2ZZ!t%C5$mPTu1khoK3V3+9f5{DXMq3>nT3O4p}}Oe zl4C7gY@m3o;$&#ff83^FtW0t*k8f;>c~ri2KUgN<_`Sd<)frn#n$Mfx_iYmN)!NIB zvv0Jj4A|dh1;l|Z6}SF#VXGI@tw>FS5bNq>`1I3dILY~DzZO~#Pj}|HhzmS8E{sQQ zDGy3ReQknq8{jAROYAw?i3{KnbU5CfFsiE*T52eD_RwuIF;HGA#|8qw68|w)D};lx zX(|pfZ5E*06OKN73Pn%he5Ceog+prr4_jjI_ zub8sI_G}}zB?s@#1h(eF(j0fA-eIVy&NZV91=A(ubXuC9MID+P6PxEmsR{f=tQ!s_v{KmVjw z%LxyiGuXf1a8Z3(lb{sa|Ay}k`I}+ePLnE?46HR-cDQ32wT%Ldc4X>L-NoKkaAbB$ zr@dD;4l6j5?)?WKNcmXS&iy%rEM!N9jx1L&s8s@gG_E(Zc>3?hi7df@yGvMSYWQ-X zTds(HNQ+#8ZEL)@;qQVo;7(c_6$3ew_`k*a#D;)SF(^Y89m0_gMBO}a5 zV4tC&X!!jneD%J}NY#^LSz$%f1&83GvFO7jghrX9IlZcAk^nCcM>uMUE2} zflUA}3h4Gi#?XibGvTjJ7tB>T`ydHkJB9fV-!XYJrHF`u6ap7c7My;$mL&(}lNKWeV3J z?l#kJyp6Pk`Ua#Ntu&^j3E}G86ynYzjLDYVW&&7NaW;OWLOyg%F?3D+cr(81*_WFj zeD+Y&i0Uf5#n^ei={X)lU?a9a+Y9RxJxZy-4%ptP)n9ac1fzD&SNGXIzpyLbL?gc5 zin2E;Nmc*yVN~y=^Y(?%uS%xH%ak<+;Z$ z00EV(Zh58Ni28ls&WD^_)v-m)+W@(y|Eg(-lkgCOq)}}8o!uiBCo6dTiP{mug~dA= zEzEM<+ynl)RV6LBAcf{NB8?2&j@I&8bCmY_>hIx5bS5UOA639| zvOYQdW&3U=xBBPLPO47sY>wdxi3#nw;YS>=5CNyU8h)yYtICVppsEFEv_c1WVx-Dt zrHLa~Nx6Dw<{zl_x$mlcI=%dt9ROkdVGE7$sf$)3{g3P@NHi+BaUplsj6D0}O76UcAJLu48B4b_PE z)5By~vZyZ~qM?a?QQbHw6K!?Qcy&mEtk~_w?!IZe++2 z2qxa*+baAgtPFP!{TSOmCL^WWPRo7CyJnC5Gd!;UlUHQr3M9?#DO-5tLAd1C1FMuV z#Rd2f1JQo*Eb2xjf>~%nHY&m(16)E%eoHGPqFA@b#7kJb_I(^ zJ4;lgGhSP4t8e4{3;QPM7ywhc#mc-NEp%ShAsrbSJP7F3B5yiEK;qe|aRhft)tz7` z?*r^McuyfmloBxeRm~P(vb9k@J>?F?$+!aBpNtAQ?Fc$$@CnD?N?$VfaYdBCU5B9E zD6x#0e5>rYS#N>Bjph$3e-}<@PV;>U&XGT_v$iOh-xl(q-_YP@eN=AY)wg7ViF0US zdt-dpw;a52Vfms6@8BILfB$6XLF@VR)Ql) zEC6i4={pWRjvH|O=>%SRVqnpVSyYVgM`5IDN^j;D)h4DV9of}7d>*_X=lgO=IC&p2 ze;8^(#!5baLL+TElCu#G<2R`e9y^Cby!yxY`PVU8qsU4m%DzMwZ9koy4J`BPI#jRV z<7Y;3VA;sfENQiYM4A+X(T<>Wg3Jk>>E&Cepq(23bA1{jf?Q_;)|$R|0eTUvA>>lu zn#vL?+l89$)%jBuQd)a4J6qih(CfIvsM!eZRs1shh<_uix?0{E)Ruu>zu3BbVd?W$ zkFQs7ptmV_L{u1Se5=XH$zD)1H5OK;TSA060=y3cPy7hcXs5M|l-0jARHN^#d$YF4 z2Y3)ahYQ91lra}U4LYV!{}05UH6eJ;r{gF>R$oAp^PNF|FRfo!+y|%Z?al zXFdP2P)DC2#OJ7GGppQV3J;b1y+IX+RRA}sHMwW`)}E=)-vbX{I&4;4K_J&BJ^dW< zkKTiGAVb&%RLI#PKD9G6e7!RUbz~QFXq*_$C^7{lmE#HN3Z2NNB(-3^q;ldhCrAc) z7_lh`;hv&C9J1IiBJ8R>oHa|u>o`V)&$RkdhbS|J;#T@ncid&3-m6?&9}N-83BP!~s& z3Em;N{)57m`#x(oLLe>gMA~M`0UH~-egfsYt!7S9x>>p^Bf9OmR-O)5sC4`pW?goF z|KruwD=+K!qLs@mpZ~bPe7Y43QoBtL6FEHShW%r4v~%jd{?wfJyznX?SV`|ONHSYS zI||i5R;eh@sessE*;0=-hfM4k8T>(v=S zFJjy{{cI&EX)iK#Z_VHSn#cR=fn`B$blD}Gd*^(zQaZoHENxUM0rG~u&A5qp20QZqVj<_@h+yj)am=3;f_}=CN{={NVEqn z0p?57hcc;X%9@?Gp0EW^8Vt`t+3xI(K3#P4AhPF!`ij)5FK15n@iH5Z>pX;i$oLz) zSkLthsA5WSB8%k=Rs-FYm-b5^Wr$Td6wfWhH8Ub*)h5+;fzogzYnn-=-Dr;*SPX&c z&tly{_h%`?*(T6x!?%I`d`N*NkJGPZoh2s&v-nk-f$hJ>Y|Wi|RzuM?jWH~;jk>e! zG38?);}7KNy(7{*KKucbo-CO62b3HwbbT zO=j9>voK0EU>BfkA3xlu+D4o_gR!Q<^j)8vN&xnK^Ok)-MDm53;Nv~jV-~)|N;>|; z70H9m)~_$QZdOk1!p9YESA7CEQqyI+BM#0Eaq7r(tJ4)S+lE^ucLHfQGRj~4MW@IL zBzG?6WO(>YgZHcFh+}RAb_7T^Fkg6&_!(8RnmAno{JqDf!k6iXXJ%>qj6%9tFCQ&W z($)-iFxHbsb=gP63{{t(w+<}|&{s!$T8qgnU!gS)o)@eC*(Sd!e=ROIwpMyQv>;j6 z>~|nK0cp`>!pYI>Ad8lnYnHS5LE|Hn(X}Uo%xiZsyGoyfr$8&c7_`|B5h2rzNE{pukY3K-h3!0B*wkr-+zS zpq4g?#UJI2_E+iN*tyZSZAUH-0VP?I7FvCokLtW3!#O+NuM{#$F~B^}e;5upbpcjL zQ?aY{;D;1O-yQPQ;r-XM8FghAS4R*1!VQ7Kmmv=@VcthxGOxF`<3l4E+7cz5pIxEA z_^9ZbYPJKX66c>A67x^~l^%$-O3fq*K=L8Hv={T7@PoIp*A^HWcHSr=bfo*`bdu!q z%!=H`OrvwIaQb(Fvmr=ve;Bd7Jqx?+t{qD{Fe<>M9)X~Cjj98lHK~7u3O1w(XE3;R zw}Yc|x$9K@ikIgP$~lP^PRAiICKCpub5?xoMi|cb6KmfEwE3UzVd>T`ry0Azb8p^( zA0J(_si5r5cj9W!@7_?Cm-;Ik%v_<`b|6oz`=XCC_IZS@ob>f;g1omwHrZ@%AM32oN#^Otq+ER8Gv~`{2TiEjT0Z}} zGY+cw=EeVB1znaQs#6%LrRxKn@|mZ!;B*3R@Dw{F7zt(Kkl)mc<0SRb&S zojN0r8Qv0hlK+vhv4(p)1i17RT@?OvLd=Z`4$T=+Cs&afat{S!z!K9kGFI)A-+k%a zGBla>-H=K$(7V?`ux_v?i%yHipcxDA$53(fNo+nJEZYH0+j2>{`>?@a z?-bZhBGbeUR+`=UwEya!Dew6+H}6mFkvJ!~AXFoP@NR+-OJv~vV=gy$beASu=Oj~L zflgon6MCOubw=LwZBp(n*W3Joo`uF6xxQ5)=UWqKYf)r?#>S??Ok@A78fWM032>1( z5lu&5s;~LzBJ9k3j}_Qqv>IiKo(^4JG-Hv#8{joAs^6W7j!%i?9`2B$6=U3@*5i{8 zz8#|a`V{Q7cD6-#cTM+r&#Cc9-ZgzZ_nhPP@|oaKVFXny9(y8+HY{=xsRNkG@m z+;fNb|FwAH>gienN|LaC_ce~upXO$cC{5)el!1+j1G~!s^E|NA*8x{j>1T<)-_Lh7 zwN`AD1$@n-*<56`Mw&&lS`I(!Bw-C4gUp}#a1NDPSegW$!~HnjUX*T+TSu0gs_XGA z_v`nY3C-$qkyPtSml8x-()hnDLlKwfi#1XHp|{+!uJZBr6dLxqg@1~yX4@l8TdvB{ zNe{r&jfrz8Y!c?Hv{AE;w{Ye;3M~N^EmStH{_1(eijTdE(9&y-M%8*DEu*1UAS$>9 zy73SFA)3_oyCJ&ESeJT2Zxi=MHjE`Lv(<~LZ)Px7dcuHLm}yIKIU$2?jb*!@wZ#u5E=9}LM>Wu6~39iD6Dp=;k88A z7o`U+{s?<2eA^&I=jS<1XgSsnDD};zN7;Wdt2?+n9hH@8WLqhyT%P&^Hw9EN`YAx# zcgd>FrIr}?x|UI0weAcrQc+Qt4KH(AP(j5lh%r0$i@o7w=J)H3pQo`o(_JQGO679q zsuyPuKe7d9Lf!9(;RPH-Xpqga*#P^v)Xb%v>mlWopJJ$qiC6?AuF9x@jLl7VCiwkd)voQtz# z=q~00Ew&+PuyS9n3*%tze^W$OVZkEGc(?!XbQkEAgmc&`SrvqHJg*8Bfixyr9E~RD zlv2=Gy9>R-51AnG0b;hiqh%&yuk9N}HM?75t9rKD&b0N=tX#@qyLI{Oro=<6ARJ zK61L58`+PPm7zu`!vD#M0B@6HnvH{ZeJwlDRweA8)ZQt@{EE`ezFAl3)bX)@XpiB- zk15HMFC4abCF}%I>Zo{^Jv-tl97btAx%YeCEavtfWa?t-T$I(r&Gnd2LjB)XQR`ci zGl*ArZDtUO#C7=y>-L=n2b-JPZ!6{9{o+yvx?$CWUD2Z_wUxL$S3qlfhxrdvnV#;^ zA2*c{ow_}(tOHruabHoxX?r^j$|c2_4j-+#pG5J_`q8R_HhY>Lc4MX6L7E=0ZyS4_ zI$1Glfrq(=|4gcbN0m7dnO_E;@_Zy?L#9qN3 zemFYub+V9P^0Jz_RI6%AGYs5OjwR3bC|Ayanj9g5J-sh2$5Q=QwT*0I%L4}`zbL%xG0g~m6f1N4Vg^Z;zK!B; zjWuTw)HClj2FSg7^(D9wk#Vi$Es$a5o3Xdi^grso2F#&*{`U*VBdA?h&AHqaHEkP& zqt85M&f^#zQ44v3ZlqX1|Jj8b=Caja@prEM4SQ=2?!3Xj-kVz!8eMm(} z01q1BE4m~JzVF$JBtbSb3zF%#Z2OEyjLU%Z{MkUr)(iOYACx5dg~yT*YbKi}cn=|t zU8}^j7oHEkEAtWa5mPJ`EeJl^g~@=yl$?Xcj>dbhDAD#T@coGK2owVa-md@pR3B68 z?oM;dXYmKO=mcNMJPD5HlR&C+Omjpkk{*~+n|n%fpDUhD;mg`5zejsSUrEC^P64Qh z!-ZUVK#{SO989J|v^ez8p)v3yxD8E&IcZr3@jUq1ligwk6gu=i zEphox=#UhWAv*>Z#1f|ZT;Ux@Q1vr>fAo7a4Dr{P}VRWlwj$Kv}(qa-h(v7`GE0CRZasw7BB9MK?I zlzd7O!JPe<@ew&CGo(`C!{aUMd9BbF;%XZpQ++MzQ+)<%cWC!%*i856>}k_p+j!MK zF7U`wtf}Gf3Y27Y_-IP#kZV6%J#Su#KMyE8{Va!LJRy@KUT(FbE2etV#Lw|KJV7kQmjPHL;rb2ekA`V9f2Z7C` zHeLOx!BlWEq`ADlCvdfS4nRp3LFYwpGmLkq&PzMRwcxgpxIg)U5Y6?2bHG2s8Nr(H zMsN=Dp9;>2)EJTUuC&!xd1s*E0S+YpYfr`NdtTO)=cU!g*Me&kcx3h#6t_caPcbfl z2QVda%nmRCu~rLrlBmVNBO$j1^*WJ{uVy>g+nfm`HSr2^Ji@wyBnWD%gM;TIrZAyC z-<1VT{(ZCf8G)7;;f~o@83s;9D*z)&MA}9N&SU^>CH)b@5kp|+Z3L_M4mzl>mz*(n4f(G!29K8k!G-R^NPnx1&v9%x9l1Qc}Ak9Ssuh|!(CdHjs z7r8;3{b?^K```t`N6Ck)5+eFp(>0+NCZ~~vFSOm2Nu?6vY2L$Ls7eO;;tb8ypr%q?4 z(3d-V%$|~;zkZ2SxlNiB<{k4wgrdPMyV9EF2B3PI%yZ5-r93Y?uWaA0a)-1PW{$iY z^ai9#5#tg)g(C#p&;<#u2wa&jo>Gq#3>5vo*2bKYDC)PMp z6Ua2pNQLSEi^-QgRWJy{6_I`OqVqm0PN*6v>>;h#m5FaPAEB^^B&(8M6#lRTsi~%e z{#Kp`Vh`}7Hq9@VV_z|-rz`q>|1Is8eoO05%|3+u)Igynb!A|`1J9OBdI%V0^CVEK z-st<2XZ-?6@*AY%plp-JV)WJXU=CsdrPn68Tk?mIi$E%wDn)}>U@5hmv3Xmaj>jdK zjLen!yAu>jcjy5VWJ*Y!O;=b)@1Tmd6T$bwq=!twt1%HtNPrWCQ26rS z5{xs9wu84wxha*zrAPoqq<*BxPH0L$*g#z2a0dvLl=_v1CpYqQiyg0h;hMZJs?lW?NJA^c83>d^R@JlBz#;}dWg|!5WBB1w>HN8gv_-$ zBIyw(=$_^sEbFSufK#3dvB=&^O_=%W$AfidCwzr^)p<5 z^1;qONXrWYhvy!tARXfWB2Weo8Hv)d?Q?P7mL?Kdz8NXmoojrMGysX+A|V(FJyOz& z{xG?pHY`Y=hCY2yTN#7ND1zRWFFjO(e(y>XfUDQfx*`sLJw!m{N}}Xbo8>NIA+bI^ z+KdA*nlp|rE1YydFOU#kxh!*10bO$OuK{-mPJ7||yV7=GL9pR3;Z#x*7StA~7N)4k zA(#R)YEKKSaU_QD^a!r-r6gjGU7`y)Ctfse1&12`o&&+`+>>cf{5ln`=#RQ` zk(jWOHkb>7AK|X2l`RvGl0cRNzSVZRfkPozl%;l+6r>&lK#O+=*j^7$4c(#6o7D`dp&>MKn4s*>ZMykYjbO}e)~mEjUm7hXkuV_q|b{(yQXK6 zz||91xecie1$c%g6Ma{)!`s7Vq-#w2ZMZBGg(X|~#{FI=C8 zi~~t{^&drOEdncv`GSh}HcqslUgjifWq|d0t+zpXAo(4*!zzLK8dneM zA-W(xubK|xg{{dyfhkWUpv(Te3FnIOwfC|h_M^A%i*2EUig_}b`V9H%ebt<|e z(cCQwrTxyoen?ldFAS};Z9ny^CJOnPRSAlCTb$ZjAg)(>e{>qQq7U@^miWJ)ll zHj++!vNTHdD*_*N5oq=o_M!hF?7gF!3c7ev5d{$e1qJCMO;L(esZmjS5m6CoA|TRB zqy}<8rAbqIj|zwlLQx1c5s?m2kS2kImPkv0AW2Aj`R-loy>-`p_kL@gKeA>{X7=ov z`R(7H*=I(i;P_qqO%BoH*kuSGOlcR-VSSa)&H3nj#x53617mf-Gp|0m{+Y&?B!Y5( z@bhx_MoCZ4`R!%TYxb;d?{9sUvK&ZOd;`<4LKJgg&5#3&^3Nl*{_`rw92h^(4VH&0 z&17?|NLGEgsIe^h`7Fnv{--{nddS}Pa*zE>SFRtX_spj)r>`Q?IYs;jS-B$7#uZZS z`Gl)L|2&5-x*Va4$=L5{xZADH3^o<*&Vwvj=~>Dm&*Q6?(f7vj*S#L9GM-Pv^yQD#r?r6hV995T%2N*U9JDlT%o1>v_`CqW9&IqZu=2ye$qKJ zVPj$IIq!dzz;U7k8)dqO?W}z1+sob@^M9D5xqJ@kfp*tPo(BL9b0j{h;NS{u>&%;m zv|Y~2S2?xdeWPWb?Fx$uiyogmarbzn$UbKp7#^YXMIDzYJull(8VYI;ISUDm?rwlp z6VC2(-WpZN5LT1hP;j-L{vV8*gFyC(XBy;3jl6GCI2F9yI+LPoWHWvBa)0a*Es#0d z`f_nSP+FhB$$MI#&FMgx#KXL%`3>5qa5Eb$dn z4)zIi5KE$aSoL-VUnhq^=5q-D`VrZgQKs$~dV$XazxA9cRS`Pu<_*twY?U8Y*yX@k z;qh3$DNc*b{^OVkM;?zn5qW|LaLB`k56xc6zRT*KWpu_|)XR!?%L)xE4tnPP1S?NC zK2Iw5hC?{4B!MI0)A}WF9yBtfX19?}&Lc%_#R;y;Z~U>`oBc-Cy{%r~o?eksA1XF5 zXU#z%+iCi#SlIJ?p&Y4pq!q_*#iZ{1APlGpIYQ5DK=bC&gcvsrQeR@OBwQ2V&5?=rmsfl zF_EVKM#MO#r#5bDlm6S}cqB7no-B609Hh`LA_00AZhAUCi{lFlXLZ8HtS_h4@Az_{ z*P&aQLhj%Ls$m*Ug=>BEn6Bq*&(sD14$YnyI|n-+E0|;47VVZW?;-~0e<|H{9Zk31 zVGC0X|I#?4KnrQJ-C;u_4H>ClTR74VSg%GW5cL1*AK?e=|6#c??AOtJ?H+F!U(z?I z<|=SbZIO}4t1G$n>70(v<}gyiksst!JIcer*^RRo6*$hQ+%6-3gL*^sN2L zQzoC5Y}FHCJ0)!vC%{r8w|?(H2Q;zNnnx@UAPfn;zKXW#sDYmSR)+{V-P-|D-`+ zNrpDA*K#GLeng~~4-IQ%1c_#0N#2W*$9+4!p3)BYL zKlroyqbCpZmcAa5Hniv~YUjx@G>6^moBU-bUDj2o6-|sg<2N_%afO{F>|q*r9c>sL z)vxun#OlPP_3Dh7XGqJ6SRVxY7$7woJwF#Ho77qOUF~arO05dmUID(T-~=XJ$srdz z=$eoxGw$B?425ngAxD;{IG0NUgYZYe_A5?SttnVg(yZx1*Yi-tlVN6N7S!C&-^RZz z_5P&Ojb<>NHu)LwFZmxn%?;lEMYVc1?q=H7oLkqfmE5%b_hzz8S@??_kJ`ke$#Cx8J`5x*s>8su(MLd#}ehV=jy0EsnfUvPKN!rm$0vsb0XPXib z@jVl$yDg7gl?RE*+l`{j)XAN9&(#GLK$l$OBsaFVA=7pX{^u~TzN#3Zh~^Csy;B8MDI$7^Y68RU-^}oCk{6!^W#;F>1uE)@4M#(}Ezz-h!(I#y_+dKQ^M)l-=HliV0Kml|}t&|K36On1^iwBeIwlD(#wk=@O6?>q<=}&_n5eYGkc+EfU|4_i{GXSLZzU- zGd<==y_XvfalP)_qnC$;_b)Bbxd+TuJrSdy|a1U1z9625(#e}6YdN9#*5-D2 zK}V*c;W?4VywvQWBv|)^)N5UGp0?lnFN6`tvENr2ed9g=!~&bembQ?Ig3JCh*pwAt zD!eJRvc#_?nCa1I!P?K-KNX%H_>0;tOt~KFUS#PLxR}m8psS_seSIc-)6EiEIskv< z{cqE!Zck0xqd(@M^6DBxapgjxiIU{VMsS8;R-^BW8;@vz<)wKA1r%DUn>v|Hb*Vbl z0U7gu>meTi*`hkP7k{1@>-}*nJZ<+Sx9%BI*23NULrY8M(pL||sT&vs=$Jxb!cuIQ zP?&XUz>@%3|64q`ZFAXyVELIQvt1L-`OC!^)Y8jq{`?m>8`531g)UC?M~q2ZArRs2 zm=^xdnssH85rv|Mqg#J;?K}lU*)f?`mK$2 zPxt|YR2{#Hax7EuMf<)i_rPW>W{*WKaJc`N2$8L-hhz&XU_O*|<&Qcs57P{Hd6Vw=*nRA z-iEn=P>x3;gO$UQM6<|_dSU#j?SnMpU@dL+cun@Q)y4g+<5RU^y)!)`PTY38K-z_9 z_JeS45$&6!sRLm#y4!9&*aS*p!&BSh^Sc-2ke4`1xD#e7>^jA|9E5-Wr!VtnOSH5b z_@6Gn2V5*8`3EYvXI!*YMz(*}H36-0$CT2gV%LIVFH3hS&@CRR$Tkq2(q?Tjx1Ymv z21MIvi7+Lk+4uV0RqvEw@JIixRD$nP*-AyP;<8>c<8IquzvjOuNHA~yQLE}#>z`|T zMihw6x1(DOKK-d3eOOLE&k#Kdl&s8k*|_#O?`clhV;(L3b`=Fp6$#ynw+AqasqGRg zIZUJFmXROX5vpg9YCDE=Z;?CCT!S~m_S0rP(;Rn!)LA>sX={V#6H_sq70@5Id@cKe zcV-ETeQkTMu$Qb2uG8%?4vR;BK{H0B6|bf4!5B&v#zoq=!fo>p8BER96~P>j zpgLWu)f{uBU1R@>6+w8%5Qxqi&n6f!ms_IF<~JP64>7YJzOp|9@Y7@sqcmCve_+4U zU=03INgI}1e*3M3;%$fYVQ5ZqDC=QYw23S9l;E+CO>PGap2jQ|Q z_IgwlrI!Ix>AmFWLb`#Wr3WB0Q7T{BP7Q8r<=7^Nz0chPb zg{Y!s74v(O1Vn+YpS($7bk!q`m~Z_R>%SLzyXD3vz?-jKfwOL^Z!fjFpUgW|w6NS3 zVK`vxs`a?;+Y6R#SZ+QdbAJZj}IB97#Z8XSNzM)7xkn&EbcW< z)_Dns<@eZL0b+~E!Yu`eZPB{th?1|TYo30RtNYX4kac`F-t0&AlOQ?P@on%KNIr=(FXhiV5^fW^c^^`kr3Va57B7{?jhfhE=?NYsvQ)TRB)NjBp=uN$i$EYU#rT zC1JhX%kU%-U#qls>_bNrkA2NBld1)Ss;e37el%q6HBgrObC!U=jmLE~)KKv2ak z6z9ILa|(JNB7eG$i;`gDAq)4ga*ImGr-J{~x>bF?%K*x0^O@;?J6~dKp05zfuV4fh zO+Js1A#Ma(XE?*$v-!7xk}tw&J+_u;QxDBiwH`3o&)D3%Yu2-U1oo{uRB~=3##BYP z!1#J2;1&05U{x)^x3VTfQ~7xh9a*#k81Fl*F-4qgF$B~$A|1WIcVdd5soWAB=alT@ zPSmP&jU9Fq*9_tt8z;)1)m};%zMzHJsfW3k>+cLs z6wogGGTD6<`2K{SonA^uc%IjT??Dp^CZ8~XQPJm6IDL0-3>M6CoYC&tud~l|Y_S-SQU8PjZ9*-kkq73MwuP{hF&KTcDPkoP&As>A^+CK9+ zo0qon^{7{qS7z%}>ZWnAN-|TSX7Xb#Ex1-O1oGEIaX{wGjQX{L%BsS4w}H^k{4+qY z;yUk0CuotWXUSJL)SL?l(1awCgEm`_j~4?CpN+|e^X5CwZ@w+R%IomT)LGJHo^m=W zdAjf>^M)toT_Ea2n0xGP@x<}68{ugM-|2mE{!cKl|X`f9SdT};evJuu&iV$Ax zBE|JZyt6f_LoOM3qMgof$Q`BQ`u97_SNA*1^g3@J5$)m09hxKKm8>UE1}p(_NYwot zc8zs;nvRXm;*-0@sktPDq9Tj@^Dbq(V^v%!X>WA( zk3rGJFgQsxn>1Q4u5!WFH#=hDW!)u7o?Lj=%_)|4TMt8h^-t%kCkruuJ;ky~vOh#n z;wit6X$;z_4mM_)0BndK#9I3hx zcHv)nnU^)W{AP9K*L}?mwWE-~OV0*A#NM+8Ja+}M!p{5Nsm$({p&t6D6BsXE9A@TW zGMJGzG>7o}4Z>LYgx&slF*a3_;qC8UOqRz6g=HXQt3JRe_l_nOu@wQDG^`OG^hKnl z909wB2pXqm;2wkvUseOT_0%fX?LCJaC_oUoaIOEGEY0b<9|P>G;XkG6w{M5j|>Y$F}E%6%8M_OHeIW2TFlroe;`Z@#?OX(3EzMEf&S=?-yN%RRRbP{4=;7N)~e;Yt>IJ{ePsR2my7B>=2zJC9xmE#U}T(XO3B$EnKGLY+TJ@ zz7{7Obwpp?F}x1-hZlSu#(F!aFSh+CvRh#hzbzYoXx!KccwSCsNkwvDC&KjxZ?x4r z_}){?-8{JmoJpGV^=Wy7ES(wz_{8+)374T~~^)&Wa_7Hcg>6jj*fW z)Mddxi?*}A{nMrIfPPwa&XXh)(BrvmzYmbrx%jz1rg@v=qDR3Q-I%poq8poz{SGVS z_8Gu@p%9~26xjHq*greq%l4Q<4%TYtLb57EPydf{0~@B&TTWRGDvSTDv+vLs-F9o3 z?J!MLe^4=Wx8Q8^Ha;?E);_%bR(K=QJc$_Wcne=6lT%AQyXG{+5TSS4K&3sd$0+Y9 z4Y{%}?MQ+-v^m>-G@4ZF{Il!tv`cS%E>Qe?VBX4eeYnkORQ2#nXxES*h~{gES2nYk z@v#KWRz33;dLl4LKDM{-llfKVk8QX}mv&Ld1-DXG7^IZAK&a7>j?+6amexQ`pKLwn zCA#vBR|6=l>vF~$iQK%hAbc`de2u?>b;DIJ%-T$MbkJ6B^j5@}-DJxJop+*SS)j+b zkw53QI?5m6lw-36}(o~a0*nIvU@NHh!>22O9&(T~e&v2pl=)1^=h4eT}UD`za z;eT&7-6m#sLUaan03UC%qO^kK{@@l_vE2f{<%A(vU(SkO1~M2hn-#rb&kWYz_qXL0 z_l*AKZZyN+1rCyej-BeS?djAR2Be(Lo_Te7?rIXg6i0_lRKalpd+~B3JtJR2U3v6G z2#UlX3wV5_f&2-k>`}-T#}Lds67LxNEFzeLl@kNitG5pP$NZMc4k$WZi5_Qk)JJ`vKlKL3bT=U(7Oj2(#8lnZ*N7ej3%t{{a?z zPee8RAeyfN&e?x@Lq=Oq_Qe&KS#OV4?_Bqc$_TXg_bi#1zNThcO4yM;BVTI0@C4Ky zHTH^;oh)fO<@*z|{vImL);>qPt(dWKi+qlknB9`PvH|h=Fqpe=3^~z!@v@mJ8+0VXbIX^ya2sJ2T zq%@gWB*l*z$0wbP+(g@zbv??NwF{qL7^4VLb~oSFyFfl+Mm zx*p%6_>L*TWoBmbINt`lFSU7tR(IHsG>QxB)P4aK+a7*v@7y;o6yLM>x5SA$00}uM z$%LWd6DK1#WItpZxK_~1wJy$9<#x-wsfS4x$IqcZ<;2Ih(ll397~>B#10g&uVJj1L zOM{j-;*?MBStFJShN`sUb&oFVFWJk0N!dtO01#~TwjrrI$k=A-+7!)sXy$1~3noi6 zH3k@GR(TD<&S%3cctZlh9lDvVVd?KXfq0G9h0oeC7NxwY?NNUXw9McGPXN_&@rTUP zMSX3JA_fA?H1Kmn;-h}vXyi}k$(?!^!)yK3pt^9`)RYuh?kR=$G-z)Q7r8~X)vr9m*xq);F<@cdWhn%p2@e3#c8 zq&YY#w@hhBAT1kvozoW}spuf_T7;>~LUhYuy@AaqLy{t9)j5l=)~#nPp^FGP{p`?# zF?Mv59QA1I&F~Ml^B3hS#in!B=C!b5uEtP{toE;$AMXYoO=k-E+(y3KaP9D&5$m|) z!(N#TM^OZk43@}HEP^RN!vcoicu%Vsw)y2nKwqbOi^e@T7lWx^gUmPc#(Rjvd{0|- zbSDbW5(Q&Hz)}wsX&I=z>>gyCv%{-)^`&j$W!9|JWjHz-^~{=sw_%)9`w)AzSjE zz`R{Vul4*{qyAch1y0GqMh;YpBke|{qBXk$hv08nHT5Z=oLJF->7pAYKJ5itUCVF0 z^@sceuGQYJz#G(E=n~MSMelgy?Fz?!UTRk%l$`{TJddu| z)CUI$d(~C3F40@g7&wBS{;Fz6K z7}$`r9k8$`NGWqEM-TX|RoA~Erz~Sks^wK~IR-Yvnaco@92aGn;W9{gmOR`#aX}U8 zyxfw`_$}VHB8n8=nYD(d<65!`efqP>a#(BVb4FM2yt0EZCuSr5l8o}SI4(*v}mU?K||3V^EY?Gn4Rs} zn?yUkW)U*1>Ju(UE@tvT%mei>NGfFB$HTfe1aOov5{DOcx{h))Pu86c=yyebT~6%Y zC19#-saf>kqmZl$pa<`Cis}*jV17>}PW3v6H$|FtPMCf zE2V(ExC0)(Fg@Aqej2Icbt2PZ@Q05MBW~&a*!xo5ypFSTWw>wKeP*g>x5M9R7R*ZA z0_wC>F30V?#OQS%S&!6y^w#V?k?qAUlLJeeE(43YA{5>E+G)1AE0}n5N0h$>a+nlw zS`&Rfok?PhB&_}>Ju_)&N%VJY7MU_uhvV9VQ*vZwXY^S;@3(c*d#CcV;ZI=x$5*%R z?ifV)51-is3Aa2$T;!N`|I}I2vXv}A$8)H{X8G6s+3@mnEwd`L4yTWBUsZ7450eKU z)IGqn7tZa2s+L|c-Yk23i8cf!L<371QNwq2m*u^SkFCz1`PO zqrt4W;%<&rLQ53vSiB00SvK$vRm9eoNd$b-n&^OPH@w(Ur==P5UH;Tfo3)PHbEOz; zaeoZH$_Sv%o+c+R&w0Nc+5IpNq;6qtSnfM8zx^Ti8{BLDtVd>wG{6~%9E$5PD}454 zk-?obXYQ%$Ycv{*z8f23l;?{E*U#~W!(Vc@~FzU(90WQ z^Cj=t)}Gn7?!o=m>swobhgBrl>J}q~^-QI@#fVpOF;UNgvIg7kSalI{?sUj#=9LM| zHL_hp>PpFaHvauUwW(8bOke6W+cLad!M9WSbvh!5pK}S)i?L}_%-zM`#Q`=aXfNZF zhUIdK>qF1cApQMx86*%CyeC2ieTt4h5KoV2G#+%dWnBB>FqMh+cFRpGji%k)(J!rY zd#Bd?O*m)9TyyA*Ja|WEk^Q-{n30{srd5765d1UN6r=gch* zwXjlTJ`b2PycgAZNZcOgjDw34%oEUIu4a!B^0H|i{bF-27P@2jt2M8NHmjRtD*wkf ztjJqPR8XKD{m@kaNDn@@X_CGn!7`hB*=pb2h^H9G!)^!+j9!${q&SC4Tu^*C2A*3b2NL8WPfP_bT<4r%tlHB`1l}*uuK&+PirIeBnZWh?DI4MoEMAG} zSgZ5cs&$VUbU~ca+)PffpORVS8tr@q)5o-#9!GKiPIljePIE2BrZsH>XNW6}a& zV%7;t)*i^!de6m%Iq|AIg=yNR@6o_cd~(Vi(AVB!3MqJ)?|4WC2ZQiMF!)Id;6Aj} z{bm^(=%e_Ry>Msq*g%ht5%FzXbWx{}IM8DLgY2AtGMK#ch7yQ16-^Am))oEw=VH&?i2P}Z#U@q@+Zywy1rDQ?dd+=9a@aF_GEl} z;|>2>H%e`21ugwFFahc3G18jqpupIDrzX{2SM&YJ>|s&PWE&8BuFpn^^qaOcFaR0kxkhvw3y6#n{cxniCOZwBSkwl;KT`Crq9+iESo6&Ja=cQ(yu(8# zGI4E|6Vj-6nWEkChU91QGLYQ-<^;)a`o@l-1*?3+?{8)OvnW8Yv(i`~m+F#QZaNT7 zFkzhyC_hJJ%gyk?BCz8)Wuz(Wdu)l1%=HbnGV<`OeMe0q5*;1d9xp}J4(puVZBVB1VUJkswfq5w2>t3b=Bq%7lz!ErB6nRC5@a8 z7Mc*6qijk8RrQ;OW?k3^HuC?nNzyY=ZlNif0%))6&2k`E$|?F2f+b3`%V1kP+8LFL zF%cli{qD!ANLW=ooRzG<=hJ#LV1D*$qf1xlLuWg*Nlp-J@#YRZ`Ocm=_*#aKVD9iR zeVinZivndK2WE5>(0hnZNRaHe4Q=0Mee~4JIzGTd#%lEXei%^Z%2q-?<<&q|&$JIT zzEj16ESjHsyQoHDOfl|@bzysV3S5R1DXR1ms!`v~M%9r&tgn+@il+_+qm{KC_hqUs z4TsY9ppbbHI`*8aP}1yWSA-eCIMU@DW*RQ2DBGN6Y0%R)@K^nwjKvo~p*@JQ1qr^+ z1`3y(bIY*)^6;Dbb7SmNgqXfVmaG8Nfv?G);*t*^+^0jFnf zaL$-ysO}-jEstd1=*s&X+6 zczwVw`%EoIZ*)0VO16kv3qj}Ms+3mmm`0X~!NJ8az0KFuw zP@Luw1?%%-S1!YDu3dnSTU=~?8l(~wZ#i-l&FsD@W=81B%&Xof_nzi@vEfwuYFZJ zDJP;<(Je{+n4*V7ab4fooa>N}zuu|=HC=Ag1T@6jAa6*z{MZKpbjA-kGNUDs0QDK2 zNDrxCkNIFzAdnrx?kDZWpt{s?I_P>HImUDGx1N*h zY`@24QciqHcLehVXImt=a{VklT5%*UcIrkr6e+v0y;bLJ2PdnL&aM$m>?O#}+GW#j0dh%Y#2x2l)4JyQF;t-R?kL>52 zd^o_}u0N#Br4_o^%;Z~d!1eyb`BPvU>qJKRzMDj$t!rWLDd84JYkpuB3Rjx4!N_FJ z+PJn9blEKmG6v@1(>E}jh2~>f%spX>N|>r9JVvv`XKuop@$~y%w7X&*PGfKNEL=uj z?JB0l?7U<)G&Jwel?00Rhs1@dVs8?b!9cWKn4Vr=~u(JilK=HpAA#IKOMNJ zl2-b7;b@x6rynl1u9>3dT45|bjbPrEVnmkS2C2TO90~FvaClW+7$nX5F@|gLt;CeM ztA+KDgRLwGJ)LW1IGgXmxjYBtXaRkL^hevfHudU=5T+0# zJrjPe%3%SjWeE0Dgv8>}I}Eb<%-Z+(Wh@K-vIA(eATH@&$B1ScUURMK71z6E(iD1@ zEz&aET}*;eiF%tW^)C_T7dihvvH-tAbHM+w|k}gXs+WI8x~z<7YP2+@(8v=K2a@M2=jso_`L=)g9O}0D*He>ufvc~Nho{R97UHjRz3$)pnn{>X})K3b;q!W9yeKmdn=V}I)W(`60H@!LbQEm{+ zbNJC9HpI&WxK#=~{RW7JNo3__+t?ICQ(<7&tdx7QAlLlc1fh(%Ix=yaY{}d zAG?w8gS*G){^hKuY(!X3xnU#Sptnl=ZrZ>18>JT51ACGe!%gVYYelBt-fkzQ_dyE% z^en<+-&T2%&&;6W?UTB?G(dCvPp8?oZ7Nz>LzmJ@0KFeja2I^vXGhOW)?N)vyt8Su z`3wLvvNCQgwQCNh4xjr?aO+|8jpTuPr3xtrP<4lUq!oyaS2GcT2JJ6coIS4iKikW` zj#z!|zSzuyiIiVc8M-1$dq@YwuvWHmRQvL5M%`_NhpcDbEwD7-(Bj-_U-6uDx_a=N z9epBzz&ul5t510UU9+y2X}{c%&M-8q9t=4gR=kw0jFs|aLDfbQYD6FrkvUz&;iUmy z*9!Fq#RZRMm@QkBrYvRh@hE@yxZN+uz>i$J+ABGUgIUUA85^#sAFI95Z&@T9>4CJp zu6#(S{s*OC-Y}%*F>`58dcmeZ9jq}Q3joT}TIiT419v6qPSQ2r*+H_y#}<<(M$G8x zM;E3hytdD~Hs48o@-L2xXjSU<+c*>at+j8&_?6#yf38fd%C_L%tE`f2DZN?HBqSBF zmfd04nQLP9Z$hEnT|MlLCCM)2>61WX^=hYK;6zIj!o`fb`MOv8)X^yjujQ0G8VHM# z<;2s!%MhE>PNQcBSF>i4`f_W8D9ni=&D{YPb&$P4Qn@}YdWZD{%)h4@IU7p8(rz&| zdA=`KU;YAIB)K$q6`4a>hI}M*FLB&88zfT zQEbdE^>W2W3Qt`vEAd!jO3n2ELGrI&1717% zTwTS}Tx)r_CFyNw$~gezPiebOa=QM(RP0^eNr#W3X}2SG{LG_f!(^nJ@^{h3YNT?z zQuC4Z&pn2!uQlMePNP$i0y(iY>3w2mKMdWB7>09}VNXUt`j9)}O#6ks(J8v#pSB&h zCI0)pVrs&&FVrMSK@uLJOzO4$Z?KcUMYS%gI>VMB2s<^^hfE;1+t_wNsdI?My&n7g zNrZct5pCjCEiY8bt+YF0=F(Wi7mW#?bQ;#)fqn(!F}~UNyUTA1G_1GT!w_3tzi3#| zAr;Z{C?i*-hZM2Uaso@}CY^NYFZyJNb>_sRs7&Z%wWV4tQ5)xoD=_D+` zX3;7CTN0XmXa0<@pk^Y#o>sPp1oDm^R53Zic$Kp*>vfD$*aOQMzchzEqvj+YeRKZE zlRYOz6Si?0^3Wc4U!2Ec49XRE3blfYKxx#UdXKk{yr>sQR1YBb;qFEW8w<1;fT+3{ ze^8c#vgyZ8C-W{vm`klobV#9=>DuRnl5_{i*ijtFlFTNA#Q=BiOXKhS?zCh|Dt@HB zYp^IXD1CK(|M#4Ze@H>Ad97pe?Qxy*<2UL26H%%YCCM$1^D@;L9uNb%yzqoR=R$$) zDjOh5ee7_Hl{pB*q*v2DLOqP)nrS2~yN`XexqNL9ZKi*byN-twketxMMdC3z5+_W( zxOD>;fNDvo0rnLtxH}GAmb}N~DiC<6nJa{QULfu`Rbk5+myViB2;s7?HD{o#6Pglz zP!lM#2a;R5xH%MWqWwYG;jSZb0#wnh-#k4+|Mw#P$wP69s2P+d&i{dYy+E|&Rxs`v zN;wgI5b!wE#S_OL?SEjKTb*l^r+~|eTQV^SHJcE{Eqlm>FHV$tc1s86f^w8R0ooGA z1wIfD)u}%*&C?+et+#a*my7aER6pp!-F4XLE8l-BqV#_+0{CCc&`G5x79Q~B9!mI> zn2FNG1)ee4aNK79I}<^pbtwMX{(m+v$@i)Vf>m$X)QI`?eXZMy~U)NB&AF)^fHp=EO?mw(&z z`+yQ2IB6fdB-DiNyjC4Rt*HvbwT4%LN!aBp5A?RgaaVEn382e220xMNRG5S1Z(7X* zh?h#}Cr9Ex;!SM9*%O$W0HT&QrMLuAhKIuNGVoR`yj4qw;#@-KE};jG7+(bgH^H#V zR!~)|vOiHMfC$#1m}^rMM&c{*CLUl9;B~8U3H{ng{40FdJ+S>G#!;K1SVF%wQeNF! z<4+vYrf83p*S3PSqw4S#b*-HxhW21W08zXI@(y27-Rc`a)Gukj0!B|@$l8>+kYQ=Zm0;^A8PL;UytpFvEBz)a( zjFTTRzogv?>=Zz}Qv&&l@4643dQ1$^rhF}dWa8aFwNCjF*R`W+@pa=ElMy#~tE6^| z2bfnosuCYOh8Za_eBTP6!mR5=S%6y}6YKF=kBRT`U01<<+EK1xW$ma>_~3Djq;^z0 ze*S%H$Zt%kHs#Ssd@dg7at34Hwi;?v%ylSz+LYQ7NIIT{z<1e#fotH92@LEv1~!Ii z2_OcQ&;>{0Yw)Zucp@k57Y4@39LLP+P~1xB!6Wh6`1v=jBu?5ChT=yoFQN07(9=uk z(2=?7R%BHx@^h=~PfWWu#Yvm;tc3oFa}FAwRe}eg7GMx3>lY^5p9l{ivVURbO6Za$ z^h+a51b)7<74{Qj?@tWzCw7+5T}tS)CG_(n%rAJT85l4J15RMdVTiLH$J$_-}{zQ;JaY~y4)24WiFnvdu zZFs0Dm?JvCc_uI%8KEOgy%A>A2vcYz{x+EH2u4=7Vka>iy&^e}4kZCE^BZGYLPy~3 zB#3}4twRd|5sVu@&6_V@&z?-dD!cF2fITm-6Dz&AElSFahirY_sLM1{?LeGZ^gc^s6Y(?VYQ7MV`Trlpg zLvh@f4;YaHQe}bvtEeAY-OK6fE)Aa%oEwDybNJlKjZ-UA1qH5=+ulnXeRiDl1JxgN z;_@BsE}8923AMKOS{WUZIs~kJ5C$FUIDT2+-hqpU?jHD*@D;V5D48I8c%JKj3@|^O zP$KdWi!1sNz>$NgohHj;cNovbpKycIzbZI`2dAUuIr1_&@?sCm9Oyd0;NDV40o+*L z_6lys9$nG>Xp}HfJXDO^iEE0hJpq0I8`aiRjQW(ovD}I11MGuFlw)a`s7+K&B7~Fc zaj+snX^ihPcQ6k}N5`r0xC^LXOIsM!Cm`_>E)qAD$eRFgyYHGTYvEK;?FpcRf(O)P z)EjsZrx3hUiO_=^AhFYjBe^JC=;6HRn0HL3Ys{6pFK!Vx7uhCiG|_%V%}u)Y1guZl zY2#?(QiBQg1n$WL()u*|D8&_b_JK4<{l5VxvA z{8Tm>D_}+KB__}XzTb^!~+m#-t{U> zi%|VKVEj0!O|>M^^?y*4r0&49DU|d9vqZgvAU=lC78h<5qG%#W(L!aDuCS8Y#iG|^ zghBj9M_7d@EI0cYz~J2q;>;fA5%!4(GWDksQul-cxmeV=4rPQMH;9LEY>OY|h6>>Z z_~Jx3eRVh%%~Qe4neW^ed1A%hc6@W=J;ot+UuSO$;#ilrtWm)@8P41%aRGq zJQ2w&!_&pb*wpV&@knNVljY57y|T!!XFdF!9*8>(p!gGM=b}DB2Y5XM7$>)UQ7ws0 zM#AwZdoK1t##Yif4^)Gg<;DS)dT8&DdMyq#y&n*UADHKg6G1$F(E~|99(E7s8|2@T z!+{c23$5Z#Aq4+@6H4&4&!0ZP&<)-3iI%|~51rFqcEoXWK+O~taI+%hFzZOPRaVdC ztr`H;(E3wogD0yri*o4Lffg>Lkko;@(TXMqr4Hd&EE$?x=eEY;(x$j!hwUm7e1OrRuwCIjDCK;9=ZM>rr{_E}K_y7IXl1z}-n{ z+Y@&Jw`qL_sP1vcF{*_j3F#M(W5bs{IDb2{RdVW_)0Gg#-GfN*sU4bC2QLl=BeQzam+h0B9SEvSIXkJF#BW}Mr?#rDWPHBOA;65{&gqQDZ4 zwz~IFZ0qxpL7}a~4;)U?M57N;dj*6|FQuWmQW9Ji`)g3`BEqtM`BXjJ=6hauF12g% zp+Z|ERMKX&hooZg0oG~6idh=yFf<^y$5HS5JSv3C3G+N$&`5xpcniRp;barYA>Zg+ ztmE&ec-svXpwd)ToCyw{z(J^!9QNPxA2;M+HkWz)?Zsl0&_VSBkGKg3hZ63f3?A@n zaA6KHPH*L+>~O{?b#B?iCOmOG2wBb`5u%FWKnZM~to_rR;TedFL1E|QwjUxUV2x2S z)PE>Q0_XsX8;Jf@D-1sf6{Z^FkO?%G*Fp$>stc|s+uD_np}lorYj^fR6s{H}gDXXm zL2_Z@TS~Y}ln1x^{EKj&4qlEU1w#$M@=kL>B{}}VhP~y*L(Lq|2>LfN?G$Q+6CCc6 zX9A1qL-C9a41h~X%ULi$0giasoP1NHe9e~Qb?`8{O zI#Eb&^=l@d4+ir<{|Q2r>iHlAC3MehWS4A z3>%$4$7b&#Mal`$H9&{|_kzXyX}(Y`*0V4smeHr{?CNTS&i(ok=+Y^B{BL-qym)CRsE}EAh0%_}Lh zex5^&XM*}V^twlZej|`CL0LkkknCSMGJf>~sCn`9Qb<;iTb!E8_aCE6-zff!B;16t z@>)n%s(il8ii;A|aC7pYaehff=Cyf0wzS_E`^@fpa6GUkh4!+@7F%^7r0kuKDdW9O z_ze|sFIT=zxZr*mK6>hrd@Iv$jI7=hP$9o-feQ(28}NO%pld`L6r>0u3er@h2}l#9MFj;BMS4#JMVf$!^pH!D-jS*hR6v>-kuH#c zR7F6k0z&Aahn|GAcYQN&=KWv!&u@ldW@mH0{q~%*=bkNhuSRM7zIyR*zYB{h7;)A< z-?T0MS@PDa7Qv7$Hf)&UM$}QcTr#aSji4*L6xv3Am|1H-4J)1=0fI3wJ>8?U%)uKi zr7n%@01%UjBFCJvApe#6kY1P(Q&t=JJ`#po?GL}9CTV5}e@h6@CQ@PHHv5gT@yt$ixied#M z({+d*fc26-To zG0gw0!US4Hj2+ZrAuHtYZUD_1M5LVDwiPO50U3&&VJCZnIhC3ai~()CkP`{&aJ|iE zlqmcUPkuJglcEF6fGa{r}9&4-&-iQ>>Ede4q}&EW$gPeiHOVy5}}ojiv|!YMWd+4sw{7O?9V8 z&i3d&#Ni?f%XS1nK=f&NM0XF+2AVlF3UnM15Rv8tIyYV9pCnO42z5(`jlM!P7KP3+TQ)1sgCG!8XGvuG0kN>l?07y|R z>mLyoP>D3N?NBv@C^%R!Cn$hKGKv}0;=(_MUb8)jt^q73;(a%S3A8hY6McOF;0F6l zKM7Cn7GnlI(G|%G;*e$=Jiz=`L}0@V%U0eu_G@^AP>g1*fY4*9TS5-CQ0 zTR>L=J)5o!4nKe&?6WJJ0n{l^B-rQ6+d#h%L=5Dp$PU^$3>?pJrhi&w`KQI;e_Ax% zj#WjogIxy4p;9uyMfro8Bmohi5*G$ysA@E(ZC0T|9*|n*csLFa{n}Eey>O-f%);5`>Q3PR0l~h+cQ^b4 ztomma-G5f`|7VrZe-Y=#KP8&{a~w=New;`M<2@V?;sHGe!VFSWWQH7i2GE4S0HMnQ zD$(a(3@c;>;{%2XjA4X-0SDLC;7Rdm2VD`sL}1_TJQ|1qy8^nQ2gT)|FM=aa70m~7 z^kD$)JjDW;Ss7iXLzvMf3t|ySXrRHT z$GG|W8@x#kcaXOb!boU_EK!cRaQ>i@oWL>Mt9Y*iwkJ{Fq5p_|tZxMH@Zp8$)TfLg zZx5CZZ3mXjVLlFFY(K!J2T8m{&1B$mG7|q`j_1Q1>`n)rb?0y(lg$6@M3;uw`(}!* zJCXC!m+674hmXP>ra50jOqV;1;aI(;Sh;qw4gw?-t|vql9@=gwox_`SK#(zXQz2T3 zD&>tk@wZq24w4yD2xP}#&U3-CVtkA`z&}8fi+){*2L1pyw2dKM8SC+Uj5Z_OY^6En znH;5=j8N0{jL=p;Dnq|?7bN-o&XyK9YHM-Ci; z4;NMVSE_8`>76G+j_W3)tqw~V@ge+Q#r?LXFNiV1DDZ-FrpX6qKOgCc(|ynUOT~05 z*3WQzfU2!t*t6=pm3p+^cE8`hMXsDO+RCK^<0GM5H3b z4utn9B@cFI4l=9>Dr^$iYR5HGf}OWjNW$h(U?d*6xx3a1IflmR<_*|?-lCri!slFk zx8+X-s)m4^k)ka*YQqrbHrTH`#R3-EB^lJm*F5M8vgT!5wpA>6Td9LGGVrF|U@$A> zbce2eTcDsbmWfD~+HoA@Pxf*vwNMB-w;K&)M6MxP3(H?XTNc>-g;qt9@ zPbL_NJ#S$1bH^nC#Uti5#FG3h*}hoRAHR>YXu(aa=b<9ifAsv>7-mNE3ZJuFC3Xat zU!6P(w9vte%AmI1=q$lkY{d^M1Gc~IUHi0iTk4QK^#(1UD|q4f*#!gM9lq^pKtiIPr`>hy)k{reH5E=0&H3wz$NdG+WWUV_B+|JYHf>K4+0Mdd4wj&q zX>wTu$}*@8dT3`T@pxzp5Cxw;Uv4n1g;-{dBb+L?8GVk6Qp~?V@_WDlwP(?KaR)jr z<-ig=sQx%)q%~^XVV2e+Apqc?l$Q!qthW9Cq?T^!Z7~&+=1#n!Nu8qGe$Sa}rD)%4 zp}G^5L>F=8yEjZ?k@2%y4+@PVD37;wFlmEi&)&iP#&tzPFTq(meus6#b}60Yw|#lh zq*wh0MsUe`x^kKuZCzOy7@mq$$h&4}GX1oepM24>iOjaRzFR^u1A6hyg~zq_Z_w1; zk?VM*MZqJgFnB3!K!=;XrlN)R?z^u(=-qD?lOF*@FQNVpxsptoYBvzeer(7_-ToyQEHogdnhtv*Krne483P` zw&+N=U38mmw`tOf=9xuvy+}U|zBNqzgaE%TGv`cl2n$kBBz{bE0`}EM0=cup@#NR7 zBDS1p;mr>rpTLEIturpeep;motGTE2zc8*~n2->(^; z?#F*NpU7bN4!^Ttn{4?i3clLxNDEd#RM=Z^X^W$+ElaaTTnk(a0b3#mSi2KJ`0=G`h`D`)^cD9uKIhgrIXrf3_7D7=&zhwJT0Y;YXI+H zox_eD{z^I8 zB70rlPRD^gnEBA+z)6KjyBvx@ABP7%_)x>MGgC=)4hJYw<31ib;IoS{2*1`7>5WnW+*zu&7V63|KiCxU`;h@x; z_CQo-i>7)t7aV`{JHQ!0tT5u)C`a25bD{6A5AMr~t}4Dn0u}pozRhM$!7BoX`b}hw zePcF!@Xg)88;GmKl_DU+8e3q)J@eW+{D5Z8C2x0_L0 z6IjMzl$FUABR05baWwEa68z9XXa!>SMhW}EJ^z_G|7|L^J{9PJvITqP`zsM?a>a-P zYLURK7IOyuGSs+WUhoFbKzygdD^fj4DU{rJb~oA$Ye|{}-pz;I`k+5OMSW@Hnw<3D z0E=Kj`$UAn_rJ_fZBSzy+CxQO9nl5xo+GNgk7y3lfX-R?IJ*mnX~9|Ra6ACUSG?Fr zIQ|B;d}Eazn~cbm#h7k)pD)KvRTP*V@l340QY_zhTUesb%AwLJ6D!xxF^Nr|KWO{g zJvF73hP*o>BnSXh664?g9? zT+gyN-E*OwClU{l4OUA*pMvj;ZzS!XiogS1M8~XQf!Ss!^#h&SkP_Um8Lj5m@VlT| zP7eoqaEWUssBRb0AHKjR{B<-t_3XiPf~%)X(-t*d_4ASF=wPOHx8T|gUA;{>SJ7-Q zh;Fue{4O-CVSV3WaW6}`V2!MSix~VX8GJBZe68s@RAD_hRL7htn0d)@w4<`k8qj@Q z2x&PB2T#1XL}EYfFa8J;yAgdLO_UxgLJ#kLG@@6nq`qS~_aiG{o&GMVns=9)hPQQA z94|FCX!)GFMtBmb_%VYbGFuus2>(lS;LOtAK%vzGlIKgOdOW>mbwof1^O<5q6a2r! zin>{6<&-{|Vg<>^i31HKEw813YsEvhZiVW)ABOhK-5V0$0g-|s59nohr^gZay}M8m z6M04$>Fr3-WdC}bE6swV0v}fwj-L=eC_}7TZBOTlTVM~4T8swOz;;59UXQn)(_tnl zyfDPD(c$<75Nbw_6G( ziAQl620G5f_PLR%ShfOvH0k3gKDvP0G zKY~UH!laJW^HA^_(xm--PPC8en%;IzTj=rxuCVWkk=A3xjCbs4n)-v(a^El=;5o1Tic8YxtV8*17_Huu!y=hW~vux>=K{lMb{2jPN;!eZL2v z3Ui|mMV2f^agYbAcMPd(NeXcH-s8Ox2>k2iXAlVUk2gL)6*PfCz6xYUR*0^!U}Jr$3dM9S zqe1P^=4T9Wx}KSd3~{_&Fnfy3Q#yDdtg z=hqdYZqo25Ezd-nr04`?q6oGVj$DpL0~s)%7CDtroLVgmX!AD&V$V6ku0LSMIBA|= zWKA%I^TK(SO-Jh#bHY)Sf{_-MUBH|~iEE|Gg3EBX%%J@~AFbdIcGt*n#|59EHK*cc zys^0Yv%98$Ya-UAlBPHw>u!uq67H|8woL1yiSI)`$s8Wk^$E_m8h^pX`dp`b-=mxV z8pfEcO|YjHh7(%6u&_Ww;5#-Zn4y!*vz<42gplm+5;;lM9$aZMq^oa`=FV+LXDLH1 zd496uMWjevaSvsJ>UiIw>4HpR)US(>moL0ovGj288SL|w6OP4~w5&Np73#Becw-9g{ z27x~s`QWfiyDGb0^2C(;dG`+xT5XA(a!DhE$t`BFuA}tVIYb2_(*2NfD4a!eF*9KR zR~(`R%xJ7Gaip@G6r*6{y|ZF@c<78Du8-1U**947>lG=i+~7cJJRIL*Flig6ew9Nf z&#rk-sn;6tI9MBRH&~YhbT=scU60V#2i8E*&>nLhQr$v_8O^!*MaUXi!?5(p_DAKB z(=yyGjcgt? zAxY_~j$FGSQhsSPkVD;NLj=AIh#j;l|$l8z-=!L=zEUPXjTyWC&V0vA^Xz+p=9h|$Po;B<(%UNiah#s`A zXtjr?4TQ~#85ZG=XAP)45N9B_0R+0`n)P}Jh7G%SzFTcYLzEFu{nASRE~3)@NJPKi zpcS@LmXldf(`KLqspsE;GNO6bVQVpULfh1(L&Qe%?D_+qO5gUaWcRh4Bo6cXO50`J^C<2RQg{+@1MiJ#N5FIp!(#vMl(`4d~9yoW~JeXpg z)%sL}+EcJuq&di){tmqG9v!mf)QK*goN7Nq^d5P1Z5EPsn|{AH(7PUQ9c)vwx1RA} zArg6Oi`0doq)3VF6TdDBAN%Si#*c0ZiR&y7C8OP zcf5W87`zu$L#g$jY_fAiy>#U{NGqc@JdsH5ZRBOLZhmtcFzy&biP?v6P)(}}4>z3y z$@Ehpny6PHx+y_G!EG8!R_9OQ$3yvi(?|-YV`c!#G}DH@ktOyd6P$L*wQEn+C{yj* zVaz(zTm!Rp{Eh8sK||{JOp`d}Nkd(cl_Epy~a1r1jd=mZO>a zYbHhgI=Z1BjpSp*CX@U|QH{spCVywd)-85C$O`DxpdJs>=3p8=^q20;Iditq_ppj? zR{r+!ceHkTwMcu-**#Tcl|&n!Wz9=WgS+-Jg;^mn*M4Z^38v}*}HXw+c1BJ}bm#8lX% zDh9rPDKCw$7#FgQ?8vY=*5PHf>hKs_=tpoSJU_a!7^;L#SruMjAZ|5=QC9+Gp@2L+ z$uaVI#Gzq-Y$4}N?)fEHB(1M~-0@V3ZAQD;lmii6wbp&JwpuFZ!O_q4B~$9JXN0cmLqtcALkx2M%OV99HNAz9zBe zbp$y$LTSm6j@j5NdG+JB82McoAo_h9F_!C9e(eNzYUSH`pX$N!FpC{ua!dO(m;p-sj^rNJn{9?>hXBgkGr$S7ULOSX z5U9dIB{VoYEbxE zJv2AjYeZvbZ&uEy8)3J&XV4b-{)s&kja0Hr4WPE6MXAb*txLJ;F*JxABB~Y8Wk8c( z*(l!D3({PcJOY$ZWWU}*b~Nsiw&Kt$&Y37g^lNoAQwt$92wGfodljaz)E`Xa*zoy{ zE0MtN2$wVT{k$zm)b+yr)fK=?mAticrLz(>Yu{L3r(tKCMPsK&LY9HWfS3l&Wl!Y|2x3CsYSfH&35=ME1aI2AV?M)0Y}BBXD9-iiA3W%MD(h+ z5yT5CNuR`|srNFhH}Q#vKy$VUf-@N48JI$xqImc#?6Q3qV}!7f2FPHt6#VmeRxh2$ z`P|DFA?>bLr^r*cW?ON8aQ&YwL!cc;$M*FzTk27vTEqd2;f=b~`5hxmx}i=Yc#%sN zeNq=G--Y;{0x^Parcxyrb?jQAvTVl3jv#)D8Sv~elYtfvQs-w}>|R@FJ@6yQpfYT7 zmV5>Eel3=95r=l&pw7sN!^%sBdC;7DBoE3LMmXL5FpQ~ScKuT(MeHHn2Nq31ZLX7> zhWuia1Sjslc&bm|HaOgUe!BZzo}DJdPI5`dj+S4wXo?;QE?fEqEBlUOg%uTCNV%En zS`s!h%P(h1`jQG@PM~>!JqaT^v;=1q3b=Ew;m{%9DXbJR30ULJT@Ri4Fv~R^Bj|zL z^Y*}ek3lKP*&-Xf0yB&g%}?eXK~!mM*p^S=o@|3l>&{uB)IE)Tg2!GyxF^(L-Ca30 z0b{|pYbFXo(A+bPL%KfjCu$o}!N*6%(2YW?<7-oA)9|`r`-X7@qprcvC_hr0fl1Dh zB_@zRh#ALeCr-cCIrVYERdJDA?Oi!`0m+JY^b`}La)5DBaZNj@FoEEyETIFQG1yB* z8s!aGWftwV2Lj(`u({P7OFPHOjYY?H!y23+0?I3vMwoh785jIRcA?y*h1LbiGy~Bw zO;|58Z-^nqMhE~Z{tpl6l)M7on0qdM__p2B~G~3N~NvgwJEdh zywHo-XabreL|enHM7MABi`|DB+cyB3bvGuq2b_Fz^A2giMkJ00vml2>2qR$u^KTc$ef}0Sl`mhc>a0)kAph>ZnLS*x@+( zpB{5CN4oN`BDzHkZe5rO?hP`c$?AvxB~`PnHg1*&ZPu18k0epuHwG*A1c98E$=E%= z5;3ys6)z8p2;9k-Z;ciUbs2U_6 zKY}P0@PJor!W_BK^w13Jz2`Rs6g5K%Sq7I(elp`fkKcVGjx9|#*JYdueH|uB7R^FD z{bNnqtWQ;Vc#vtWpCsU;U4LQPgVU$6e5g1biD3KfSCtq^rKVu6t}0# zfKRcs;ZHZv-?ZOpj?c1UBZ2{hKkXAQ94nJSKez9|J`?3XBif7oaF}@d4>`2j^8u{& zxK`2mm2jkSZJ&n-#e3V|`mPRp8-xM9YY~bL;e*oyODJ-)0D|iXf~_8uqn+H1hSZ?6 zD8j27zt$>!|KQW(18kGYx-4kd6U*rq5dM}3Kd-g?F6V9ky0i5-z|LR(5b#((d0<30 zC~@|OKDt-`?tEukI>EMUk{L3I>^GE;AvVeBxuJi7dQhZS_V0I@6}<>Sc~*TgW<Jftw12A4SODnELrOr%Xoq7E1O+3~={V;BtmTx6|-dWf(SSk$UR)G79V zrL*W#3B{8kv*GxQan+q3bnY(La5Xx@95endi{aI6;ac@4()rgYu~4LZ#0Y3O;!ef zzIr`MTw$&YnmPBqGVJ$oTPQYzg31zUvvv*(P$mj>DZXma#D;lRh0;jpmzBOMWbLl~ z9iIL*eDJ88p+_Ytww-{K6WiBZp-T(^w*uTrq|!7+eF!+;!^%RN>5qBgQkN(RZKjw)Y@iFj8^k$Qo>HYU2Q>z5P2p)0!Y)G2S#))pTDM53SNQmZttmJz^q) zt8gj3L1GL*>bx)1{$LpWexGu8*XM!`)-vEa1h&)x-PS3~K`_t^3w|~L&2@0HNlEE2 zI@R+%Pe{sS&xK*Q+2HAWgHL-6s!{gSC^K%fAYqP}4z6b3u55+9W`GgmGOfuCT9zTY zl8^Een81)T*P#rpGg&^{rEUn_goWHF*@J$sBy}{j1tM%^5sq0sA)S>4kl70g}|~!cMBo1=ptjKo&A$;|W1ZM5(sG&|*q|1;!DNgaCyxboTvy zy3Lv?HJP9`+f;kdoY)rn%qQwKyWG<@?I{FV+qG$^=s@mMdvL_pCnN_9GvF8%s`lbDg;dfHZ zWUnJ%r;OJ;5Mg+aLehn8^oJ-^(jDk#2bjfdGlpLK$_FQ)HWrTjjZ*8y@PjL;0B{LW zFrj>ZHlO=Tx^F2q3^L`Ng^ZUDOar=8DIZ%2VDKG(R47JdB@W1KYzFa#e62m&>tHPA zrz-gaJlA!Jv^|xlgLS9im$uY{3@6y(l4G_bnm(uC>PI+Pd>9iP@0Le{MB&YL?I4T6 z+m1@1kVR|8KRAin2SK05vk>_hT~5**jFCVAo?KWY^zN61Z3sr7I={ZF5IY@({}%M1 zYIrmPAKv02=xs)^mP8%76I7k4)}B3LF#sF_?hhR$liLVR)cXg8z!czZqs4IgBVmC8vqdYZPQnS*r{QygE;pY` zU^&q1OrbQ&38L;_t>OWluzRcR=wBtXVm<*4IG!wITBbGbE%0IJY#nO+oN2(XExNX3 z9PSp~d=pC)CmL_gRNUOw;RI?Ax#4&VvW6!!5p7-KG!nRhXTti6cK3PAT!{0{uAkZQ zDxn*n#S4F?CpWUc-6QAb#2|6vSW?9)irhX$=t!lo0YkvZ275V>@DD3Y+^AZ(kQNk} zLRPfqa-a#?p}H;STG(+cqePdgp*{ZZ^kX63XwikGo{hc3z?%TN(xSVWHDrq!8y^7M zY*@A0d0$|O--(?K<;$S!?9#5nhs;c~kkz$*T{% z{KQX58l30*T5pqE+?Dgtv#Ut=wBoyur#QzN&L@3VDaiu7PpGT*oS!q4^MtW^qRz{i zX)`VZYhgR#KY2FZ5(LE;3sOw?ETl8fFL`t1LS!(} zk3T+{nTzF7zjk(@u=A6Rm4m%y>lZj_K4EtautO~mR30o}D~C&|b=>KHVOEnr@>EK# z=Z=6zdDH!h-aGQo&ARSabl%C*ELUdF!qr+MhoQmuLzk{?{-*_etu=ZWN&;oHHnaYZ z0`UK-QtNO>oxLC0|7_ATW*DlHfB1T?cKk$X>l2O5|E_A-9hC=4y_od;07?}czFw^z zHwL9NH`V^%RlxtO>vKohf@04nJ+p_QHDF^)wd0pTv1^-Z|4r4DJ8A-y3Yhe)2c^Kq zHfzV9f>K(WYX41Dk2~rbDCIlpnKBIZ1Hi@>YRAu&wl0G``>%EXdzIyEt#!mOl=Xh7 zfX3#3SWp{xM>T;>`A>R&0-FK}uGfw`fGvUA_+M4P|6OOfSZkd=47~+5b#3!MEY$V7 zqd)<9P=laoV*a7sV(s|(($>@0HvgL{%Z*yVx_TJe1vaI%36}n&!hEfD{4iAVerOh` ziT|*$R%=~83@rg=G&e2(_X6-=O8>R6Tx*>-40Qp8Kt22?h1Em5TIXBQFRm@OUxt3A zTFEQIpioDsg(*V5RSJnX(dnF?n@hZVnFIWgM}57%VBOWL0GspeyW*k*HP!^Z^q{wz zzmU&(t;x=F4;$~4xu1w(%3WBBH=#Ur-?=3Z@f1EUFQ5MTMbDJbtbn&yIZx-j^2siN zbT+Y7zE{6as}JfKgek$KU)~vEDAl}l$vRBpn%X&KSK;U)8F3|j!_|+1sa-1N*{e=U zQ|AO#s+^IgGH!stUd6oiWg8Khq8?7N9g`gMy;G`It9MmwGbJXmI(th4cGCCxo>z&&|6FYNFAAz!E7 zw}#1!+dJS2yl9|*C-&SC$65!X;LSRxn!5vXTrhRj0eTns?1;c7Gn|KO{40s!N^WUUj&kT>&we_+sh&{%gM=$5CY`hk^8vh4G(L@vO`{OZ(3A zF)m?mt0l{>RbBi0&HMVDxec3B97ppW$J>R%9(gw2Sh_8zm>n!+Z>Oet+AXe=a@oP& zzW$4wt-IIiE|4*4r}t5OXWmTPOis``X)?nzTTot3@lwFfu4P@xUEYUxz8k%-kl{SP zGM^xjm>}z6otb5sWi@UkC|@C6B`|!TB>O0lIo=HV_iZh^FudiVi|R`zw2kQnl42^6 zS@Lj{WjxmYtXq67D`n}quYOfoXR_O4J+BIu@$T}UWM`n|Z3gVe@5H#^{tnC4EOd-A zw9muqxL=m3WP&m(sCI`lt?yy|LE2=uO>s$mI@?tHGv-^uJZTBeO*bx!1{TElLSa>l0JkJG%ImzegQm%ijT<(dx( zxfrr+vzP)#E-zV1)I6#zEj)r{#GiXuikp6~^gWj}zQWhvVqvvTkc8K*GYiY$x>l1} zji&gOV5;ZM9d9D#ZX#4dEDQHMOis3LC&xeR$g=5FDHf1N#aZ^j72x^`?)z#io#!95 zDgJEy13z1q#;@)8BT=`jKL1fItB`c?VNT~==V}eW@|&geTzRhb`_7v94>#h!3{d!@ zl0-S{r|v!Om)%$R(eZnX1)_4g&aBU+CBZV$<>O(@^~~MtmwBZx7hm!-_4Pt{BepK< z>6F;be{oN(y=2&sCzkvr;eD4yiFNjA%W}Vei{wRaLUH!fN2DyXki#ozjWfRy$8*nVd$4 za>{YlY{&n;b%UPa>nqvkwn`L~{@B;|PVW8hW38H>PE_P`zG2G}Cg}%x{8*ul#k`!W zOzq@5e9?1jI-po9&P#Y8 zvspWw7b04#zhnl~lQjo_9fAyK6(QJx*wpnWcQ(;W6jD3fo7?7YP|_cZcy(xvMz+4@P!NUNMii(A<< z_l~z>uV%Q_5O)98Bc3f-xm@-Bc4oI~s5kr|FZzAgcuM3+>*V|{H!s~$wai=Sj1fs< z-u&Uq<}9gJ&RZRAx6>0hZwLww`{~>I6qQ`_t&)^(1^UXFEtVE~8~gM`=4&khs7GSV zkIZ<(s(LTI(HZ3-ZA?kO|5d%oY%#ryufME&e5u?FGfXV8v$yA;8=C6lM|n!(i%w2d zj(rjS0jKX@HK*$QE=drkV}?Fmx}C8oi@W^D=4JS;o4sFLzO&yyW;fMZa|M0R-}=Bf zn4G$WR02dLMxR(eq5iVXtINuwKDxk3V&Y?~dinCQ?_3*eJhL@-IFGKlZ-lUVCznLp zvU8u5B3@A$n;2K&-dE-55_s?SlgFKj1T>*l@r#v!=-}Lw3_kNG;-m^k*V^t&#B%{e zC8MY=1;wqoq+5K)AAQ#-6vW9)@V&e){D9s0=P6sj?MKF&uZyd->U@p<+}&NC#`b&8 z&=B>}?bQxluDu)3MIBGn6^Jg5LkxREbBQ%8@f2s!ZaxZMwGn9Yh;u2F!yNjp`eD)QxQIM3vo}+l5 z94Xdm3dlZ=+L2mmEUajMy-yv#!6cZFl3xFJ>vgm43z5L?lvm%pO9h#m`uRdRq*NCk zn_0~M84fzn78W}9tbAC>F!~Mr=pp zug1^ich32kyH`s_H3tz5cVwjVaN;&x09!Vp1PAFiNmzF$*K8mA}S2rlpM9L z{$Tz6s~5Ov>k5RflF(zE&kV0>4%i+9rWdL5-)9)q-{SdZ=%0w3<*jzUJl8$9(PEi{ z{Ia8vzawJ78E4rP_4zPQb!+#$tmGBFpieB+ngknIqQ0TJ|H(@Bk2wvwFh$9s`{;`r z<4&tQz}dUw$E=*-dtW}dm7b4(;n(r{sX?Ld{4J@X;t1RggUdNRO!3f64#cFA9{xLj z+Ud=jAxrJR`wvoezm;cnc0S`six#nTXKSzUuv%y|s0q-YPU!Y~q3Q8J6EC3C%#S77wyZJ!@sM6g$6D_0v`z8a83v~Lt>NEIa~ zh`^o(%l?tLQI;zAdD%(eyH# zqq1#Fgi%62HwXI1@`JT2QbH)-VLVgsHoYD8K^k3d?)ey3LGr$H&^upMm`pAE0RBw7FcTx=1tD}4& zvRG@;3tyiT%=RA6B9!>&*vT_RroOSheG6PcI$!lI$Lb2k&xRa-`TCaH(kQEyc4DZm zLqdMxi)*?*rVNji%@YOQ6mLD;W5dS=VvOG)?yFuo(e0Elu=q5~)sGVGAkca2%%@`S z)93}kmw@D->5f>w9EZ1+g2!H~ZXfY3!R*0jU+xHB79Q4DPl!Qy6Nbsv? zJ`agwO?Thy+PmS-GBrPKG+UZSeoEKC{qpx3$rJ&)3ZhT6Ipt8pI@sgJx`G~)YacD; z+_a6z6Tevie%;iGSL)u#ZT8RObus6=^;Vqt*>5=X&GP?bTeaQmdL9M~Jw=MmhzM`> zb-N&E`pT0rQ!QT^BVXoR!R+rwF`X)YSNkgY&tLew(gEKk0X7BexDgf=Z-#IBpG>!- z%9D|EKt{Y>>{t-X_H*hHqPo{tCxqGW`3t2;`o*p{Mw)HzxZ~+K!L*K^+?e9>H>5;Au;ap=XhK{Q^&1XpjCljFHrmzOXp+aWwz-))oiW%dG8x9oFv| zj$M^um=$mEJP%1{Hs0CRRc*UGek~|>ZuJg64mf;rRuKL`;9+yf((8*(blOwPcNlD} z)Yq%Q)NbVz=eOv2>D0H<@%J-szmjOY#d^|%|6O~`DexYM-Ybc-lJ5sjPG9#gASSW2 z{dkcu`SRB_t%pxtcP=gI+nGl!s(qhs*GTVkEDlsN?g+Y+z8(a<^rE|R=Gxna4s&Q% zwK#hWfLsi`i;|t&=E?nhTr)3Glcl#n>P39Dny?#2@aaUtuZW<)shYzRYZk?KlKkBJ zggWh_k8xbCgF4HY>B#8XW4=jpw?4b|OS}7gF8p#|Pw#H~>vm;ZMnScA#uUG7o++Er z9Yq$AH7EBkXUW8Q9Ezy!?PE_?Wd69FhFp{@ z&8s&Rt#m6kNI0>nxBJ9zz*|`xe2=lL!yx}k?e=mM8e@6JlIfS0&GCm8BvZ8lw z4-S0#wP!5ddZ0=jx#>jy0#@~dknV_@@JtouI}F8L-}%09U1sa=t$byl@ut!1URH2z zgaVN6f9zq6x+#u+|3r_|^;NE44LcluTh)cHIG%Kr=+?f_7hZQdy(Y>H`}EM85a;wV z`s~F?+eaDRu=rCUCxAL^mu*Kb)%PMgl zm4BRj9>d3OFMRJ_y0W#9Mc?OL5=NZVO7A>dczi;tKs(k*%Qe{HhQ>40fp~>`^qpf9s`2HIXRZKO=3h-q zeeYWRUD@6w`TONV1r{N3Xi7A9Wx%TlW(r$s+*Sh~b?%vkM#!M`-ep6r{*RvV4X>Ej zIF;=FqFyV9X5DoQ(i;fpVCVZAR&q4J)6l`mr z$w=B5^Sf!vggq_&RWf=}xSCLebVN{}rs)GACXB*kyX{M)bYIUH+E7H?Uy%D+-&SoA?(K{gx1eOD1s^YutH_7_Ax?;JJX z%j_c&kCrkV+_@%1^DK2Po8&Mk6_hpfGqx0`GJlpIuGAPc<;|9vsc?)q1B^~ERJ{TE z&&6vbTJxsG*QZ{*ReT|ul}|hfb$Me{=h7|n!KciEoF6Y-_^|Wrcca<0t?xJ9&PVR~ zqLJ-s1|CP*T&rs~k0EXjQrzBh*DrInu+BE-pW{`6+C^M2Rr@P%p3;|b+Nq#YE<~xy zH|o6l7IH8=uQbJ5^{suz5XZT{KW+~DNCSo+J@Y6QKEuWjTJN!zST)MCeP?bnO$m|M z7Nslv5)N#ZIqx1}%94KJlHhy8J8?AQ(8`ou)Js<9Eb^6S0UNo?rhn~}zeY0~YZc_H zNSBOzZl5B&WKz7N8MIJY`9ePgJKPpj0r|Ll^MPHs*SQEzeANdoYvVJUTOC8!#GSQ) z+i&;!MxMbXZi3Hhx9Vz9b$v)IgA1$7o|;; zB3N+@qoE_ZN#NEMGOZdSJ=1@pJD*{voWT6*NE4%GY*K(5|E(Waq0MN&DR?|BcN4jw@% zhkJ_H=Uk>fCS2QJ(CW|mdBUcm$ICy@PruW4*-CULZ<|}XiZ4S;Klj{}Q`VGQZgY=n$ z^ryCd@MFKgllPp!LJ-ZW&_m$uIa{vlUttcG&%UL3D@J;!pJFY-?UWaM`L1haU!IT| z{qPnF*Edg#j4@^Me9A#?ZMP37{cP9@yqZ4w6<_x5KH_)UR!F73^lRzihh~ixS;e=` zF8-Fmacx}bw5|V|Eg#Z<|5%;v9p2ntg_TB-{|KpBY}uBWRa4QEqF6w&Jr}*w&X*Z#LoTSK4 zrp4y`BN`Q)TvZ%!H9c7R4V}k1VoRGVfk6xE$4#5lPD+M*S+%+6tV?3B<9v%=`g1oG zs^3C?y&GU(ejA%`Hm7f9+f6(b>x-IvnRlmHnU}{t4}0g7sd}8xx6TI=CzSmSa{wn4 z*MX6Jb<3s(Zq!TPO_)<4Qc}kb`@O|8_+7is*x>Hdn#W$&>&V)p6E}~$uia1lIrZps zgsfG0IoFrX6hD}HYLK=+!eKwJY%VIr419>M6W{7jMAs*1gnLyr?izFp8Y;0v5pFf`k3c)4A@~ zP#Wm;wboA1uS50wJ%J&bP&nt+=Dg1O#(2cR0b{mgtGx71Rc5tE&AvD<>#2tVtll{- z?u5HXC4+yKRI$WjO!QgY`029E4^jer?{9@^9zSvx(6mFTe_J;w{c3Hz2fp$Eh>agN ze>7V#|2BT6Z0{sTqMN7WUGdbo*ssS%)XD;{UtH{)(4PDxmme|}|LpH6!cyggSNzJR z_RBs^>GA!5#aA8fb-hW0$)T!WgjIE(?~e$M-pYXGg`F`b?R_#SY0CL*-#`w(mWJ`p z8si=5`s8?RtW+fK41?m`uC;1d%R^u}PkqYPHbb@{{e-3JO{!}STqeL1dP zp|(kBHq|YcvTb7z<~79)tE3pujK#}t995V6{$%*~<{Rd}#`^)ISmei-``qUbft%l7 zt>u>XSqd zE4thei0P#rpc8qHzP!oG9&@?n{c9}>D&ws6{)M0YReSUGG&48kGt(0-arB#8rBh$a zW`ak8PaKbvead2TAHN)j&MT^_U10^1qf7!GV?|GDgjwq^`utfbnyEYcs%=s+&he(G z<6y=4g@)YD7qY4;O8yR?gH3(r{8oO-g=mbFG5oC`b&LG`l*!Whbk~Wz0wrFbKuNfN zXtnMR%w%}E*6M#W_7qT2eNQheTcnh9i%54$NvTLlqjYx*D(Zq_At44&5k*D8LZlTG zln|s55CkNprMthkA-l`Z|2d!M5YC;MJ3aT__wF*i0;&J?$ckI)%;d1Ui2O3*XV<*; z?e6o9sA+k!P+6^x>MWHf%jizW(QoYAmnDt9v!11n4c;^O(Qy9bV`-=SUu{ZFuP1@6 zC7F*xU>kDOI1}l zQ;r$ci|xy#JwkJ`1a7xcxp{Hys6+UVY+2_EnF7a;)buXC&8*CjF?j4FiuP(f6fe2j zeOv9ge%yyU+rcu$f+5I~+GFNTFKro%$AW?^RsiRM-{kny+=eo1Q*}XqrtK}HdUtKVe zpwfwHzA%|^Nj3C-J46>>&mef+Q_loIOTETi{vx5Ur&}zrao8nk9U9bnEU0H>)W8+;dN2qP&+Fgm^K?<~FWBBhI_HCzfE@gG=eC^NJUUe?(MTt0^yr+7; ziW~jw;CA-%GVR88d87Jk+lb}On^P-_4t;6s_G{sc>+?)o+ptsH;iaw#;D`PGI?wZR zX(~c6^;^E(y5jQE%Kh~+&($-o+XnW1%CEKqwko-|$3}d&duQV%`?q;sFQK;s&`YBw zhv&0x%h6i~TlM+qrRk-W^ql48)Y3ak>vPNNpAHYNjExj8mG1RI12gyDPiGjbUvQ0g zSXi5AL{H{#=j4S{4Mi+%EPos<{J4}V!E--N!+!c~mBv!%c0M?k*59~qt!lz(&cl9v zd-H15@XC#z)vfvM%X#7Rz`5ycwDMN3i%-zj=5~$G_V9YDY}Ei&*YtLw{d&Uoe8_Oe zcD!0IkMYKZDhGX)^|RZ?LlHMhS96=^H)pp!R<;3)^^EUZW6-Yc*9*P*=>3zaN7s4I zOm5DuCq$3D{j?qadkcL)Rs=fuwf`DkPHyX#x@ zJPxO!eTCaRS(`t@Km7WcIy1X&>>IJY+FPA(yDnL~Jg_ycF*p0E_ua@k_;-bt(Y@>4 z`P;9SSJSFzAMV>&weH*=?b=XWODsLdw3eD`b-rDrbUBu5I{e1U=C8&qU(fB~>Gzun ziVMA)8yY6ZpcG6H2m}eq)Dc3Ts5JjHOaq5No(n=COb||pUx2HduZ~-YOR!g9s8@i$ z#A!cY3v(0%S~+sg1M_w2G!=x5g8lh7ataRc+vJIp0bL4A7pAkC7V^|Oa+s=usfu-z zYT4f$jZqnk{KT3U`uknlk*H^?#pO#~;V*8uRIxN4QgpoJaPis(ExNHPi=KH+T={IYF-Ytgsu|F18 zcn&{x%#^iAAlaR|VSx47*!^KS?jp3#C;OYx8PDXS`cA#(NZprtL2=<^|Gtz3hxMS2 z#c!!_9*qy|Md?o8a#(55N=KYkrY}2cFH1n}rJm6THRvWEhkv*6S?oGgczzD(P5$cC zF@5#Rni7jnIa|W?%YN?Ai!GK_0jJ8=jkfELuIyLZ%#87PAV&MEEJ zd?PWkXUMq42x(>#cDSrhzM|c>+?*XPK=U)Rsn8QSt8#CKr$1TLu!+Y=wPesYs7hc3 zIhV#ne_)X_0jhT3eUy21B~8C8rSPp!HD_E$eLthgP0?rT@dw! zxti7;kcgrZc@n4@IC&JUQf^+q=lYi`{I^(Z6RX`QtnLL`6gWOO6TC9-))8-8Z`=QY>NBk+QYY#`jKi@D)?$am_KgCslI~IZWPksuMf2B&1rPU9#WT z`|`p;ou6*K%VsKt+Hi*>u2)s*@7t+vO6107pkM9nru}&X)xoD-b05u>lO1c?k)FX<6L4E=3XS{;eVr5m=m!P`raT&$AElV@hZavWlSQ&-CP2IH-XcpMxkBFyu z^m+alTb85r!{cU6>_PlhF(Kb(skb#P2H)xGrX_M&#H2M;iCp?>eca4K>abxonCtUqO0xX^ z+P)gmBZDcj{3w^tUlTs(s0rp=1i}S#0@m*;@z(ig-ZL6x5cwr%aYek|1Q zxP-VU_nB;ccBf%@_3dA7A19ydXcU+~a?|pbOq0!)+%yiEdzW$zRf*RAx{@s`ea8NJ z4cV2r*^^{hr=`qz(8nX_GAEO+h#aSU<|cQ?@pS)kAY$6^S2`@_mh?TJ!pT4SxdXn! z3SSPnSH@RF^SC~fIafg?EEm{ukW~){b?n7PhUmKNhRb zcR}QYw_<@Rg_y^l@}^Mm{oh~b`!Z2!PseOylTu0&P5C#}`lT;5-c8p2bZ@xZQ!bjL zSx(Ew^m=S(*$a{TsNYdV!nOG)CXK&sNVIpXJv%<->id4RmP~zU<4wb%_@jg$%BBa&ORX>d`KU>_qU?MLj;$UEKBf(+ZtIx0kNk`6U zb>T7RHBRw6S3s}U zBxBR1U^(h%slVYYb6nWKBYJ2C_3p%>FOSU>tcsmS%TjcTEYnlpA(L{2Us_39FK4IP zG}TcDH1r3i|S=19!F69+?Io+-`#mK|d=;wC+z zz?kAIbb4-jkE-AP1KwGU!eNRBVg^n%!K+wi#%*RP3m6Y>BCcPqub@=vy1zK??xJ(d zAN3H>NVRtHl_;~iK5C)d?7Q28ownX;{K>X~_46BLR+~X0AhZmRQKbWSskJ3KsshCXBoT1q-HKV{u z^R_mmU@q)tgizi-N9I~TwzUFo`$w7LhhA9@*T`_5K6$}&aVY@PXDGow1Htr}6WCwy z-zdR06YLfea4OgZi2Om0M0^F?3it)k|3lx6O)rQ=iU#8kaW8fDmAQUSU{dqfLJ@Zr0V~J-F`UI zQRk_X?@i8Ktbm*C(| z>tv5)1u{LLtbYfwiG^c5?g7>Q3oexNgi=u8;Hth*$-i(S^qam!lC=6lYfxB8$ZVR# zM?pytNWiHB$_vzmLFxWd7Y>9$$?;rA&k_ZQU<8yL?{U#6BDiN16oK=`g{O11dK3^y z6cPdv1*Bb}tia2DMK z0BqaBSqlShL%);bh;);n2X~lVO@dy;TazZ42yUDV6~yPn=`^OUe)sh%C-_CQ(or@T(EE2Z$|$((cIgqznqW9b2qke1+=nz!$zkpW-twxelti z1Mjbc4&nV)(*o7mfit&4)$x3+I*2kG=z#Lz@r!z($9C|kd!bQ#aq}iF64|JC5&E4K z8&(;)GP0*Y9RlFMX9gHM;OGm51HH5q1m~~QQY7H*5YIqCNSM$-n*;pdcitnE=IgybbyeQ0!EnLP23LQE;-0ixbToC{7WDhc8MK z@QE_WT86@C#{`)&6n|@Ei~6^YW+-mr&4ym2Ahf8$ zMT$jydcE5wn6q$ko8l7#PM!h=7{Of|3@|&q+chu4Sn2)TLY*XuB_MLdGj+Zq8HL`4(=;RCYS zVcY!&wNd`DUuA6Pto_S8TE z900XoE*uSnao|-!Tp(hmZ~+D~#31bk##XTxOv>QF%RyKP4$S(SAS!0n!(Qn78y1PT$IJ+U zrch`U_6e`Y=oDdU+)_zl8dmU^Xby&33W6);LcVzzngZ)to{0p*DX`Nc_W;?Jh`r3N z4&lLGp>7f+>#o9|%P=Gii!Z`-M^g|5fdFC{d~VY9St0u-Oat#6J2;#WVYlG$A-o(B zlyJhrIg=7DiFaBb1Dud^T#RrweA!dshwm7uFo++%A8*eG!f=89LUt)K8$OC<%vVh3=Mc7reRZZHXiSv-uKU!Yht&LSE)O!YlBeJ?{o5gioOx zoBM@b(+K=I7_)>++B%1S3CVU0&Otk`n^8s+F z#$0%{0N#Z69QZ7PAn5H9_#!VZ=3{$_>|Wdp52V7uB_{}mEcBazf5T6rvOpwM=mPu! z4*xDALjG^|=NluA;#Ek2AqZ8q35IZC!f{t*N9=g7(3>6c5N}SZ14MB31Bg4ge93iW zo$&|b>ly>5#<2j@3n(OUe4)e<*Wg&gb5zdJyz^AHS|!w7(^1jY!+|Ay_^<-Xn{h(dhiCL0jRTyKE*fzQPZ zO9UYoyDbsPc#EG6CxT~%Bg*mkVNnP|F6Krd7?g0f`t|{_lZypKh+#$?c)T7#NNLUn zL=}F8Gtxr@XX{0L!bcf)TXgt0f{}V>g+RJLmHdq$tQ^n&MqI<|2X1Fs0q#)*!(aar zbu^1n1R<*8MiD4{mM2dkaM?^c#mE$bu-KQHMlc`3>2g#GNmy*gOCekFY}^%*ghlR4 zMdS^9dr;Cs5|$9*THs%x!|@r>MGE{wN=ZHBP7MLP^pHnz*!g-$xgG2oJ!C!({PHjn z`1fJt6&%?A2oV#)j{EBoQ(X-z7Y*#H3Ia0Aks?{0x#gB3Gxw6 zKKtXyoiG9J9!K89frU+pz>cQKHXK;li~ueSG(&2WW2c(f$dX+p0}~DSY=IO)|Bn-* zOmn+{PR%{%N*%KK&qHp|9W-SSesM&iY43GNnUVd|YL#<7L$7iT|AgqotW4W6wwRDt zygdEx_xw~teM5Yq+h^WeCxQjhA>xC`*u4jDCuYe+nFP5iS$R@LU1#}9ZZV%7%PhMAn1RO9cn5r$%LDHdBv;p3Ofc zQf|OpYPd&HMJxTnMfPG_VVTnWuF6EO$;p?k;_i=D6Tl~24b{<1Cx%23dO^zEF;`{3 z75r~-p#bKgB zOf+HXu={!2b2B^RE-9w-%USP5CH<_015_Vd)aV!L@|(yH{ghdv$sbngOYlc7mQJg< zid-`AOm;mMcJADi+K5svb#TblLL*Pq&sZ)mQA2ZNn!e&PJmh)PY`TS~QGtNoNPZc+ z60p2~O%BbrMLjAOyI0_}^I^}sZ`oXOsmFh8?lF2oe(oCo=#Mq?r&ZG@GW*=<&#f?I z8c1DGD5+LF1J|dRr7!#O#Dpbpm@KESK=Rhk+-$+5fz0 zR@i9%LZ#bCzlz?IXESm`(BYSPZ&61d6qX_WYWCEJKDQu?JF9{Yy|#Wa!ODE9hrZHK z@^HHM6!Y&zfz1f3~+lw(j zXtkdSq0bskJC3q+ZkH@mk%mVU4AK7`EL{rN0>=*Yl2M|F8aMFL7WoHPknYaC>tY0?hfMi>ytzP4xUjJiF z%C!j!e(k;}i*vQkZA|SS>vQ%*cP&lnz6-P&NxHE7kV>WUX=JCcUu>qW(ui_nh|=vu{>6C-E~g~ zT<~q~wqMF2x&!yA%HsbNL(NM06x@%kGrJbW8`E6nKgSbIcJ2z*-ZzOZ)YsTFXsMom zBh%re+CH5?@dR8^Z*Y5!>lQvA%}IZ`U2x1_(#0quk7#ib5b;_SC7)lxr%qu!i}6L?LVR#U9z4E=P7g6 zl3#y7xo?Oj??`nV>ytqPXwcLJL(XU~?V0zdWu?-%R2mMFkBr%dFQCPFB2ceVU4u%E zbrs!1N_00l$5WY%H)9pX_GPdbv1lBcKX5BsGo*>XKl#P-8r9oxuE0vH#JbmOF-es+ zso6>&;LVlmjgzlNs!ui=X5{s*rN6Yk;^miX&Xkr{af>scx--SMhJW@!&XHrS2YX)y ziXQUcOBbAW`sPICrE>*o=$rEzPkwj!{eJ2zH^&|k`*FSN#z$XB2Bkiy)^(@ac&CH| z(n2QbbjuS~kDA`8uU_Z7I0mOxb8}N@OENuFzCT-9HcR`D=jXVDV)*lpkdE1CHQ^p+#Ai*KA-^3OiBuFE~;YY`Md zBNB2Y92E*{75hUe+BdV&b9dhq+%s@#3cQh{l~~{Nlr}IRGW*LO&h!LvCO*yM=$r>E zy60rJI@;y`c>9y}h4a{SNO{XMm4E6duM#Sxb^qqcW_?D~T;R~F7s7>df^RN$^Gejr zH6p_F*`)pG%_`3~9r(=|s)j~>u5>k3ZCq?B=Q3-jV(e}B;l?=g$HW$y*enxk^5f>5 z%QZvFdp*_`p%bPH!yfMxzwRwV9#Z?t#=*!~*-%?+Y83ySRr$n2I`D3;O8l*Fr)Q8f z>(F4fM;@{KGG&HQT#QZ7C*d=q-h3J-+)q27xAhRJnn_T$u6APOmY9EyK7w>W@a1S+ zuPnW3ZJ;lvd%beRKe#_y7{Dvx`X+*Dzgd=|e(^>XmIZ~KIG^WJZ|!8*a*vP?9-7Y5xAP>Q(9X+I2`IMH4- z_dI>+ZFt2V)Yz3H9Z7r)1+Ldtq&Lc!(SFIUdgGK?FDRKyu*veB^k?rD3qo7a*}E8!av8>wlJXXSTc74<#8VJdoN` zIejjM{&m}auQo$Z|7Px-pU0j*>P)+S^O7M8iW(LHv!4u~X4T$e@_cWe*{L1%FKCb@dm#TNb_=3nneR(=X$ydiZw_R_k%(R_3O+n_G`rhBXq&m+4EdMWNFH|j6) z)-hTnH+J{m{e2k{K2r8Nq#SlbK&VbfnD1OVOst{hR{9GXnx_}j&nbl4b46Y}=BC9? zel$X%;SJNqyzJ^*N!x|@5M_BbYaVsfx6*#I&(Jp}lICYWXW39Ze|8HfDPFxSstbho z$LPNO&1}^Z;)d3b?4rZuOx zai;3PZ|(e))S`g>zrg<&GMtjgcF9jp{=ME~%N}e6>*s37 zJhrSI{@Uj%s~Ak3Gh|)s((=Pm6a(Z^e4jJ7l53^E+}dsM+J4j$MyE&08b#2_kS%T z@=vS>bSW`x{X)YYd^*|j_IAh7+kX^1$to>PC)X0^(?|Xk??D}BYKKKj z-mpKqy22q-i(Bc5dZ*9yld2lYio%EYHP@3kIw>Zj-h4PF*R{WOf&9UHL}7$ax$lQo zKC146T6EvIW|bKatb|)14jVl~wP3@+zFA9FRuU%>_f~>OQ&Mz>jU_F8L+>JB%wy7L zTE6rapVzPFov)X?f-i zuN=3x->;94V%07BUW%muL5D)Aipl5mSN*mH3<90pmKH}j$jzjR%3Hsf9rdDVy)pbo zX^+Sh`VRXyulj(I%TE)YkB+^4deU^ECiaW`x%B9(?|}OG8Z-LiGov#exfZ@F-zmoA zxK^dJ(KfdN7bYPgMi0HJzmW~zo1162c(U?hf-I~w@?{_`)vd&v3v9)FPLj_uRRx~g z`C94OH-1^`=;E&~s$^=LsAdt!mlK{_uXcGwUtno)2inPf?f%FyR3_Ix&VZvGl?HsP zL$mG~LsuvUo(;V>`e@cTu&?W@b9 z&&pUr=D6cxdrny?<~LWrAGfL)J{Q@wDsGz=5Ow%kGjp~06nRxo)BTO}_bV1|kGzaE zx$_RGz<~Zm{e(SHxeT$;;hmz&?6CIJ$YNCdzE^eE$UfHEqg_*?>2@xZ8LZ#6g47+~ zx-!}MXlDXSBi`+ga+b#ALQ=Qdny(^~j!bCT=N-z(Dm;`SsPICN2J!HfB~9GdPHV+O ztELa0(XzsxJ$#w6j{Kpj;`vl1C&u$&gj#2Vep5OU?Qv07>M?w;`8-UA))k&H? z7K{w08y4_S1wWoMNk(Xp8GD=lpG7t$obB^V2p|oxs^IL z@cehG-kdB$ro76d5s7Wjl%kY{E*F<3#X}F{BA=@!_f1b70blSq{>QOCr7&PnZuvuy zaIIO6bDby!xgcGQ%LkRakGT##@@)Nro;{<>_VkIQrNhBDWjqE^3RZrfoT6XFNE-ya zd9m@Ty?@>gioQE{E5zb<)~N-qlMf6_;`#?28{EPZg4j=R(hOHjUzh31o?)IHYI7A5 z(yAB~F1YY6POV@dpwdtz{xJIcq(X%>S^RJ`xna)$ZHt9K0JR@#nEcvu)6}#I?yr4wHG=!eNiUcq^H~b z-I>{OCk__XNY{VjGmGK#KEU@bI@nkC;xk83ql->n7WDhugbWNjJh`6h6&7*7oQ>nx z@CkjzZ71^WGurOx)%T|l{ULAljIT5b;|;Fuae8Ze$mBkX{wT|#QAv)YC%!*0pU$OM zwg~pq`)sNGC|FM&X8-xpD*0sEaKt`Qs?Z1LtS(bmM{V46K_*Eor7OIZH_`Z^`lHqdfl zBFS1q0{Ti??|@aV|CX+2Q#zSTJi5H~wfkV(yCb!HLCkX(-d>O5^*QeG2>kG%q?=cN zhMI`MO9f6iVsI+>a()?=3XMa-4AOq<+c9Nl*s*bRwA?lz33<2pEiBXb=!Xfd zFY*%hCm+|mm3()%=JpGhr+--P^RG`{jDDidZg9oqioD^hivCok9&;{muqw<`b!d-& zK=8F$e%C^3<0oH>s-~RKeKzZws$IuA_a2|l6Jo!$HsnbS-de1FrJt(zx~;m6IVSg) z;H4jNgYM~l6_4|cY76>~Mx+RzZdLL|-cjK6-j=WGw4hOo8F6CM=3^1L4BPZJes}w& zJp;vqKnec(BTOd_=4rgE^L5<&rAPj<%!eW^x#R$K9hs$2eQ3;7KqWfC)$$Y-!wa>a zP939N*UtH~Qly-pr0skfA;lV7pV<5$RQ@OBo^e49W9xj8_LyG|xiOAWwpmXGuPHn+ zqBGU2j{ITaFlhF{B|l$%taCkJ^>~GdiR+-)i{^nU?RF;G8ts?LciuG(Ih%frJ>u2& z{U9x~GgZUAvs-Nyqe!xZALy~M=0ATZrj5TfPp?n^8T`2wFx_xs-J>OUETFXTPj0yQ z#vSn^zH5NJl`7K&6;J8uw;TPMXB#ZKDtsku{VX@-9X^Y>BROY!{{+fLH(b0lEjF{v zet!YZmyd)FE}b=5m|>ocNd+WYfY z<-2rGuhY=k4fM`*1p+}Dd)`#bOYFTuG!`Q*v3(h3^cSNhP6 zf7pM9H|ck0ij$H?n7sa$yDUra^;hrjr?uYL4VR_$;lfugl1bMt9i=TYvw5!G%=1tj_2 zb?LOIZHbz+eANdDLUiGmDo-mcR<(5f)LqYXOeww4v(^HxNha(Ch2vUabT;e9il-Me zM34P0c%@k-(h=5xwyGL+u`m>Y-SEm2p(`%}Y6g#FyUa#E=V1Ho_Aqk)_0bn1Zb|;R z3uw!)e16TOV)8PBDet58+dajwAT6&Rt(j}}HztAQnOHMXVSBBPS8WFAlR;FkZWmQOZTb+ zY-&=(2K36$9HUVENx8P*$VGkT4x*@h;viaP#h*JZ;Y^)E{&2#Z?zUmk#CI?28WSmB z!(!3)vTf_PkBhtvJFX3Hi}cO{zYXteupH@Jc^U;Yto&t z==5cu6sVSOxJO?3LsOdRz!kDe-p3tlE==XX?#hV-^QYD zxoGCYkrbncUVl<}t+!tjMR6b-uK!pQMSXybOz;Q(C*-j|&5JPE_wtWd8-peTZI3;j z8oORpbkZ$XNt7o_zA{SPju{9kF)5s=5O7aD7#j~`&1(G1=>pB z)n?@&7ei&|*y$v_Pc0ma@oa4}GrW3qCnmqx$iIrcAk%Pv12WdzW>$+vSw@snhUXq`m?|xATR4+t1v}2@ z$J^Z$n()2E?XH>A4+;()o_+P!KF)B30{Jn$z9QdC!G`9`aW;iIhCT8>_b()mYM_}k zwqd0CD}7UM?IHNI`X?VSdmg#(e{Ci!ZTyS}^qICAvXq=|#|7)!>)8EjjBJ)Fw=Xmrob)~RWpCg#!lf%@Ek(95rI0qCowe0f0rrdk5kld< z32y;eN&VKMuvFTDg!%i)@H-ralLtN}cA_orl2QM3^=>Y3GEscKuxdZ4xtehy_`95O z!qLS?cs^8km64E#hyjoO8MB&M?5Py9aW{PfZF5_DAhm|5*|g~Rm*`l>5)r`Rqe^&IbV zbbl!qa*vFANZZJEOH(ZMY(!z%8}Epe4wcI>QwHhlJS@pv-VD7p>lc5IUx+!Wf7_dR zk2ivM)Fk?r_X8urCKdq6s{_>+JN?vlqY>Ves3+m{OcCmB3Q%ge`(KNC8{ErS5)jS6 zq92ZS*Al3(3<`N9mDN-^6C5F_2#cW-C>D7zK{qA}zY3{>4d`0Mvr#C$r+?5>rdQo+{GvQ$-03je3p()cxbY6r+(8V+jb<@+ zOZ|KZQ{owXx35t9{)CIOM*kni{LUBK-wK$f>+gNp%DW_s*#9AG`c!9{f_Vk?MVIf^ zc@}&vO6K-cQV@Q+T=r9x!EyQI^$r?3ZRenxFGQ5k?55l%Q&GYNWek(GZolcPG;O*2 zn;Yn}J^WwTj`!O8aGmNXDLS6F_3MS_d=(kg)hpBfsgHAh+p!Oi`WWo;xvx!JFD{*M zphH#7t&N79WnBp*iz{EN+Sn5(c8woWZxE@a&gnUwnhz;g*mbGhuDBa%*YjVIfhuJ@NdteDt7bg0q}RSb zcC)1^h*n$q)_&N`oVc)hkm&&#NUB>~$?^-bZu*~m5y^3%sFWTw^{tOW?p=47>ZL2( z{94wkb?K>%tQx0wc7J5xYd>lJt521gqoRH2y*kh;k%Ql5<2Aa_500wZR`t|fHV$Oy zhIS6ChmD;pmJ1nDP^(HbY8S}ty?Fnmn#~o}8@w=O-*{Uw>1fGEH7qP)KR4!{(xN4h zl&ntk=asH{&pDk+sNeSdGky4qcC>x55OiR&#Azr;sz)cKQeR$-{LIVGh{f}<0(J&G z2*E2V=x=6B$}Bby&-zII%#GZ5YdWpK*e!=r$4u|;J1KXU(hO^C zxTL~l7%bm<2t$l49kXa_m0F#(Ve6ZvmWO?3O~jqu%MV;R#;GM0|H+(_M@KsJ5yR!? zSeE80U;bBa(hOCnZ*=rjvgzj}x85*k+}mMmV`7gcJ2q{K1Ug=V=H<#vl{~p zr`NV0^<>A1rSvb_vOS`_%m1^mMzdt*o0G8Ur?6AwLUzj~(r*(ggP%TU{nFm|;p5TM zO@c`*w>~nB6l=(c{jopN#1M3`{&Hy-x8~ltSGM+FeoV!S_g;Bkc#z^(c5!~qfY=ev z?K9Sm>FVf&hdS4z9z+}8_q^73LptTBRAo)n(1EZksJos137+L`;#n+I@Yw@$jSeRe zs?LL3BQCb*^7bg24lp@Txxb3j)Q;|EkdIf*{iUqPk*Zkw)yPNfU{&cQE{@+eTicNJ-hJ8@09iX^)PYv``_ekT(tKtc3mTau@Q8F!r~jFx2@}>~G(mp^TVuGjJ54q%pKWkO7nl zc>D>4e^D-^2qgx^yn#ekg~GpIURH#X!-Afapg6Hdzs4^OFFAcDhe+iE0>vn$ok3pA zi-V-sgITff8iNlpAbC+oT7cwY6bE)%412lVuOm}JaOe=o9?0iIX+O#TUit!GPqr|J zK}aVex$?C`iJ55RCy@OS#fP~8MhOSWFnVHy$I7>JqYIQtfP~DY6GJAlgO0P*)<+aO zc{_H9gXWNgvzI?s6bw&{5;D7$K<@CwC?VHHzVn9ljtT_ZzWan?=L{pl@$z@^J>}|# zQ}?gj>X6gR2z94$WI42hPot@B5F5BZZ5z*uWb(?BK0& zP7+bmmoZEQc1(=*$jxWzb(H%ckdZ482pb7HGXywZfnow?Hpo%`nI-U{H;*j%hHpI= z1j0c=7Q7E|`i$b=2{i01*g(X8+lxoX1=^Z8!2JcqiX^cFmPY4Z9cL+M^Ko!#%11JV zz66Hmz|Isn^VY3^hI5qx`o5r8{=rV+f%5!5kkzF<5Xe3fcDYy@(H*kS4R9z=Fes_&IFw&ys6D$R+EWgW`)Faz$ovn9B-2d9PlF2bfI-DgBGG#s z4Uk-p=ZQ%m+?@3op4arSJOS_y2h)z&Sfi2LkR#!F{}_g5m-iEwIXdoxB1D2B2$P_P z8v}`zpyu`lP#R**c`)ff8UA%E0wjwD?MX@&0^FD`d&FKR_zXo8clP zlO4ChOopq8JGH_)j6?hmDAzH1@Kz}a`i+wS*;kYVspf8miifX*+{HnX10?8?E*Q=C z0(Oqjy~L{E4Ep&i#z`JP_BSvqU3}a^iE@rmEBLBFtZr|d*=anX$ba~A<{FLOI4FSv zn0*o?5=i^(5OV#M0K@w~xxtPn!{x@M0E|?e|Cnmxkh{k_(2QGP`jINzj-vtMt5J-k z1|r-7+?@bpXE5_{0h`q*+8r8v7JLKsmb`!+r*#Sm@g=g(j;AQsU+$j-6>kGgPnxO0 z(HKfxrV?U!1H-iF8df>r;bkb}Uz-q6{AxfHw>T^cy<1?2moNC)7M}kt64jra9vFfz zuERlN@RQg)D-p+^wiZRRODXkQkU97_PBF#>4&H_`{CEDh7?jxiIFx`SDD8hy*y}(P z^M^PT#biStDEb{FEaNAB zoly)RV;wNkNZp^9wL^#iBHlnrDmTuhef8i3usj?dYXgdvly}gjxI;~#Gp#_ONz<;W z5Jz*P0mZOGgHNIckSEzk+<0$0iV8*QD;$sZOP}-PrDDN34=!h+c(?hidM7hm{L%ESg!Sqi(vN2c!{5Y(! z78KnsSvXq3ar6DSaYccD$#MgOA|!@Gp%kY0M`K*WpoB`{P^`oW;{LPf{p}#QqNgg4 zArt*a(W9wyoxm&zJiy3QCNXX*mPTcl##oO?&X*Taae}FGW;ao@GG-#p9Rpy|&o1Z{ zyazARR7>nGWyY2`b*S4>)H^gd(;sXH)pd5ojf2I43b@dYg6|+<86POp`6CEkHq@Sk zK=zXe-|USe1k+|$n)i-J%Pi1AAm<%-S8U5991R$+yFGZm0~F>(I&K_{0~X>?!HSZ{ zzkG@oWXq|vn{7q~j!fj|f7sgo1lcmS;>MSn2rk{}0wi}#Ni-ZE!jLgzmMgR<@N&z4 z*1Xc4AoHnt9G0ykly9h-v91iMb-tbiy(hQ; z>0hXQ^bXDervpQS|JA>=voEqVgH`;f$!_8fK>~4@(?5w1W+q0x2Hyh8Kd{@8w2~O& z-Ah{ZFVKK2DwrY8e{U`NF&N6a7z{4Vg;?Z&f@f?f+}IdwJ@$5l9UN{aL}!L2+vDeh z(RY`!IKyZ6fb5H{aO_jf|G}RAn*`G~_%4+l*aS%Xd8!?jjLyX=)Ws9?SPAb3g2N(v zQS2;hjs%BcZ`BgH3nxS~h(@mA1WbZIcXAXrB+>_hc>Q6(+dvpA;Lt~u#C(`_Z$sYz zbT3#P281A>qjCFXobtmVSRxK$d#Pba^t%mo|A5QntF#aZBMFI76p5sm&7%kQ?$>`5 z!$#s&urL0-E_^D~%a?SQiWM*R;M6sY;ReCENcVCL*I>Y*P&nJJ3W?KtwjVUeg?NBw zVE6WbaHAIj`MYhy=_`^11Pq{dce1Pj(0(?_Fn|w$u@jpDHxcz9I1%4t;J_f!M7SY; za0uF%4g(Ad{)d$H7>w3eFd!uZ#`9m^lD|RTsd>P$->7{%Ud39)#ytHMW))@#rWp%~ z2l_w4fW*oFwauOeIYQOHU{Sbsrwcaog-&K1R0i|UTV!`QELH&lLnxjdL2ycD4}lyb zzrz5{c@)b(T|qkxYLVChhzz5+cM(x zK3La?E_W7DgA==O@AhVtARJg*LU91x6GZh)17pKVr#+eaVi1Vh9|(k*BsB)acBy_U z)YB=%^IvKcE>rK;Bf{eVrocM!c=c#wuHNCG_2=T^z zXVDSd;bF7t|wE}OfgD!gcpIX4=4+`WBC#hOkH>K>G zqlpC5y>Zyjnfrns{1AC^3Y>pLya8-MEl$;5NV~R8< zu}!cfO`7cd{fywQ!ejT59>rxiKs$iq{O5XUZ3>j+;C(m{IDp!_3j@nn_P34l9Thz-zX11V@dtOC+$0TXoZfBn$bzJt9?;xpd3FnL@(vD+ zrNB4=lbPK{a(;4Qz7~w;GkbR%Df$B((wiDU*SerW9)>R+xeI)S)ejX_Pc{2Yzh+$0=o=`3yF1b4)UmF3<5VL(b5IO7Kh<5 z2V$In0ugg4p`CcbTD$0-hx}2@Q7-t|A*4%=K_t*WN7Q4m=oO4x-!FmgtN>dQsY@?# zVbB?Yz9AIspHfE7gEE+k;;^#jQB*sE;AH%T8RwG1je|GZcaN(qfaBc?xN$Gyajaxj z9}QK@!B&$1W-DojYCDRbQx8G1SjWaF;colxX!~;vfoVD1|5WDWA}CD1F%Z5;)K;)^ zL{y<-Bf&V?%HADXQ#L^BB2ksb&Y%A7^NW|@;Zg_~zNAI{%UJ}_KZau4H9wZB(nutD zMG^dv6DhJ?Ct{zX5tstbw~GpI;w4b(J)eNpCHw(8Awtxb!DNP!q~Gf&kQiBRB!c6%_V~7WQckR*OsaG$;Kr)e>y? zk|gaR3`ZpMw^R{$Vo%`>CxFov6z5I=VGXZvYY{PxS)GCh@T3nWQYHZHRs52WzzK7D z4(*=>jl70pB0UbkN*B?+b#q-40-?K31`#KbPGSyA!~z6JAsGOkX$0c0oI8okt~Oy^ z1gfO~3iJ2;0k>YdDGLOxp`=JR2<| zM41Oa0DF}hs16T_SvK%A^@ov^K>P+#&BYSdt*4v$f`njR>>`}e$4v$j?mP=1NY1$l z%9DQ#hhnrzBo8hwBQfJN2DtI@BS?}Q$IeXn^nu-L7cN6A86Eaxj{mjzAbcXhZqhfV z7*ZBmX}8nF(Z3S`JD*Yjs!O{3tuw<=;+|mbpi3lIR)m45#s5KnZjPM@P~IjgiCDeR zB6pu+@C`v%kWLY3fuk`pMgDh-eGGcVQW)F==Yq*XN)~B}BeOUA2ieYNA%I~`O15Q< zCA(`&xvOU1jiF>GH#mtK{q&#F32@ZK7B?k$Bc4Ev)sAoze)oLb_Ba~wZajeo7YvzX zAWwQnpqYY_1?c%2Nr*STD%4(3#k?qpGZu|j+RtnEaRXLB!maDw z0$SiHWxSn;cicn?3kAif^T6@PVv(L_WBDhjeSLicbnVc8{B+I>M=2W+7)n%mcY?n3!_iIQhNYeiwGM!)?8MpsG$kvWZd~X3s9{wj$ zltOT1RB)nD!YV|#0=Ikb;uwkvKjz&lEi6@;H_@xgB-L^L0)}cY@D)x;I8w#Z5Z;g6 zy#)Ap5l16}prj;i7@x?QUsHnKT>%%dr0cLZ*Rj%3OSrnZlT>%?T#anCE#R>%c)q>6 zNg`sgbo3H_PCjn#UcPQ5#Mq9gGSC}X4tA+Wg1dKOcVm$N{XOKro{PJ7zlDJ95u);+ z1F9DQ5RarJoRI-lNJ?(#dJ2{mCqRLs)P#;@Ac5_tNEW~z{2#eVAMhAOX-UpZ39O(f tEy(Y@1dJ&u@t+HHgA(M$REh$kY>~`>MiFY~^^kf>N?y2kDY(jl{2z{Nvortz delta 209127 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

Ab~R2%$yh6QZywLX zLZjS}D{*-tQ`9L>JgjQNH{j$g66ki?YJp|+&Zq$5ivNMjxP>b4D@?S|Sz znDtzatMT=Kvz|GM&=Hf+7!;QHSm+<^a3oFBD@T$pc6R*|{i$~T@@|3eQWHFS5~9;C zp|bS8)?K`jHZ_ISr#3?y6mxT9; zXQZ>PrB6}zn7hK@ds3w%ed>S5#0~^4MvCt1TxevzY~CbO_^Rymz2V2x3Jb7@N{m>u z1&g$3205qi#r8Tc90yg>74^^kXOU(mP31D1AkT5X@9}y6nTn5@?+=`DA2D8z2~Rlq z_4JSLW{Z0Fm#!HNgkGG`bvG-x=eSbZ3B;M73is9y%kQ!+54n)qN^L0k@%C-so*~HD&fZMD@H|< zjhT=0Hp_jK-A`VfjXQsD&xcZIW}a>3vAEK1nxVmUN%2;lpEvu~8Gm%QwjUp#Z#w;3 zJE%rV%50_E?M`g>;fbrxPAAn3%ThAlcZ~I`LL8mtUpj8=M? zyMo=z-<8N*D#-0#E1ZYZNeQzzPPo%NE4$k)Ainzk)oGmrQdOIbOW5 zYu>|2yTPV}HG`6q{4+iw3n|0sJD|IuyooD4y zxSx&nLJL3i@W{>9+T#cQnY|O}(w@05!{gq+jq_Dc)3H;RqOXbaQ?c12cLFvlpc=gUg=Xo=pQOCp|;mSSq-=q1(Cs!7)+aLG> z1QP&w+nPE@_x_2GX|RYildAIimkyEB=kH>p{4+||)5paUy;m1^`<=LTw6RHQ zD1D)sA>qcGgjQ*`s%palN_l>*n(vH(TD6wWr3);=)3$48hsTu8(1xlYo{Xm7d95{k z`P$iTp@p?ZyNC3pr`?5+xd&KM0a2QF2NZ8kdv_htt63RYaV(lfz!q{xo}V~x*!tv0 zvpmNdC$>f`w{C+;@ZqaI8K$pJYuv+|I)<&k=Hu6t^+(3W{H|w3I*Mw|PRb^DPmZik zy%-uEn?aR@#;$W5j*T1Y86MzJE}9#OdZne66Wa4p&qGnxGlbcuV#zzKGA?vRu8aOZ zXSn!gd3AA_y#Urq@aWQdFfSj|DW+QXi|%oBiH+D}>jy&IJ6f|Yd5ugNN%*I=C=V?9 zi9|4eEL_4oF+0xFa@&)UhN|S#o$=FqocrpOI`98V4LHGiee7pZshVQxp+4q?JJ6TQ z!ASp;gAo~qK35*Nwtw)>P4s7skqFS4i2o{k_HMt&bq~8di^#CmvK-@xeZ}n^KgTk7 zn^6nwp;-3_^r5B=kq2L5Wi4`?{GTN$?Or;3*rW(eV`QAj__T>5Io*i0)ivJt+>=U^ zfpdBk9O>Nq^3p#qJ^Vb*Xq!w4Rox}Rc_k;iu*v;`=@lBCUFG83Hz=<=WF{%bsQ>=< zm9@TTraQ0nSlKnG^as~vZTTODgU7V3oPFF)vwLImuv3}9@l!L;?5eU-8$Zh4UP|w- zvsB_|{B7FH!QwQbWz5~?z-Gzn5Pa%(0-QEEec!!_vVg1m*#s@^XjLv&{WdrF)@h}Y zS(7kX+#YvlS7}Yod-%+Q_^Mi`V{=vU%TB%Pdq&S?maper91H1nOH2rFh53|9Uy+ur z>F>){v_xPZbWiir#Ob3vYjxF_)}R36x5va!*=3J@S8(@8jx9}Vje-L|xa(b< zeLh>!Rf)38G}J280YQnk4#hM!FuI31nK`+?>A#eZJsESvtM~h7?evI5mYAsPuM)Fa z(|Py&$QqDjWeHdsFo>7LV>6k-=9lxgd}Qr^8;-l^aKm;0>p^5@6y!b&NLu?xGSy`}kYw(k=ilz!5! zxO#Ybu@r^9Vb4}?p7ZSq6Gyps*{ZlJpZ_(t3>7}cOFV8uJdb02JyfD|t$XaUqZ}Tb zq8E@e)unf*-eZ`r66Wg~mOt5Y_Ew&W$w6svlTQNTmJH<(Idf~D|2!IdeY;beO?Kx| zY1-%JbN@8SLGHiq_@MPm{7sP;@_g1$#dB_^BuBE&S!0V29m$)gX;PzjcXjrm0fmwQ zmoHV*r#D~g>YpjQR%*#8q%%SF`+2f@?^mBIzD-d&ns>?L)wlN?rdMxj^Q-vg3Cl-W z+}(^gN5O=-?r3l(sQuKvlN~qD^cO)NN*xw5G3#5u{Nc7Xw0*4N?Bl+DhMjk!$>nQZ=P;(sK z6THset8q-|-bVFlNuS4AEN3Z>yWNJ|Yd;BTdnp{@qRm`g($R#bJSbC8O?_Zd7&`h8 zcJCBrn|S04@yM0D+UEnq4Nngu+`+eeGL9J8vE!(oB3RVEjBGq;&v&Q zgng;?6`EoG47PAC-X_0r>vlOlpX+6-33K$H?<#yg+P~lZyB?(w1GSYi^EFBM$Cp0~ zl&H@O^`Mp+=RjB>fuMfY3p}p%{4)EpJ^q7}H@0}}1p}yosDsw*UYdrHZrR0ecc4TH3b!Xl=uSk*ds_N4s`gf8qKD4sGG*JfKi&%ZE&Z)7dL-Nfo zs$do~ixy4s#OV*|!!S=T->7+Md%rlr4|aWaOZ;=-NB^m+=+v`(q$&{=NLXF;hTZ9d zG(XhM40&qWdW|{b>2Iaz&x=nF3k)5~PCVKyKIG$;+H~%WdciJis{C%}O}Qi8dejFl zjcKxJXS0aJ!dG1Nz5jb)%D{ian@9Scf$Z7P1og*{+#@bo&!!!`VQ#@Uk!Ui@$B9(F zOQF5wH~;ifKz|f$LgU2l(oy+4nR{RqPFBjl)6zMAuJQRf`!{gtB~iwlYuYuvnCF}G z<+ofunx*j_r}BGey9um+wtWo7+2|*AsEGcfQYpc{b9N%?m(1Z&?r);l_lJA>9^*0K z)ct5sdl1-l=iHu?pB}y_;}Gd$sY}n;aC9$$M>jOR_}w#m-;4c-Uhm_i_XRFPi?uM93%SNrV_Yo8ZZ=r&6jKp zIh}6z-@6xfH11AG*MrjX2W~U`_c@Mo;qbuRNk-oKvIL{4VNN&7%D~&$Wif3YLF?7m zl>dn|2jz>l=!kCGuP4-)aAqu1xhASykQj;(N4&K+j2S(xX4`(1NjhVg&t8_nEHEiu zyk@ZUYLWlvrg}XGm7gpe<2qWZj7!GGH9!45tk>_}nD$xfm~yVTQ5=|aqc7ZScK3e0 z%7Yi(Oy)bHLkf>_#_#j+IB{4iCfOpCKvf0DT4|vCILP%W8DoT9R-hz-|fOs{U3XqXI*A)J(5hwxux@% zs}&`&U1`Gs(Zx|cxnft2@qFK%V8dj_L-_?$Y435QYR$f(qQ%GlsF(!$===VLj*k*$ zw6bVZdiwmVkI$_Y@2jGDZ`y^n%`T3%-LO`)1pnmVNy#d+Um+^k6MSx(L;Lx{nNnnR z&`D82Co>+ce6dxn$XafmKhKijRQ6-nWP!DqrTXme@1}`k2ouw!w)02rY{QhGJ_K13)RiKjUjWkv& z^Rl!3g@Z7Yw($#L*CwtsNF9CXVEC}evm-SHkbfW7P*<~D+$hgx>4EiL-6M0MVL-#& zfAp0ZoyzFM{@r?wcY4F`G#nW*Zv1i#<@$_$Uy5f=80>-5+uzlUzs?*SM-8aze}C9` z(SqODMbYbGyrWqh*X*-sDZNA1lCLshMw=HxsMO=rJze#~7*w9aKp7dqg4_jbt_9c^LBlbDig zlty=}j|)p0V!BgZAH_Vuk(3ArjIebXi1hhC&SpAx1S%I<+Xjtoh~OPb75NZ-`kDN# z!)vIyRGEhw+42U?7^V!0*uxsxzZ!&Op_s&ogV5V~6vi(j4WQ$?SY`HHG}A3kA!UTI zE95%lfb+A?d+j=KA!&+BC6T;Fw;QR1q$zPf%G_>52}wgHmA(p}x3x7)woIGvL*E;y1Pa#H_Bo|Eop8NLZN`o)r};Sk3|2l(z}Dt}bD zd8se!Y6(VuQ_^+qi)&lvIjNG@kGMUeU6TXS5BLUdaunvahE{mc?fXbm%b~X)-Z#5Lx8ei015 z$X39Od(RZyz_9`|Q7Fv+dPeic)I+~VhYmbkmD0V7@r`*!r<=@jB2w{Mz?Iwlw{`Eo zJv$|5+FtDUOIgoqc~+r3ye4q( zdd}llk$xoGHHGZj#mD!}U0n6qj$=PpK1{G~arC?XG^AEA2z|S(W;7^&Y`@ zA(v$rLyjoiSX{cbmJ{;BQgFJXDfE-{+2nTJyB{HrlWATCl1^&Jg*9~~uWMI8zx9tX z)g1WVo-kfGbn5G&39bX4l4IT>nIR+6YdpVc?jN?u3^559{hX+s!XA=%v^z+^K43?>6HyvJ?CG@auq}L}H3Op)k z@^)z%{VaF!M3zy9xw+N5lE&z|LkAeKVNkucv9#HXRTvB2Bp7U?^$&ZW^H>5Jy8U^{vBrSWpHw-%NEwYAK-Fuv4W1MJJE zbnHB(moEQ^Si@LQag{Ck8r{y7ToC;}89Q_)wX3#s&L`AL0j`VTtNO1p$)S?I5TV}_ z`jY2F^@k6)N-oep;Z_=n7y%+ePOp>R)bf!ui~83l1>_+Rg6*i z8H|zGJxgcc4t%pne(4@*>L2;tmeC$7*Z86Amr2ixV<_T{t8JsP&4&?jI=BJ)_({vx z!xB$r&rKl{Cu_d_T0O|-^14z!(_irI?z2j~BQ%#Q@6m)zI0Q-WP0YTYBWw85#7@za zucpY$Q_#+Ntg*f3&`R#3NDlqO6W@#~r)(8p8NQ4@pS=)#CjrrypVx&hI3!kk5QEhT zOG>$;9AwUI`Exj(qNXPC&i6O+wK~6J1XLX@f2F2`bl%DfGW>cl_`}!uRpr<4tL>%{ zKZNCv`6{K}RonLq`b+KPgT#p}v13i$2`XJ#OOMB@^=2kRD`s+c$$gBrz9Gx(aH>(3 z=GlFE2JQvPA_FJxtm4ymmM1Udoqa@sI)&9vs82=mELAfdv$?o1BJG>(!F?im&UqyM zjEsVDAytvYvrOd~OLO7wIY-0NWxHq!>1Q3SALXQaR$tuIpNlU%8&tPgH=Z>LXw^zp zd|kWAiBP+#Ir(@cTk()6?Hwu&71rCSv`2ch{N-=fQ~S(EM5+e!WF{AB#G5!B{AJgS z{dkf2p?X+yw8x6!2ZQ{CY;#Ly+0Wv3LO>Xtn&X;OYV*gaIAu5$^EY3_x9{f#lBn63 znl2x?j`lj>3JHR_INW+%%n5NiCk;3yL9GQc-ORW#XK_~7#$&!3Z-&ratXQ>LA4!C4OPE;bn>6Qvw{juJc`HIWzppGy z^sdf}3?GFF^S(~o9e-`^+lATT3&j?ts_JgFRz49?@sCH;ZXP`F`$=U(8n)!u65nf{ z+S@8u+|N~LW#44^Pw$FYiW-mOyzVDz-x;RR+!(b$PO5-UoeWPt9^WtZDr{G1jW3M4 z6Z^&VL!zs1{JZFM+JoM?8%0)gk6b^cx~u)l(H2Q(zoMd4e(JqbETqBhac7$}npq+* zeq(NSF=4-cyR@Ucz0^&+Q%5*M**Lil?Nxpke^7o%%U}6;@mWV+9|;z}P*#B9yZa}x zt9oaw{X7l^Q;3!8@|e43XIM#VI*+B*89tXY&X<|mA43^{*Z9EsJm99Wr|{ia*aig4cN&6`nN z)$?`y+QP%L7K{abebCAisg|>+eHv!uUFSzGJ=gysne{F#^LaOYpFFx>`XpTq>jA~5 zHc8m)(6*C71o@jplhn?e9e`!}Cm+&DeYdEPd=J3N*9~ zJom{ay@K@xyUSNrhY2g+diL?lz)z1WWoC`@I+}r>>IGyZux;k5u*F*j5qD>A@SLZ+ zOZ(|-!6CuiLn6`?E}38CZhQr*n1GHSd+tB&Nff=B*zJ{I`oIcixxY_m-bc*Si|+F) zu4t|ytB6e~`4k4cyoe&azk_=}X!;_GpLpoWoPZ*|KLY3_pbQBonK=n4!pkf;^7aH& zAMw49QUJB}cGn1iIt_<9f)r}EP@@?9Pl9{7)YqpieOG zc$P7lyxO6HEPhP5PoEDZ#PvbZxP@c-pxK*gJsNg|<0JC?4w9Xm+Su7bVcw zL&^G=54No={jSsyNT@UfvWF<1kcz9lK=f-gFDYCaIBVp-FIrciAP@uaJ{1cYx#7f? zHD2Bho*rKI4(={?Hn{iPx7mab`o-VQtpMni9;k_jOp4VHK%)*VL{ds{;c^|Co$};Q z;C>yNZwG1`L_w?9DFL5bP)6K^BdQ$)$KQYi96V4UnIFnYbRS<@ICp>o5KDPi1ZaAL z<{&Zh;82IsTlQ9g8@mZ`sY!0*q6idl5{L5tCCs}X6h7rd^H>;)N{!0eC@Ef#wmBP)>UM5=+UlKj06+H}E*AR4@0h1qU4 zUhrHz_2CInusk>r#mOw{-C{!lu5M7~zjFV(1(YuCj+4IkU%9t<2jZg$c!1U$O4c!n zWsD<94aAAyq;s&s4fKBvay^$Crg5;A&A{$LK7hJIaJ)MQ`OWvBSdK^>ii5_%-6r^7 zLc{MYG#>^FFA{WJl+1O9Sit53ngdX6MKhDD^_BR+ z@MeSIl_0b0RU3h^xqVyH!}GutfFE#BSqu2_82n^BN4p3-;GHU3k}r6k&Ls`byxT10PNUPddG1?pg|CfRuGtd@@T2C1GoFol0b6T zw!Vv3bdf8iy$!Uk70f(&wBB;^N>5{ z%=}*5{Z+C=Qri%Cf$0zEU1)Mvf{MakoEc6;GH}%Xn~c2=B%`=WB*R@8+#wBL;(AM6 z8fhG0aopco97Z{~;*cj5Hs0>7D}+1i!mtL^geyR>E_Q8?3!cxiq5>XB4}oYN-P&0Y zmdKSK@Tn8rVI0BL4_`qat_{L~`9?G^K-<5wOr7@gQ3QgeUHDg@y*`I`oC&wEAX~8$ z3XXIs5ljJD_Af{O__@olHAry8x3 zwZH;z1XE5v;PV*ZxcbC%;#XO_fJ!1eZb_rCa|=UFw3x8-(PxK~c0L{s{x<*GzbFrY zaZF=}17b$-y#PNaoMC&02(iw`(M0nSX@&>3^&SGj-a(Myg(!i5-ZsF+-QUG?$C$)N zSBT0-x&>U-zTLYsy0!9n?ma-|AR0+tSq}XnpF_cAjxzX85&2SQSd*ZV#@@z%=T?Yd znf?&iZC~mGZbN7ha;ZXCcG-*Is-p6*1(c&PQL3MVQ{WCS2vWI*L8-cD0hM92Fit8i z7jipuv5PDhcP+MZ;bcdii32#+Qf_zpRB*2=#t(rok`>+rIjfh4ot2Z_Kkbql0qtsb z1@uPHTqJJ^CMV-iDc(2~+dl`?G)Ta;5=LevQ5C7x{D0J(bnKP{TxTZt6kh}hDl_q* zOOPQN@DT%|Fv~{9L?t7B;?F9!IT39o@DzQ5|Gm9`hVTt~1Nx8!j%acpRNHZETgy>=P`57I?j$$t#*s5o`vv}c zn6*xV<{ug)n6fdsZA(jVPhtwh(+?5wmQ&l-js*M_93C?aJeoq|53#m({@UusZ8=~( zUBJ8eTF~DaU%|!2E&hRwaWA!24e&1`KQiloKP`d z1VFKl-t$k26LTO%qA&v36hrL(OYNZ}S|^W!8My;4QOJ+v1WqG>6CaT~y9z!P3o6wv zxT7O&kdvIN${gUC-*y^Ch@H(m7~6B!Kp6P{jPR<1oUFC`Z zq&yL1TL}U>6hu?*@&Y)Q(44^I83cSs2=LbaxZdD81zJ1!SK*9#0h-Eb!4u;t)=)0a*kdDIDY;W0t*lY&B#CiRGI&BBLpBhj^6uEiU=HqL@5Hue(|sR>&Kz>DiOf^L-@Z1X#N5z!ut`x+#rJG zZ|^cdG$qd@DcFCvDC0Ap&5^j%NAME_$or+(PaNA;JHXex&0VUq5?~`72ipNR*${<8 zkU$U=$@jMrElZ${NpNHu8oB3htjIEm6=p{Qw?4sH0n6o`)9lLWlt&?8Q%L)39{4SY zO#6Xm-4R%P{XDhaJ?9BldOW!LC2u^9VreUXm5=5M7^Vy0+uOtzOIlvw`yio?GC>+> z#9<^b1@_tGZq+Ji z-SyKz&+1>r&AA4ubPqz}`q#e~QDGnjrE?^3eQnzT93e*Lw~#H25Xf=sot?e>McVqd z2LX6$VM;T*0`RlY#=++X{i@zB~rZ{{Cxb5XYT(9~sA?Sb{1|B++>F_)LZNZd_T@fIw(3QtUXF{{pfU$e@G)iA^+SM`JYK1ZAp7qHqLa ze^+`l4t+)ek7flnH_T>Kn8T%^gi^7t$!7~~C!L%>-8{3ajmJcj{#3e47M$CH~z8vEV=Ne_T#aFLDn z`*?B|V2%QV*)nKb{wgUj?36c-;Dq9-=y1N`48{uvWs<)B*ntC-L|W?*SWZ!5b{udC z-%pr&0n+e;#*nWepA&i1kE6(T^Mv6uL5X3fXawjeFsy*S9_mjiY_s(k6tvYupP=}w z;lD^8;YflEh$K+V&Zkb)Fc5EWl7I)Vrtidi;_!P63HaN8<6CifQ6oH_R#MI((0|8C z3_fgIchz?e*$1ZxqQTqi|8Ot`4oaUfA&`Wdk(1yhUwPikT62nR(Y2)YnFNFgoV=8`NAxwbZ zos0tp8OBw2MQkQiY85)qEW zPkP|-G=FYwZ97;bNa@E>^?MSi6#cyaSzlxqxX79i(i>M_%$R=_$<}+iJ3F0W2%##$ zH+Oe*7_v7F$uxZlBk+JP9wPs4?3PrZ8+T*y#sJAw81#;KmgDe{3k3WW6=vrnP!4Jk zpL7A3p~mbcKk~(&!`Sy0LO`VuWpJ-Seh!oU-&V`lvvG59ba8hOckv|a`@nl@3@7DJ z04Lpoa_|nIM}s*-d9(+(PlGv4NdpHqX)vdtiMeQ?_yPEA)14N>i=T~fW8}}*pYsFU c? Date: Wed, 8 Jan 2025 16:25:48 +0530 Subject: [PATCH 0656/1337] test: update `StateSpace.fmu` --- test/extensions/fmus/StateSpace.fmu | Bin 34391 -> 34301 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/test/extensions/fmus/StateSpace.fmu b/test/extensions/fmus/StateSpace.fmu index c181081ca4222af0c306205c1d721c06905dd3a1..6138e1fdb265d3a94397dc6f8bdad51c46c12414 100644 GIT binary patch delta 20296 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 delta 20481 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 Date: Wed, 8 Jan 2025 16:26:02 +0530 Subject: [PATCH 0657/1337] test: add more comprehensive tests for FMIComponent --- test/extensions/fmi.jl | 220 ++++++++++++++++++++++++++++++++++------- 1 file changed, 183 insertions(+), 37 deletions(-) diff --git a/test/extensions/fmi.jl b/test/extensions/fmi.jl index 4dc0fc9a67..145989ab29 100644 --- a/test/extensions/fmi.jl +++ b/test/extensions/fmi.jl @@ -81,83 +81,229 @@ const FMU_DIR = joinpath(@__DIR__, "fmus") end end -@testset "IO Model" begin - @testset "v2, ME" begin - fmu = loadFMU(joinpath(FMU_DIR, "SimpleAdder.fmu"); type = :ME) - @named adder = MTK.FMIComponent(Val(2); fmu, type = :ME) - @test MTK.isinput(adder.a) - @test MTK.isinput(adder.b) - @test MTK.isoutput(adder.out) - @test !MTK.isinput(adder.c) && !MTK.isoutput(adder.c) +@mtkmodel SimpleAdder begin + @variables begin + a(t) + b(t) + c(t) + out(t) + out2(t) + end + @parameters begin + value = 1.0 + end + @equations begin + out ~ a + b + value + D(c) ~ out + out2 ~ 2c + end +end + +@mtkmodel StateSpace begin + @variables begin + x(t) + y(t) + u(t) + end + @parameters begin + A = 1.0 + B = 1.0 + C = 1.0 + _D = 1.0 + end + @equations begin + D(x) ~ A * x + B * u + y ~ C * x + _D * u + end +end +@testset "IO Model" begin + function build_simple_adder(adder) @variables a(t) b(t) c(t) [guess = 1.0] @mtkbuild sys = ODESystem( [adder.a ~ a, adder.b ~ b, D(a) ~ t, D(b) ~ adder.out + adder.c, c^2 ~ adder.out + adder.value], t; systems = [adder]) - # c will be solved for by initialization # this tests that initialization also works with FMUs - prob = ODEProblem(sys, [sys.adder.c => 1.0, sys.a => 1.0, sys.b => 1.0], (0.0, 1.0)) - sol = solve(prob, Rodas5P(autodiff = false)) + prob = ODEProblem(sys, [sys.adder.c => 2.0, sys.a => 1.0, sys.b => 1.0], + (0.0, 1.0), [sys.adder.value => 2.0]) + return sys, prob + end + + @named adder = SimpleAdder() + truesys, trueprob = build_simple_adder(adder) + truesol = solve(trueprob, abstol = 1e-8, reltol = 1e-8) + @test SciMLBase.successful_retcode(truesol) + + @testset "v2, ME" begin + fmu = loadFMU(joinpath(FMU_DIR, "SimpleAdder.fmu"); type = :ME) + @named adder = MTK.FMIComponent(Val(2); fmu, type = :ME) + @test MTK.isinput(adder.a) + @test MTK.isinput(adder.b) + @test MTK.isoutput(adder.out) + @test MTK.isoutput(adder.out2) + @test !MTK.isinput(adder.c) && !MTK.isoutput(adder.c) + + sys, prob = build_simple_adder(adder) + sol = solve(prob, Rodas5P(autodiff = false), abstol = 1e-8, reltol = 1e-8) @test SciMLBase.successful_retcode(sol) + + @test truesol(sol.t; + idxs = [truesys.a, truesys.b, truesys.c, truesys.adder.c]).u≈sol[[ + sys.a, sys.b, sys.c, sys.adder.c]] rtol=1e-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 = 0.001) + Val(2); fmu, type = :CS, communication_step_size = 1e-3, + reinitializealg = BrownFullBasicInit()) @test MTK.isinput(adder.a) @test MTK.isinput(adder.b) @test MTK.isoutput(adder.out) + @test MTK.isoutput(adder.out2) @test !MTK.isinput(adder.c) && !MTK.isoutput(adder.c) - @variables a(t) b(t) c(t) [guess = 1.0] - @mtkbuild sys = ODESystem( - [adder.a ~ a, adder.b ~ b, D(a) ~ t, - D(b) ~ adder.out + adder.c, c^2 ~ adder.out + adder.value], - t; - systems = [adder]) - # c will be solved for by initialization - # this tests that initialization also works with FMUs - prob = ODEProblem(sys, [sys.adder.c => 1.0, sys.a => 1.0, sys.b => 1.0], - (0.0, 1.0); use_scc = false) - sol = solve(prob, Rodas5P(autodiff = false)) + sys, prob = build_simple_adder(adder) + sol = solve(prob, Rodas5P(autodiff = false), abstol = 1e-8, reltol = 1e-8) @test SciMLBase.successful_retcode(sol) + + @test truesol(sol.t; idxs = [truesys.a, truesys.b, truesys.c]).u≈sol.u rtol=1e-2 + # sys.adder.c is a discrete variable + @test truesol(sol.t; idxs = truesys.adder.c)≈sol(sol.t; idxs = sys.adder.c) rtol=1e-3 end + function build_sspace_model(sspace) + @variables u(t)=1.0 x(t)=1.0 y(t) [guess = 1.0] + @mtkbuild sys = ODESystem( + [sspace.u ~ u, D(u) ~ t, D(x) ~ sspace.x + sspace.y, y^2 ~ sspace.y + sspace.x], t; + systems = [sspace] + ) + + prob = ODEProblem( + sys, [sys.sspace.x => 1.0], (0.0, 1.0), [sys.sspace.A => 2.0]; use_scc = false) + return sys, prob + end + + @named sspace = StateSpace() + truesys, trueprob = build_sspace_model(sspace) + truesol = solve(trueprob, abstol = 1e-8, reltol = 1e-8) + @test SciMLBase.successful_retcode(truesol) + @testset "v3, ME" begin fmu = loadFMU(joinpath(FMU_DIR, "StateSpace.fmu"); type = :ME) @named sspace = MTK.FMIComponent(Val(3); fmu, type = :ME) @test MTK.isinput(sspace.u) @test MTK.isoutput(sspace.y) @test !MTK.isinput(sspace.x) && !MTK.isoutput(sspace.x) - @variables u(t)=1.0 x(t)=1.0 y(t) [guess = 1.0] - @mtkbuild sys = ODESystem( - [sspace.u ~ u, D(u) ~ t, D(x) ~ sspace.x + sspace.y, y^2 ~ sspace.y + x], t; - systems = [sspace] - ) - prob = ODEProblem(sys, [sys.sspace.x => 1.0], (0.0, 1.0); use_scc = false) - sol = solve(prob, Rodas5P(autodiff = false)) + sys, prob = build_sspace_model(sspace) + sol = solve(prob, Rodas5P(autodiff = false); abstol = 1e-8, reltol = 1e-8) @test SciMLBase.successful_retcode(sol) + + @test truesol(sol.t; + idxs = [truesys.u, truesys.x, truesys.y, truesys.sspace.x]).u≈sol[[ + sys.u, sys.x, sys.y, sys.sspace.x]] rtol=1e-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-3, type = :CS) + Val(3); fmu, communication_step_size = 1e-4, type = :CS, + reinitializealg = BrownFullBasicInit()) @test MTK.isinput(sspace.u) @test MTK.isoutput(sspace.y) @test !MTK.isinput(sspace.x) && !MTK.isoutput(sspace.x) - @variables u(t)=1.0 x(t)=1.0 y(t) [guess = 1.0] + + sys, prob = build_sspace_model(sspace) + sol = solve(prob, Rodas5P(autodiff = false); abstol = 1e-8, reltol = 1e-8) + @test SciMLBase.successful_retcode(sol) + + @test truesol( + sol.t; idxs = [truesys.u, truesys.x, truesys.y]).u≈sol[[sys.u, sys.x, sys.y]] rtol=1e-2 + @test truesol(sol.t; idxs = truesys.sspace.x).u≈sol(sol.t; idxs = sys.sspace.x).u rtol=1e-2 + end +end + +@testset "FMUs in a loop" begin + function build_looped_adders(adder1, adder2) + @variables x(t) = 1 @mtkbuild sys = ODESystem( - [sspace.u ~ u, D(u) ~ t, D(x) ~ sspace.x + sspace.y, y^2 ~ sspace.y + x], t; - systems = [sspace] - ) + [D(x) ~ x, adder1.a ~ adder2.out2, + adder2.a ~ adder1.out2, adder1.b ~ 1.0, adder2.b ~ 2.0], + t; + systems = [adder1, adder2]) + prob = ODEProblem( + sys, [adder1.c => 1.0, adder2.c => 1.0, adder1.a => 2.0], (0.0, 1.0)) + return sys, prob + end + @named adder1 = SimpleAdder() + @named adder2 = SimpleAdder() + truesys, trueprob = build_looped_adders(adder1, adder2) + truesol = solve(trueprob, Tsit5(), reltol = 1e-8) + @test SciMLBase.successful_retcode(truesol) + + @testset "v2, ME" begin + fmu = loadFMU(joinpath(FMU_DIR, "SimpleAdder.fmu"); type = :ME) + @named adder1 = MTK.FMIComponent(Val(2); fmu, type = :ME) + @named adder2 = MTK.FMIComponent(Val(2); fmu, type = :ME) + sys, prob = build_looped_adders(adder1, adder2) + sol = solve(prob, Rodas5P(autodiff = false); reltol = 1e-8) + @test SciMLBase.successful_retcode(sol) + @test truesol(sol.t; + idxs = [truesys.adder1.c, truesys.adder2.c]).u≈sol( + sol.t; idxs = [sys.adder1.c, sys.adder2.c]).u rtol=1e-7 + end + @testset "v2, CS" begin + fmu = loadFMU(joinpath(FMU_DIR, "SimpleAdder.fmu"); type = :CS) + @named adder1 = MTK.FMIComponent( + Val(2); fmu, type = :CS, communication_step_size = 1e-3) + @named adder2 = MTK.FMIComponent( + Val(2); fmu, type = :CS, communication_step_size = 1e-3) + sys, prob = build_looped_adders(adder1, adder2) + sol = solve(prob, Tsit5(); reltol = 1e-8) + @test truesol(sol.t; + idxs = [truesys.adder1.c, truesys.adder2.c]).u≈sol( + sol.t; idxs = [sys.adder1.c, sys.adder2.c]).u rtol=1e-3 + end + + function build_looped_sspace(sspace1, sspace2) + @variables x(t) = 1 + @mtkbuild sys = ODESystem([D(x) ~ x, sspace1.u ~ sspace2.x, sspace2.u ~ sspace1.y], + t; systems = [sspace1, sspace2]) + prob = ODEProblem(sys, [sspace1.x => 1.0, sspace2.x => 1.0], (0.0, 1.0)) + return sys, prob + end + @named sspace1 = StateSpace() + @named sspace2 = StateSpace() + truesys, trueprob = build_looped_sspace(sspace1, sspace2) + truesol = solve(trueprob, Rodas5P(), reltol = 1e-8) + @test SciMLBase.successful_retcode(truesol) + + @testset "v3, ME" begin + fmu = loadFMU(joinpath(FMU_DIR, "StateSpace.fmu"); type = :ME) + @named sspace1 = MTK.FMIComponent(Val(3); fmu, type = :ME) + @named sspace2 = MTK.FMIComponent(Val(3); fmu, type = :ME) + sys, prob = build_looped_sspace(sspace1, sspace2) + sol = solve(prob, Rodas5P(autodiff = false); reltol = 1e-8) + @test SciMLBase.successful_retcode(sol) + @test truesol(sol.t; + idxs = [truesys.sspace1.x, truesys.sspace2.x]).u≈sol( + sol.t; idxs = [sys.sspace1.x, sys.sspace2.x]).u rtol=1e-7 + end - prob = ODEProblem(sys, [sys.sspace.x => 1.0], (0.0, 1.0); use_scc = false) - sol = solve(prob, Rodas5P(autodiff = false)) + @testset "v3, CS" begin + fmu = loadFMU(joinpath(FMU_DIR, "StateSpace.fmu"); type = :CS) + @named sspace1 = MTK.FMIComponent( + Val(3); fmu, type = :CS, communication_step_size = 1e-4) + @named sspace2 = MTK.FMIComponent( + Val(3); fmu, type = :CS, communication_step_size = 1e-4) + sys, prob = build_looped_sspace(sspace1, sspace2) + sol = solve(prob, Rodas5P(autodiff = false); reltol = 1e-8) @test SciMLBase.successful_retcode(sol) + @test truesol(sol.t; + idxs = [truesys.sspace1.x, truesys.sspace2.x]).u≈sol( + sol.t; idxs = [sys.sspace1.x, sys.sspace2.x]).u rtol=1e-2 end end From a148354b6051de4ba2f277714a4986d88f958372 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 8 Jan 2025 16:29:37 +0530 Subject: [PATCH 0658/1337] test: add instructions for reproducibly building `SimpleAdder.fmu` --- test/extensions/fmus/SimpleAdder/README.md | 3 +++ test/extensions/fmus/SimpleAdder/SimpleAdder.mo | 12 ++++++++++++ test/extensions/fmus/SimpleAdder/buildFmu.mos | 12 ++++++++++++ 3 files changed, 27 insertions(+) create mode 100644 test/extensions/fmus/SimpleAdder/README.md create mode 100644 test/extensions/fmus/SimpleAdder/SimpleAdder.mo create mode 100644 test/extensions/fmus/SimpleAdder/buildFmu.mos diff --git a/test/extensions/fmus/SimpleAdder/README.md b/test/extensions/fmus/SimpleAdder/README.md new file mode 100644 index 0000000000..b26c247bce --- /dev/null +++ b/test/extensions/fmus/SimpleAdder/README.md @@ -0,0 +1,3 @@ +SimpleAdder.fmu is a v2 FMU built using OpenModelica. The file `SimpleAdder.mo` contains the +modelica source code for the model. The file `buildFMU.mos` is the OpenModelica script used +to build the FMU. diff --git a/test/extensions/fmus/SimpleAdder/SimpleAdder.mo b/test/extensions/fmus/SimpleAdder/SimpleAdder.mo new file mode 100644 index 0000000000..ce97f09a86 --- /dev/null +++ b/test/extensions/fmus/SimpleAdder/SimpleAdder.mo @@ -0,0 +1,12 @@ +block SimpleAdder + parameter Real value = 1.0; + input Real a; + input Real b; + Real c(start = 1.0, fixed = true); + output Real out; + output Real out2; +equation + der(c) = out; + out = a + b + value; + out2 = 2 * c; +end SimpleAdder; diff --git a/test/extensions/fmus/SimpleAdder/buildFmu.mos b/test/extensions/fmus/SimpleAdder/buildFmu.mos new file mode 100644 index 0000000000..df2c9bd58e --- /dev/null +++ b/test/extensions/fmus/SimpleAdder/buildFmu.mos @@ -0,0 +1,12 @@ +OpenModelica.Scripting.loadFile("SimpleAdder.mo"); getErrorString(); + +installPackage(Modelica); + +setCommandLineOptions("-d=newInst"); getErrorString(); +setCommandLineOptions("-d=initialization"); getErrorString(); +setCommandLineOptions("-d=-disableDirectionalDerivatives"); getErrorString(); + +cd("output"); getErrorString(); +buildModelFMU(SimpleAdder, version = "2.0", fmuType = "me_cs"); getErrorString(); +system("unzip -l SimpleAdder.fmu | egrep -v 'sources|files' | tail -n+3 | grep -o '[A-Za-z._0-9/]*$' > BB.log") + From 7e3540365f4b07597cbbe4271bddf87257a98328 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 8 Jan 2025 16:37:49 +0530 Subject: [PATCH 0659/1337] test: add instructions for reproducibly building StateSpace.fmu --- .../fmus/StateSpace/0001-tmp-commit.patch | 425 ++++++++++++++++++ test/extensions/fmus/StateSpace/README.md | 7 + 2 files changed, 432 insertions(+) create mode 100644 test/extensions/fmus/StateSpace/0001-tmp-commit.patch create mode 100644 test/extensions/fmus/StateSpace/README.md diff --git a/test/extensions/fmus/StateSpace/0001-tmp-commit.patch b/test/extensions/fmus/StateSpace/0001-tmp-commit.patch new file mode 100644 index 0000000000..951af2d4fd --- /dev/null +++ b/test/extensions/fmus/StateSpace/0001-tmp-commit.patch @@ -0,0 +1,425 @@ +From 10ac73996a7c09772ff31ccd6e030112d3bb5c43 Mon Sep 17 00:00:00 2001 +From: Aayush Sabharwal +Date: Wed, 8 Jan 2025 11:04:01 +0000 +Subject: tmp commit + +--- + StateSpace/FMI3.xml | 31 ++---- + StateSpace/config.h | 18 ++-- + StateSpace/model.c | 230 +++++++++++++------------------------------- + 3 files changed, 83 insertions(+), 196 deletions(-) + +diff --git a/StateSpace/FMI3.xml b/StateSpace/FMI3.xml +index 8d2e4d9..002fbab 100644 +--- a/StateSpace/FMI3.xml ++++ b/StateSpace/FMI3.xml +@@ -33,36 +33,23 @@ + + + +- +- +- ++ + +- +- +- ++ + +- +- +- ++ + +- +- +- ++ + +- +- ++ + +- +- ++ + +- +- ++ + +- +- ++ + +- +- ++ + + + +diff --git a/StateSpace/config.h b/StateSpace/config.h +index 707e500..8b422e2 100644 +--- a/StateSpace/config.h ++++ b/StateSpace/config.h +@@ -44,15 +44,15 @@ typedef struct { + uint64_t m; + uint64_t n; + uint64_t r; +- double A[M_MAX][N_MAX]; +- double B[M_MAX][N_MAX]; +- double C[M_MAX][N_MAX]; +- double D[M_MAX][N_MAX]; +- double x0[N_MAX]; +- double u[N_MAX]; +- double y[N_MAX]; +- double x[N_MAX]; +- double der_x[N_MAX]; ++ double A; ++ double B; ++ double C; ++ double D; ++ double x0; ++ double u; ++ double y; ++ double x; ++ double der_x; + } ModelData; + + #endif /* config_h */ +diff --git a/StateSpace/model.c b/StateSpace/model.c +index 8a47e74..9c170d8 100644 +--- a/StateSpace/model.c ++++ b/StateSpace/model.c +@@ -3,77 +3,29 @@ + + + void setStartValues(ModelInstance *comp) { +- +- M(m) = 3; +- M(n) = 3; +- M(r) = 3; +- + // identity matrix +- for (int i = 0; i < M_MAX; i++) +- for (int j = 0; j < N_MAX; j++) { +- M(A)[i][j] = i == j ? 1 : 0; +- M(B)[i][j] = i == j ? 1 : 0; +- M(C)[i][j] = i == j ? 1 : 0; +- M(D)[i][j] = i == j ? 1 : 0; +- } +- +- for (int i = 0; i < M_MAX; i++) { +- M(u)[i] = i + 1; +- } +- +- for (int i = 0; i < N_MAX; i++) { +- M(y)[i] = 0; +- } +- +- for (int i = 0; i < N_MAX; i++) { +- M(x)[i] = M(x0)[i]; +- M(x)[i] = 0; +- } +- ++ M(m) = 1; ++ M(n) = 1; ++ M(r) = 1; ++ ++ M(A) = 1; ++ M(B) = 1; ++ M(C) = 1; ++ M(D) = 1; ++ ++ M(u) = 1; ++ M(y) = 0; ++ M(x) = 0; ++ M(x0) = 0; + } + + Status calculateValues(ModelInstance *comp) { +- +- // der(x) = Ax + Bu +- for (size_t i = 0; i < M(n); i++) { +- +- M(der_x)[i] = 0; +- +- for (size_t j = 0; j < M(n); j++) { +- M(der_x)[i] += M(A)[i][j] * M(x)[j]; +- } +- } +- +- for (size_t i = 0; i < M(n); i++) { +- +- for (size_t j = 0; j < M(r); j++) { +- M(der_x)[i] += M(B)[i][j] * M(u)[j]; +- } +- } +- +- +- // y = Cx + Du +- for (size_t i = 0; i < M(r); i++) { +- +- M(y)[i] = 0; +- +- for (size_t j = 0; j < M(n); j++) { +- M(y)[i] += M(C)[i][j] * M(x)[j]; +- } +- } +- +- for (size_t i = 0; i < M(r); i++) { +- +- for (size_t j = 0; j < M(m); j++) { +- M(y)[i] += M(D)[i][j] * M(u)[j]; +- } +- } +- ++ M(der_x) = M(A) * M(x) + M(B) * M(u); ++ M(y) = M(C) * M(x) + M(D) * M(u); + return OK; + } + + Status getFloat64(ModelInstance* comp, ValueReference vr, double values[], size_t nValues, size_t* index) { +- + calculateValues(comp); + + switch (vr) { +@@ -82,66 +34,40 @@ Status getFloat64(ModelInstance* comp, ValueReference vr, double values[], size_ + values[(*index)++] = comp->time; + return OK; + case vr_A: +- ASSERT_NVALUES((size_t)(M(n) * M(n))); +- for (size_t i = 0; i < M(n); i++) { +- for (size_t j = 0; j < M(n); j++) { +- values[(*index)++] = M(A)[i][j]; +- } +- } ++ ASSERT_NVALUES(1); ++ values[(*index)++] = M(A); + return OK; + case vr_B: +- ASSERT_NVALUES((size_t)(M(m) * M(n))); +- for (size_t i = 0; i < M(m); i++) { +- for (size_t j = 0; j < M(n); j++) { +- values[(*index)++] = M(B)[i][j]; +- } +- } ++ ASSERT_NVALUES(1); ++ values[(*index)++] = M(B); + return OK; + case vr_C: +- ASSERT_NVALUES((size_t)(M(r) * M(n))); +- for (size_t i = 0; i < M(r); i++) { +- for (size_t j = 0; j < M(n); j++) { +- values[(*index)++] = M(C)[i][j]; +- } +- } ++ ASSERT_NVALUES(1); ++ values[(*index)++] = M(C); + return OK; + case vr_D: +- ASSERT_NVALUES((size_t)(M(r) * M(m))); +- for (size_t i = 0; i < M(r); i++) { +- for (size_t j = 0; j < M(m); j++) { +- values[(*index)++] = M(D)[i][j]; +- } +- } ++ ASSERT_NVALUES(1); ++ values[(*index)++] = M(D); + return OK; + case vr_x0: +- ASSERT_NVALUES((size_t)M(n)); +- for (size_t i = 0; i < M(n); i++) { +- values[(*index)++] = M(x0)[i]; +- } ++ ASSERT_NVALUES(1); ++ values[(*index)++] = M(x0); + return OK; + case vr_u: +- ASSERT_NVALUES((size_t)M(m)); +- for (size_t i = 0; i < M(m); i++) { +- values[(*index)++] = M(u)[i]; +- } ++ ASSERT_NVALUES(1); ++ values[(*index)++] = M(u); + return OK; + case vr_y: +- ASSERT_NVALUES((size_t)M(r)); +- for (size_t i = 0; i < M(r); i++) { +- values[(*index)++] = M(y)[i]; +- } ++ ASSERT_NVALUES(1); ++ values[(*index)++] = M(y); + return OK; + case vr_x: +- ASSERT_NVALUES((size_t)M(n)); +- for (size_t i = 0; i < M(n); i++) { +- values[(*index)++] = M(x)[i]; +- } ++ ASSERT_NVALUES(1); ++ values[(*index)++] = M(x); + return OK; + case vr_der_x: +- ASSERT_NVALUES((size_t)M(n)); +- for (size_t i = 0; i < M(n); i++) { +- values[(*index)++] = M(der_x)[i]; +- } ++ ASSERT_NVALUES(1); ++ values[(*index)++] = M(der_x); + return OK; + default: + logError(comp, "Get Float64 is not allowed for value reference %u.", vr); +@@ -153,58 +79,40 @@ Status setFloat64(ModelInstance* comp, ValueReference vr, const double values[], + + switch (vr) { + case vr_A: +- ASSERT_NVALUES((size_t)(M(n) * M(n))); +- for (size_t i = 0; i < M(n); i++) { +- for (size_t j = 0; j < M(n); j++) { +- M(A)[i][j] = values[(*index)++]; +- } +- } ++ ASSERT_NVALUES(1); ++ M(A) = values[(*index)++]; + break; + case vr_B: +- ASSERT_NVALUES((size_t)(M(n) * M(m))); +- for (size_t i = 0; i < M(n); i++) { +- for (size_t j = 0; j < M(m); j++) { +- M(B)[i][j] = values[(*index)++]; +- } +- } ++ ASSERT_NVALUES(1); ++ M(B) = values[(*index)++]; + break; + case vr_C: +- ASSERT_NVALUES((size_t)(M(r) * M(n))); +- for (size_t i = 0; i < M(r); i++) { +- for (size_t j = 0; j < M(n); j++) { +- M(C)[i][j] = values[(*index)++]; +- } +- } ++ ASSERT_NVALUES(1); ++ M(C) = values[(*index)++]; + break; + case vr_D: +- ASSERT_NVALUES((size_t)(M(r) * M(m))); +- for (size_t i = 0; i < M(r); i++) { +- for (size_t j = 0; j < M(m); j++) { +- M(D)[i][j] = values[(*index)++]; +- } +- } ++ ASSERT_NVALUES(1); ++ M(D) = values[(*index)++]; + break; + case vr_x0: +- ASSERT_NVALUES((size_t)M(n)); +- for (size_t i = 0; i < M(n); i++) { +- M(x0)[i] = values[(*index)++]; +- } ++ ASSERT_NVALUES(1); ++ M(x0) = values[(*index)++]; + break; + case vr_u: +- ASSERT_NVALUES((size_t)M(m)); +- for (size_t i = 0; i < M(m); i++) { +- M(u)[i] = values[(*index)++]; +- } ++ ASSERT_NVALUES(1); ++ M(u) = values[(*index)++]; ++ break; ++ case vr_y: ++ ASSERT_NVALUES(1); ++ M(y) = values[(*index)++]; + break; + case vr_x: +- if (comp->state != ContinuousTimeMode && comp->state != EventMode) { +- logError(comp, "Variable \"x\" can only be set in Continuous Time Mode and Event Mode."); +- return Error; +- } +- ASSERT_NVALUES((size_t)M(n)); +- for (size_t i = 0; i < M(n); i++) { +- M(x)[i] = values[(*index)++]; +- } ++ ASSERT_NVALUES(1); ++ M(x) = values[(*index)++]; ++ break; ++ case vr_der_x: ++ ASSERT_NVALUES(1); ++ M(der_x) = values[(*index)++]; + break; + default: + logError(comp, "Set Float64 is not allowed for value reference %u.", vr); +@@ -217,8 +125,6 @@ Status setFloat64(ModelInstance* comp, ValueReference vr, const double values[], + } + + Status getUInt64(ModelInstance* comp, ValueReference vr, uint64_t values[], size_t nValues, size_t* index) { +- +- + switch (vr) { + case vr_m: + ASSERT_NVALUES(1); +@@ -289,49 +195,43 @@ Status eventUpdate(ModelInstance *comp) { + } + + size_t getNumberOfContinuousStates(ModelInstance* comp) { +- return (size_t)M(n); ++ return M(n) == 1 ? M(n) : 1; + } + + Status getContinuousStates(ModelInstance* comp, double x[], size_t nx) { + +- if (nx != M(n)) { +- logError(comp, "Expected nx=%zu but was %zu.", M(n), nx); ++ if (nx != 1) { ++ logError(comp, "Expected nx=%zu but was %zu.", 1, nx); + return Error; + } + +- for (size_t i = 0; i < M(n); i++) { +- x[i] = M(x)[i]; +- } ++ x[0] = M(x); + + return OK; + } + + Status setContinuousStates(ModelInstance* comp, const double x[], size_t nx) { + +- if (nx != M(n)) { +- logError(comp, "Expected nx=%zu but was %zu.", M(n), nx); ++ if (nx != 1) { ++ logError(comp, "Expected nx=%zu but was %zu.", 1, nx); + return Error; + } + +- for (size_t i = 0; i < M(n); i++) { +- M(x)[i] = x[i]; +- } ++ M(x) = x[0]; + + return OK; + } + + Status getDerivatives(ModelInstance* comp, double dx[], size_t nx) { + +- if (nx != M(n)) { +- logError(comp, "Expected nx=%zu but was %zu.", M(n), nx); ++ if (nx != 1) { ++ logError(comp, "Expected nx=%zu but was %zu.", 1, nx); + return Error; + } + + calculateValues(comp); + +- for (size_t i = 0; i < M(n); i++) { +- dx[i] = M(der_x)[i]; +- } ++ dx[0] = M(der_x); + + return OK; + } +-- +2.34.1 + diff --git a/test/extensions/fmus/StateSpace/README.md b/test/extensions/fmus/StateSpace/README.md new file mode 100644 index 0000000000..108147443d --- /dev/null +++ b/test/extensions/fmus/StateSpace/README.md @@ -0,0 +1,7 @@ +StateSpace.fmu is a v3 FMU built using a modified version of the StateSpace FMU in Modelica's +Reference-FMUs. The link to the specific commit hash used is + +[https://github.com/modelica/Reference-FMUs/tree/0e1374cad0a7f0583a583ce189060ba7a67c27a7]() + +The git patch containing the changes is provided as `0001-tmp-commit.patch`. The FMU can be +built using the instructions provided in the repository's README. From 051f551917f5ecf36dd91b0c931fe92c23fb3551 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 16 Jan 2025 19:56:15 +0530 Subject: [PATCH 0660/1337] build: bump SymbolicUtils compat --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 13684a6a52..e8a8fea71a 100644 --- a/Project.toml +++ b/Project.toml @@ -146,7 +146,7 @@ StaticArrays = "0.10, 0.11, 0.12, 1.0" StochasticDiffEq = "6.72.1" StochasticDelayDiffEq = "1.8.1" SymbolicIndexingInterface = "0.3.37" -SymbolicUtils = "3.10" +SymbolicUtils = "3.10.1" Symbolics = "6.23" URIs = "1" UnPack = "0.1, 1.0" From fe2075bf4d9bddbab8b42e9e9ad3a7b7a9920569 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 20 Jan 2025 15:56:15 +0530 Subject: [PATCH 0661/1337] fix: fix type promotion in `InitializationProblem` --- src/systems/diffeqs/abstractodesystem.jl | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index 921825dac8..6571aed9cc 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -1391,16 +1391,16 @@ function InitializationProblem{iip, specialize}(sys::AbstractSystem, fullmap = merge(u0map, parammap) u0T = Union{} for sym in unknowns(isys) - haskey(fullmap, sym) || continue - symbolic_type(fullmap[sym]) == NotSymbolic() || continue - is_array_of_symbolics(fullmap[sym]) && continue - u0T = promote_type(u0T, typeof(fullmap[sym])) + val = fixpoint_sub(sym, fullmap) + symbolic_type(val) == NotSymbolic() || continue + u0T = promote_type(u0T, typeof(val)) end for eq in observed(isys) - haskey(fullmap, eq.lhs) || continue - symbolic_type(fullmap[eq.lhs]) == NotSymbolic() || continue - is_array_of_symbolics(fullmap[eq.lhs]) && continue - u0T = promote_type(u0T, typeof(fullmap[eq.lhs])) + # ignore HACK-ed observed equations + symbolic_type(eq.lhs) == ArraySymbolic() && continue + val = fixpoint_sub(eq.lhs, fullmap) + symbolic_type(val) == NotSymbolic() || continue + u0T = promote_type(u0T, typeof(val)) end if u0T != Union{} u0T = eltype(u0T) From a7ff2041669457f819f1d25eb79a7ae226ef347b Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 20 Jan 2025 15:56:26 +0530 Subject: [PATCH 0662/1337] test: fix CSE hack test --- test/structural_transformation/utils.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/structural_transformation/utils.jl b/test/structural_transformation/utils.jl index 863e091aad..621aa8b8a8 100644 --- a/test/structural_transformation/utils.jl +++ b/test/structural_transformation/utils.jl @@ -92,7 +92,7 @@ end @mtkbuild sys = ODESystem( [D(y) ~ foo(x), D(x) ~ sum(y), zeros(2) ~ foo(prod(z))], t) @test length(equations(sys)) == 5 - @test length(observed(sys)) == 2 + @test length(observed(sys)) == 4 prob = ODEProblem( sys, [y => ones(2), z => 2ones(2), x => 3.0], (0.0, 1.0), [foo => _tmp_fn2]) @test_nowarn prob.f(prob.u0, prob.p, 0.0) From 06b4d1190950c349ea66866ba25afdec51603a71 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 21 Jan 2025 15:03:09 +0530 Subject: [PATCH 0663/1337] build: bump Symbolics compat --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index e8a8fea71a..c4feb9b593 100644 --- a/Project.toml +++ b/Project.toml @@ -147,7 +147,7 @@ StochasticDiffEq = "6.72.1" StochasticDelayDiffEq = "1.8.1" SymbolicIndexingInterface = "0.3.37" SymbolicUtils = "3.10.1" -Symbolics = "6.23" +Symbolics = "6.25" URIs = "1" UnPack = "0.1, 1.0" Unitful = "1.1" From 6c4ea64ccf1007b4796a99768a2f006c422838be Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 21 Jan 2025 17:17:20 +0530 Subject: [PATCH 0664/1337] test: move FMI tests to separate environment --- .github/workflows/Tests.yml | 1 + test/extensions/Project.toml | 2 -- test/fmi/Project.toml | 8 ++++++++ test/{extensions => fmi}/fmi.jl | 0 test/{extensions => fmi}/fmus/SimpleAdder.fmu | Bin test/{extensions => fmi}/fmus/SimpleAdder/README.md | 0 .../fmus/SimpleAdder/SimpleAdder.mo | 0 .../fmus/SimpleAdder/buildFmu.mos | 0 test/{extensions => fmi}/fmus/StateSpace.fmu | Bin .../fmus/StateSpace/0001-tmp-commit.patch | 0 test/{extensions => fmi}/fmus/StateSpace/README.md | 0 test/runtests.jl | 12 +++++++++++- 12 files changed, 20 insertions(+), 3 deletions(-) create mode 100644 test/fmi/Project.toml rename test/{extensions => fmi}/fmi.jl (100%) rename test/{extensions => fmi}/fmus/SimpleAdder.fmu (100%) rename test/{extensions => fmi}/fmus/SimpleAdder/README.md (100%) rename test/{extensions => fmi}/fmus/SimpleAdder/SimpleAdder.mo (100%) rename test/{extensions => fmi}/fmus/SimpleAdder/buildFmu.mos (100%) rename test/{extensions => fmi}/fmus/StateSpace.fmu (100%) rename test/{extensions => fmi}/fmus/StateSpace/0001-tmp-commit.patch (100%) rename test/{extensions => fmi}/fmus/StateSpace/README.md (100%) diff --git a/.github/workflows/Tests.yml b/.github/workflows/Tests.yml index a95f19a085..52c5482970 100644 --- a/.github/workflows/Tests.yml +++ b/.github/workflows/Tests.yml @@ -38,6 +38,7 @@ jobs: - Extensions - Downstream - RegressionI + - FMI uses: "SciML/.github/.github/workflows/tests.yml@v1" with: julia-version: "${{ matrix.version }}" diff --git a/test/extensions/Project.toml b/test/extensions/Project.toml index 72501554b5..5f7afe222a 100644 --- a/test/extensions/Project.toml +++ b/test/extensions/Project.toml @@ -2,8 +2,6 @@ BifurcationKit = "0f109fa4-8a5d-4b75-95aa-f515264e7665" ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4" ChainRulesTestUtils = "cdddcdb0-9152-4a09-a978-84456f9df70a" -FMI = "14a09403-18e3-468f-ad8a-74f8dda2d9ac" -FMIZoo = "724179cf-c260-40a9-bd27-cccc6fe2f195" ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210" HomotopyContinuation = "f213a82b-91d6-5c5d-acf7-10f1c761b327" InfiniteOpt = "20393b10-9daf-11e9-18c9-8db751c92c57" diff --git a/test/fmi/Project.toml b/test/fmi/Project.toml new file mode 100644 index 0000000000..59bd3c90da --- /dev/null +++ b/test/fmi/Project.toml @@ -0,0 +1,8 @@ +[deps] +FMI = "14a09403-18e3-468f-ad8a-74f8dda2d9ac" +FMIZoo = "724179cf-c260-40a9-bd27-cccc6fe2f195" +ModelingToolkit = "961ee093-0014-501f-94e3-6117800e7a78" +OrdinaryDiffEq = "1dea7af3-3e70-54e6-95c3-0bf5283fa5ed" + +[compat] +FMI = "0.14" diff --git a/test/extensions/fmi.jl b/test/fmi/fmi.jl similarity index 100% rename from test/extensions/fmi.jl rename to test/fmi/fmi.jl diff --git a/test/extensions/fmus/SimpleAdder.fmu b/test/fmi/fmus/SimpleAdder.fmu similarity index 100% rename from test/extensions/fmus/SimpleAdder.fmu rename to test/fmi/fmus/SimpleAdder.fmu diff --git a/test/extensions/fmus/SimpleAdder/README.md b/test/fmi/fmus/SimpleAdder/README.md similarity index 100% rename from test/extensions/fmus/SimpleAdder/README.md rename to test/fmi/fmus/SimpleAdder/README.md diff --git a/test/extensions/fmus/SimpleAdder/SimpleAdder.mo b/test/fmi/fmus/SimpleAdder/SimpleAdder.mo similarity index 100% rename from test/extensions/fmus/SimpleAdder/SimpleAdder.mo rename to test/fmi/fmus/SimpleAdder/SimpleAdder.mo diff --git a/test/extensions/fmus/SimpleAdder/buildFmu.mos b/test/fmi/fmus/SimpleAdder/buildFmu.mos similarity index 100% rename from test/extensions/fmus/SimpleAdder/buildFmu.mos rename to test/fmi/fmus/SimpleAdder/buildFmu.mos diff --git a/test/extensions/fmus/StateSpace.fmu b/test/fmi/fmus/StateSpace.fmu similarity index 100% rename from test/extensions/fmus/StateSpace.fmu rename to test/fmi/fmus/StateSpace.fmu diff --git a/test/extensions/fmus/StateSpace/0001-tmp-commit.patch b/test/fmi/fmus/StateSpace/0001-tmp-commit.patch similarity index 100% rename from test/extensions/fmus/StateSpace/0001-tmp-commit.patch rename to test/fmi/fmus/StateSpace/0001-tmp-commit.patch diff --git a/test/extensions/fmus/StateSpace/README.md b/test/fmi/fmus/StateSpace/README.md similarity index 100% rename from test/extensions/fmus/StateSpace/README.md rename to test/fmi/fmus/StateSpace/README.md diff --git a/test/runtests.jl b/test/runtests.jl index bfb9e0b6f9..52875fdae5 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -4,6 +4,12 @@ import REPL const GROUP = get(ENV, "GROUP", "All") +function activate_fmi_env() + Pkg.activate("fmi") + Pkg.develop(PackageSpec(path = dirname(@__DIR__))) + Pkg.instantiate() +end + function activate_extensions_env() Pkg.activate("extensions") Pkg.develop(PackageSpec(path = dirname(@__DIR__))) @@ -115,9 +121,13 @@ end @safetestset "Analysis Points Test" include("downstream/analysis_points.jl") end + if GROUP == "All" || GROUP == "FMI" + activate_fmi_env() + @safetestset "FMI Extension Test" include("fmi/fmi.jl") + end + if GROUP == "All" || GROUP == "Extensions" activate_extensions_env() - @safetestset "FMI Extension Test" include("extensions/fmi.jl") @safetestset "HomotopyContinuation Extension Test" include("extensions/homotopy_continuation.jl") @safetestset "Auto Differentiation Test" include("extensions/ad.jl") @safetestset "LabelledArrays Test" include("labelledarrays.jl") From 110e086ab0b99b48d422c58e7af914cdd726418c Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 22 Jan 2025 11:39:03 +0530 Subject: [PATCH 0665/1337] fix: fix equality comparison of `SDESystem` --- src/systems/diffeqs/odesystem.jl | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/systems/diffeqs/odesystem.jl b/src/systems/diffeqs/odesystem.jl index 431261f102..41bcaf1d01 100644 --- a/src/systems/diffeqs/odesystem.jl +++ b/src/systems/diffeqs/odesystem.jl @@ -687,13 +687,22 @@ function populate_delays(delays::Set, obsexprs, histfn, sys, sym) end function _eq_unordered(a, b) + # a and b may be multidimensional + # e.g. comparing noiseeqs of SDESystem + a = vec(a) + b = vec(b) length(a) === length(b) || return false n = length(a) idxs = Set(1:n) for x in a idx = findfirst(isequal(x), b) + # loop since there might be multiple identical entries in a/b + # and while we might have already matched the first there could + # be a second that is equal to x + while idx !== nothing && !(idx in idxs) + idx = findnext(isequal(x), b, idx + 1) + end idx === nothing && return false - idx ∈ idxs || return false delete!(idxs, idx) end return true From 2646b81ae8d393a7cd137de56c5ca3b955f07955 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 27 Jan 2025 12:49:46 +0530 Subject: [PATCH 0666/1337] fix: fix initialization of v2 CS FMUs --- ext/MTKFMIExt.jl | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ext/MTKFMIExt.jl b/ext/MTKFMIExt.jl index 603ec8906c..5cfe9a82ef 100644 --- a/ext/MTKFMIExt.jl +++ b/ext/MTKFMIExt.jl @@ -745,6 +745,9 @@ function (fn::FMI2CSFunctor)(wrapper::FMI2InstanceWrapper, states, inputs, param states = states isa SubArray ? copy(states) : states inputs = inputs isa SubArray ? copy(inputs) : inputs params = params isa SubArray ? copy(params) : params + if wrapper.instance !== nothing + reset_instance!(wrapper) + end instance = get_instance_CS!(wrapper, states, inputs, params, t) if isempty(fn.output_value_references) return eltype(states)[] From c850df7bb2e037e01b007d4f032ca8f6a053b642 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 27 Jan 2025 12:50:02 +0530 Subject: [PATCH 0667/1337] fix: don't wrap arrays in `Origin` in array hack if not offset --- src/structural_transformation/symbolics_tearing.jl | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/structural_transformation/symbolics_tearing.jl b/src/structural_transformation/symbolics_tearing.jl index f1a152b8e7..d95951c41e 100644 --- a/src/structural_transformation/symbolics_tearing.jl +++ b/src/structural_transformation/symbolics_tearing.jl @@ -771,6 +771,9 @@ getindex_wrapper(x, i) = x[i...] # PART OF HACK 2 function change_origin(origin, arr) + if all(isone, Tuple(origin)) + return arr + end return Origin(origin)(arr) end From 2e1d3c59f36fa1d9e3dcbf19221c1f6d9472448a Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 27 Jan 2025 12:50:18 +0530 Subject: [PATCH 0668/1337] fix: fix SCCNonlinearProblem codegen --- src/systems/nonlinear/nonlinearsystem.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/systems/nonlinear/nonlinearsystem.jl b/src/systems/nonlinear/nonlinearsystem.jl index 6b0b0cc759..89663f7dbc 100644 --- a/src/systems/nonlinear/nonlinearsystem.jl +++ b/src/systems/nonlinear/nonlinearsystem.jl @@ -710,7 +710,7 @@ function SciMLBase.SCCNonlinearProblem{iip}(sys::NonlinearSystem, u0map, # precomputed subexpressions should not contain `banned_vars` banned_vars = Set{Any}(vcat(_dvs, getproperty.(_obs, (:lhs,)))) filter!(banned_vars) do var - symbolic_type(var) != ArraySymbolic() || all(x -> var[i] in banned_vars, eachindex(var)) + symbolic_type(var) != ArraySymbolic() || all(j -> var[j] in banned_vars, eachindex(var)) end state = Dict() for i in eachindex(_obs) @@ -772,6 +772,7 @@ function SciMLBase.SCCNonlinearProblem{iip}(sys::NonlinearSystem, u0map, _obs = scc_obs[i] cachevars = scc_cachevars[i] cacheexprs = scc_cacheexprs[i] + _prevobsidxs = vcat(_prevobsidxs, observed_equations_used_by(sys, reduce(vcat, values(cacheexprs); init = []))) if isempty(cachevars) push!(explicitfuns, Returns(nothing)) From afdd072fb6553de6f3aeda68dbe59e1772df2e3d Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 27 Jan 2025 12:50:34 +0530 Subject: [PATCH 0669/1337] test: fix CS tests --- test/fmi/fmi.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/fmi/fmi.jl b/test/fmi/fmi.jl index 145989ab29..bf5b60459f 100644 --- a/test/fmi/fmi.jl +++ b/test/fmi/fmi.jl @@ -1,4 +1,4 @@ -using ModelingToolkit, FMI, FMIZoo, OrdinaryDiffEq +using ModelingToolkit, FMI, FMIZoo, OrdinaryDiffEq, NonlinearSolve, SciMLBase using ModelingToolkit: t_nounits as t, D_nounits as D import ModelingToolkit as MTK @@ -171,7 +171,7 @@ end @test truesol(sol.t; idxs = [truesys.a, truesys.b, truesys.c]).u≈sol.u rtol=1e-2 # sys.adder.c is a discrete variable - @test truesol(sol.t; idxs = truesys.adder.c)≈sol(sol.t; idxs = sys.adder.c) rtol=1e-3 + @test truesol(sol.t; idxs = truesys.adder.c).u≈sol(sol.t; idxs = sys.adder.c).u rtol=1e-3 end function build_sspace_model(sspace) @@ -262,7 +262,7 @@ end @named adder2 = MTK.FMIComponent( Val(2); fmu, type = :CS, communication_step_size = 1e-3) sys, prob = build_looped_adders(adder1, adder2) - sol = solve(prob, Tsit5(); reltol = 1e-8) + sol = solve(prob, Tsit5(); reltol = 1e-8, initializealg = SciMLBase.OverrideInit(nlsolve = FastShortcutNLLSPolyalg(autodiff = AutoFiniteDiff()))) @test truesol(sol.t; idxs = [truesys.adder1.c, truesys.adder2.c]).u≈sol( sol.t; idxs = [sys.adder1.c, sys.adder2.c]).u rtol=1e-3 From 1f1dae7e744b924cfbc5d0afec0e71c17e5cc99e Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 27 Jan 2025 13:29:33 +0530 Subject: [PATCH 0670/1337] fix: fix `ReconstructInitializeprob` overriding guesses --- src/systems/nonlinear/initializesystem.jl | 24 +++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/src/systems/nonlinear/initializesystem.jl b/src/systems/nonlinear/initializesystem.jl index 87be4338b8..296078bf43 100644 --- a/src/systems/nonlinear/initializesystem.jl +++ b/src/systems/nonlinear/initializesystem.jl @@ -232,15 +232,31 @@ struct ReconstructInitializeprob end function ReconstructInitializeprob(srcsys::AbstractSystem, dstsys::AbstractSystem) - syms = [unknowns(dstsys); - reduce(vcat, reorder_parameters(dstsys, parameters(dstsys)); init = [])] + syms = reduce(vcat, reorder_parameters(dstsys, parameters(dstsys)); init = []) getter = getu(srcsys, syms) - setter = setsym_oop(dstsys, syms) + setter = setp_oop(dstsys, syms) return ReconstructInitializeprob(getter, setter) end function (rip::ReconstructInitializeprob)(srcvalp, dstvalp) - rip.setter(dstvalp, rip.getter(srcvalp)) + newp = rip.setter(dstvalp, rip.getter(srcvalp)) + if state_values(dstvalp) === nothing + return nothing, newp + end + T = eltype(state_values(srcvalp)) + if parameter_values(dstvalp) isa MTKParameters + if !isempty(newp.tunable) + T = promote_type(eltype(newp.tunable), T) + end + elseif !isempty(newp) + T = promote_type(eltype(newp), T) + end + if T == eltype(state_values(dstvalp)) + u0 = state_values(dstvalp) + else + u0 = T.(state_values(dstvalp)) + end + return u0, newp end struct InitializationSystemMetadata From 5ee8edb48bf9931cd02c6e87a66df8f122886bd9 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 27 Jan 2025 15:08:46 +0530 Subject: [PATCH 0671/1337] test: make `NonlinearSystem` parameter initializaiton test less temperamental --- test/initializationsystem.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/initializationsystem.jl b/test/initializationsystem.jl index 0a4ef18038..def45a7abf 100644 --- a/test/initializationsystem.jl +++ b/test/initializationsystem.jl @@ -792,10 +792,10 @@ end @variables x=1.0 y=3.0 @parameters p q - @mtkbuild sys = NonlinearSystem( - [(x - p)^2 + (y - q)^3 ~ 0, x - q ~ 0]; defaults = [q => missing], + @named sys = NonlinearSystem( + [x + y - p ~ 0, x - q ~ 0]; defaults = [q => missing], guesses = [q => 1.0], initialization_eqs = [p^2 + q^2 + 2p * q ~ 0]) - + sys = complete(sys) for (probT, algs) in prob_alg_combinations prob = probT(sys, [], [p => 2.0]) @test prob.f.initialization_data !== nothing From ec386fe041595284c9ab44a0f54fe9a6c232155e Mon Sep 17 00:00:00 2001 From: vyudu Date: Mon, 27 Jan 2025 23:02:07 -0500 Subject: [PATCH 0672/1337] Refactor constraints --- src/ModelingToolkit.jl | 8 +- src/systems/abstractsystem.jl | 1 + src/systems/diffeqs/abstractodesystem.jl | 121 ++---- src/systems/diffeqs/bvpsystem.jl | 217 ++++++++++ src/systems/diffeqs/odesystem.jl | 86 +++- .../optimization/constraints_system.jl | 5 + test/bvproblem.jl | 373 +++++++++--------- test/odesystem.jl | 30 ++ 8 files changed, 568 insertions(+), 273 deletions(-) create mode 100644 src/systems/diffeqs/bvpsystem.jl diff --git a/src/ModelingToolkit.jl b/src/ModelingToolkit.jl index 10aba4d8a9..2d53c61401 100644 --- a/src/ModelingToolkit.jl +++ b/src/ModelingToolkit.jl @@ -150,6 +150,10 @@ include("systems/imperative_affect.jl") include("systems/callbacks.jl") include("systems/problem_utils.jl") +include("systems/optimization/constraints_system.jl") +include("systems/optimization/optimizationsystem.jl") +include("systems/optimization/modelingtoolkitize.jl") + include("systems/nonlinear/nonlinearsystem.jl") include("systems/nonlinear/homotopy_continuation.jl") include("systems/diffeqs/odesystem.jl") @@ -165,10 +169,6 @@ include("systems/discrete_system/discrete_system.jl") include("systems/jumps/jumpsystem.jl") -include("systems/optimization/constraints_system.jl") -include("systems/optimization/optimizationsystem.jl") -include("systems/optimization/modelingtoolkitize.jl") - include("systems/pde/pdesystem.jl") include("systems/sparsematrixclil.jl") diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index 168260ae69..7c0fe0285d 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -983,6 +983,7 @@ for prop in [:eqs :structure :op :constraints + :constraintsystem :controls :loss :bcs diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index cf6e0962fd..d0d6ed7937 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -827,6 +827,12 @@ function DiffEqBase.ODEProblem{iip, specialize}(sys::AbstractODESystem, u0map = if !iscomplete(sys) error("A completed system is required. Call `complete` or `structural_simplify` on the system before creating an `ODEProblem`") end + + if !isnothing(get_constraintsystem(sys)) + error("An ODESystem with constraints cannot be used to construct a regular ODEProblem. + Consider a BVProblem instead.") + end + f, u0, p = process_SciMLProblem(ODEFunction{iip, specialize}, sys, u0map, parammap; t = tspan !== nothing ? tspan[1] : tspan, check_length, warn_initialize_determined, eval_expression, eval_module, kwargs...) @@ -866,18 +872,23 @@ Create a boundary value problem from the [`ODESystem`](@ref). must have either an initial guess supplied using `guesses` or a fixed initial value specified using `u0map`. -`constraints` are used to specify boundary conditions to the ODESystem in the -form of equations. These values should specify values that state variables should +Boundary value conditions are supplied to ODESystems +in the form of a ConstraintsSystem. These equations +should specify values that state variables should take at specific points, as in `x(0.5) ~ 1`). More general constraints that should hold over the entire solution, such as `x(t)^2 + y(t)^2`, should be -specified as one of the equations used to build the `ODESystem`. Below is an example. +specified as one of the equations used to build the `ODESystem`. + +If an ODESystem without `constraints` is specified, it will be treated as an initial value problem. ```julia - @parameters g + @parameters g t_c = 0.5 @variables x(..) y(t) [state_priority = 10] λ(t) eqs = [D(D(x(t))) ~ λ * x(t) D(D(y)) ~ λ * y - g x(t)^2 + y^2 ~ 1] + cstr = [x(0.5) ~ 1] + @named cstrs = ConstraintsSystem(cstr, t) @mtkbuild pend = ODESystem(eqs, t) tspan = (0.0, 1.5) @@ -889,9 +900,7 @@ specified as one of the equations used to build the `ODESystem`. Below is an exa bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(pend, u0map, tspan, parammap; constraints, guesses, check_length = false) ``` -If no `constraints` are specified, the problem will be treated as an initial value problem. - -If the `ODESystem` has algebraic equations like `x(t)^2 + y(t)^2`, the resulting +If the `ODESystem` has algebraic equations, like `x(t)^2 + y(t)^2`, the resulting `BVProblem` must be solved using BVDAE solvers, such as Ascher. """ function SciMLBase.BVProblem(sys::AbstractODESystem, args...; kwargs...) @@ -916,7 +925,7 @@ end function SciMLBase.BVProblem{iip, specialize}(sys::AbstractODESystem, u0map = [], tspan = get_tspan(sys), parammap = DiffEqBase.NullParameters(); - constraints = nothing, guesses = Dict(), + guesses = Dict(), version = nothing, tgrad = false, callback = nothing, check_length = true, @@ -930,21 +939,14 @@ function SciMLBase.BVProblem{iip, specialize}(sys::AbstractODESystem, u0map = [] end !isnothing(callback) && error("BVP solvers do not support callbacks.") - has_alg_eqs(sys) && error("The BVProblem currently does not support ODESystems with algebraic equations.") # Remove this when the BVDAE solvers get updated, the codegen should work when it does. + has_alg_eqs(sys) && error("The BVProblem constructor currently does not support ODESystems with algebraic equations.") # Remove this when the BVDAE solvers get updated, the codegen should work when it does. - constraintsts = nothing - constraintps = nothing sts = unknowns(sys) ps = parameters(sys) - # Constraint validation if !isnothing(constraints) - constraints isa Equation || - constraints isa Vector{Equation} || - error("Constraints must be specified as an equation or a vector of equations.") - (length(constraints) + length(u0map) > length(sts)) && - error("The BVProblem is overdetermined. The total number of conditions (# constraints + # fixed initial values given by u0map) cannot exceed the total number of states.") + @warn "The BVProblem is overdetermined. The total number of conditions (# constraints + # fixed initial values given by u0map) exceeds the total number of states. The BVP solvers will default to doing a nonlinear least-squares optimization." end # ODESystems without algebraic equations should use both fixed values + guesses @@ -957,48 +959,25 @@ function SciMLBase.BVProblem{iip, specialize}(sys::AbstractODESystem, u0map = [] stidxmap = Dict([v => i for (i, v) in enumerate(sts)]) u0_idxs = has_alg_eqs(sys) ? collect(1:length(sts)) : [stidxmap[k] for (k,v) in u0map] - bc = process_constraints(sys, constraints, u0, u0_idxs, tspan, iip) - + bc = generate_function_bc(sys, u0, u0_idxs, tspan, iip) return BVProblem{iip}(f, bc, u0, tspan, p; kwargs...) end get_callback(prob::BVProblem) = error("BVP solvers do not support callbacks.") -# Validate that all the variables in the BVP constraints are well-formed states or parameters. -function validate_constraint_syms(eq, constraintsts, constraintps, sts, ps, iv) - for var in constraintsts - if length(arguments(var)) > 1 - error("Too many arguments for variable $var.") - elseif isequal(arguments(var)[1], iv) - var ∈ sts || error("Constraint equation $eq contains a variable $var that is not a variable of the ODESystem.") - error("Constraint equation $eq contains a variable $var that does not have a specified argument. Such equations should be specified as algebraic equations to the ODESystem rather than a boundary constraints.") - else - operation(var)(iv) ∈ sts || error("Constraint equation $eq contains a variable $(operation(var)) that is not a variable of the ODESystem.") - end - end - - for var in constraintps - if !iscall(var) - var ∈ ps || error("Constraint equation $eq contains a parameter $var that is not a parameter of the ODESystem.") - else - length(arguments(var)) > 1 && error("Too many arguments for parameter $var.") - operation(var) ∈ ps || error("Constraint equations contain a parameter $var that is not a parameter of the ODESystem.") - end - end -end - """ - process_constraints(sys, constraints, u0, tspan, iip) + generate_function_bc(sys::ODESystem, u0, u0_idxs, tspan, iip) - Given an ODESystem with some constraints, generate the boundary condition function. + Given an ODESystem with constraints, generate the boundary condition function to pass to boundary value problem solvers. """ -function process_constraints(sys::ODESystem, constraints, u0, u0_idxs, tspan, iip) - +function generate_function_bc(sys::ODESystem, u0, u0_idxs, tspan, iip) iv = get_iv(sys) sts = get_unknowns(sys) ps = get_ps(sys) np = length(ps) ns = length(sts) + conssys = get_constraintsystem(sys) + cons = constraints(conssys) stidxmap = Dict([v => i for (i, v) in enumerate(sts)]) pidxmap = Dict([v => i for (i, v) in enumerate(ps)]) @@ -1006,48 +985,34 @@ function process_constraints(sys::ODESystem, constraints, u0, u0_idxs, tspan, ii @variables sol(..)[1:ns] p[1:np] exprs = Any[] - constraintsts = OrderedSet() - constraintps = OrderedSet() + for st in get_unknowns(cons) + x = operation(st) + t = first(arguments(st)) + idx = stidxmap[x(iv)] - !isnothing(constraints) && for cons in constraints - collect_vars!(constraintsts, constraintps, cons, iv) - validate_constraint_syms(cons, constraintsts, constraintps, Set(sts), Set(ps), iv) - expr = cons.rhs - cons.lhs - - for st in constraintsts - x = operation(st) - t = arguments(st)[1] - idx = stidxmap[x(iv)] - - expr = Symbolics.substitute(expr, Dict(x(t) => sol(t)[idx])) - end + cons = Symbolics.substitute(cons, Dict(x(t) => sol(t)[idx])) + end - for var in constraintps - if iscall(var) - x = operation(var) - t = arguments(var)[1] - idx = pidxmap[x] + for var in get_parameters(cons) + if iscall(var) + x = operation(var) + t = arguments(var)[1] + idx = pidxmap[x] - expr = Symbolics.substitute(expr, Dict(x(t) => p[idx])) - else - idx = pidxmap[var] - expr = Symbolics.substitute(expr, Dict(var => p[idx])) - end + cons = Symbolics.substitute(cons, Dict(x(t) => p[idx])) + else + idx = pidxmap[var] + cons = Symbolics.substitute(cons, Dict(var => p[idx])) end - - empty!(constraintsts) - empty!(constraintps) - push!(exprs, expr) end - init_cond_exprs = Any[] - + init_conds = Any[] for i in u0_idxs expr = sol(tspan[1])[i] - u0[i] - push!(init_cond_exprs, expr) + push!(init_conds, expr) end - exprs = vcat(init_cond_exprs, exprs) + exprs = vcat(init_conds, cons) bcs = Symbolics.build_function(exprs, sol, p, expression = Val{false}) if iip return (resid, u, p, t) -> bcs[2](resid, u, p) diff --git a/src/systems/diffeqs/bvpsystem.jl b/src/systems/diffeqs/bvpsystem.jl new file mode 100644 index 0000000000..ae3029fa14 --- /dev/null +++ b/src/systems/diffeqs/bvpsystem.jl @@ -0,0 +1,217 @@ +""" +$(TYPEDEF) + +A system of ordinary differential equations. + +# Fields +$(FIELDS) + +# Example + +```julia +using ModelingToolkit +using ModelingToolkit: t_nounits as t, D_nounits as D + +@parameters σ ρ β +@variables x(t) y(t) z(t) + +eqs = [D(x) ~ σ*(y-x), + D(y) ~ x*(ρ-z)-y, + D(z) ~ x*y - β*z] + +@named de = ODESystem(eqs,t,[x,y,z],[σ,ρ,β],tspan=(0, 1000.0)) +``` +""" +struct ODESystem <: AbstractODESystem + """ + A tag for the system. If two systems have the same tag, then they are + structurally identical. + """ + tag::UInt + """The ODEs defining the system.""" + eqs::Vector{Equation} + """Independent variable.""" + iv::BasicSymbolic{Real} + """ + Dependent (unknown) variables. Must not contain the independent variable. + + N.B.: If `torn_matching !== nothing`, this includes all variables. Actual + ODE unknowns are determined by the `SelectedState()` entries in `torn_matching`. + """ + unknowns::Vector + """Parameter variables. Must not contain the independent variable.""" + ps::Vector + """Time span.""" + tspan::Union{NTuple{2, Any}, Nothing} + """Array variables.""" + var_to_name::Any + """Control parameters (some subset of `ps`).""" + ctrls::Vector + """Observed variables.""" + observed::Vector{Equation} + """ + Time-derivative matrix. Note: this field will not be defined until + [`calculate_tgrad`](@ref) is called on the system. + """ + tgrad::RefValue{Vector{Num}} + """ + Jacobian matrix. Note: this field will not be defined until + [`calculate_jacobian`](@ref) is called on the system. + """ + jac::RefValue{Any} + """ + Control Jacobian matrix. Note: this field will not be defined until + [`calculate_control_jacobian`](@ref) is called on the system. + """ + ctrl_jac::RefValue{Any} + """ + Note: this field will not be defined until + [`generate_factorized_W`](@ref) is called on the system. + """ + Wfact::RefValue{Matrix{Num}} + """ + Note: this field will not be defined until + [`generate_factorized_W`](@ref) is called on the system. + """ + Wfact_t::RefValue{Matrix{Num}} + """ + The name of the system. + """ + name::Symbol + """ + A description of the system. + """ + description::String + """ + The internal systems. These are required to have unique names. + """ + systems::Vector{ODESystem} + """ + The default values to use when initial conditions and/or + parameters are not supplied in `ODEProblem`. + """ + defaults::Dict + """ + The guesses to use as the initial conditions for the + initialization system. + """ + guesses::Dict + """ + Tearing result specifying how to solve the system. + """ + torn_matching::Union{Matching, Nothing} + """ + The system for performing the initialization. + """ + initializesystem::Union{Nothing, NonlinearSystem} + """ + Extra equations to be enforced during the initialization sequence. + """ + initialization_eqs::Vector{Equation} + """ + The schedule for the code generation process. + """ + schedule::Any + """ + Type of the system. + """ + connector_type::Any + """ + Inject assignment statements before the evaluation of the RHS function. + """ + preface::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. + """ + discrete_events::Vector{SymbolicDiscreteCallback} + """ + Topologically sorted parameter dependency equations, where all symbols are parameters and + the LHS is a single parameter. + """ + parameter_dependencies::Vector{Equation} + """ + Metadata for the system, to be used by downstream packages. + """ + metadata::Any + """ + Metadata for MTK GUI. + """ + gui_metadata::Union{Nothing, GUIMetadata} + """ + A boolean indicating if the given `ODESystem` represents a system of DDEs. + """ + is_dde::Bool + """ + A list of points to provide to the solver as tstops. Uses the same syntax as discrete + events. + """ + tstops::Vector{Any} + """ + Cache for intermediate tearing state. + """ + tearing_state::Any + """ + Substitutions generated by tearing. + """ + substitutions::Any + """ + If a model `sys` is complete, then `sys.x` no longer performs namespacing. + """ + complete::Bool + """ + Cached data for fast symbolic indexing. + """ + index_cache::Union{Nothing, IndexCache} + """ + A list of discrete subsystems. + """ + discrete_subsystems::Any + """ + A list of actual unknowns needed to be solved by solvers. + """ + solved_unknowns::Union{Nothing, Vector{Any}} + """ + A vector of vectors of indices for the split parameters. + """ + split_idxs::Union{Nothing, Vector{Vector{Int}}} + """ + The hierarchical parent system before simplification. + """ + parent::Any + + function ODESystem(tag, deqs, iv, dvs, ps, tspan, var_to_name, ctrls, observed, tgrad, + jac, ctrl_jac, Wfact, Wfact_t, name, description, systems, defaults, guesses, + torn_matching, initializesystem, initialization_eqs, schedule, + connector_type, preface, cevents, + devents, parameter_dependencies, + metadata = nothing, gui_metadata = nothing, is_dde = false, + tstops = [], tearing_state = nothing, + substitutions = nothing, complete = false, index_cache = nothing, + discrete_subsystems = nothing, solved_unknowns = nothing, + split_idxs = nothing, parent = nothing; checks::Union{Bool, Int} = true) + if checks == true || (checks & CheckComponents) > 0 + check_independent_variables([iv]) + check_variables(dvs, iv) + check_parameters(ps, iv) + check_equations(deqs, iv) + check_equations(equations(cevents), iv) + end + if checks == true || (checks & CheckUnits) > 0 + u = __get_unit_type(dvs, ps, iv) + check_units(u, deqs) + end + new(tag, deqs, iv, dvs, ps, tspan, var_to_name, ctrls, observed, tgrad, jac, + ctrl_jac, Wfact, Wfact_t, name, description, systems, defaults, guesses, torn_matching, + initializesystem, initialization_eqs, schedule, connector_type, preface, + cevents, devents, parameter_dependencies, metadata, + gui_metadata, is_dde, tstops, tearing_state, substitutions, complete, index_cache, + discrete_subsystems, solved_unknowns, split_idxs, parent) + end +end diff --git a/src/systems/diffeqs/odesystem.jl b/src/systems/diffeqs/odesystem.jl index 61f16fd926..6ca6cdf838 100644 --- a/src/systems/diffeqs/odesystem.jl +++ b/src/systems/diffeqs/odesystem.jl @@ -49,6 +49,8 @@ struct ODESystem <: AbstractODESystem ctrls::Vector """Observed variables.""" observed::Vector{Equation} + """System of constraints that must be satisfied by the solution to the system.""" + constraintsystem::Union{Nothing, ConstraintsSystem} """ Time-derivative matrix. Note: this field will not be defined until [`calculate_tgrad`](@ref) is called on the system. @@ -186,7 +188,7 @@ struct ODESystem <: AbstractODESystem """ parent::Any - function ODESystem(tag, deqs, iv, dvs, ps, tspan, var_to_name, ctrls, observed, tgrad, + function ODESystem(tag, deqs, iv, dvs, ps, tspan, var_to_name, ctrls, observed, constraints, tgrad, jac, ctrl_jac, Wfact, Wfact_t, name, description, systems, defaults, guesses, torn_matching, initializesystem, initialization_eqs, schedule, connector_type, preface, cevents, @@ -207,7 +209,7 @@ struct ODESystem <: AbstractODESystem u = __get_unit_type(dvs, ps, iv) check_units(u, deqs) end - new(tag, deqs, iv, dvs, ps, tspan, var_to_name, ctrls, observed, tgrad, jac, + new(tag, deqs, iv, dvs, ps, tspan, var_to_name, ctrls, observed, constraints, tgrad, jac, ctrl_jac, Wfact, Wfact_t, name, description, systems, defaults, guesses, torn_matching, initializesystem, initialization_eqs, schedule, connector_type, preface, cevents, devents, parameter_dependencies, metadata, @@ -219,6 +221,7 @@ end function ODESystem(deqs::AbstractVector{<:Equation}, iv, dvs, ps; controls = Num[], observed = Equation[], + constraints = Equation[], systems = ODESystem[], tspan = nothing, name = nothing, @@ -283,11 +286,26 @@ function ODESystem(deqs::AbstractVector{<:Equation}, iv, dvs, ps; cont_callbacks = SymbolicContinuousCallbacks(continuous_events) disc_callbacks = SymbolicDiscreteCallbacks(discrete_events) + constraintsys = nothing + if !isempty(constraints) + constraintsys = process_constraint_system(constraints, dvs′, ps′, iv, systems) + dvset = Set(dvs′) + pset = Set(ps′) + for st in get_unknowns(constraintsys) + iscall(st) ? + !in(operation(st)(iv), dvset) && push!(dvs′, st) : + !in(st, dvset) && push!(dvs′, st) + end + for p in parameters(constraintsys) + !in(p, pset) && push!(ps′, p) + end + end + if is_dde === nothing is_dde = _check_if_dde(deqs, iv′, systems) end ODESystem(Threads.atomic_add!(SYSTEM_COUNT, UInt(1)), - deqs, iv′, dvs′, ps′, tspan, var_to_name, ctrl′, observed, tgrad, jac, + deqs, iv′, dvs′, ps′, tspan, var_to_name, ctrl′, observed, constraintsys, tgrad, jac, ctrl_jac, Wfact, Wfact_t, name, description, systems, defaults, guesses, nothing, initializesystem, initialization_eqs, schedule, connector_type, preface, cont_callbacks, @@ -295,7 +313,7 @@ function ODESystem(deqs::AbstractVector{<:Equation}, iv, dvs, ps; metadata, gui_metadata, is_dde, tstops, checks = checks) end -function ODESystem(eqs, iv; kwargs...) +function ODESystem(eqs, iv; constraints = Equation[], kwargs...) eqs = collect(eqs) # NOTE: this assumes that the order of algebraic equations doesn't matter diffvars = OrderedSet() @@ -358,9 +376,10 @@ function ODESystem(eqs, iv; kwargs...) end end algevars = setdiff(allunknowns, diffvars) + # the orders here are very important! return ODESystem(Equation[diffeq; algeeq; compressed_eqs], iv, - collect(Iterators.flatten((diffvars, algevars))), collect(new_ps); kwargs...) + collect(Iterators.flatten((diffvars, algevars))), collect(new_ps); constraints, kwargs...) end # NOTE: equality does not check cached Jacobian @@ -770,3 +789,60 @@ function Base.show(io::IO, mime::MIME"text/plain", sys::ODESystem; hint = true, return nothing end + +# Validate that all the variables in the BVP constraints are well-formed states or parameters. +# - Any callable with multiple arguments will error. +# - Callable/delay variables (e.g. of the form x(0.6) should be unknowns of the system (and have one arg, etc.) +# - Callable/delay parameters should be parameters of the system (and have one arg, etc.) +function validate_constraint_syms(constraintsts, constraintps, sts, ps, iv) + for var in constraintsts + if !iscall(var) + occursin(iv, var) && var ∈ sts || throw(ArgumentError("Time-dependent variable $var is not an unknown of the system.")) + elseif length(arguments(var)) > 1 + throw(ArgumentError("Too many arguments for variable $var.")) + elseif length(arguments(var)) == 1 + arg = first(arguments(var)) + operation(var)(iv) ∈ sts || throw(ArgumentError("Variable $var is not a variable of the ODESystem. Called variables must be variables of the ODESystem.")) + + isequal(arg, iv) || + isparameter(arg) || + arg isa Integer || + arg isa AbstractFloat || + throw(ArgumentError("Invalid argument specified for variable $var. The argument of the variable should be either $iv, a parameter, or a value specifying the time that the constraint holds.")) + else + var ∈ sts && @warn "Variable $var has no argument. It will be interpreted as $var($iv), and the constraint will apply to the entire interval." + end + end + + for var in constraintps + !iscall(var) && continue + + if length(arguments(var)) > 1 + throw(ArgumentError("Too many arguments for parameter $var in equation $eq.")) + elseif length(arguments(var)) == 1 + arg = first(arguments(var)) + operation(var) ∈ ps || throw(ArgumentError("Parameter $var is not a parameter of the ODESystem. Called parameters must be parameters of the ODESystem.")) + + isequal(arg, iv) || + isparameter(arg) || + arg isa Integer || + arg isa AbstractFloat || + throw(ArgumentError("Invalid argument specified for callable parameter $var. The argument of the parameter should be either $iv, a parameter, or a value specifying the time that the constraint holds.")) + end + end +end + +function process_constraint_system(constraints::Vector{Equation}, sts, ps, iv, subsys::Vector{ODESystem}; name = :cons) + isempty(constraints) && return nothing + + constraintsts = OrderedSet() + constraintps = OrderedSet() + + for cons in constraints + syms = collect_vars!(constraintsts, constraintps, cons, iv) + end + validate_constraint_syms(constraintsts, constraintps, Set(sts), Set(ps), iv) + + constraint_subsys = get_constraintsystem.(subsys) + ConstraintsSystem(constraints, collect(constraintsts), collect(constraintps); systems = constraint_subsys, name) +end diff --git a/src/systems/optimization/constraints_system.jl b/src/systems/optimization/constraints_system.jl index 03225fc900..61e190e672 100644 --- a/src/systems/optimization/constraints_system.jl +++ b/src/systems/optimization/constraints_system.jl @@ -89,6 +89,10 @@ struct ConstraintsSystem <: AbstractTimeIndependentSystem tearing_state = nothing, substitutions = nothing, complete = false, index_cache = nothing; checks::Union{Bool, Int} = true) + + ##if checks == true || (checks & CheckComponents) > 0 + ## check_variables(unknowns, constraints) + ##end if checks == true || (checks & CheckUnits) > 0 u = __get_unit_type(unknowns, ps) check_units(u, constraints) @@ -253,3 +257,4 @@ function get_cmap(sys::ConstraintsSystem, exprs = nothing) cmap = map(x -> x ~ getdefault(x), cs) return cmap, cs end + diff --git a/test/bvproblem.jl b/test/bvproblem.jl index 2f278e6135..16c12d1be6 100644 --- a/test/bvproblem.jl +++ b/test/bvproblem.jl @@ -8,163 +8,163 @@ using ModelingToolkit: t_nounits as t, D_nounits as D import ModelingToolkit: process_constraints ### Test Collocation solvers on simple problems -solvers = [MIRK4, RadauIIa5, LobattoIIIa3] +solvers = [MIRK4] daesolvers = [Ascher2, Ascher4, Ascher6] -# let -# @parameters α=7.5 β=4.0 γ=8.0 δ=5.0 -# @variables x(t)=1.0 y(t)=2.0 -# -# eqs = [D(x) ~ α * x - β * x * y, -# D(y) ~ -γ * y + δ * x * y] -# -# u0map = [x => 1.0, y => 2.0] -# parammap = [α => 7.5, β => 4, γ => 8.0, δ => 5.0] -# tspan = (0.0, 10.0) -# -# @mtkbuild lotkavolterra = ODESystem(eqs, t) -# op = ODEProblem(lotkavolterra, u0map, tspan, parammap) -# osol = solve(op, Vern9()) -# -# bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}( -# lotkavolterra, u0map, tspan, parammap; eval_expression = true) -# -# for solver in solvers -# sol = solve(bvp, solver(), dt = 0.01) -# @test isapprox(sol.u[end], osol.u[end]; atol = 0.01) -# @test sol.u[1] == [1.0, 2.0] -# end -# -# # Test out of place -# bvp2 = SciMLBase.BVProblem{false, SciMLBase.AutoSpecialize}( -# lotkavolterra, u0map, tspan, parammap; eval_expression = true) -# -# for solver in solvers -# sol = solve(bvp2, solver(), dt = 0.01) -# @test isapprox(sol.u[end], osol.u[end]; atol = 0.01) -# @test sol.u[1] == [1.0, 2.0] -# end -# end -# -# ### Testing on pendulum -# let -# @parameters g=9.81 L=1.0 -# @variables θ(t) = π / 2 θ_t(t) -# -# eqs = [D(θ) ~ θ_t -# D(θ_t) ~ -(g / L) * sin(θ)] -# -# @mtkbuild pend = ODESystem(eqs, t) -# -# u0map = [θ => π / 2, θ_t => π / 2] -# parammap = [:L => 1.0, :g => 9.81] -# tspan = (0.0, 6.0) -# -# op = ODEProblem(pend, u0map, tspan, parammap) -# osol = solve(op, Vern9()) -# -# bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(pend, u0map, tspan, parammap) -# for solver in solvers -# sol = solve(bvp, solver(), dt = 0.01) -# @test isapprox(sol.u[end], osol.u[end]; atol = 0.01) -# @test sol.u[1] == [π / 2, π / 2] -# end -# -# # Test out-of-place -# bvp2 = SciMLBase.BVProblem{false, SciMLBase.FullSpecialize}(pend, u0map, tspan, parammap) -# -# for solver in solvers -# sol = solve(bvp2, solver(), dt = 0.01) -# @test isapprox(sol.u[end], osol.u[end]; atol = 0.01) -# @test sol.u[1] == [π / 2, π / 2] -# end -# end -# -# ################################################################## -# ### ODESystem with constraint equations, DAEs with constraints ### -# ################################################################## -# -# # Test generation of boundary condition function using `process_constraints`. Compare solutions to manually written boundary conditions -# let -# @parameters α=1.5 β=1.0 γ=3.0 δ=1.0 -# @variables x(..) y(..) -# eqs = [D(x(t)) ~ α * x(t) - β * x(t) * y(t), -# D(y(t)) ~ -γ * y(t) + δ * x(t) * y(t)] -# -# tspan = (0., 1.) -# @mtkbuild lksys = ODESystem(eqs, t) -# -# function lotkavolterra!(du, u, p, t) -# du[1] = p[1]*u[1] - p[2]*u[1]*u[2] -# du[2] = -p[4]*u[2] + p[3]*u[1]*u[2] -# end -# -# function lotkavolterra(u, p, t) -# [p[1]*u[1] - p[2]*u[1]*u[2], -p[4]*u[2] + p[3]*u[1]*u[2]] -# end -# # Compare the built bc function to the actual constructed one. -# function bc!(resid, u, p, t) -# resid[1] = u[1][1] - 1. -# resid[2] = u[1][2] - 2. -# nothing -# end -# function bc(u, p, t) -# [u[1][1] - 1., u[1][2] - 2.] -# end -# -# u0 = [1., 2.]; p = [1.5, 1., 1., 3.] -# genbc_iip = ModelingToolkit.process_constraints(lksys, nothing, u0, [1, 2], tspan, true) -# genbc_oop = ModelingToolkit.process_constraints(lksys, nothing, u0, [1, 2], tspan, false) -# -# bvpi1 = SciMLBase.BVProblem(lotkavolterra!, bc!, [1.,2.], tspan, p) -# bvpi2 = SciMLBase.BVProblem(lotkavolterra!, genbc_iip, [1.,2.], tspan, p) -# -# sol1 = solve(bvpi1, MIRK4(), dt = 0.05) -# sol2 = solve(bvpi2, MIRK4(), dt = 0.05) -# @test sol1 ≈ sol2 -# -# bvpo1 = BVProblem(lotkavolterra, bc, [1,2], tspan, p) -# bvpo2 = BVProblem(lotkavolterra, genbc_oop, [1,2], tspan, p) -# -# sol1 = solve(bvpo1, MIRK4(), dt = 0.05) -# sol2 = solve(bvpo2, MIRK4(), dt = 0.05) -# @test sol1 ≈ sol2 -# -# # Test with a constraint. -# constraints = [y(0.5) ~ 2.] -# -# function bc!(resid, u, p, t) -# resid[1] = u(0.0)[1] - 1. -# resid[2] = u(0.5)[2] - 2. -# end -# function bc(u, p, t) -# [u(0.0)[1] - 1., u(0.5)[2] - 2.] -# end -# -# u0 = [1, 1.] -# genbc_iip = ModelingToolkit.process_constraints(lksys, constraints, u0, [1], tspan, true) -# genbc_oop = ModelingToolkit.process_constraints(lksys, constraints, u0, [1], tspan, false) -# -# bvpi1 = SciMLBase.BVProblem(lotkavolterra!, bc!, u0, tspan, p) -# bvpi2 = SciMLBase.BVProblem(lotkavolterra!, genbc_iip, u0, tspan, p) -# bvpi3 = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(lksys, [x(t) => 1.], tspan; guesses = [y(t) => 1.], constraints) -# bvpi4 = SciMLBase.BVProblem{true, SciMLBase.FullSpecialize}(lksys, [x(t) => 1.], tspan; guesses = [y(t) => 1.], constraints) -# -# sol1 = @btime solve($bvpi1, MIRK4(), dt = 0.01) -# sol2 = @btime solve($bvpi2, MIRK4(), dt = 0.01) -# sol3 = @btime solve($bvpi3, MIRK4(), dt = 0.01) -# sol4 = @btime solve($bvpi4, MIRK4(), dt = 0.01) -# @test sol1 ≈ sol2 ≈ sol3 ≈ sol4 # don't get true equality here, not sure why -# -# bvpo1 = BVProblem(lotkavolterra, bc, u0, tspan, p) -# bvpo2 = BVProblem(lotkavolterra, genbc_oop, u0, tspan, p) -# bvpo3 = SciMLBase.BVProblem{false, SciMLBase.FullSpecialize}(lksys, [x(t) => 1.], tspan; guesses = [y(t) => 1.], constraints) -# -# sol1 = @btime solve($bvpo1, MIRK4(), dt = 0.05) -# sol2 = @btime solve($bvpo2, MIRK4(), dt = 0.05) -# sol3 = @btime solve($bvpo3, MIRK4(), dt = 0.05) -# @test sol1 ≈ sol2 ≈ sol3 -# end +let + @parameters α=7.5 β=4.0 γ=8.0 δ=5.0 + @variables x(t)=1.0 y(t)=2.0 + + eqs = [D(x) ~ α * x - β * x * y, + D(y) ~ -γ * y + δ * x * y] + + u0map = [x => 1.0, y => 2.0] + parammap = [α => 7.5, β => 4, γ => 8.0, δ => 5.0] + tspan = (0.0, 10.0) + + @mtkbuild lotkavolterra = ODESystem(eqs, t) + op = ODEProblem(lotkavolterra, u0map, tspan, parammap) + osol = solve(op, Vern9()) + + bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}( + lotkavolterra, u0map, tspan, parammap; eval_expression = true) + + for solver in solvers + sol = solve(bvp, solver(), dt = 0.01) + @test isapprox(sol.u[end], osol.u[end]; atol = 0.01) + @test sol.u[1] == [1.0, 2.0] + end + + # Test out of place + bvp2 = SciMLBase.BVProblem{false, SciMLBase.AutoSpecialize}( + lotkavolterra, u0map, tspan, parammap; eval_expression = true) + + for solver in solvers + sol = solve(bvp2, solver(), dt = 0.01) + @test isapprox(sol.u[end], osol.u[end]; atol = 0.01) + @test sol.u[1] == [1.0, 2.0] + end +end + +### Testing on pendulum +let + @parameters g=9.81 L=1.0 + @variables θ(t) = π / 2 θ_t(t) + + eqs = [D(θ) ~ θ_t + D(θ_t) ~ -(g / L) * sin(θ)] + + @mtkbuild pend = ODESystem(eqs, t) + + u0map = [θ => π / 2, θ_t => π / 2] + parammap = [:L => 1.0, :g => 9.81] + tspan = (0.0, 6.0) + + op = ODEProblem(pend, u0map, tspan, parammap) + osol = solve(op, Vern9()) + + bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(pend, u0map, tspan, parammap) + for solver in solvers + sol = solve(bvp, solver(), dt = 0.01) + @test isapprox(sol.u[end], osol.u[end]; atol = 0.01) + @test sol.u[1] == [π / 2, π / 2] + end + + # Test out-of-place + bvp2 = SciMLBase.BVProblem{false, SciMLBase.FullSpecialize}(pend, u0map, tspan, parammap) + + for solver in solvers + sol = solve(bvp2, solver(), dt = 0.01) + @test isapprox(sol.u[end], osol.u[end]; atol = 0.01) + @test sol.u[1] == [π / 2, π / 2] + end +end + +################################################################## +### ODESystem with constraint equations, DAEs with constraints ### +################################################################## + +# Test generation of boundary condition function using `generate_function_bc`. Compare solutions to manually written boundary conditions +let + @parameters α=1.5 β=1.0 γ=3.0 δ=1.0 + @variables x(..) y(..) + eqs = [D(x(t)) ~ α * x(t) - β * x(t) * y(t), + D(y(t)) ~ -γ * y(t) + δ * x(t) * y(t)] + + tspan = (0., 1.) + @mtkbuild lksys = ODESystem(eqs, t) + + function lotkavolterra!(du, u, p, t) + du[1] = p[1]*u[1] - p[2]*u[1]*u[2] + du[2] = -p[4]*u[2] + p[3]*u[1]*u[2] + end + + function lotkavolterra(u, p, t) + [p[1]*u[1] - p[2]*u[1]*u[2], -p[4]*u[2] + p[3]*u[1]*u[2]] + end + # Compare the built bc function to the actual constructed one. + function bc!(resid, u, p, t) + resid[1] = u[1][1] - 1. + resid[2] = u[1][2] - 2. + nothing + end + function bc(u, p, t) + [u[1][1] - 1., u[1][2] - 2.] + end + + u0 = [1., 2.]; p = [1.5, 1., 1., 3.] + genbc_iip = ModelingToolkit.generate_function_bc(lksys, nothing, u0, [1, 2], tspan, true) + genbc_oop = ModelingToolkit.generate_function_bc(lksys, nothing, u0, [1, 2], tspan, false) + + bvpi1 = SciMLBase.BVProblem(lotkavolterra!, bc!, [1.,2.], tspan, p) + bvpi2 = SciMLBase.BVProblem(lotkavolterra!, genbc_iip, [1.,2.], tspan, p) + + sol1 = solve(bvpi1, MIRK4(), dt = 0.05) + sol2 = solve(bvpi2, MIRK4(), dt = 0.05) + @test sol1 ≈ sol2 + + bvpo1 = BVProblem(lotkavolterra, bc, [1,2], tspan, p) + bvpo2 = BVProblem(lotkavolterra, genbc_oop, [1,2], tspan, p) + + sol1 = solve(bvpo1, MIRK4(), dt = 0.05) + sol2 = solve(bvpo2, MIRK4(), dt = 0.05) + @test sol1 ≈ sol2 + + # Test with a constraint. + constraints = [y(0.5) ~ 2.] + + function bc!(resid, u, p, t) + resid[1] = u(0.0)[1] - 1. + resid[2] = u(0.5)[2] - 2. + end + function bc(u, p, t) + [u(0.0)[1] - 1., u(0.5)[2] - 2.] + end + + u0 = [1, 1.] + genbc_iip = ModelingToolkit.generate_function_bc(lksys, constraints, u0, [1], tspan, true) + genbc_oop = ModelingToolkit.generate_function_bc(lksys, constraints, u0, [1], tspan, false) + + bvpi1 = SciMLBase.BVProblem(lotkavolterra!, bc!, u0, tspan, p) + bvpi2 = SciMLBase.BVProblem(lotkavolterra!, genbc_iip, u0, tspan, p) + bvpi3 = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(lksys, [x(t) => 1.], tspan; guesses = [y(t) => 1.], constraints) + bvpi4 = SciMLBase.BVProblem{true, SciMLBase.FullSpecialize}(lksys, [x(t) => 1.], tspan; guesses = [y(t) => 1.], constraints) + + sol1 = @btime solve($bvpi1, MIRK4(), dt = 0.01) + sol2 = @btime solve($bvpi2, MIRK4(), dt = 0.01) + sol3 = @btime solve($bvpi3, MIRK4(), dt = 0.01) + sol4 = @btime solve($bvpi4, MIRK4(), dt = 0.01) + @test sol1 ≈ sol2 ≈ sol3 ≈ sol4 # don't get true equality here, not sure why + + bvpo1 = BVProblem(lotkavolterra, bc, u0, tspan, p) + bvpo2 = BVProblem(lotkavolterra, genbc_oop, u0, tspan, p) + bvpo3 = SciMLBase.BVProblem{false, SciMLBase.FullSpecialize}(lksys, [x(t) => 1.], tspan; guesses = [y(t) => 1.], constraints) + + sol1 = @btime solve($bvpo1, MIRK4(), dt = 0.05) + sol2 = @btime solve($bvpo2, MIRK4(), dt = 0.05) + sol3 = @btime solve($bvpo3, MIRK4(), dt = 0.05) + @test sol1 ≈ sol2 ≈ sol3 +end function test_solvers(solvers, prob, u0map, constraints, equations = []; dt = 0.05, atol = 1e-3) for solver in solvers @@ -214,33 +214,32 @@ let u0map = [] tspan = (0.0, 1.0) guesses = [x(t) => 4.0, y(t) => 2.] + constr = [x(.6) ~ 3.5, x(.3) ~ 7.] + @mtkbuild lksys = ODESystem(eqs, t; constraints = constr) - @mtkbuild lksys = ODESystem(eqs, t) - - constraints = [x(.6) ~ 3.5, x(.3) ~ 7.] - bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(lksys, u0map, tspan; guesses, constraints) - test_solvers(solvers, bvp, u0map, constraints; dt = 0.05) + bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(lksys, u0map, tspan; guesses) + test_solvers(solvers, bvp, u0map, constr; dt = 0.05) - # Testing that more complicated constraints give correct solutions. - constraints = [y(.2) + x(.8) ~ 3., y(.3) ~ 2.] - bvp = SciMLBase.BVProblem{false, SciMLBase.FullSpecialize}(lksys, u0map, tspan; guesses, constraints) - test_solvers(solvers, bvp, u0map, constraints; dt = 0.05) + # Testing that more complicated constr give correct solutions. + constr = [y(.2) + x(.8) ~ 3., y(.3) ~ 2.] + bvp = SciMLBase.BVProblem{false, SciMLBase.FullSpecialize}(lksys, u0map, tspan; guesses) + test_solvers(solvers, bvp, u0map, constr; dt = 0.05) - constraints = [α * β - x(.6) ~ 0.0, y(.2) ~ 3.] - bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(lksys, u0map, tspan; guesses, constraints) - test_solvers(solvers, bvp, u0map, constraints) + constr = [α * β - x(.6) ~ 0.0, y(.2) ~ 3.] + bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(lksys, u0map, tspan; guesses) + test_solvers(solvers, bvp, u0map, constr) - # Testing that errors are properly thrown when malformed constraints are given. + # Testing that errors are properly thrown when malformed constr are given. @variables bad(..) - constraints = [x(1.) + bad(3.) ~ 10] - @test_throws ErrorException bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(lksys, u0map, tspan; guesses, constraints) + constr = [x(1.) + bad(3.) ~ 10] + @test_throws ErrorException lksys = ODESystem(eqs, t; constraints = constr) - constraints = [x(t) + y(t) ~ 3] - @test_throws ErrorException bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(lksys, u0map, tspan; guesses, constraints) + constr = [x(t) + y(t) ~ 3] + @test_throws ErrorException lksys = ODESystem(eqs, t; constraints = constr) @parameters bad2 - constraints = [bad2 + x(0.) ~ 3] - @test_throws ErrorException bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(lksys, u0map, tspan; guesses, constraints) + constr = [bad2 + x(0.) ~ 3] + @test_throws ErrorException lksys = ODESystem(eqs, t; constraints = constr) end # Cartesian pendulum from the docs. @@ -288,31 +287,33 @@ end # eqs = [D(D(x(t))) ~ λ * x(t) # D(D(y)) ~ λ * y - g # x(t)^2 + y^2 ~ 1] -# @mtkbuild pend = ODESystem(eqs, t) +# constr = [x(0.5) ~ 1] +# @mtkbuild pend = ODESystem(eqs, t; constr) # # tspan = (0.0, 1.5) # u0map = [x(t) => 0.6, y => 0.8] # parammap = [g => 1] # guesses = [λ => 1] # -# constraints = [x(0.5) ~ 1] -# bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(pend, u0map, tspan, parammap; constraints, guesses, check_length = false) -# test_solvers(daesolvers, bvp, u0map, constraints) +# bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(pend, u0map, tspan, parammap; guesses, check_length = false) +# test_solvers(daesolvers, bvp, u0map, constr) # # bvp2 = SciMLBase.BVProblem{false, SciMLBase.FullSpecialize}(pend, u0map, tspan, parammap) -# test_solvers(daesolvers, bvp2, u0map, constraints, get_alg_eqs(pend)) +# test_solvers(daesolvers, bvp2, u0map, constr, get_alg_eqs(pend)) # -# # More complicated constraints. +# # More complicated constr. # u0map = [x(t) => 0.6] # guesses = [λ => 1, y(t) => 0.8] # -# constraints = [x(0.5) ~ 1, +# constr = [x(0.5) ~ 1, # x(0.3)^3 + y(0.6)^2 ~ 0.5] -# bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(pend, u0map, tspan, parammap; constraints, guesses, check_length = false) -# test_solvers(daesolvers, bvp, u0map, constraints, get_alg_eqs(pend)) +# @mtkbuild pend = ODESystem(eqs, t; constr) +# bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(pend, u0map, tspan, parammap; guesses, check_length = false) +# test_solvers(daesolvers, bvp, u0map, constr, get_alg_eqs(pend)) # -# constraints = [x(0.4) * g ~ y(0.2), +# constr = [x(0.4) * g ~ y(0.2), # y(0.7) ~ 0.3] -# bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(pend, u0map, tspan, parammap; constraints, guesses, check_length = false) -# test_solvers(daesolvers, bvp, u0map, constraints, get_alg_eqs(pend)) +# @mtkbuild pend = ODESystem(eqs, t; constr) +# bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(pend, u0map, tspan, parammap; guesses, check_length = false) +# test_solvers(daesolvers, bvp, u0map, constr, get_alg_eqs(pend)) # end diff --git a/test/odesystem.jl b/test/odesystem.jl index 85d135b338..516fa7d415 100644 --- a/test/odesystem.jl +++ b/test/odesystem.jl @@ -1552,3 +1552,33 @@ end expected_tstops = unique!(sort!(vcat(0.0:0.075:10.0, 0.1, 0.2, 0.65, 0.35, 0.45))) @test all(x -> any(isapprox(x, atol = 1e-6), sol2.t), expected_tstops) end + +@testset "Constraint system construction" begin + @variables x(..) y(..) z(..) + @parameters a b c d e + eqs = [D(x(t)) ~ 3*a*y(t), D(y(t)) ~ x(t) - z(t), D(z(t)) ~ e*x(t)^2] + cons = [x(0.3) ~ c*d, y(0.7) ~ 3] + + # Test variables + parameters infer correctly. + @mtkbuild sys = ODESystem(eqs, t; constraints = cons) + @test issetequal(parameters(sys), [a, c, d, e]) + @test issetequal(unknowns(sys), [x(t), y(t)]) + + @parameters t_c + cons = [x(t_c) ~ 3] + @mtkbuild sys = ODESystem(eqs, t; constraints = cons) + @test_broken issetequal(parameters(sys), [a, e, t_c]) # TODO: unbreak this. + + # Test that bad constraints throw errors. + cons = [x(3, 4) ~ 3] + @test_throws ArgumentError @mtkbuild sys = ODESystem(eqs, t; constraints = cons) + + cons = [x(y(t)) ~ 2] + @test_throws ArgumentError @mtkbuild sys = ODESystem(eqs, t; constraints = cons) + + @variables u(t) v + cons = [x(t) * u ~ 3] + @test_throws ArgumentError @mtkbuild sys = ODESystem(eqs, t; constraints = cons) + cons = [x(t) * v ~ 3] + @test_nowarn @mtkbuild sys = ODESystem(eqs, t; constraints = cons) +end From 90ce80d658eabd599b99c9e5c9bbc5efc9d41470 Mon Sep 17 00:00:00 2001 From: vyudu Date: Tue, 28 Jan 2025 11:57:37 -0500 Subject: [PATCH 0673/1337] refactor tests --- src/systems/diffeqs/abstractodesystem.jl | 45 +++++++++++++----------- src/systems/diffeqs/odesystem.jl | 11 +++--- test/bvproblem.jl | 38 ++++++-------------- 3 files changed, 39 insertions(+), 55 deletions(-) diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index d0d6ed7937..47e5c3b88c 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -943,9 +943,10 @@ function SciMLBase.BVProblem{iip, specialize}(sys::AbstractODESystem, u0map = [] sts = unknowns(sys) ps = parameters(sys) + constraintsys = get_constraintsystem(sys) - if !isnothing(constraints) - (length(constraints) + length(u0map) > length(sts)) && + if !isnothing(constraintsys) + (length(constraints(constraintsys)) + length(u0map) > length(sts)) && @warn "The BVProblem is overdetermined. The total number of conditions (# constraints + # fixed initial values given by u0map) exceeds the total number of states. The BVP solvers will default to doing a nonlinear least-squares optimization." end @@ -976,33 +977,35 @@ function generate_function_bc(sys::ODESystem, u0, u0_idxs, tspan, iip) ps = get_ps(sys) np = length(ps) ns = length(sts) - conssys = get_constraintsystem(sys) - cons = constraints(conssys) - stidxmap = Dict([v => i for (i, v) in enumerate(sts)]) pidxmap = Dict([v => i for (i, v) in enumerate(ps)]) @variables sol(..)[1:ns] p[1:np] - exprs = Any[] - for st in get_unknowns(cons) - x = operation(st) - t = first(arguments(st)) - idx = stidxmap[x(iv)] + conssys = get_constraintsystem(sys) + cons = Any[] + if !isnothing(conssys) + cons = [con.lhs - con.rhs for con in constraints(conssys)] - cons = Symbolics.substitute(cons, Dict(x(t) => sol(t)[idx])) - end + for st in get_unknowns(conssys) + x = operation(st) + t = only(arguments(st)) + idx = stidxmap[x(iv)] - for var in get_parameters(cons) - if iscall(var) - x = operation(var) - t = arguments(var)[1] - idx = pidxmap[x] + cons = map(c -> Symbolics.substitute(c, Dict(x(t) => sol(t)[idx])), cons) + end - cons = Symbolics.substitute(cons, Dict(x(t) => p[idx])) - else - idx = pidxmap[var] - cons = Symbolics.substitute(cons, Dict(var => p[idx])) + for var in parameters(conssys) + if iscall(var) + x = operation(var) + t = only(arguments(var)) + idx = pidxmap[x] + + cons = map(c -> Symbolics.substitute(c, Dict(x(t) => p[idx])), cons) + else + idx = pidxmap[var] + cons = map(c -> Symbolics.substitute(c, Dict(var => p[idx])), cons) + end end end diff --git a/src/systems/diffeqs/odesystem.jl b/src/systems/diffeqs/odesystem.jl index 6ca6cdf838..4ef9f89c6a 100644 --- a/src/systems/diffeqs/odesystem.jl +++ b/src/systems/diffeqs/odesystem.jl @@ -802,13 +802,11 @@ function validate_constraint_syms(constraintsts, constraintps, sts, ps, iv) throw(ArgumentError("Too many arguments for variable $var.")) elseif length(arguments(var)) == 1 arg = first(arguments(var)) - operation(var)(iv) ∈ sts || throw(ArgumentError("Variable $var is not a variable of the ODESystem. Called variables must be variables of the ODESystem.")) + operation(var)(iv) ∈ sts || + throw(ArgumentError("Variable $var is not a variable of the ODESystem. Called variables must be variables of the ODESystem.")) - isequal(arg, iv) || - isparameter(arg) || - arg isa Integer || - arg isa AbstractFloat || - throw(ArgumentError("Invalid argument specified for variable $var. The argument of the variable should be either $iv, a parameter, or a value specifying the time that the constraint holds.")) + isequal(arg, iv) || isparameter(arg) || arg isa Integer || arg isa AbstractFloat || + throw(ArgumentError("Invalid argument specified for variable $var. The argument of the variable should be either $iv, a parameter, or a value specifying the time that the constraint holds.")) else var ∈ sts && @warn "Variable $var has no argument. It will be interpreted as $var($iv), and the constraint will apply to the entire interval." end @@ -824,7 +822,6 @@ function validate_constraint_syms(constraintsts, constraintps, sts, ps, iv) operation(var) ∈ ps || throw(ArgumentError("Parameter $var is not a parameter of the ODESystem. Called parameters must be parameters of the ODESystem.")) isequal(arg, iv) || - isparameter(arg) || arg isa Integer || arg isa AbstractFloat || throw(ArgumentError("Invalid argument specified for callable parameter $var. The argument of the parameter should be either $iv, a parameter, or a value specifying the time that the constraint holds.")) diff --git a/test/bvproblem.jl b/test/bvproblem.jl index 16c12d1be6..c6c124d09e 100644 --- a/test/bvproblem.jl +++ b/test/bvproblem.jl @@ -5,7 +5,6 @@ using BenchmarkTools using ModelingToolkit using SciMLBase using ModelingToolkit: t_nounits as t, D_nounits as D -import ModelingToolkit: process_constraints ### Test Collocation solvers on simple problems solvers = [MIRK4] @@ -113,8 +112,8 @@ let end u0 = [1., 2.]; p = [1.5, 1., 1., 3.] - genbc_iip = ModelingToolkit.generate_function_bc(lksys, nothing, u0, [1, 2], tspan, true) - genbc_oop = ModelingToolkit.generate_function_bc(lksys, nothing, u0, [1, 2], tspan, false) + genbc_iip = ModelingToolkit.generate_function_bc(lksys, u0, [1, 2], tspan, true) + genbc_oop = ModelingToolkit.generate_function_bc(lksys, u0, [1, 2], tspan, false) bvpi1 = SciMLBase.BVProblem(lotkavolterra!, bc!, [1.,2.], tspan, p) bvpi2 = SciMLBase.BVProblem(lotkavolterra!, genbc_iip, [1.,2.], tspan, p) @@ -131,7 +130,8 @@ let @test sol1 ≈ sol2 # Test with a constraint. - constraints = [y(0.5) ~ 2.] + constr = [y(0.5) ~ 2.] + @mtkbuild lksys = ODESystem(eqs, t; constraints = constr) function bc!(resid, u, p, t) resid[1] = u(0.0)[1] - 1. @@ -142,13 +142,13 @@ let end u0 = [1, 1.] - genbc_iip = ModelingToolkit.generate_function_bc(lksys, constraints, u0, [1], tspan, true) - genbc_oop = ModelingToolkit.generate_function_bc(lksys, constraints, u0, [1], tspan, false) + genbc_iip = ModelingToolkit.generate_function_bc(lksys, u0, [1], tspan, true) + genbc_oop = ModelingToolkit.generate_function_bc(lksys, u0, [1], tspan, false) bvpi1 = SciMLBase.BVProblem(lotkavolterra!, bc!, u0, tspan, p) bvpi2 = SciMLBase.BVProblem(lotkavolterra!, genbc_iip, u0, tspan, p) - bvpi3 = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(lksys, [x(t) => 1.], tspan; guesses = [y(t) => 1.], constraints) - bvpi4 = SciMLBase.BVProblem{true, SciMLBase.FullSpecialize}(lksys, [x(t) => 1.], tspan; guesses = [y(t) => 1.], constraints) + bvpi3 = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(lksys, [x(t) => 1.], tspan; guesses = [y(t) => 1.]) + bvpi4 = SciMLBase.BVProblem{true, SciMLBase.FullSpecialize}(lksys, [x(t) => 1.], tspan; guesses = [y(t) => 1.]) sol1 = @btime solve($bvpi1, MIRK4(), dt = 0.01) sol2 = @btime solve($bvpi2, MIRK4(), dt = 0.01) @@ -158,7 +158,7 @@ let bvpo1 = BVProblem(lotkavolterra, bc, u0, tspan, p) bvpo2 = BVProblem(lotkavolterra, genbc_oop, u0, tspan, p) - bvpo3 = SciMLBase.BVProblem{false, SciMLBase.FullSpecialize}(lksys, [x(t) => 1.], tspan; guesses = [y(t) => 1.], constraints) + bvpo3 = SciMLBase.BVProblem{false, SciMLBase.FullSpecialize}(lksys, [x(t) => 1.], tspan; guesses = [y(t) => 1.]) sol1 = @btime solve($bvpo1, MIRK4(), dt = 0.05) sol2 = @btime solve($bvpo2, MIRK4(), dt = 0.05) @@ -197,12 +197,6 @@ function test_solvers(solvers, prob, u0map, constraints, equations = []; dt = 0. end end -solvers = [RadauIIa3, RadauIIa5, RadauIIa7, - LobattoIIIa2, LobattoIIIa4, LobattoIIIa5, - LobattoIIIb2, LobattoIIIb3, LobattoIIIb4, LobattoIIIb5, - LobattoIIIc2, LobattoIIIc3, LobattoIIIc4, LobattoIIIc5] -weird = [MIRK2, MIRK5, RadauIIa2] -daesolvers = [] # Simple ODESystem with BVP constraints. let @parameters α=1.5 β=1.0 γ=3.0 δ=1.0 @@ -222,24 +216,14 @@ let # Testing that more complicated constr give correct solutions. constr = [y(.2) + x(.8) ~ 3., y(.3) ~ 2.] + @mtkbuild lksys = ODESystem(eqs, t; constraints = constr) bvp = SciMLBase.BVProblem{false, SciMLBase.FullSpecialize}(lksys, u0map, tspan; guesses) test_solvers(solvers, bvp, u0map, constr; dt = 0.05) constr = [α * β - x(.6) ~ 0.0, y(.2) ~ 3.] + @mtkbuild lksys = ODESystem(eqs, t; constraints = constr) bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(lksys, u0map, tspan; guesses) test_solvers(solvers, bvp, u0map, constr) - - # Testing that errors are properly thrown when malformed constr are given. - @variables bad(..) - constr = [x(1.) + bad(3.) ~ 10] - @test_throws ErrorException lksys = ODESystem(eqs, t; constraints = constr) - - constr = [x(t) + y(t) ~ 3] - @test_throws ErrorException lksys = ODESystem(eqs, t; constraints = constr) - - @parameters bad2 - constr = [bad2 + x(0.) ~ 3] - @test_throws ErrorException lksys = ODESystem(eqs, t; constraints = constr) end # Cartesian pendulum from the docs. From a15c67024d4630193afe0f662d7c4c4183d07037 Mon Sep 17 00:00:00 2001 From: vyudu Date: Tue, 28 Jan 2025 14:23:08 -0500 Subject: [PATCH 0674/1337] fix sym validation --- src/systems/diffeqs/odesystem.jl | 91 ++++++++++++++------------------ test/odesystem.jl | 15 ++++-- 2 files changed, 49 insertions(+), 57 deletions(-) diff --git a/src/systems/diffeqs/odesystem.jl b/src/systems/diffeqs/odesystem.jl index 4ef9f89c6a..0c8304e428 100644 --- a/src/systems/diffeqs/odesystem.jl +++ b/src/systems/diffeqs/odesystem.jl @@ -221,7 +221,7 @@ end function ODESystem(deqs::AbstractVector{<:Equation}, iv, dvs, ps; controls = Num[], observed = Equation[], - constraints = Equation[], + constraintsystem = nothing, systems = ODESystem[], tspan = nothing, name = nothing, @@ -286,26 +286,17 @@ function ODESystem(deqs::AbstractVector{<:Equation}, iv, dvs, ps; cont_callbacks = SymbolicContinuousCallbacks(continuous_events) disc_callbacks = SymbolicDiscreteCallbacks(discrete_events) - constraintsys = nothing - if !isempty(constraints) - constraintsys = process_constraint_system(constraints, dvs′, ps′, iv, systems) - dvset = Set(dvs′) - pset = Set(ps′) - for st in get_unknowns(constraintsys) - iscall(st) ? - !in(operation(st)(iv), dvset) && push!(dvs′, st) : - !in(st, dvset) && push!(dvs′, st) - end - for p in parameters(constraintsys) - !in(p, pset) && push!(ps′, p) - end - end - if is_dde === nothing is_dde = _check_if_dde(deqs, iv′, systems) end + + if !isempty(systems) + cons = get_constraintsystems.(systems) + @set! constraintsystem.systems = cons + end + ODESystem(Threads.atomic_add!(SYSTEM_COUNT, UInt(1)), - deqs, iv′, dvs′, ps′, tspan, var_to_name, ctrl′, observed, constraintsys, tgrad, jac, + deqs, iv′, dvs′, ps′, tspan, var_to_name, ctrl′, observed, constraintsystem, tgrad, jac, ctrl_jac, Wfact, Wfact_t, name, description, systems, defaults, guesses, nothing, initializesystem, initialization_eqs, schedule, connector_type, preface, cont_callbacks, @@ -377,9 +368,22 @@ function ODESystem(eqs, iv; constraints = Equation[], kwargs...) end algevars = setdiff(allunknowns, diffvars) + if !isempty(constraints) + consvars = OrderedSet() + constraintsystem = process_constraint_system(constraints, allunknowns, new_ps, iv) + for st in get_unknowns(constraintsystem) + iscall(st) ? + !in(operation(st)(iv), allunknowns) && push!(consvars, st) : + !in(st, allunknowns) && push!(consvars, st) + end + for p in parameters(constraintsystem) + !in(p, new_ps) && push!(new_ps, p) + end + end + # the orders here are very important! return ODESystem(Equation[diffeq; algeeq; compressed_eqs], iv, - collect(Iterators.flatten((diffvars, algevars))), collect(new_ps); constraints, kwargs...) + collect(Iterators.flatten((diffvars, algevars, consvars))), collect(new_ps); constraintsystem, kwargs...) end # NOTE: equality does not check cached Jacobian @@ -791,55 +795,38 @@ function Base.show(io::IO, mime::MIME"text/plain", sys::ODESystem; hint = true, end # Validate that all the variables in the BVP constraints are well-formed states or parameters. -# - Any callable with multiple arguments will error. # - Callable/delay variables (e.g. of the form x(0.6) should be unknowns of the system (and have one arg, etc.) # - Callable/delay parameters should be parameters of the system (and have one arg, etc.) -function validate_constraint_syms(constraintsts, constraintps, sts, ps, iv) +function process_constraint_system(constraints::Vector{Equation}, sts, ps, iv; consname = :cons) + isempty(constraints) && return nothing + + constraintsts = OrderedSet() + constraintps = OrderedSet() + + # Hack? to extract parameters from callable variables in constraints. + for cons in constraints + collect_vars!(constraintsts, constraintps, cons, iv) + end + + # Validate the states. for var in constraintsts if !iscall(var) - occursin(iv, var) && var ∈ sts || throw(ArgumentError("Time-dependent variable $var is not an unknown of the system.")) + occursin(iv, var) && (var ∈ sts || throw(ArgumentError("Time-dependent variable $var is not an unknown of the system."))) elseif length(arguments(var)) > 1 throw(ArgumentError("Too many arguments for variable $var.")) elseif length(arguments(var)) == 1 - arg = first(arguments(var)) + arg = only(arguments(var)) operation(var)(iv) ∈ sts || throw(ArgumentError("Variable $var is not a variable of the ODESystem. Called variables must be variables of the ODESystem.")) isequal(arg, iv) || isparameter(arg) || arg isa Integer || arg isa AbstractFloat || throw(ArgumentError("Invalid argument specified for variable $var. The argument of the variable should be either $iv, a parameter, or a value specifying the time that the constraint holds.")) + + isparameter(arg) && push!(constraintps, arg) else var ∈ sts && @warn "Variable $var has no argument. It will be interpreted as $var($iv), and the constraint will apply to the entire interval." end end - for var in constraintps - !iscall(var) && continue - - if length(arguments(var)) > 1 - throw(ArgumentError("Too many arguments for parameter $var in equation $eq.")) - elseif length(arguments(var)) == 1 - arg = first(arguments(var)) - operation(var) ∈ ps || throw(ArgumentError("Parameter $var is not a parameter of the ODESystem. Called parameters must be parameters of the ODESystem.")) - - isequal(arg, iv) || - arg isa Integer || - arg isa AbstractFloat || - throw(ArgumentError("Invalid argument specified for callable parameter $var. The argument of the parameter should be either $iv, a parameter, or a value specifying the time that the constraint holds.")) - end - end -end - -function process_constraint_system(constraints::Vector{Equation}, sts, ps, iv, subsys::Vector{ODESystem}; name = :cons) - isempty(constraints) && return nothing - - constraintsts = OrderedSet() - constraintps = OrderedSet() - - for cons in constraints - syms = collect_vars!(constraintsts, constraintps, cons, iv) - end - validate_constraint_syms(constraintsts, constraintps, Set(sts), Set(ps), iv) - - constraint_subsys = get_constraintsystem.(subsys) - ConstraintsSystem(constraints, collect(constraintsts), collect(constraintps); systems = constraint_subsys, name) + ConstraintsSystem(constraints, collect(constraintsts), collect(constraintps); name = consname) end diff --git a/test/odesystem.jl b/test/odesystem.jl index 516fa7d415..0276bb4b4f 100644 --- a/test/odesystem.jl +++ b/test/odesystem.jl @@ -1562,23 +1562,28 @@ end # Test variables + parameters infer correctly. @mtkbuild sys = ODESystem(eqs, t; constraints = cons) @test issetequal(parameters(sys), [a, c, d, e]) - @test issetequal(unknowns(sys), [x(t), y(t)]) + @test issetequal(unknowns(sys), [x(t), y(t), z(t)]) @parameters t_c cons = [x(t_c) ~ 3] @mtkbuild sys = ODESystem(eqs, t; constraints = cons) - @test_broken issetequal(parameters(sys), [a, e, t_c]) # TODO: unbreak this. + @test issetequal(parameters(sys), [a, e, t_c]) + + @parameters g(..) h i + cons = [g(h, i) * x(3) ~ c] + @mtkbuild sys = ODESystem(eqs, t; constraints = cons) + @test issetequal(parameters(sys), [g, h, i, a, e, c]) # Test that bad constraints throw errors. - cons = [x(3, 4) ~ 3] + cons = [x(3, 4) ~ 3] # unknowns cannot have multiple args. @test_throws ArgumentError @mtkbuild sys = ODESystem(eqs, t; constraints = cons) - cons = [x(y(t)) ~ 2] + cons = [x(y(t)) ~ 2] # unknown arg must be parameter, value, or t @test_throws ArgumentError @mtkbuild sys = ODESystem(eqs, t; constraints = cons) @variables u(t) v cons = [x(t) * u ~ 3] @test_throws ArgumentError @mtkbuild sys = ODESystem(eqs, t; constraints = cons) cons = [x(t) * v ~ 3] - @test_nowarn @mtkbuild sys = ODESystem(eqs, t; constraints = cons) + @test_throws ArgumentError @mtkbuild sys = ODESystem(eqs, t; constraints = cons) # Need time argument. end From c6ef04adac961b386f0f37c4750eee28273cff3f Mon Sep 17 00:00:00 2001 From: vyudu Date: Tue, 28 Jan 2025 14:24:38 -0500 Subject: [PATCH 0675/1337] remove file --- src/systems/diffeqs/bvpsystem.jl | 217 ------------------------------- 1 file changed, 217 deletions(-) delete mode 100644 src/systems/diffeqs/bvpsystem.jl diff --git a/src/systems/diffeqs/bvpsystem.jl b/src/systems/diffeqs/bvpsystem.jl deleted file mode 100644 index ae3029fa14..0000000000 --- a/src/systems/diffeqs/bvpsystem.jl +++ /dev/null @@ -1,217 +0,0 @@ -""" -$(TYPEDEF) - -A system of ordinary differential equations. - -# Fields -$(FIELDS) - -# Example - -```julia -using ModelingToolkit -using ModelingToolkit: t_nounits as t, D_nounits as D - -@parameters σ ρ β -@variables x(t) y(t) z(t) - -eqs = [D(x) ~ σ*(y-x), - D(y) ~ x*(ρ-z)-y, - D(z) ~ x*y - β*z] - -@named de = ODESystem(eqs,t,[x,y,z],[σ,ρ,β],tspan=(0, 1000.0)) -``` -""" -struct ODESystem <: AbstractODESystem - """ - A tag for the system. If two systems have the same tag, then they are - structurally identical. - """ - tag::UInt - """The ODEs defining the system.""" - eqs::Vector{Equation} - """Independent variable.""" - iv::BasicSymbolic{Real} - """ - Dependent (unknown) variables. Must not contain the independent variable. - - N.B.: If `torn_matching !== nothing`, this includes all variables. Actual - ODE unknowns are determined by the `SelectedState()` entries in `torn_matching`. - """ - unknowns::Vector - """Parameter variables. Must not contain the independent variable.""" - ps::Vector - """Time span.""" - tspan::Union{NTuple{2, Any}, Nothing} - """Array variables.""" - var_to_name::Any - """Control parameters (some subset of `ps`).""" - ctrls::Vector - """Observed variables.""" - observed::Vector{Equation} - """ - Time-derivative matrix. Note: this field will not be defined until - [`calculate_tgrad`](@ref) is called on the system. - """ - tgrad::RefValue{Vector{Num}} - """ - Jacobian matrix. Note: this field will not be defined until - [`calculate_jacobian`](@ref) is called on the system. - """ - jac::RefValue{Any} - """ - Control Jacobian matrix. Note: this field will not be defined until - [`calculate_control_jacobian`](@ref) is called on the system. - """ - ctrl_jac::RefValue{Any} - """ - Note: this field will not be defined until - [`generate_factorized_W`](@ref) is called on the system. - """ - Wfact::RefValue{Matrix{Num}} - """ - Note: this field will not be defined until - [`generate_factorized_W`](@ref) is called on the system. - """ - Wfact_t::RefValue{Matrix{Num}} - """ - The name of the system. - """ - name::Symbol - """ - A description of the system. - """ - description::String - """ - The internal systems. These are required to have unique names. - """ - systems::Vector{ODESystem} - """ - The default values to use when initial conditions and/or - parameters are not supplied in `ODEProblem`. - """ - defaults::Dict - """ - The guesses to use as the initial conditions for the - initialization system. - """ - guesses::Dict - """ - Tearing result specifying how to solve the system. - """ - torn_matching::Union{Matching, Nothing} - """ - The system for performing the initialization. - """ - initializesystem::Union{Nothing, NonlinearSystem} - """ - Extra equations to be enforced during the initialization sequence. - """ - initialization_eqs::Vector{Equation} - """ - The schedule for the code generation process. - """ - schedule::Any - """ - Type of the system. - """ - connector_type::Any - """ - Inject assignment statements before the evaluation of the RHS function. - """ - preface::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. - """ - discrete_events::Vector{SymbolicDiscreteCallback} - """ - Topologically sorted parameter dependency equations, where all symbols are parameters and - the LHS is a single parameter. - """ - parameter_dependencies::Vector{Equation} - """ - Metadata for the system, to be used by downstream packages. - """ - metadata::Any - """ - Metadata for MTK GUI. - """ - gui_metadata::Union{Nothing, GUIMetadata} - """ - A boolean indicating if the given `ODESystem` represents a system of DDEs. - """ - is_dde::Bool - """ - A list of points to provide to the solver as tstops. Uses the same syntax as discrete - events. - """ - tstops::Vector{Any} - """ - Cache for intermediate tearing state. - """ - tearing_state::Any - """ - Substitutions generated by tearing. - """ - substitutions::Any - """ - If a model `sys` is complete, then `sys.x` no longer performs namespacing. - """ - complete::Bool - """ - Cached data for fast symbolic indexing. - """ - index_cache::Union{Nothing, IndexCache} - """ - A list of discrete subsystems. - """ - discrete_subsystems::Any - """ - A list of actual unknowns needed to be solved by solvers. - """ - solved_unknowns::Union{Nothing, Vector{Any}} - """ - A vector of vectors of indices for the split parameters. - """ - split_idxs::Union{Nothing, Vector{Vector{Int}}} - """ - The hierarchical parent system before simplification. - """ - parent::Any - - function ODESystem(tag, deqs, iv, dvs, ps, tspan, var_to_name, ctrls, observed, tgrad, - jac, ctrl_jac, Wfact, Wfact_t, name, description, systems, defaults, guesses, - torn_matching, initializesystem, initialization_eqs, schedule, - connector_type, preface, cevents, - devents, parameter_dependencies, - metadata = nothing, gui_metadata = nothing, is_dde = false, - tstops = [], tearing_state = nothing, - substitutions = nothing, complete = false, index_cache = nothing, - discrete_subsystems = nothing, solved_unknowns = nothing, - split_idxs = nothing, parent = nothing; checks::Union{Bool, Int} = true) - if checks == true || (checks & CheckComponents) > 0 - check_independent_variables([iv]) - check_variables(dvs, iv) - check_parameters(ps, iv) - check_equations(deqs, iv) - check_equations(equations(cevents), iv) - end - if checks == true || (checks & CheckUnits) > 0 - u = __get_unit_type(dvs, ps, iv) - check_units(u, deqs) - end - new(tag, deqs, iv, dvs, ps, tspan, var_to_name, ctrls, observed, tgrad, jac, - ctrl_jac, Wfact, Wfact_t, name, description, systems, defaults, guesses, torn_matching, - initializesystem, initialization_eqs, schedule, connector_type, preface, - cevents, devents, parameter_dependencies, metadata, - gui_metadata, is_dde, tstops, tearing_state, substitutions, complete, index_cache, - discrete_subsystems, solved_unknowns, split_idxs, parent) - end -end From 78782256b5f3c3f82ae1869b2d6737ea7a4b7acc Mon Sep 17 00:00:00 2001 From: vyudu Date: Tue, 28 Jan 2025 14:26:12 -0500 Subject: [PATCH 0676/1337] up --- src/systems/optimization/constraints_system.jl | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/systems/optimization/constraints_system.jl b/src/systems/optimization/constraints_system.jl index 61e190e672..3c2aed192d 100644 --- a/src/systems/optimization/constraints_system.jl +++ b/src/systems/optimization/constraints_system.jl @@ -90,9 +90,6 @@ struct ConstraintsSystem <: AbstractTimeIndependentSystem complete = false, index_cache = nothing; checks::Union{Bool, Int} = true) - ##if checks == true || (checks & CheckComponents) > 0 - ## check_variables(unknowns, constraints) - ##end if checks == true || (checks & CheckUnits) > 0 u = __get_unit_type(unknowns, ps) check_units(u, constraints) From 0493b5d2b49e72872976caf2e9c57381e272557a Mon Sep 17 00:00:00 2001 From: vyudu Date: Tue, 28 Jan 2025 14:28:46 -0500 Subject: [PATCH 0677/1337] remove lines --- src/systems/optimization/constraints_system.jl | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/systems/optimization/constraints_system.jl b/src/systems/optimization/constraints_system.jl index 3c2aed192d..03225fc900 100644 --- a/src/systems/optimization/constraints_system.jl +++ b/src/systems/optimization/constraints_system.jl @@ -89,7 +89,6 @@ struct ConstraintsSystem <: AbstractTimeIndependentSystem tearing_state = nothing, substitutions = nothing, complete = false, index_cache = nothing; checks::Union{Bool, Int} = true) - if checks == true || (checks & CheckUnits) > 0 u = __get_unit_type(unknowns, ps) check_units(u, constraints) @@ -254,4 +253,3 @@ function get_cmap(sys::ConstraintsSystem, exprs = nothing) cmap = map(x -> x ~ getdefault(x), cs) return cmap, cs end - From 1d32b6ea8438bda25dc91e095458c5f377c801c0 Mon Sep 17 00:00:00 2001 From: vyudu Date: Tue, 28 Jan 2025 14:50:42 -0500 Subject: [PATCH 0678/1337] up --- src/systems/diffeqs/odesystem.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/systems/diffeqs/odesystem.jl b/src/systems/diffeqs/odesystem.jl index 8d0e09fce5..1154bcd4c8 100644 --- a/src/systems/diffeqs/odesystem.jl +++ b/src/systems/diffeqs/odesystem.jl @@ -368,8 +368,8 @@ function ODESystem(eqs, iv; constraints = Equation[], kwargs...) end algevars = setdiff(allunknowns, diffvars) + consvars = OrderedSet() if !isempty(constraints) - consvars = OrderedSet() constraintsystem = process_constraint_system(constraints, allunknowns, new_ps, iv) for st in get_unknowns(constraintsystem) iscall(st) ? From 2b3ca96d23f0010609867f6586449aa7dd9c6ddb Mon Sep 17 00:00:00 2001 From: vyudu Date: Tue, 28 Jan 2025 15:05:02 -0500 Subject: [PATCH 0679/1337] up --- src/systems/diffeqs/odesystem.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/src/systems/diffeqs/odesystem.jl b/src/systems/diffeqs/odesystem.jl index 1154bcd4c8..1754079745 100644 --- a/src/systems/diffeqs/odesystem.jl +++ b/src/systems/diffeqs/odesystem.jl @@ -369,6 +369,7 @@ function ODESystem(eqs, iv; constraints = Equation[], kwargs...) algevars = setdiff(allunknowns, diffvars) consvars = OrderedSet() + constraintsystem = nothing if !isempty(constraints) constraintsystem = process_constraint_system(constraints, allunknowns, new_ps, iv) for st in get_unknowns(constraintsystem) From 0324522a419219c8f59f1b584c1ccd65454dc794 Mon Sep 17 00:00:00 2001 From: vyudu Date: Tue, 28 Jan 2025 15:05:57 -0500 Subject: [PATCH 0680/1337] fix typo --- src/systems/diffeqs/odesystem.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/systems/diffeqs/odesystem.jl b/src/systems/diffeqs/odesystem.jl index 1754079745..2ed7155f6c 100644 --- a/src/systems/diffeqs/odesystem.jl +++ b/src/systems/diffeqs/odesystem.jl @@ -291,7 +291,7 @@ function ODESystem(deqs::AbstractVector{<:Equation}, iv, dvs, ps; end if !isempty(systems) - cons = get_constraintsystems.(systems) + cons = get_constraintsystem.(systems) @set! constraintsystem.systems = cons end From 2a079becdd97c10431ad8a85f707adbb39519e59 Mon Sep 17 00:00:00 2001 From: vyudu Date: Tue, 28 Jan 2025 15:46:23 -0500 Subject: [PATCH 0681/1337] Fix setter --- src/systems/diffeqs/odesystem.jl | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/systems/diffeqs/odesystem.jl b/src/systems/diffeqs/odesystem.jl index 2ed7155f6c..dac0878d26 100644 --- a/src/systems/diffeqs/odesystem.jl +++ b/src/systems/diffeqs/odesystem.jl @@ -290,9 +290,14 @@ function ODESystem(deqs::AbstractVector{<:Equation}, iv, dvs, ps; is_dde = _check_if_dde(deqs, iv′, systems) end - if !isempty(systems) - cons = get_constraintsystem.(systems) - @set! constraintsystem.systems = cons + if !isempty(systems) && !isnothing(constraintsystem) + conssystems = ConstraintsSystem[] + for sys in systems + cons = get_constraintsystem(sys) + cons !== nothing && push!(conssystems, cons) + end + @show conssystems + @set! constraintsystem.systems = conssystems end ODESystem(Threads.atomic_add!(SYSTEM_COUNT, UInt(1)), From d70a470d546e9c79417fb4c0869956cd1800de00 Mon Sep 17 00:00:00 2001 From: vyudu Date: Tue, 28 Jan 2025 16:21:54 -0500 Subject: [PATCH 0682/1337] fix --- test/odesystem.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/test/odesystem.jl b/test/odesystem.jl index 963847731c..53f75bf377 100644 --- a/test/odesystem.jl +++ b/test/odesystem.jl @@ -1660,3 +1660,4 @@ end @test_throws ArgumentError @mtkbuild sys = ODESystem(eqs, t; constraints = cons) cons = [x(t) * v ~ 3] @test_throws ArgumentError @mtkbuild sys = ODESystem(eqs, t; constraints = cons) # Need time argument. +end From 1b7261a05c98d65218fc748524b6cc443f9160a4 Mon Sep 17 00:00:00 2001 From: Christopher Rackauckas Date: Wed, 29 Jan 2025 05:42:54 -0500 Subject: [PATCH 0683/1337] Update Project.toml --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 38d2b7057a..e4b2238170 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "ModelingToolkit" uuid = "961ee093-0014-501f-94e3-6117800e7a78" authors = ["Yingbo Ma ", "Chris Rackauckas and contributors"] -version = "9.61.0" +version = "9.62.0" [deps] AbstractTrees = "1520ce14-60c1-5f80-bbc7-55ef81b5835c" From eea66d053b5b0ad52c180d9662165e0060a3345c Mon Sep 17 00:00:00 2001 From: vyudu Date: Wed, 29 Jan 2025 09:47:01 -0500 Subject: [PATCH 0684/1337] up --- src/utils.jl | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/utils.jl b/src/utils.jl index a83ba2fde7..f956c90a67 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -156,11 +156,11 @@ function check_variables(dvs, iv) end function check_var_types(sys_type::Type{T}, dvs) where T <: AbstractSystem - if any(u -> !(symtype(u) <: Number || eltype(symtype(u)) <: Number), dvs) - error("The type of unknown variables must be a numeric type.") - elseif any(u -> (eltype(symtype(u)) !== eltype(symtype(dvs[1]))), dvs) - error("The element type of all the unknown variables in a system must all be the same.") - elseif sys_type == ODESystem || sys_type == SDESystem || sys_type == PDESystem + # if any(u -> !(symtype(u) <: Number || eltype(symtype(u)) <: Number), dvs) + # error("The type of unknown variables must be a numeric type.") + # elseif any(u -> (eltype(symtype(u)) !== eltype(symtype(dvs[1]))), dvs) + # error("The element type of all the unknown variables in a system must all be the same.") + if sys_type == ODESystem || sys_type == SDESystem || sys_type == PDESystem any(u -> !(symtype(u) == Real || eltype(symtype(u)) == Real), dvs) && error("The type of unknown variables for an SDESystem, PDESystem, or ODESystem should not be a concrete numeric type.") end nothing From a9b7fa143b2b3dcfcab5b266d2131b479daf83ce Mon Sep 17 00:00:00 2001 From: vyudu Date: Wed, 29 Jan 2025 10:42:10 -0500 Subject: [PATCH 0685/1337] up --- src/systems/diffeqs/odesystem.jl | 2 ++ src/utils.jl | 5 +++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/systems/diffeqs/odesystem.jl b/src/systems/diffeqs/odesystem.jl index 8edbb46cf1..4a3a172d60 100644 --- a/src/systems/diffeqs/odesystem.jl +++ b/src/systems/diffeqs/odesystem.jl @@ -329,6 +329,8 @@ function ODESystem(eqs, iv; kwargs...) throw(ArgumentError("The differential variable $diffvar is not unique in the system of equations.")) push!(diffvars, diffvar) end + !(symtype(diffvar) === Real || eltype(symtype(var)) === Real) && throw(ArgumentError("Differential variable $var has type $(symtype(var)). Differential variables should not be concretely typed.")) + push!(diffeq, eq) else push!(algeeq, eq) diff --git a/src/utils.jl b/src/utils.jl index 8691111b01..34058818c8 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -161,9 +161,10 @@ function check_var_types(sys_type::Type{T}, dvs) where T <: AbstractSystem # elseif any(u -> (eltype(symtype(u)) !== eltype(symtype(dvs[1]))), dvs) # error("The element type of all the unknown variables in a system must all be the same.") if sys_type == ODESystem || sys_type == SDESystem || sys_type == PDESystem - any(u -> !(symtype(u) == Real || eltype(symtype(u)) == Real), dvs) && error("The type of unknown variables for an SDESystem, PDESystem, or ODESystem should not be a concrete numeric type.") + for var in dvs + !(symtype(var) === Real || eltype(symtype(var)) === Real) && throw(ArgumentError("Differential variable $var has type $(symtype(var)). The type of unknown variables for an SDESystem, PDESystem, or ODESystem should be Real.")) + end end - nothing end function check_lhs(eq::Equation, op, dvs::Set) From 68bb1b3a3543bc1c3b340cb0bd9d72941de7d9b7 Mon Sep 17 00:00:00 2001 From: vyudu Date: Wed, 29 Jan 2025 11:53:30 -0500 Subject: [PATCH 0686/1337] up --- src/systems/diffeqs/odesystem.jl | 1 - src/systems/diffeqs/sdesystem.jl | 1 - src/utils.jl | 12 ------------ 3 files changed, 14 deletions(-) diff --git a/src/systems/diffeqs/odesystem.jl b/src/systems/diffeqs/odesystem.jl index 4a3a172d60..32169c1ecd 100644 --- a/src/systems/diffeqs/odesystem.jl +++ b/src/systems/diffeqs/odesystem.jl @@ -202,7 +202,6 @@ struct ODESystem <: AbstractODESystem check_parameters(ps, iv) check_equations(deqs, iv) check_equations(equations(cevents), iv) - check_var_types(ODESystem, dvs) end if checks == true || (checks & CheckUnits) > 0 u = __get_unit_type(dvs, ps, iv) diff --git a/src/systems/diffeqs/sdesystem.jl b/src/systems/diffeqs/sdesystem.jl index 69e984d4ce..df950ee01c 100644 --- a/src/systems/diffeqs/sdesystem.jl +++ b/src/systems/diffeqs/sdesystem.jl @@ -170,7 +170,6 @@ struct SDESystem <: AbstractODESystem check_parameters(ps, iv) check_equations(deqs, iv) check_equations(neqs, dvs) - check_var_types(SDESystem, dvs) if size(neqs, 1) != length(deqs) throw(ArgumentError("Noise equations ill-formed. Number of rows must match number of drift equations. size(neqs,1) = $(size(neqs,1)) != length(deqs) = $(length(deqs))")) end diff --git a/src/utils.jl b/src/utils.jl index 34058818c8..29b9e424ac 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -155,18 +155,6 @@ function check_variables(dvs, iv) end end -function check_var_types(sys_type::Type{T}, dvs) where T <: AbstractSystem - # if any(u -> !(symtype(u) <: Number || eltype(symtype(u)) <: Number), dvs) - # error("The type of unknown variables must be a numeric type.") - # elseif any(u -> (eltype(symtype(u)) !== eltype(symtype(dvs[1]))), dvs) - # error("The element type of all the unknown variables in a system must all be the same.") - if sys_type == ODESystem || sys_type == SDESystem || sys_type == PDESystem - for var in dvs - !(symtype(var) === Real || eltype(symtype(var)) === Real) && throw(ArgumentError("Differential variable $var has type $(symtype(var)). The type of unknown variables for an SDESystem, PDESystem, or ODESystem should be Real.")) - end - end -end - function check_lhs(eq::Equation, op, dvs::Set) v = unwrap(eq.lhs) _iszero(v) && return From 5c1cebf0658d64328a353d1beb7913d535590480 Mon Sep 17 00:00:00 2001 From: vyudu Date: Wed, 29 Jan 2025 13:09:29 -0500 Subject: [PATCH 0687/1337] add constructor --- src/systems/diffeqs/odesystem.jl | 67 ++----------------------------- src/systems/diffeqs/sdesystem.jl | 15 +++++++ src/utils.jl | 69 ++++++++++++++++++++++++++++++++ 3 files changed, 87 insertions(+), 64 deletions(-) diff --git a/src/systems/diffeqs/odesystem.jl b/src/systems/diffeqs/odesystem.jl index 41bcaf1d01..efd7509572 100644 --- a/src/systems/diffeqs/odesystem.jl +++ b/src/systems/diffeqs/odesystem.jl @@ -296,71 +296,10 @@ function ODESystem(deqs::AbstractVector{<:Equation}, iv, dvs, ps; end function ODESystem(eqs, iv; kwargs...) - eqs = collect(eqs) - # NOTE: this assumes that the order of algebraic equations doesn't matter - diffvars = OrderedSet() - allunknowns = OrderedSet() - ps = OrderedSet() - # reorder equations such that it is in the form of `diffeq, algeeq` - diffeq = Equation[] - algeeq = Equation[] - # initial loop for finding `iv` - if iv === nothing - for eq in eqs - if !(eq.lhs isa Number) # assume eq.lhs is either Differential or Number - iv = iv_from_nested_derivative(eq.lhs) - break - end - end - end - iv = value(iv) - iv === nothing && throw(ArgumentError("Please pass in independent variables.")) - compressed_eqs = Equation[] # equations that need to be expanded later, like `connect(a, b)` - for eq in eqs - eq.lhs isa Union{Symbolic, Number} || (push!(compressed_eqs, eq); continue) - collect_vars!(allunknowns, ps, eq, iv) - if isdiffeq(eq) - diffvar, _ = var_from_nested_derivative(eq.lhs) - if check_scope_depth(getmetadata(diffvar, SymScope, LocalScope()), 0) - isequal(iv, iv_from_nested_derivative(eq.lhs)) || - throw(ArgumentError("An ODESystem can only have one independent variable.")) - diffvar in diffvars && - throw(ArgumentError("The differential variable $diffvar is not unique in the system of equations.")) - push!(diffvars, diffvar) - end - push!(diffeq, eq) - else - push!(algeeq, eq) - end - end - for eq in get(kwargs, :parameter_dependencies, Equation[]) - collect_vars!(allunknowns, ps, eq, iv) - end - for ssys in get(kwargs, :systems, ODESystem[]) - collect_scoped_vars!(allunknowns, ps, ssys, iv) - end - for v in allunknowns - isdelay(v, iv) || continue - collect_vars!(allunknowns, ps, arguments(v)[1], iv) - end - new_ps = OrderedSet() - for p in ps - if iscall(p) && operation(p) === getindex - par = arguments(p)[begin] - if Symbolics.shape(Symbolics.unwrap(par)) !== Symbolics.Unknown() && - all(par[i] in ps for i in eachindex(par)) - push!(new_ps, par) - else - push!(new_ps, p) - end - else - push!(new_ps, p) - end - end - algevars = setdiff(allunknowns, diffvars) + param_deps = get(kwargs, :parameter_dependencies, Equation[]) + eqs, dvs, ps = process_equations_DESystem(eqs, param_deps, iv) # the orders here are very important! - return ODESystem(Equation[diffeq; algeeq; compressed_eqs], iv, - collect(Iterators.flatten((diffvars, algevars))), collect(new_ps); kwargs...) + return ODESystem(eqs, iv, dvs, ps; kwargs...) end # NOTE: equality does not check cached Jacobian diff --git a/src/systems/diffeqs/sdesystem.jl b/src/systems/diffeqs/sdesystem.jl index df950ee01c..0462d4d12d 100644 --- a/src/systems/diffeqs/sdesystem.jl +++ b/src/systems/diffeqs/sdesystem.jl @@ -273,6 +273,21 @@ function SDESystem(sys::ODESystem, neqs; kwargs...) SDESystem(equations(sys), neqs, get_iv(sys), unknowns(sys), parameters(sys); kwargs...) end +function SDESystem(eqs, noiseeqs, iv; kwargs...) + param_deps = get(kwargs, :parameter_dependencies, Equation[]) + eqs, dvs, ps = process_equations_DESystem(eqs, param_deps, iv) + + # validate noise equations + noisedvs = OrderedSet() + noiseps = OrderedSet() + collect_vars!(noisedvs, noiseps, noiseeqs, iv) + for dv in noisedvs + var ∈ Set(dvs) || throw(ArgumentError("Variable $dv in noise equations is not an unknown of the system.")) + end + + return SDESystem(eqs, noiseeqs, iv, dvs, [ps; collect(noiseps)]; kwargs...) +end + function Base.:(==)(sys1::SDESystem, sys2::SDESystem) sys1 === sys2 && return true iv1 = get_iv(sys1) diff --git a/src/utils.jl b/src/utils.jl index b6544972b9..9cb109c1d3 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -1185,3 +1185,72 @@ function guesses_from_metadata!(guesses, vars) guesses[vars[i]] = varguesses[i] end end + +""" + $(TYPEDSIGNATURES) + +Find all the unknowns and parameters from the equations and parameter dependencies of an ODESystem or SDESystem. Return re-ordered equations, unknowns, and parameters. +""" +function process_equations_DESystem(eqs, param_deps, iv) + eqs = collect(eqs) + + diffvars = OrderedSet() + allunknowns = OrderedSet() + ps = OrderedSet() + + # NOTE: this assumes that the order of algebraic equations doesn't matter + # reorder equations such that it is in the form of `diffeq, algeeq` + diffeq = Equation[] + algeeq = Equation[] + # initial loop for finding `iv` + if iv === nothing + for eq in eqs + if !(eq.lhs isa Number) # assume eq.lhs is either Differential or Number + iv = iv_from_nested_derivative(eq.lhs) + break + end + end + end + iv = value(iv) + iv === nothing && throw(ArgumentError("Please pass in independent variables.")) + + compressed_eqs = Equation[] # equations that need to be expanded later, like `connect(a, b)` + for eq in eqs + eq.lhs isa Union{Symbolic, Number} || (push!(compressed_eqs, eq); continue) + collect_vars!(allunknowns, ps, eq, iv) + if isdiffeq(eq) + diffvar, _ = var_from_nested_derivative(eq.lhs) + if check_scope_depth(getmetadata(diffvar, SymScope, LocalScope()), 0) + isequal(iv, iv_from_nested_derivative(eq.lhs)) || + throw(ArgumentError("An ODESystem can only have one independent variable.")) + diffvar in diffvars && + throw(ArgumentError("The differential variable $diffvar is not unique in the system of equations.")) + push!(diffvars, diffvar) + end + push!(diffeq, eq) + else + push!(algeeq, eq) + end + end + for eq in param_deps + collect_vars!(allunknowns, ps, eq, iv) + end + + new_ps = OrderedSet() + for p in ps + if iscall(p) && operation(p) === getindex + par = arguments(p)[begin] + if Symbolics.shape(Symbolics.unwrap(par)) !== Symbolics.Unknown() && + all(par[i] in ps for i in eachindex(par)) + push!(new_ps, par) + else + push!(new_ps, p) + end + else + push!(new_ps, p) + end + end + algevars = setdiff(allunknowns, diffvars) + + Equation[diffeq; algeeq; compressed_eqs], collect(Iterators.flatten((diffvars, algevars))), collect(new_ps) +end From 3ea2dd389e425c9adb26545cd4ca3428fc48c3d4 Mon Sep 17 00:00:00 2001 From: vyudu Date: Wed, 29 Jan 2025 13:15:42 -0500 Subject: [PATCH 0688/1337] remove check_var_type --- src/systems/discrete_system/discrete_system.jl | 1 - src/systems/jumps/jumpsystem.jl | 1 - src/systems/nonlinear/nonlinearsystem.jl | 3 --- src/systems/optimization/optimizationsystem.jl | 3 --- src/systems/pde/pdesystem.jl | 3 --- 5 files changed, 11 deletions(-) diff --git a/src/systems/discrete_system/discrete_system.jl b/src/systems/discrete_system/discrete_system.jl index fb6dfe0236..bd5c72eec7 100644 --- a/src/systems/discrete_system/discrete_system.jl +++ b/src/systems/discrete_system/discrete_system.jl @@ -122,7 +122,6 @@ struct DiscreteSystem <: AbstractTimeDependentSystem check_independent_variables([iv]) check_variables(dvs, iv) check_parameters(ps, iv) - check_var_types(DiscreteSystem, dvs) end if checks == true || (checks & CheckUnits) > 0 u = __get_unit_type(dvs, ps, iv) diff --git a/src/systems/jumps/jumpsystem.jl b/src/systems/jumps/jumpsystem.jl index 4d77f4106d..f39b5fa46a 100644 --- a/src/systems/jumps/jumpsystem.jl +++ b/src/systems/jumps/jumpsystem.jl @@ -147,7 +147,6 @@ struct JumpSystem{U <: ArrayPartition} <: AbstractTimeDependentSystem check_independent_variables([iv]) check_variables(unknowns, iv) check_parameters(ps, iv) - check_var_types(JumpSystem, unknowns) end if checks == true || (checks & CheckUnits) > 0 u = __get_unit_type(unknowns, ps, iv) diff --git a/src/systems/nonlinear/nonlinearsystem.jl b/src/systems/nonlinear/nonlinearsystem.jl index 41aaf65d1a..89663f7dbc 100644 --- a/src/systems/nonlinear/nonlinearsystem.jl +++ b/src/systems/nonlinear/nonlinearsystem.jl @@ -115,9 +115,6 @@ struct NonlinearSystem <: AbstractTimeIndependentSystem tearing_state = nothing, substitutions = nothing, complete = false, index_cache = nothing, parent = nothing, isscheduled = false; checks::Union{Bool, Int} = true) - if checks == true || (checks & CheckComponents) > 0 - check_var_types(NonlinearSystem, unknowns) - end if checks == true || (checks & CheckUnits) > 0 u = __get_unit_type(unknowns, ps) check_units(u, eqs) diff --git a/src/systems/optimization/optimizationsystem.jl b/src/systems/optimization/optimizationsystem.jl index de4696bfa2..860e063e35 100644 --- a/src/systems/optimization/optimizationsystem.jl +++ b/src/systems/optimization/optimizationsystem.jl @@ -73,9 +73,6 @@ struct OptimizationSystem <: AbstractOptimizationSystem gui_metadata = nothing, complete = false, index_cache = nothing, parent = nothing, isscheduled = false; checks::Union{Bool, Int} = true) - if checks == true || (checks & CheckComponents) > 0 - check_var_types(OptimizationSystem, unknowns) - end if checks == true || (checks & CheckUnits) > 0 u = __get_unit_type(unknowns, ps) unwrap(op) isa Symbolic && check_units(u, op) diff --git a/src/systems/pde/pdesystem.jl b/src/systems/pde/pdesystem.jl index e2a5bb5734..eac540e401 100644 --- a/src/systems/pde/pdesystem.jl +++ b/src/systems/pde/pdesystem.jl @@ -102,9 +102,6 @@ struct PDESystem <: ModelingToolkit.AbstractMultivariateSystem checks::Union{Bool, Int} = true, description = "", name) - if checks == true || (checks & CheckComponents) > 0 - check_var_types(PDESystem, [dv(ivs...) for dv in dvs]) - end if checks == true || (checks & CheckUnits) > 0 u = __get_unit_type(dvs, ivs, ps) check_units(u, eqs) From 3329ce5e838177fcd4a369bf73e0e6dfd837aedf Mon Sep 17 00:00:00 2001 From: vyudu Date: Wed, 29 Jan 2025 13:29:36 -0500 Subject: [PATCH 0689/1337] up --- src/systems/diffeqs/sdesystem.jl | 5 ++++- src/utils.jl | 2 +- test/odesystem.jl | 5 ++++- test/sdesystem.jl | 4 +++- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/systems/diffeqs/sdesystem.jl b/src/systems/diffeqs/sdesystem.jl index 0462d4d12d..747d8e844c 100644 --- a/src/systems/diffeqs/sdesystem.jl +++ b/src/systems/diffeqs/sdesystem.jl @@ -273,7 +273,7 @@ function SDESystem(sys::ODESystem, neqs; kwargs...) SDESystem(equations(sys), neqs, get_iv(sys), unknowns(sys), parameters(sys); kwargs...) end -function SDESystem(eqs, noiseeqs, iv; kwargs...) +function SDESystem(eqs::Vector{Equation}, noiseeqs::AbstractArray, iv; kwargs...) param_deps = get(kwargs, :parameter_dependencies, Equation[]) eqs, dvs, ps = process_equations_DESystem(eqs, param_deps, iv) @@ -288,6 +288,9 @@ function SDESystem(eqs, noiseeqs, iv; kwargs...) return SDESystem(eqs, noiseeqs, iv, dvs, [ps; collect(noiseps)]; kwargs...) end +SDESystem(eq::Equation, noiseeqs::AbstractArray, args...; kwargs...) = SDESystem([eq], noiseeqs, args...; kwargs...) +SDESystem(eq::Equation, noiseeq, args...; kwargs...) = SDESystem([eq], [noiseeq], args...; kwargs...) + function Base.:(==)(sys1::SDESystem, sys2::SDESystem) sys1 === sys2 && return true iv1 = get_iv(sys1) diff --git a/src/utils.jl b/src/utils.jl index f3828bea4f..a71e9ba799 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -1225,7 +1225,7 @@ function process_equations_DESystem(eqs, param_deps, iv) throw(ArgumentError("An ODESystem can only have one independent variable.")) diffvar in diffvars && throw(ArgumentError("The differential variable $diffvar is not unique in the system of equations.")) - !(symtype(diffvar) === Real || eltype(symtype(diffvar)) === Real) && throw(ArgumentError("Differential variable $var has type $(symtype(diffvar)). Differential variables should not be concretely typed.")) + !(symtype(diffvar) === Real || eltype(symtype(diffvar)) === Real) && throw(ArgumentError("Differential variable $diffvar has type $(symtype(diffvar)). Differential variables should not be concretely typed.")) push!(diffvars, diffvar) end push!(diffeq, eq) diff --git a/test/odesystem.jl b/test/odesystem.jl index 29076416fa..dce9275efe 100644 --- a/test/odesystem.jl +++ b/test/odesystem.jl @@ -1548,7 +1548,10 @@ end @parameters p d @variables X(t)::Int64 eq = D(X) ~ p - d*X - @test_throws Exception @mtkbuild osys = ODESystem([eq], t) + @test_throws ArgumentError @mtkbuild osys = ODESystem([eq], t) + @variables Y(t)[1:3]::String + eq = D(Y) ~ [p, p, p] + @test_throws ArgumentError @mtkbuild osys = ODESystem([eq], t) end # Test `isequal` diff --git a/test/sdesystem.jl b/test/sdesystem.jl index dee497f62c..7cac3645c4 100644 --- a/test/sdesystem.jl +++ b/test/sdesystem.jl @@ -875,7 +875,9 @@ end @variables X(t)::Int64 @brownian z eq2 = D(X) ~ p - d*X + z - @test_throws Exception @mtkbuild ssys = System([eq2], t) + @test_throws ArgumentError @mtkbuild ssys = System([eq2], t) + noiseeq = [1] + @test_throws ArgumentError @named ssys = SDESystem([eq2], [noiseeq], t) end @testset "SDEFunctionExpr" begin From 288a4a01030df20a9f106012c0bfcea52a1632fc Mon Sep 17 00:00:00 2001 From: vyudu Date: Wed, 29 Jan 2025 14:01:23 -0500 Subject: [PATCH 0690/1337] up --- src/systems/diffeqs/odesystem.jl | 35 ++++++++++++++++++++++++++++--- src/systems/diffeqs/sdesystem.jl | 36 ++++++++++++++++++++++++++++---- src/utils.jl | 25 +++------------------- 3 files changed, 67 insertions(+), 29 deletions(-) diff --git a/src/systems/diffeqs/odesystem.jl b/src/systems/diffeqs/odesystem.jl index 4aaf79f3f1..9953e28428 100644 --- a/src/systems/diffeqs/odesystem.jl +++ b/src/systems/diffeqs/odesystem.jl @@ -296,9 +296,38 @@ function ODESystem(deqs::AbstractVector{<:Equation}, iv, dvs, ps; end function ODESystem(eqs, iv; kwargs...) - param_deps = get(kwargs, :parameter_dependencies, Equation[]) - eqs, dvs, ps = process_equations_DESystem(eqs, param_deps, iv) - return ODESystem(eqs, iv, dvs, ps; kwargs...) + diffvars, allunknowns, ps, eqs = process_equations(eqs, iv) + + for eq in get(kwargs, :parameter_dependencies, Equation[]) + collect_vars!(allunknowns, ps, eq, iv) + end + + for ssys in get(kwargs, :systems, ODESystem[]) + collect_scoped_vars!(allunknowns, ps, ssys, iv) + end + + for v in allunknowns + isdelay(v, iv) || continue + collect_vars!(allunknowns, ps, arguments(v)[1], iv) + end + + new_ps = OrderedSet() + for p in ps + if iscall(p) && operation(p) === getindex + par = arguments(p)[begin] + if Symbolics.shape(Symbolics.unwrap(par)) !== Symbolics.Unknown() && + all(par[i] in ps for i in eachindex(par)) + push!(new_ps, par) + else + push!(new_ps, p) + end + else + push!(new_ps, p) + end + end + algevars = setdiff(allunknowns, diffvars) + + return ODESystem(eqs, iv, collect(Iterators.flatten((diffvars, algevars))), collect(new_ps); kwargs...) end # NOTE: equality does not check cached Jacobian diff --git a/src/systems/diffeqs/sdesystem.jl b/src/systems/diffeqs/sdesystem.jl index 747d8e844c..0d0a2ec0c7 100644 --- a/src/systems/diffeqs/sdesystem.jl +++ b/src/systems/diffeqs/sdesystem.jl @@ -274,18 +274,46 @@ function SDESystem(sys::ODESystem, neqs; kwargs...) end function SDESystem(eqs::Vector{Equation}, noiseeqs::AbstractArray, iv; kwargs...) - param_deps = get(kwargs, :parameter_dependencies, Equation[]) - eqs, dvs, ps = process_equations_DESystem(eqs, param_deps, iv) + diffvars, allunknowns, ps, eqs = process_equations(eqs, iv) + + for eq in get(kwargs, :parameter_dependencies, Equation[]) + collect_vars!(allunknowns, ps, eq, iv) + end + + for ssys in get(kwargs, :systems, ODESystem[]) + collect_scoped_vars!(allunknowns, ps, ssys, iv) + end + + for v in allunknowns + isdelay(v, iv) || continue + collect_vars!(allunknowns, ps, arguments(v)[1], iv) + end + + new_ps = OrderedSet() + for p in ps + if iscall(p) && operation(p) === getindex + par = arguments(p)[begin] + if Symbolics.shape(Symbolics.unwrap(par)) !== Symbolics.Unknown() && + all(par[i] in ps for i in eachindex(par)) + push!(new_ps, par) + else + push!(new_ps, p) + end + else + push!(new_ps, p) + end + end # validate noise equations noisedvs = OrderedSet() noiseps = OrderedSet() collect_vars!(noisedvs, noiseps, noiseeqs, iv) for dv in noisedvs - var ∈ Set(dvs) || throw(ArgumentError("Variable $dv in noise equations is not an unknown of the system.")) + var ∈ allunknowns || throw(ArgumentError("Variable $dv in noise equations is not an unknown of the system.")) end + algevars = setdiff(allunknowns, diffvars) - return SDESystem(eqs, noiseeqs, iv, dvs, [ps; collect(noiseps)]; kwargs...) + return SDESystem(eqs, noiseeqs, iv, Iterators.flatten((diffvars, algevars)), [ps; collect(noiseps)]; kwargs...) end SDESystem(eq::Equation, noiseeqs::AbstractArray, args...; kwargs...) = SDESystem([eq], noiseeqs, args...; kwargs...) diff --git a/src/utils.jl b/src/utils.jl index a71e9ba799..7bb1ebcbe9 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -1189,9 +1189,9 @@ end """ $(TYPEDSIGNATURES) -Find all the unknowns and parameters from the equations and parameter dependencies of an ODESystem or SDESystem. Return re-ordered equations, unknowns, and parameters. +Find all the unknowns and parameters from the equations of a SDESystem or ODESystem. Return re-ordered equations, differential variables, all variables, and parameters. """ -function process_equations_DESystem(eqs, param_deps, iv) +function process_equations(eqs, iv) eqs = collect(eqs) diffvars = OrderedSet() @@ -1233,25 +1233,6 @@ function process_equations_DESystem(eqs, param_deps, iv) push!(algeeq, eq) end end - for eq in param_deps - collect_vars!(allunknowns, ps, eq, iv) - end - - new_ps = OrderedSet() - for p in ps - if iscall(p) && operation(p) === getindex - par = arguments(p)[begin] - if Symbolics.shape(Symbolics.unwrap(par)) !== Symbolics.Unknown() && - all(par[i] in ps for i in eachindex(par)) - push!(new_ps, par) - else - push!(new_ps, p) - end - else - push!(new_ps, p) - end - end - algevars = setdiff(allunknowns, diffvars) - Equation[diffeq; algeeq; compressed_eqs], collect(Iterators.flatten((diffvars, algevars))), collect(new_ps) + diffvars, allunknowns, ps, Equation[diffeq; algeeq; compressed_eqs] end From 84759d821283c3ff51c34600350a03605bdbf7d9 Mon Sep 17 00:00:00 2001 From: vyudu Date: Wed, 29 Jan 2025 14:06:16 -0500 Subject: [PATCH 0691/1337] Fix typo --- src/systems/diffeqs/sdesystem.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/systems/diffeqs/sdesystem.jl b/src/systems/diffeqs/sdesystem.jl index 0d0a2ec0c7..f82bc5cadc 100644 --- a/src/systems/diffeqs/sdesystem.jl +++ b/src/systems/diffeqs/sdesystem.jl @@ -309,7 +309,7 @@ function SDESystem(eqs::Vector{Equation}, noiseeqs::AbstractArray, iv; kwargs... noiseps = OrderedSet() collect_vars!(noisedvs, noiseps, noiseeqs, iv) for dv in noisedvs - var ∈ allunknowns || throw(ArgumentError("Variable $dv in noise equations is not an unknown of the system.")) + dv ∈ allunknowns || throw(ArgumentError("Variable $dv in noise equations is not an unknown of the system.")) end algevars = setdiff(allunknowns, diffvars) From 37092f12c1fb6958b4d1ab6a917be99221371e61 Mon Sep 17 00:00:00 2001 From: vyudu Date: Wed, 29 Jan 2025 14:32:17 -0500 Subject: [PATCH 0692/1337] lower tol --- test/bvproblem.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/bvproblem.jl b/test/bvproblem.jl index c6c124d09e..30fde44531 100644 --- a/test/bvproblem.jl +++ b/test/bvproblem.jl @@ -1,6 +1,6 @@ ### TODO: update when BoundaryValueDiffEqAscher is updated to use the normal boundary condition conventions -using BoundaryValueDiffEq, OrdinaryDiffEq, BoundaryValueDiffEqAscher +using BoundaryValueDiffEq, OrdinaryDiffEqDefault, BoundaryValueDiffEqAscher using BenchmarkTools using ModelingToolkit using SciMLBase @@ -166,7 +166,7 @@ let @test sol1 ≈ sol2 ≈ sol3 end -function test_solvers(solvers, prob, u0map, constraints, equations = []; dt = 0.05, atol = 1e-3) +function test_solvers(solvers, prob, u0map, constraints, equations = []; dt = 0.05, atol = 1e-2) for solver in solvers println("Solver: $solver") sol = @btime solve($prob, $solver(), dt = $dt, abstol = $atol) @@ -214,7 +214,7 @@ let bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(lksys, u0map, tspan; guesses) test_solvers(solvers, bvp, u0map, constr; dt = 0.05) - # Testing that more complicated constr give correct solutions. + # Testing that more complicated constraints give correct solutions. constr = [y(.2) + x(.8) ~ 3., y(.3) ~ 2.] @mtkbuild lksys = ODESystem(eqs, t; constraints = constr) bvp = SciMLBase.BVProblem{false, SciMLBase.FullSpecialize}(lksys, u0map, tspan; guesses) From 4142923e7e7e38e90c65952265c02e93fc081472 Mon Sep 17 00:00:00 2001 From: vyudu Date: Wed, 29 Jan 2025 15:04:39 -0500 Subject: [PATCH 0693/1337] up --- .../implicit_discrete_system.jl | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/src/systems/discrete_system/implicit_discrete_system.jl b/src/systems/discrete_system/implicit_discrete_system.jl index 404fa7ff49..56e7545c6a 100644 --- a/src/systems/discrete_system/implicit_discrete_system.jl +++ b/src/systems/discrete_system/implicit_discrete_system.jl @@ -23,7 +23,7 @@ struct ImplicitDiscreteSystem <: AbstractTimeDependentSystem structurally identical. """ tag::UInt - """The differential equations defining the discrete system.""" + """The difference equations defining the discrete system.""" eqs::Vector{Equation} """Independent variable.""" iv::BasicSymbolic{Real} @@ -48,10 +48,10 @@ struct ImplicitDiscreteSystem <: AbstractTimeDependentSystem """ The internal systems. These are required to have unique names. """ - systems::Vector{DiscreteSystem} + systems::Vector{ImplicitDiscreteSystem} """ The default values to use when initial conditions and/or - parameters are not supplied in `DiscreteProblem`. + parameters are not supplied in `ImplicitDiscreteProblem`. """ defaults::Dict """ @@ -136,11 +136,11 @@ end """ $(TYPEDSIGNATURES) -Constructs a DiscreteSystem. +Constructs a ImplicitDiscreteSystem. """ function ImplicitDiscreteSystem(eqs::AbstractVector{<:Equation}, iv, dvs, ps; observed = Num[], - systems = DiscreteSystem[], + systems = ImplicitDiscreteSystem[], tspan = nothing, name = nothing, description = "", @@ -162,12 +162,12 @@ function ImplicitDiscreteSystem(eqs::AbstractVector{<:Equation}, iv, dvs, ps; dvs′ = value.(dvs) ps′ = value.(ps) if any(hasderiv, eqs) || any(hashold, eqs) || any(hassample, eqs) || any(hasdiff, eqs) - error("Equations in a `DiscreteSystem` can only have `Shift` operators.") + error("Equations in a `ImplicitDiscreteSystem` can only have `Shift` operators.") end if !(isempty(default_u0) && isempty(default_p)) Base.depwarn( "`default_u0` and `default_p` are deprecated. Use `defaults` instead.", - :DiscreteSystem, force = true) + :ImplicitDiscreteSystem, force = true) end defaults = Dict{Any, Any}(todict(defaults)) @@ -190,7 +190,7 @@ function ImplicitDiscreteSystem(eqs::AbstractVector{<:Equation}, iv, dvs, ps; if length(unique(sysnames)) != length(sysnames) throw(ArgumentError("System names must be unique.")) end - DiscreteSystem(Threads.atomic_add!(SYSTEM_COUNT, UInt(1)), + ImplicitDiscreteSystem(Threads.atomic_add!(SYSTEM_COUNT, UInt(1)), eqs, iv′, dvs′, ps′, tspan, var_to_name, observed, name, description, systems, defaults, guesses, initializesystem, initialization_eqs, preface, connector_type, parameter_dependencies, metadata, gui_metadata, kwargs...) @@ -206,7 +206,7 @@ function ImplicitDiscreteSystem(eqs, iv; kwargs...) collect_vars!(allunknowns, ps, eq, iv; op = Shift) if iscall(eq.lhs) && operation(eq.lhs) isa Shift isequal(iv, operation(eq.lhs).t) || - throw(ArgumentError("A DiscreteSystem can only have one independent variable.")) + throw(ArgumentError("An ImplicitDiscreteSystem can only have one independent variable.")) eq.lhs in diffvars && throw(ArgumentError("The shift variable $(eq.lhs) is not unique in the system of equations.")) push!(diffvars, eq.lhs) @@ -233,16 +233,16 @@ function ImplicitDiscreteSystem(eqs, iv; kwargs...) push!(new_ps, p) end end - return DiscreteSystem(eqs, iv, + return ImplicitDiscreteSystem(eqs, iv, collect(allunknowns), collect(new_ps); kwargs...) end -function flatten(sys::DiscreteSystem, noeqs = false) +function flatten(sys::ImplicitDiscreteSystem, noeqs = false) systems = get_systems(sys) if isempty(systems) return sys else - return DiscreteSystem(noeqs ? Equation[] : equations(sys), + return ImplicitDiscreteSystem(noeqs ? Equation[] : equations(sys), get_iv(sys), unknowns(sys), parameters(sys), @@ -258,14 +258,14 @@ function flatten(sys::DiscreteSystem, noeqs = false) end function generate_function( - sys::DiscreteSystem, dvs = unknowns(sys), ps = parameters(sys); wrap_code = identity, kwargs...) + sys::ImplicitDiscreteSystem, dvs = unknowns(sys), ps = parameters(sys); wrap_code = identity, kwargs...) exprs = [eq.rhs for eq in equations(sys)] wrap_code = wrap_code .∘ wrap_array_vars(sys, exprs) .∘ wrap_parameter_dependencies(sys, false) generate_custom_function(sys, exprs, dvs, ps; wrap_code, kwargs...) end -function shift_u0map_forward(sys::DiscreteSystem, u0map, defs) +function shift_u0map_forward(sys::ImplicitDiscreteSystem, u0map, defs) iv = get_iv(sys) updated = AnyDict() for k in collect(keys(u0map)) @@ -291,7 +291,7 @@ end Generates an ImplicitDiscreteProblem from an ImplicitDiscreteSystem. """ function SciMLBase.ImplicitDiscreteProblem( - sys::DiscreteSystem, u0map = [], tspan = get_tspan(sys), + sys::ImplicitDiscreteSystem, u0map = [], tspan = get_tspan(sys), parammap = SciMLBase.NullParameters(); eval_module = @__MODULE__, eval_expression = false, @@ -309,7 +309,7 @@ function SciMLBase.ImplicitDiscreteProblem( u0map = to_varmap(u0map, dvs) u0map = shift_u0map_forward(sys, u0map, defaults(sys)) f, u0, p = process_SciMLProblem( - DiscreteFunction, sys, u0map, parammap; eval_expression, eval_module) + ImplicitDiscreteFunction, sys, u0map, parammap; eval_expression, eval_module) u0 = f(u0, p, tspan[1]) ImplicitDiscreteProblem(f, u0, tspan, p; kwargs...) end @@ -348,7 +348,7 @@ function SciMLBase.ImplicitDiscreteFunction{iip, specialize}( if specialize === SciMLBase.FunctionWrapperSpecialize && iip if u0 === nothing || p === nothing || t === nothing - error("u0, p, and t must be specified for FunctionWrapperSpecialize on DiscreteFunction.") + error("u0, p, and t must be specified for FunctionWrapperSpecialize on ImplicitDiscreteFunction.") end f = SciMLBase.wrapfun_iip(f, (u0, u0, p, t)) end @@ -395,7 +395,7 @@ function ImplicitDiscreteFunctionExpr{iip}(sys::ImplicitDiscreteSystem, dvs = un ex = quote $_f - DiscreteFunction{$iip}($fsym) + ImplicitDiscreteFunction{$iip}($fsym) end !linenumbers ? Base.remove_linenums!(ex) : ex end From e5eb8bd35a89ab8b0d5346c218ea0bdf6cb15f37 Mon Sep 17 00:00:00 2001 From: vyudu Date: Wed, 29 Jan 2025 15:05:35 -0500 Subject: [PATCH 0694/1337] fix Project.toml --- Project.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/Project.toml b/Project.toml index 69ad24c8df..865b88658b 100644 --- a/Project.toml +++ b/Project.toml @@ -143,7 +143,6 @@ SparseArrays = "1" SpecialFunctions = "0.7, 0.8, 0.9, 0.10, 1.0, 2" StaticArrays = "0.10, 0.11, 0.12, 1.0" StochasticDelayDiffEq = "1.8.1" -StochasticDiffEq = "6.72.1" SymbolicIndexingInterface = "0.3.36" SymbolicUtils = "3.10" Symbolics = "6.22.1" From afc468996d65bff484d66327149f2f8ccabfa83e Mon Sep 17 00:00:00 2001 From: vyudu Date: Thu, 30 Jan 2025 11:03:18 -0500 Subject: [PATCH 0695/1337] add test file --- src/ModelingToolkit.jl | 2 + .../discrete_system/discrete_system.jl | 1 + .../implicit_discrete_system.jl | 48 +++++++++++++------ src/systems/systemstructure.jl | 4 +- test/implicit_discrete_system.jl | 2 + test/runtests.jl | 3 +- 6 files changed, 44 insertions(+), 16 deletions(-) create mode 100644 test/implicit_discrete_system.jl diff --git a/src/ModelingToolkit.jl b/src/ModelingToolkit.jl index 2710d7d1e4..4083f2609d 100644 --- a/src/ModelingToolkit.jl +++ b/src/ModelingToolkit.jl @@ -164,6 +164,7 @@ 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") @@ -229,6 +230,7 @@ export DAEFunctionExpr, DAEProblemExpr export SDESystem, SDEFunction, SDEFunctionExpr, SDEProblemExpr export SystemStructure export DiscreteSystem, DiscreteProblem, DiscreteFunction, DiscreteFunctionExpr +export ImplicitDiscreteSystem, ImplicitDiscreteProblem, ImplicitDiscreteFunction, ImplicitDiscreteFunctionExpr export JumpSystem export ODEProblem, SDEProblem export NonlinearFunction, NonlinearFunctionExpr diff --git a/src/systems/discrete_system/discrete_system.jl b/src/systems/discrete_system/discrete_system.jl index bd5c72eec7..0ad3317ea1 100644 --- a/src/systems/discrete_system/discrete_system.jl +++ b/src/systems/discrete_system/discrete_system.jl @@ -233,6 +233,7 @@ function DiscreteSystem(eqs, iv; kwargs...) push!(new_ps, p) end end + @show allunknowns return DiscreteSystem(eqs, iv, collect(allunknowns), collect(new_ps); kwargs...) end diff --git a/src/systems/discrete_system/implicit_discrete_system.jl b/src/systems/discrete_system/implicit_discrete_system.jl index 56e7545c6a..1e1b5d5f6f 100644 --- a/src/systems/discrete_system/implicit_discrete_system.jl +++ b/src/systems/discrete_system/implicit_discrete_system.jl @@ -10,11 +10,10 @@ using ModelingToolkit: t_nounits as t @parameters σ=28.0 ρ=10.0 β=8/3 δt=0.1 @variables x(t)=1.0 y(t)=0.0 z(t)=0.0 k = ShiftIndex(t) -eqs = [x(k+1) ~ σ*(y-x), - y(k+1) ~ x*(ρ-z)-y, - z(k+1) ~ x*y - β*z] -@named de = ImplicitDiscreteSystem(eqs,t,[x,y,z],[σ,ρ,β]; tspan = (0, 1000.0)) # or -@named de = ImplicitDiscreteSystem(eqs) +eqs = [x ~ σ*(y(k-1)-x), + y ~ x*(ρ-z(k-1))-y, + z ~ x(k-1)*y - β*z] +@named ide = ImplicitDiscreteSystem(eqs,t,[x,y,z],[σ,ρ,β]; tspan = (0, 1000.0)) ``` """ struct ImplicitDiscreteSystem <: AbstractTimeDependentSystem @@ -136,6 +135,7 @@ end """ $(TYPEDSIGNATURES) + Constructs a ImplicitDiscreteSystem. """ function ImplicitDiscreteSystem(eqs::AbstractVector{<:Equation}, iv, dvs, ps; @@ -170,6 +170,8 @@ function ImplicitDiscreteSystem(eqs::AbstractVector{<:Equation}, iv, dvs, ps; :ImplicitDiscreteSystem, force = true) end + # Copy equations to canonical form, but do not touch array expressions + eqs = [wrap(eq.lhs) isa Symbolics.Arr ? eq : 0 ~ eq.rhs - eq.lhs for eq in eqs] defaults = Dict{Any, Any}(todict(defaults)) guesses = Dict{Any, Any}(todict(guesses)) var_to_name = Dict() @@ -236,6 +238,8 @@ function ImplicitDiscreteSystem(eqs, iv; kwargs...) return ImplicitDiscreteSystem(eqs, iv, collect(allunknowns), collect(new_ps); kwargs...) end +# basically at every timestep it should build a nonlinear solve +# Previous timesteps should be treated as parameters? is this right? function flatten(sys::ImplicitDiscreteSystem, noeqs = false) systems = get_systems(sys) @@ -259,10 +263,25 @@ end function generate_function( sys::ImplicitDiscreteSystem, dvs = unknowns(sys), ps = parameters(sys); wrap_code = identity, kwargs...) - exprs = [eq.rhs for eq in equations(sys)] - wrap_code = wrap_code .∘ wrap_array_vars(sys, exprs) .∘ - wrap_parameter_dependencies(sys, false) - generate_custom_function(sys, exprs, dvs, ps; wrap_code, kwargs...) + if !iscomplete(sys) + error("A completed system is required. Call `complete` or `structural_simplify` on the system.") + end + p = (reorder_parameters(sys, unwrap.(ps))..., cachesyms...) + isscalar = !(exprs isa AbstractArray) + pre, sol_states = get_substitutions_and_solved_unknowns(sys, isscalar ? [exprs] : exprs) + if postprocess_fbody === nothing + postprocess_fbody = pre + end + if states === nothing + states = sol_states + end + exprs = [eq.lhs - eq.rhs for eq in equations(sys)] + u = map(Shift(iv, -1), dvs) + u_next = dvs + + wrap_code = wrap_code .∘ wrap_array_vars(sys, exprs) .∘ wrap_parameter_dependencies(sys, false) + + build_function(exprs, u_next, u, p..., get_iv(sys)) end function shift_u0map_forward(sys::ImplicitDiscreteSystem, u0map, defs) @@ -311,7 +330,7 @@ function SciMLBase.ImplicitDiscreteProblem( f, u0, p = process_SciMLProblem( ImplicitDiscreteFunction, sys, u0map, parammap; eval_expression, eval_module) u0 = f(u0, p, tspan[1]) - ImplicitDiscreteProblem(f, u0, tspan, p; kwargs...) + NonlinearProblem(f, u0, tspan, p; kwargs...) end function SciMLBase.ImplicitDiscreteFunction(sys::ImplicitDiscreteSystem, args...; kwargs...) @@ -337,14 +356,15 @@ function SciMLBase.ImplicitDiscreteFunction{iip, specialize}( eval_module = @__MODULE__, analytic = nothing, kwargs...) where {iip, specialize} + if !iscomplete(sys) error("A completed `ImplicitDiscreteSystem` is required. Call `complete` or `structural_simplify` on the system before creating a `ImplicitDiscreteProblem`") end f_gen = generate_function(sys, dvs, ps; expression = Val{true}, expression_module = eval_module, kwargs...) f_oop, f_iip = eval_or_rgf.(f_gen; eval_expression, eval_module) - f(u, p, t) = f_oop(u, p, t) - f(du, u, p, t) = f_iip(du, u, p, t) + 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 specialize === SciMLBase.FunctionWrapperSpecialize && iip if u0 === nothing || p === nothing || t === nothing @@ -379,8 +399,8 @@ struct ImplicitDiscreteFunctionClosure{O, I} <: Function f_oop::O f_iip::I end -(f::ImplicitDiscreteFunctionClosure)(u, p, t) = f.f_oop(u, p, t) -(f::ImplicitDiscreteFunctionClosure)(du, u, p, t) = f.f_iip(du, u, p, t) +(f::ImplicitDiscreteFunctionClosure)(u_next, u, p, t) = f.f_oop(u_next, u, p, t) +(f::ImplicitDiscreteFunctionClosure)(resid, u_next, u, p, t) = f.f_iip(resid, u_next, u, p, t) function ImplicitDiscreteFunctionExpr{iip}(sys::ImplicitDiscreteSystem, dvs = unknowns(sys), ps = parameters(sys), u0 = nothing; diff --git a/src/systems/systemstructure.jl b/src/systems/systemstructure.jl index 1bdc11f06a..215d843c2e 100644 --- a/src/systems/systemstructure.jl +++ b/src/systems/systemstructure.jl @@ -432,7 +432,7 @@ function TearingState(sys; quick_cancel = false, check = true) SystemStructure(complete(var_to_diff), complete(eq_to_diff), complete(graph), nothing, var_types, sys isa DiscreteSystem), Any[]) - if sys isa DiscreteSystem + if sys isa DiscreteSystem || sys isa ImplicitDiscreteSystem ts = shift_discrete_system(ts) end return ts @@ -456,6 +456,8 @@ function lower_order_var(dervar, t) diffvar end +""" +""" function shift_discrete_system(ts::TearingState) @unpack fullvars, sys = ts discvars = OrderedSet() diff --git a/test/implicit_discrete_system.jl b/test/implicit_discrete_system.jl new file mode 100644 index 0000000000..adfb9d2fc1 --- /dev/null +++ b/test/implicit_discrete_system.jl @@ -0,0 +1,2 @@ + +#init diff --git a/test/runtests.jl b/test/runtests.jl index 52875fdae5..bd5eecacd0 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -79,7 +79,8 @@ end @safetestset "Variable Utils Test" include("variable_utils.jl") @safetestset "Variable Metadata Test" include("test_variable_metadata.jl") @safetestset "OptimizationSystem Test" include("optimizationsystem.jl") - @safetestset "Discrete System" include("discrete_system.jl") + @safetestset "DiscreteSystem Test" include("discrete_system.jl") + @safetestset "ImplicitDiscreteSystem Test" include("implicit_discrete_system.jl") @safetestset "SteadyStateSystem Test" include("steadystatesystems.jl") @safetestset "SDESystem Test" include("sdesystem.jl") @safetestset "DDESystem Test" include("dde.jl") From 397298fea4c78b4ccfbfc6f8d1c61fde5aabe16d Mon Sep 17 00:00:00 2001 From: vyudu Date: Thu, 30 Jan 2025 11:34:02 -0500 Subject: [PATCH 0696/1337] working on structural simplification --- src/ModelingToolkit.jl | 1 + src/systems/discrete_system/discrete_system.jl | 2 +- src/systems/discrete_system/implicit_discrete_system.jl | 8 ++++---- src/systems/systemstructure.jl | 4 ++-- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/ModelingToolkit.jl b/src/ModelingToolkit.jl index 4083f2609d..2dbef8f272 100644 --- a/src/ModelingToolkit.jl +++ b/src/ModelingToolkit.jl @@ -123,6 +123,7 @@ abstract type AbstractTimeIndependentSystem <: AbstractSystem end abstract type AbstractODESystem <: AbstractTimeDependentSystem end abstract type AbstractMultivariateSystem <: AbstractSystem end abstract type AbstractOptimizationSystem <: AbstractTimeIndependentSystem end +abstract type AbstractDiscreteSystem <: AbstractTimeDependentSystem end function independent_variable end diff --git a/src/systems/discrete_system/discrete_system.jl b/src/systems/discrete_system/discrete_system.jl index 0ad3317ea1..f2d271dfe4 100644 --- a/src/systems/discrete_system/discrete_system.jl +++ b/src/systems/discrete_system/discrete_system.jl @@ -17,7 +17,7 @@ eqs = [x(k+1) ~ σ*(y-x), @named de = DiscreteSystem(eqs) ``` """ -struct DiscreteSystem <: AbstractTimeDependentSystem +struct DiscreteSystem <: AbstractDiscreteSystem """ A tag for the system. If two systems have the same tag, then they are structurally identical. diff --git a/src/systems/discrete_system/implicit_discrete_system.jl b/src/systems/discrete_system/implicit_discrete_system.jl index 1e1b5d5f6f..3a137eb8c1 100644 --- a/src/systems/discrete_system/implicit_discrete_system.jl +++ b/src/systems/discrete_system/implicit_discrete_system.jl @@ -10,13 +10,13 @@ using ModelingToolkit: t_nounits as t @parameters σ=28.0 ρ=10.0 β=8/3 δt=0.1 @variables x(t)=1.0 y(t)=0.0 z(t)=0.0 k = ShiftIndex(t) -eqs = [x ~ σ*(y(k-1)-x), - y ~ x*(ρ-z(k-1))-y, - z ~ x(k-1)*y - β*z] +eqs = [x ~ σ*(y-x), + y ~ x*(ρ-z)-y, + z ~ x*y - β*z] @named ide = ImplicitDiscreteSystem(eqs,t,[x,y,z],[σ,ρ,β]; tspan = (0, 1000.0)) ``` """ -struct ImplicitDiscreteSystem <: AbstractTimeDependentSystem +struct ImplicitDiscreteSystem <: AbstractDiscreteSystem """ A tag for the system. If two systems have the same tag, then they are structurally identical. diff --git a/src/systems/systemstructure.jl b/src/systems/systemstructure.jl index 215d843c2e..86d1702041 100644 --- a/src/systems/systemstructure.jl +++ b/src/systems/systemstructure.jl @@ -430,9 +430,9 @@ function TearingState(sys; quick_cancel = false, check = true) ts = TearingState(sys, fullvars, SystemStructure(complete(var_to_diff), complete(eq_to_diff), - complete(graph), nothing, var_types, sys isa DiscreteSystem), + complete(graph), nothing, var_types, sys isa AbstractDiscreteSystem), Any[]) - if sys isa DiscreteSystem || sys isa ImplicitDiscreteSystem + if sys isa AbstractDiscreteSystem ts = shift_discrete_system(ts) end return ts From 2ae79ae19941d5502a7a83d13770284cad7b2cc3 Mon Sep 17 00:00:00 2001 From: vyudu Date: Thu, 30 Jan 2025 11:34:36 -0500 Subject: [PATCH 0697/1337] revert to OrdinaryDiffEq --- test/bvproblem.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/bvproblem.jl b/test/bvproblem.jl index 30fde44531..069372aef7 100644 --- a/test/bvproblem.jl +++ b/test/bvproblem.jl @@ -1,6 +1,6 @@ ### TODO: update when BoundaryValueDiffEqAscher is updated to use the normal boundary condition conventions -using BoundaryValueDiffEq, OrdinaryDiffEqDefault, BoundaryValueDiffEqAscher +using BoundaryValueDiffEq, OrdinaryDiffEq, BoundaryValueDiffEqAscher using BenchmarkTools using ModelingToolkit using SciMLBase From be1b270298f11b23306cf4b5a9ea677a0ba26894 Mon Sep 17 00:00:00 2001 From: vyudu Date: Thu, 30 Jan 2025 14:26:04 -0500 Subject: [PATCH 0698/1337] change variable renaming --- src/structural_transformation/utils.jl | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/structural_transformation/utils.jl b/src/structural_transformation/utils.jl index bd24a1d017..29c0f66756 100644 --- a/src/structural_transformation/utils.jl +++ b/src/structural_transformation/utils.jl @@ -452,8 +452,15 @@ end function lower_varname_withshift(var, iv, order) order == 0 && return var if ModelingToolkit.isoperator(var, ModelingToolkit.Shift) - op = operation(var) - return Shift(op.t, order)(var) + O = only(arguments(var)) + oldop = operation(O) + ds = "$iv-$order" + d_separator = 'ˍ' + newname = Symbol(string(nameof(oldop)), d_separator, ds) + + newvar = maketerm(typeof(O), Symbolics.rename(oldop, newname), Symbolics.children(O), Symbolics.metadata(O)) + setmetadata(newvar, Symbolics.VariableSource, (:variables, newname)) + return ModelingToolkit._with_unit(identity, newvar, iv) end return lower_varname_with_unit(var, iv, order) end From cc23c888984a3e811d8be32f7f229b214dd7c7b0 Mon Sep 17 00:00:00 2001 From: vyudu Date: Thu, 30 Jan 2025 14:29:14 -0500 Subject: [PATCH 0699/1337] up --- src/systems/discrete_system/discrete_system.jl | 3 +-- src/systems/systems.jl | 14 +++++++------- src/systems/systemstructure.jl | 6 ++---- 3 files changed, 10 insertions(+), 13 deletions(-) diff --git a/src/systems/discrete_system/discrete_system.jl b/src/systems/discrete_system/discrete_system.jl index f2d271dfe4..bd5c72eec7 100644 --- a/src/systems/discrete_system/discrete_system.jl +++ b/src/systems/discrete_system/discrete_system.jl @@ -17,7 +17,7 @@ eqs = [x(k+1) ~ σ*(y-x), @named de = DiscreteSystem(eqs) ``` """ -struct DiscreteSystem <: AbstractDiscreteSystem +struct DiscreteSystem <: AbstractTimeDependentSystem """ A tag for the system. If two systems have the same tag, then they are structurally identical. @@ -233,7 +233,6 @@ function DiscreteSystem(eqs, iv; kwargs...) push!(new_ps, p) end end - @show allunknowns return DiscreteSystem(eqs, iv, collect(allunknowns), collect(new_ps); kwargs...) end diff --git a/src/systems/systems.jl b/src/systems/systems.jl index 5ff28eba26..9c8c272c5c 100644 --- a/src/systems/systems.jl +++ b/src/systems/systems.jl @@ -39,13 +39,13 @@ function structural_simplify( else newsys = newsys′ end - # if newsys isa DiscreteSystem && - # any(eq -> symbolic_type(eq.lhs) == NotSymbolic(), equations(newsys)) - # error(""" - # Encountered algebraic equations when simplifying discrete system. This is \ - # not yet supported. - # """) - # end + if newsys isa DiscreteSystem && + any(eq -> symbolic_type(eq.lhs) == NotSymbolic(), equations(newsys)) + error(""" + Encountered algebraic equations when simplifying discrete system. This is \ + not yet supported. + """) + end for pass in additional_passes newsys = pass(newsys) end diff --git a/src/systems/systemstructure.jl b/src/systems/systemstructure.jl index 86d1702041..1bdc11f06a 100644 --- a/src/systems/systemstructure.jl +++ b/src/systems/systemstructure.jl @@ -430,9 +430,9 @@ function TearingState(sys; quick_cancel = false, check = true) ts = TearingState(sys, fullvars, SystemStructure(complete(var_to_diff), complete(eq_to_diff), - complete(graph), nothing, var_types, sys isa AbstractDiscreteSystem), + complete(graph), nothing, var_types, sys isa DiscreteSystem), Any[]) - if sys isa AbstractDiscreteSystem + if sys isa DiscreteSystem ts = shift_discrete_system(ts) end return ts @@ -456,8 +456,6 @@ function lower_order_var(dervar, t) diffvar end -""" -""" function shift_discrete_system(ts::TearingState) @unpack fullvars, sys = ts discvars = OrderedSet() From f5e5aff51476b66e8fc152ed9b8c8fb4307e8f4c Mon Sep 17 00:00:00 2001 From: vyudu Date: Thu, 30 Jan 2025 14:30:08 -0500 Subject: [PATCH 0700/1337] revert all --- src/ModelingToolkit.jl | 3 - .../implicit_discrete_system.jl | 426 ------------------ test/implicit_discrete_system.jl | 2 - test/runtests.jl | 3 +- 4 files changed, 1 insertion(+), 433 deletions(-) delete mode 100644 src/systems/discrete_system/implicit_discrete_system.jl delete mode 100644 test/implicit_discrete_system.jl diff --git a/src/ModelingToolkit.jl b/src/ModelingToolkit.jl index 2dbef8f272..2710d7d1e4 100644 --- a/src/ModelingToolkit.jl +++ b/src/ModelingToolkit.jl @@ -123,7 +123,6 @@ abstract type AbstractTimeIndependentSystem <: AbstractSystem end abstract type AbstractODESystem <: AbstractTimeDependentSystem end abstract type AbstractMultivariateSystem <: AbstractSystem end abstract type AbstractOptimizationSystem <: AbstractTimeIndependentSystem end -abstract type AbstractDiscreteSystem <: AbstractTimeDependentSystem end function independent_variable end @@ -165,7 +164,6 @@ 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") @@ -231,7 +229,6 @@ export DAEFunctionExpr, DAEProblemExpr export SDESystem, SDEFunction, SDEFunctionExpr, SDEProblemExpr export SystemStructure export DiscreteSystem, DiscreteProblem, DiscreteFunction, DiscreteFunctionExpr -export ImplicitDiscreteSystem, ImplicitDiscreteProblem, ImplicitDiscreteFunction, ImplicitDiscreteFunctionExpr export JumpSystem export ODEProblem, SDEProblem export NonlinearFunction, NonlinearFunctionExpr diff --git a/src/systems/discrete_system/implicit_discrete_system.jl b/src/systems/discrete_system/implicit_discrete_system.jl deleted file mode 100644 index 3a137eb8c1..0000000000 --- a/src/systems/discrete_system/implicit_discrete_system.jl +++ /dev/null @@ -1,426 +0,0 @@ -""" -$(TYPEDEF) -An implicit system of difference equations. -# Fields -$(FIELDS) -# Example -``` -using ModelingToolkit -using ModelingToolkit: t_nounits as t -@parameters σ=28.0 ρ=10.0 β=8/3 δt=0.1 -@variables x(t)=1.0 y(t)=0.0 z(t)=0.0 -k = ShiftIndex(t) -eqs = [x ~ σ*(y-x), - y ~ x*(ρ-z)-y, - z ~ x*y - β*z] -@named ide = ImplicitDiscreteSystem(eqs,t,[x,y,z],[σ,ρ,β]; tspan = (0, 1000.0)) -``` -""" -struct ImplicitDiscreteSystem <: AbstractDiscreteSystem - """ - A tag for the system. If two systems have the same tag, then they are - structurally identical. - """ - tag::UInt - """The difference equations defining the discrete system.""" - eqs::Vector{Equation} - """Independent variable.""" - iv::BasicSymbolic{Real} - """Dependent (state) variables. Must not contain the independent variable.""" - unknowns::Vector - """Parameter variables. Must not contain the independent variable.""" - ps::Vector - """Time span.""" - tspan::Union{NTuple{2, Any}, Nothing} - """Array variables.""" - var_to_name::Any - """Observed states.""" - observed::Vector{Equation} - """ - The name of the system - """ - name::Symbol - """ - A description of the system. - """ - description::String - """ - The internal systems. These are required to have unique names. - """ - systems::Vector{ImplicitDiscreteSystem} - """ - The default values to use when initial conditions and/or - parameters are not supplied in `ImplicitDiscreteProblem`. - """ - defaults::Dict - """ - The guesses to use as the initial conditions for the - initialization system. - """ - guesses::Dict - """ - The system for performing the initialization. - """ - initializesystem::Union{Nothing, NonlinearSystem} - """ - Extra equations to be enforced during the initialization sequence. - """ - initialization_eqs::Vector{Equation} - """ - Inject assignment statements before the evaluation of the RHS function. - """ - preface::Any - """ - Type of the system. - """ - connector_type::Any - """ - Topologically sorted parameter dependency equations, where all symbols are parameters and - the LHS is a single parameter. - """ - parameter_dependencies::Vector{Equation} - """ - Metadata for the system, to be used by downstream packages. - """ - metadata::Any - """ - Metadata for MTK GUI. - """ - gui_metadata::Union{Nothing, GUIMetadata} - """ - Cache for intermediate tearing state. - """ - tearing_state::Any - """ - Substitutions generated by tearing. - """ - substitutions::Any - """ - If a model `sys` is complete, then `sys.x` no longer performs namespacing. - """ - complete::Bool - """ - Cached data for fast symbolic indexing. - """ - index_cache::Union{Nothing, IndexCache} - """ - The hierarchical parent system before simplification. - """ - parent::Any - isscheduled::Bool - - function ImplicitDiscreteSystem(tag, discreteEqs, iv, dvs, ps, tspan, var_to_name, - observed, name, description, systems, defaults, guesses, initializesystem, - initialization_eqs, preface, connector_type, parameter_dependencies = Equation[], - metadata = nothing, gui_metadata = nothing, - tearing_state = nothing, substitutions = nothing, - complete = false, index_cache = nothing, parent = nothing, - isscheduled = false; - checks::Union{Bool, Int} = true) - if checks == true || (checks & CheckComponents) > 0 - check_independent_variables([iv]) - check_variables(dvs, iv) - check_parameters(ps, iv) - end - if checks == true || (checks & CheckUnits) > 0 - u = __get_unit_type(dvs, ps, iv) - check_units(u, discreteEqs) - end - new(tag, discreteEqs, iv, dvs, ps, tspan, var_to_name, observed, name, description, - systems, defaults, guesses, initializesystem, initialization_eqs, - preface, connector_type, parameter_dependencies, metadata, gui_metadata, - tearing_state, substitutions, complete, index_cache, parent, isscheduled) - end -end - -""" - $(TYPEDSIGNATURES) - -Constructs a ImplicitDiscreteSystem. -""" -function ImplicitDiscreteSystem(eqs::AbstractVector{<:Equation}, iv, dvs, ps; - observed = Num[], - systems = ImplicitDiscreteSystem[], - tspan = nothing, - name = nothing, - description = "", - default_u0 = Dict(), - default_p = Dict(), - guesses = Dict(), - initializesystem = nothing, - initialization_eqs = Equation[], - defaults = _merge(Dict(default_u0), Dict(default_p)), - preface = nothing, - connector_type = nothing, - parameter_dependencies = Equation[], - metadata = nothing, - gui_metadata = nothing, - kwargs...) - name === nothing && - throw(ArgumentError("The `name` keyword must be provided. Please consider using the `@named` macro")) - iv′ = value(iv) - dvs′ = value.(dvs) - ps′ = value.(ps) - if any(hasderiv, eqs) || any(hashold, eqs) || any(hassample, eqs) || any(hasdiff, eqs) - error("Equations in a `ImplicitDiscreteSystem` can only have `Shift` operators.") - end - if !(isempty(default_u0) && isempty(default_p)) - Base.depwarn( - "`default_u0` and `default_p` are deprecated. Use `defaults` instead.", - :ImplicitDiscreteSystem, force = true) - end - - # Copy equations to canonical form, but do not touch array expressions - eqs = [wrap(eq.lhs) isa Symbolics.Arr ? eq : 0 ~ eq.rhs - eq.lhs for eq in eqs] - defaults = Dict{Any, Any}(todict(defaults)) - guesses = Dict{Any, Any}(todict(guesses)) - var_to_name = Dict() - process_variables!(var_to_name, defaults, guesses, dvs′) - process_variables!(var_to_name, defaults, guesses, ps′) - process_variables!( - var_to_name, defaults, guesses, [eq.lhs for eq in parameter_dependencies]) - process_variables!( - var_to_name, defaults, guesses, [eq.rhs for eq in parameter_dependencies]) - defaults = Dict{Any, Any}(value(k) => value(v) - for (k, v) in pairs(defaults) if v !== nothing) - guesses = Dict{Any, Any}(value(k) => value(v) - for (k, v) in pairs(guesses) if v !== nothing) - - isempty(observed) || collect_var_to_name!(var_to_name, (eq.lhs for eq in observed)) - - sysnames = nameof.(systems) - if length(unique(sysnames)) != length(sysnames) - throw(ArgumentError("System names must be unique.")) - end - ImplicitDiscreteSystem(Threads.atomic_add!(SYSTEM_COUNT, UInt(1)), - eqs, iv′, dvs′, ps′, tspan, var_to_name, observed, name, description, systems, - defaults, guesses, initializesystem, initialization_eqs, preface, connector_type, - parameter_dependencies, metadata, gui_metadata, kwargs...) -end - -function ImplicitDiscreteSystem(eqs, iv; kwargs...) - eqs = collect(eqs) - diffvars = OrderedSet() - allunknowns = OrderedSet() - ps = OrderedSet() - iv = value(iv) - for eq in eqs - collect_vars!(allunknowns, ps, eq, iv; op = Shift) - if iscall(eq.lhs) && operation(eq.lhs) isa Shift - isequal(iv, operation(eq.lhs).t) || - throw(ArgumentError("An ImplicitDiscreteSystem can only have one independent variable.")) - eq.lhs in diffvars && - throw(ArgumentError("The shift variable $(eq.lhs) is not unique in the system of equations.")) - push!(diffvars, eq.lhs) - end - end - for eq in get(kwargs, :parameter_dependencies, Equation[]) - if eq isa Pair - collect_vars!(allunknowns, ps, eq, iv) - else - collect_vars!(allunknowns, ps, eq, iv) - end - end - new_ps = OrderedSet() - for p in ps - if iscall(p) && operation(p) === getindex - par = arguments(p)[begin] - if Symbolics.shape(Symbolics.unwrap(par)) !== Symbolics.Unknown() && - all(par[i] in ps for i in eachindex(par)) - push!(new_ps, par) - else - push!(new_ps, p) - end - else - push!(new_ps, p) - end - end - return ImplicitDiscreteSystem(eqs, iv, - collect(allunknowns), collect(new_ps); kwargs...) -end -# basically at every timestep it should build a nonlinear solve -# Previous timesteps should be treated as parameters? is this right? - -function flatten(sys::ImplicitDiscreteSystem, noeqs = false) - systems = get_systems(sys) - if isempty(systems) - return sys - else - return ImplicitDiscreteSystem(noeqs ? Equation[] : equations(sys), - get_iv(sys), - unknowns(sys), - parameters(sys), - observed = observed(sys), - defaults = defaults(sys), - guesses = guesses(sys), - initialization_eqs = initialization_equations(sys), - name = nameof(sys), - description = description(sys), - metadata = get_metadata(sys), - checks = false) - end -end - -function generate_function( - sys::ImplicitDiscreteSystem, dvs = unknowns(sys), ps = parameters(sys); wrap_code = identity, kwargs...) - if !iscomplete(sys) - error("A completed system is required. Call `complete` or `structural_simplify` on the system.") - end - p = (reorder_parameters(sys, unwrap.(ps))..., cachesyms...) - isscalar = !(exprs isa AbstractArray) - pre, sol_states = get_substitutions_and_solved_unknowns(sys, isscalar ? [exprs] : exprs) - if postprocess_fbody === nothing - postprocess_fbody = pre - end - if states === nothing - states = sol_states - end - exprs = [eq.lhs - eq.rhs for eq in equations(sys)] - u = map(Shift(iv, -1), dvs) - u_next = dvs - - wrap_code = wrap_code .∘ wrap_array_vars(sys, exprs) .∘ wrap_parameter_dependencies(sys, false) - - build_function(exprs, u_next, u, p..., get_iv(sys)) -end - -function shift_u0map_forward(sys::ImplicitDiscreteSystem, u0map, defs) - iv = get_iv(sys) - updated = AnyDict() - for k in collect(keys(u0map)) - v = u0map[k] - if !((op = operation(k)) isa Shift) - 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)).") - end - updated[Shift(iv, op.steps + 1)(arguments(k)[1])] = v - end - for var in unknowns(sys) - op = operation(var) - op isa Shift || continue - haskey(updated, var) && continue - root = first(arguments(var)) - haskey(defs, root) || error("Initial condition for $var not provided.") - updated[var] = defs[root] - end - return updated -end - -""" - $(TYPEDSIGNATURES) -Generates an ImplicitDiscreteProblem from an ImplicitDiscreteSystem. -""" -function SciMLBase.ImplicitDiscreteProblem( - sys::ImplicitDiscreteSystem, u0map = [], tspan = get_tspan(sys), - parammap = SciMLBase.NullParameters(); - eval_module = @__MODULE__, - eval_expression = false, - use_union = false, - kwargs... -) - if !iscomplete(sys) - error("A completed `ImplicitDiscreteSystem` is required. Call `complete` or `structural_simplify` on the system before creating a `ImplicitDiscreteProblem`") - end - dvs = unknowns(sys) - ps = parameters(sys) - eqs = equations(sys) - iv = get_iv(sys) - - u0map = to_varmap(u0map, dvs) - u0map = shift_u0map_forward(sys, u0map, defaults(sys)) - f, u0, p = process_SciMLProblem( - ImplicitDiscreteFunction, sys, u0map, parammap; eval_expression, eval_module) - u0 = f(u0, p, tspan[1]) - NonlinearProblem(f, u0, tspan, p; kwargs...) -end - -function SciMLBase.ImplicitDiscreteFunction(sys::ImplicitDiscreteSystem, args...; kwargs...) - ImplicitDiscreteFunction{true}(sys, args...; kwargs...) -end - -function SciMLBase.ImplicitDiscreteFunction{true}(sys::ImplicitDiscreteSystem, args...; kwargs...) - ImplicitDiscreteFunction{true, SciMLBase.AutoSpecialize}(sys, args...; kwargs...) -end - -function SciMLBase.ImplicitDiscreteFunction{false}(sys::ImplicitDiscreteSystem, args...; kwargs...) - ImplicitDiscreteFunction{false, SciMLBase.FullSpecialize}(sys, args...; kwargs...) -end -function SciMLBase.ImplicitDiscreteFunction{iip, specialize}( - sys::ImplicitDiscreteSystem, - dvs = unknowns(sys), - ps = parameters(sys), - u0 = nothing; - version = nothing, - p = nothing, - t = nothing, - eval_expression = false, - eval_module = @__MODULE__, - analytic = nothing, - kwargs...) where {iip, specialize} - - if !iscomplete(sys) - error("A completed `ImplicitDiscreteSystem` is required. Call `complete` or `structural_simplify` on the system before creating a `ImplicitDiscreteProblem`") - end - f_gen = generate_function(sys, dvs, ps; expression = Val{true}, - expression_module = eval_module, kwargs...) - f_oop, f_iip = eval_or_rgf.(f_gen; eval_expression, eval_module) - 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 specialize === SciMLBase.FunctionWrapperSpecialize && iip - if u0 === nothing || p === nothing || t === nothing - error("u0, p, and t must be specified for FunctionWrapperSpecialize on ImplicitDiscreteFunction.") - end - f = SciMLBase.wrapfun_iip(f, (u0, u0, p, t)) - end - - observedfun = ObservedFunctionCache( - sys; eval_expression, eval_module, checkbounds = get(kwargs, :checkbounds, false)) - - ImplicitDiscreteFunction{iip, specialize}(f; - sys = sys, - observed = observedfun, - analytic = analytic) -end - -""" -```julia -ImplicitDiscreteFunctionExpr{iip}(sys::ImplicitDiscreteSystem, dvs = states(sys), - ps = parameters(sys); - version = nothing, - kwargs...) where {iip} -``` - -Create a Julia expression for an `ImplicitDiscreteFunction` from the [`ImplicitDiscreteSystem`](@ref). -The arguments `dvs` and `ps` are used to set the order of the dependent -variable and parameter vectors, respectively. -""" -struct ImplicitDiscreteFunctionExpr{iip} end -struct ImplicitDiscreteFunctionClosure{O, I} <: Function - f_oop::O - f_iip::I -end -(f::ImplicitDiscreteFunctionClosure)(u_next, u, p, t) = f.f_oop(u_next, u, p, t) -(f::ImplicitDiscreteFunctionClosure)(resid, u_next, u, p, t) = f.f_iip(resid, u_next, u, p, t) - -function ImplicitDiscreteFunctionExpr{iip}(sys::ImplicitDiscreteSystem, dvs = unknowns(sys), - ps = parameters(sys), u0 = nothing; - version = nothing, p = nothing, - linenumbers = false, - simplify = false, - kwargs...) where {iip} - f_oop, f_iip = generate_function(sys, dvs, ps; expression = Val{true}, kwargs...) - - fsym = gensym(:f) - _f = :($fsym = $ImplicitDiscreteFunctionClosure($f_oop, $f_iip)) - - ex = quote - $_f - ImplicitDiscreteFunction{$iip}($fsym) - end - !linenumbers ? Base.remove_linenums!(ex) : ex -end - -function ImplicitDiscreteFunctionExpr(sys::ImplicitDiscreteSystem, args...; kwargs...) - ImplicitDiscreteFunctionExpr{true}(sys, args...; kwargs...) -end - diff --git a/test/implicit_discrete_system.jl b/test/implicit_discrete_system.jl deleted file mode 100644 index adfb9d2fc1..0000000000 --- a/test/implicit_discrete_system.jl +++ /dev/null @@ -1,2 +0,0 @@ - -#init diff --git a/test/runtests.jl b/test/runtests.jl index bd5eecacd0..52875fdae5 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -79,8 +79,7 @@ end @safetestset "Variable Utils Test" include("variable_utils.jl") @safetestset "Variable Metadata Test" include("test_variable_metadata.jl") @safetestset "OptimizationSystem Test" include("optimizationsystem.jl") - @safetestset "DiscreteSystem Test" include("discrete_system.jl") - @safetestset "ImplicitDiscreteSystem Test" include("implicit_discrete_system.jl") + @safetestset "Discrete System" include("discrete_system.jl") @safetestset "SteadyStateSystem Test" include("steadystatesystems.jl") @safetestset "SDESystem Test" include("sdesystem.jl") @safetestset "DDESystem Test" include("dde.jl") From 690a25522fa898b1b046947bd44a17c250aa6833 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 31 Jan 2025 18:03:40 +0530 Subject: [PATCH 0701/1337] refactor: format --- src/systems/diffeqs/odesystem.jl | 5 +++-- src/systems/diffeqs/sdesystem.jl | 3 +-- src/systems/nonlinear/nonlinearsystem.jl | 6 ++++-- test/fmi/fmi.jl | 5 ++++- test/initial_values.jl | 3 ++- test/odesystem.jl | 20 ++++++++++---------- test/sdesystem.jl | 15 ++++++++------- 7 files changed, 32 insertions(+), 25 deletions(-) diff --git a/src/systems/diffeqs/odesystem.jl b/src/systems/diffeqs/odesystem.jl index 41bcaf1d01..096ee30588 100644 --- a/src/systems/diffeqs/odesystem.jl +++ b/src/systems/diffeqs/odesystem.jl @@ -374,7 +374,7 @@ function Base.:(==)(sys1::ODESystem, sys2::ODESystem) _eq_unordered(get_unknowns(sys1), get_unknowns(sys2)) && _eq_unordered(get_ps(sys1), get_ps(sys2)) && _eq_unordered(continuous_events(sys1), continuous_events(sys2)) && - _eq_unordered(discrete_events(sys1), discrete_events(sys2)) && + _eq_unordered(discrete_events(sys1), discrete_events(sys2)) && all(s1 == s2 for (s1, s2) in zip(get_systems(sys1), get_systems(sys2))) end @@ -631,7 +631,8 @@ function build_explicit_observed_function(sys, ts; oop_fn = Func(args, [], pre(Let(obsexprs, return_value, - false)), [Expr(:meta, :propagate_inbounds)]) |> array_wrapper[1] |> oop_mtkp_wrapper |> toexpr + false)), [Expr(:meta, :propagate_inbounds)]) |> array_wrapper[1] |> + oop_mtkp_wrapper |> toexpr if !checkbounds oop_fn.args[end] = quote diff --git a/src/systems/diffeqs/sdesystem.jl b/src/systems/diffeqs/sdesystem.jl index df950ee01c..f677850bf5 100644 --- a/src/systems/diffeqs/sdesystem.jl +++ b/src/systems/diffeqs/sdesystem.jl @@ -285,7 +285,7 @@ function Base.:(==)(sys1::SDESystem, sys2::SDESystem) _eq_unordered(get_unknowns(sys1), get_unknowns(sys2)) && _eq_unordered(get_ps(sys1), get_ps(sys2)) && _eq_unordered(continuous_events(sys1), continuous_events(sys2)) && - _eq_unordered(discrete_events(sys1), discrete_events(sys2)) && + _eq_unordered(discrete_events(sys1), discrete_events(sys2)) && all(s1 == s2 for (s1, s2) in zip(get_systems(sys1), get_systems(sys2))) end @@ -771,7 +771,6 @@ function DiffEqBase.SDEProblem{iip, specialize}( end function DiffEqBase.SDEProblem(sys::ODESystem, args...; kwargs...) - if any(ModelingToolkit.isbrownian, unknowns(sys)) error("SDESystem constructed by defining Brownian variables with @brownian must be simplified by calling `structural_simplify` before a SDEProblem can be constructed.") else diff --git a/src/systems/nonlinear/nonlinearsystem.jl b/src/systems/nonlinear/nonlinearsystem.jl index 89663f7dbc..58f61d3b22 100644 --- a/src/systems/nonlinear/nonlinearsystem.jl +++ b/src/systems/nonlinear/nonlinearsystem.jl @@ -710,7 +710,8 @@ function SciMLBase.SCCNonlinearProblem{iip}(sys::NonlinearSystem, u0map, # precomputed subexpressions should not contain `banned_vars` banned_vars = Set{Any}(vcat(_dvs, getproperty.(_obs, (:lhs,)))) filter!(banned_vars) do var - symbolic_type(var) != ArraySymbolic() || all(j -> var[j] in banned_vars, eachindex(var)) + symbolic_type(var) != ArraySymbolic() || + all(j -> var[j] in banned_vars, eachindex(var)) end state = Dict() for i in eachindex(_obs) @@ -772,7 +773,8 @@ function SciMLBase.SCCNonlinearProblem{iip}(sys::NonlinearSystem, u0map, _obs = scc_obs[i] cachevars = scc_cachevars[i] cacheexprs = scc_cacheexprs[i] - _prevobsidxs = vcat(_prevobsidxs, observed_equations_used_by(sys, reduce(vcat, values(cacheexprs); init = []))) + _prevobsidxs = vcat(_prevobsidxs, + observed_equations_used_by(sys, reduce(vcat, values(cacheexprs); init = []))) if isempty(cachevars) push!(explicitfuns, Returns(nothing)) diff --git a/test/fmi/fmi.jl b/test/fmi/fmi.jl index bf5b60459f..39f624f616 100644 --- a/test/fmi/fmi.jl +++ b/test/fmi/fmi.jl @@ -262,7 +262,10 @@ end @named adder2 = MTK.FMIComponent( Val(2); fmu, type = :CS, communication_step_size = 1e-3) sys, prob = build_looped_adders(adder1, adder2) - sol = solve(prob, Tsit5(); reltol = 1e-8, initializealg = SciMLBase.OverrideInit(nlsolve = FastShortcutNLLSPolyalg(autodiff = AutoFiniteDiff()))) + sol = solve(prob, + Tsit5(); + reltol = 1e-8, + initializealg = SciMLBase.OverrideInit(nlsolve = FastShortcutNLLSPolyalg(autodiff = AutoFiniteDiff()))) @test truesol(sol.t; idxs = [truesys.adder1.c, truesys.adder2.c]).u≈sol( sol.t; idxs = [sys.adder1.c, sys.adder2.c]).u rtol=1e-3 diff --git a/test/initial_values.jl b/test/initial_values.jl index c31eca33f8..4543f3549e 100644 --- a/test/initial_values.jl +++ b/test/initial_values.jl @@ -180,5 +180,6 @@ end @parameters p[1:2, 1:2] @mtkbuild sys = ODESystem(D(x) ~ p * x, t) # used to throw a `MethodError` complaining about `getindex(::Nothing, ::CartesianIndex{2})` - @test_throws ModelingToolkit.MissingParametersError ODEProblem(sys, [x => ones(2)], (0.0, 1.0)) + @test_throws ModelingToolkit.MissingParametersError ODEProblem( + sys, [x => ones(2)], (0.0, 1.0)) end diff --git a/test/odesystem.jl b/test/odesystem.jl index a635c3dad9..eb766a1380 100644 --- a/test/odesystem.jl +++ b/test/odesystem.jl @@ -1548,7 +1548,7 @@ end @testset "`isequal`" begin @variables X(t) @parameters p d - eq = D(X) ~ p - d*X + eq = D(X) ~ p - d * X osys1 = complete(ODESystem([eq], t; name = :osys)) osys2 = complete(ODESystem([eq], t; name = :osys)) @@ -1559,7 +1559,7 @@ end osys1 = complete(ODESystem([eq], t; name = :osys, continuous_events)) osys2 = complete(ODESystem([eq], t; name = :osys)) - @test osys1 !== osys2 + @test osys1 !== osys2 osys1 = complete(ODESystem([eq], t; name = :osys, discrete_events)) osys2 = complete(ODESystem([eq], t; name = :osys)) @@ -1567,28 +1567,28 @@ end osys1 = complete(ODESystem([eq], t; name = :osys, continuous_events)) osys2 = complete(ODESystem([eq], t; name = :osys, discrete_events)) - @test osys1 !== osys2 + @test osys1 !== osys2 end @testset "dae_order_lowering basic test" begin @parameters a @variables x(t) y(t) z(t) @named dae_sys = ODESystem([ - D(x) ~ y, - 0 ~ x + z, - 0 ~ x - y + z - ], t, [z, y, x], []) + D(x) ~ y, + 0 ~ x + z, + 0 ~ x - y + z + ], t, [z, y, x], []) lowered_dae_sys = dae_order_lowering(dae_sys) - @variables x1(t) y1(t) z1(t) + @variables x1(t) y1(t) z1(t) expected_eqs = [ 0 ~ x + z, 0 ~ x - y + z, Differential(t)(x) ~ y ] lowered_eqs = equations(lowered_dae_sys) - sorted_lowered_eqs = sort(lowered_eqs, by=string) - sorted_expected_eqs = sort(expected_eqs, by=string) + sorted_lowered_eqs = sort(lowered_eqs, by = string) + sorted_expected_eqs = sort(expected_eqs, by = string) @test sorted_lowered_eqs == sorted_expected_eqs expected_vars = Set([z, y, x]) diff --git a/test/sdesystem.jl b/test/sdesystem.jl index ade91b60b9..40a0a52c26 100644 --- a/test/sdesystem.jl +++ b/test/sdesystem.jl @@ -882,10 +882,10 @@ end 0.1 * z] @named sys = ODESystem(eqs, tt, [x, y, z], [σ, ρ, β]) - + @named de = SDESystem(eqs, noiseeqs, tt, [x, y, z], [σ, ρ, β], tspan = (0.0, 10.0)) de = complete(de) - + f = SDEFunctionExpr(de) @test f isa Expr @@ -915,7 +915,7 @@ end @variables X(t) @parameters p d @brownian a - seq = D(X) ~ p - d*X + a + seq = D(X) ~ p - d * X + a @mtkbuild ssys1 = System([seq], t; name = :ssys) @mtkbuild ssys2 = System([seq], t; name = :ssys) @test ssys1 == ssys2 # true @@ -925,7 +925,7 @@ end @mtkbuild ssys1 = System([seq], t; name = :ssys, continuous_events) @mtkbuild ssys2 = System([seq], t; name = :ssys) - @test ssys1 !== ssys2 + @test ssys1 !== ssys2 @mtkbuild ssys1 = System([seq], t; name = :ssys, discrete_events) @mtkbuild ssys2 = System([seq], t; name = :ssys) @@ -933,7 +933,7 @@ end @mtkbuild ssys1 = System([seq], t; name = :ssys, continuous_events) @mtkbuild ssys2 = System([seq], t; name = :ssys, discrete_events) - @test ssys1 !== ssys2 + @test ssys1 !== ssys2 end @testset "Error when constructing SDESystem without `structural_simplify`" begin @@ -950,7 +950,8 @@ end u0map = [x => 1.0, y => 0.0, z => 0.0] parammap = [σ => 10.0, β => 26.0, ρ => 2.33] - @test_throws ErrorException("SDESystem constructed by defining Brownian variables with @brownian must be simplified by calling `structural_simplify` before a SDEProblem can be constructed.") SDEProblem(de, u0map, (0.0, 100.0), parammap) + @test_throws ErrorException("SDESystem constructed by defining Brownian variables with @brownian must be simplified by calling `structural_simplify` before a SDEProblem can be constructed.") SDEProblem( + de, u0map, (0.0, 100.0), parammap) de = structural_simplify(de) @test SDEProblem(de, u0map, (0.0, 100.0), parammap) isa SDEProblem -end \ No newline at end of file +end From 6b7d5dcd5eac5d021478371001166c2c9bd0a6bc Mon Sep 17 00:00:00 2001 From: Christopher Rackauckas Date: Fri, 31 Jan 2025 08:25:13 -0500 Subject: [PATCH 0702/1337] Update Downstream.yml --- .github/workflows/Downstream.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/Downstream.yml b/.github/workflows/Downstream.yml index f0aefa87f6..0debdbdc7c 100644 --- a/.github/workflows/Downstream.yml +++ b/.github/workflows/Downstream.yml @@ -37,7 +37,6 @@ jobs: - {user: SciML, repo: MethodOfLines.jl, group: Interface} - {user: SciML, repo: MethodOfLines.jl, group: 2D_Diffusion} - {user: SciML, repo: MethodOfLines.jl, group: DAE} - - {user: ai4energy, repo: Ai4EComponentLib.jl, group: Downstream} steps: - uses: actions/checkout@v4 - uses: julia-actions/setup-julia@v1 From 2ab8ee8fd0e5280937cd38481055bb041c7dd450 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 30 Jan 2025 17:52:04 +0530 Subject: [PATCH 0703/1337] fix: handle scalarized array parameters in initialization --- src/systems/nonlinear/initializesystem.jl | 34 +++++++++++++++++------ test/initializationsystem.jl | 11 ++++++++ 2 files changed, 37 insertions(+), 8 deletions(-) diff --git a/src/systems/nonlinear/initializesystem.jl b/src/systems/nonlinear/initializesystem.jl index 296078bf43..0ead9b3908 100644 --- a/src/systems/nonlinear/initializesystem.jl +++ b/src/systems/nonlinear/initializesystem.jl @@ -112,9 +112,9 @@ function generate_initializesystem(sys::AbstractSystem; # If either of them are `missing` the parameter is an unknown # But if the parameter is passed a value, use that as an additional # equation in the system - _val1 = get(pmap, p, nothing) - _val2 = get(defs, p, nothing) - _val3 = get(guesses, p, nothing) + _val1 = get_possibly_array_fallback_singletons(pmap, p) + _val2 = get_possibly_array_fallback_singletons(defs, p) + _val3 = get_possibly_array_fallback_singletons(guesses, p) varp = tovar(p) paramsubs[p] = varp # Has a default of `missing`, and (either an equation using the value passed to `ODEProblem` or a guess) @@ -139,7 +139,7 @@ function generate_initializesystem(sys::AbstractSystem; error("Invalid setup: parameter $(p) has no default value, initial value, or guess") end # given a symbolic value to ODEProblem - elseif symbolic_type(_val1) != NotSymbolic() + elseif symbolic_type(_val1) != NotSymbolic() || is_array_of_symbolics(_val1) push!(eqs_ics, varp ~ _val1) push!(defs, varp => _val3) # No value passed to `ODEProblem`, but a default and a guess are present @@ -268,16 +268,34 @@ struct InitializationSystemMetadata oop_reconstruct_u0_p::Union{Nothing, ReconstructInitializeprob} end +function get_possibly_array_fallback_singletons(varmap, p) + if haskey(varmap, p) + return varmap[p] + end + symbolic_type(p) == ArraySymbolic() || return nothing + scal = collect(p) + if all(x -> haskey(varmap, x), scal) + res = [varmap[x] for x in scal] + if any(x -> x === nothing, res) + return nothing + elseif any(x -> x === missing, res) + return missing + end + return res + end + return nothing +end + function is_parameter_solvable(p, pmap, defs, guesses) p = unwrap(p) is_variable_floatingpoint(p) || return false - _val1 = pmap isa AbstractDict ? get(pmap, p, nothing) : nothing - _val2 = get(defs, p, nothing) - _val3 = get(guesses, p, nothing) + _val1 = pmap isa AbstractDict ? get_possibly_array_fallback_singletons(pmap, p) : nothing + _val2 = get_possibly_array_fallback_singletons(defs, p) + _val3 = get_possibly_array_fallback_singletons(guesses, p) # either (missing is a default or was passed to the ODEProblem) or (nothing was passed to # the ODEProblem and it has a default and a guess) return ((_val1 === missing || _val2 === missing) || - (symbolic_type(_val1) != NotSymbolic() || + (symbolic_type(_val1) != NotSymbolic() || is_array_of_symbolics(_val1) || _val1 === nothing && _val2 !== nothing)) && _val3 !== nothing end diff --git a/test/initializationsystem.jl b/test/initializationsystem.jl index def45a7abf..ff6620a8b1 100644 --- a/test/initializationsystem.jl +++ b/test/initializationsystem.jl @@ -1281,3 +1281,14 @@ end @test sol[S, 1] ≈ 999 @test SciMLBase.successful_retcode(sol) end + +@testset "Solvable array parameters with scalarized guesses" begin + @variables x(t) + @parameters p[1:2] q + @mtkbuild sys = ODESystem(D(x) ~ p[1] + p[2] + q, t; defaults = [p[1] => q, p[2] => 2q], guesses = [p[1] => q, p[2] => 2q]) + @test ModelingToolkit.is_parameter_solvable(p, Dict(), defaults(sys), guesses(sys)) + prob = ODEProblem(sys, [x => 1.0], (0.0, 1.0), [q => 2.0]) + @test length(ModelingToolkit.observed(prob.f.initialization_data.initializeprob.f.sys)) == 3 + sol = solve(prob, Tsit5()) + @test sol.ps[p] ≈ [2.0, 4.0] +end From 47b636c14d2b97dfc9a514eae1758d59bebdf63b Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Sat, 1 Feb 2025 03:07:55 +0530 Subject: [PATCH 0704/1337] refactor: format --- src/systems/diffeqs/odesystem.jl | 3 ++- src/systems/diffeqs/sdesystem.jl | 16 +++++++++++----- src/systems/nonlinear/initializesystem.jl | 3 ++- src/utils.jl | 3 ++- test/initializationsystem.jl | 7 +++++-- test/odesystem.jl | 2 +- test/sdesystem.jl | 2 +- 7 files changed, 24 insertions(+), 12 deletions(-) diff --git a/src/systems/diffeqs/odesystem.jl b/src/systems/diffeqs/odesystem.jl index 1a1a2367b7..c95e9f19db 100644 --- a/src/systems/diffeqs/odesystem.jl +++ b/src/systems/diffeqs/odesystem.jl @@ -327,7 +327,8 @@ function ODESystem(eqs, iv; kwargs...) end algevars = setdiff(allunknowns, diffvars) - return ODESystem(eqs, iv, collect(Iterators.flatten((diffvars, algevars))), collect(new_ps); kwargs...) + return ODESystem(eqs, iv, collect(Iterators.flatten((diffvars, algevars))), + collect(new_ps); kwargs...) end # NOTE: equality does not check cached Jacobian diff --git a/src/systems/diffeqs/sdesystem.jl b/src/systems/diffeqs/sdesystem.jl index 12d860af7f..0426a77858 100644 --- a/src/systems/diffeqs/sdesystem.jl +++ b/src/systems/diffeqs/sdesystem.jl @@ -273,7 +273,7 @@ function SDESystem(sys::ODESystem, neqs; kwargs...) SDESystem(equations(sys), neqs, get_iv(sys), unknowns(sys), parameters(sys); kwargs...) end -function SDESystem(eqs::Vector{Equation}, noiseeqs::AbstractArray, iv; kwargs...) +function SDESystem(eqs::Vector{Equation}, noiseeqs::AbstractArray, iv; kwargs...) diffvars, allunknowns, ps, eqs = process_equations(eqs, iv) for eq in get(kwargs, :parameter_dependencies, Equation[]) @@ -309,15 +309,21 @@ function SDESystem(eqs::Vector{Equation}, noiseeqs::AbstractArray, iv; kwargs... noiseps = OrderedSet() collect_vars!(noisedvs, noiseps, noiseeqs, iv) for dv in noisedvs - dv ∈ allunknowns || throw(ArgumentError("Variable $dv in noise equations is not an unknown of the system.")) + dv ∈ allunknowns || + throw(ArgumentError("Variable $dv in noise equations is not an unknown of the system.")) end algevars = setdiff(allunknowns, diffvars) - return SDESystem(eqs, noiseeqs, iv, Iterators.flatten((diffvars, algevars)), [ps; collect(noiseps)]; kwargs...) + return SDESystem(eqs, noiseeqs, iv, Iterators.flatten((diffvars, algevars)), + [ps; collect(noiseps)]; kwargs...) end -SDESystem(eq::Equation, noiseeqs::AbstractArray, args...; kwargs...) = SDESystem([eq], noiseeqs, args...; kwargs...) -SDESystem(eq::Equation, noiseeq, args...; kwargs...) = SDESystem([eq], [noiseeq], args...; kwargs...) +function SDESystem(eq::Equation, noiseeqs::AbstractArray, args...; kwargs...) + SDESystem([eq], noiseeqs, args...; kwargs...) +end +function SDESystem(eq::Equation, noiseeq, args...; kwargs...) + SDESystem([eq], [noiseeq], args...; kwargs...) +end function Base.:(==)(sys1::SDESystem, sys2::SDESystem) sys1 === sys2 && return true diff --git a/src/systems/nonlinear/initializesystem.jl b/src/systems/nonlinear/initializesystem.jl index 0ead9b3908..68c9d62f62 100644 --- a/src/systems/nonlinear/initializesystem.jl +++ b/src/systems/nonlinear/initializesystem.jl @@ -289,7 +289,8 @@ end function is_parameter_solvable(p, pmap, defs, guesses) p = unwrap(p) is_variable_floatingpoint(p) || return false - _val1 = pmap isa AbstractDict ? get_possibly_array_fallback_singletons(pmap, p) : nothing + _val1 = pmap isa AbstractDict ? get_possibly_array_fallback_singletons(pmap, p) : + nothing _val2 = get_possibly_array_fallback_singletons(defs, p) _val3 = get_possibly_array_fallback_singletons(guesses, p) # either (missing is a default or was passed to the ODEProblem) or (nothing was passed to diff --git a/src/utils.jl b/src/utils.jl index 7bb1ebcbe9..5edd8fe792 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -1225,7 +1225,8 @@ function process_equations(eqs, iv) throw(ArgumentError("An ODESystem can only have one independent variable.")) diffvar in diffvars && throw(ArgumentError("The differential variable $diffvar is not unique in the system of equations.")) - !(symtype(diffvar) === Real || eltype(symtype(diffvar)) === Real) && throw(ArgumentError("Differential variable $diffvar has type $(symtype(diffvar)). Differential variables should not be concretely typed.")) + !(symtype(diffvar) === Real || eltype(symtype(diffvar)) === Real) && + throw(ArgumentError("Differential variable $diffvar has type $(symtype(diffvar)). Differential variables should not be concretely typed.")) push!(diffvars, diffvar) end push!(diffeq, eq) diff --git a/test/initializationsystem.jl b/test/initializationsystem.jl index ff6620a8b1..026fd2c43d 100644 --- a/test/initializationsystem.jl +++ b/test/initializationsystem.jl @@ -1285,10 +1285,13 @@ end @testset "Solvable array parameters with scalarized guesses" begin @variables x(t) @parameters p[1:2] q - @mtkbuild sys = ODESystem(D(x) ~ p[1] + p[2] + q, t; defaults = [p[1] => q, p[2] => 2q], guesses = [p[1] => q, p[2] => 2q]) + @mtkbuild sys = ODESystem( + D(x) ~ p[1] + p[2] + q, t; defaults = [p[1] => q, p[2] => 2q], + guesses = [p[1] => q, p[2] => 2q]) @test ModelingToolkit.is_parameter_solvable(p, Dict(), defaults(sys), guesses(sys)) prob = ODEProblem(sys, [x => 1.0], (0.0, 1.0), [q => 2.0]) - @test length(ModelingToolkit.observed(prob.f.initialization_data.initializeprob.f.sys)) == 3 + @test length(ModelingToolkit.observed(prob.f.initialization_data.initializeprob.f.sys)) == + 3 sol = solve(prob, Tsit5()) @test sol.ps[p] ≈ [2.0, 4.0] end diff --git a/test/odesystem.jl b/test/odesystem.jl index 40515fe570..30465d2975 100644 --- a/test/odesystem.jl +++ b/test/odesystem.jl @@ -1547,7 +1547,7 @@ end @testset "Validate input types" begin @parameters p d @variables X(t)::Int64 - eq = D(X) ~ p - d*X + eq = D(X) ~ p - d * X @test_throws ArgumentError @mtkbuild osys = ODESystem([eq], t) @variables Y(t)[1:3]::String eq = D(Y) ~ [p, p, p] diff --git a/test/sdesystem.jl b/test/sdesystem.jl index 24c74c89cb..40b8747d39 100644 --- a/test/sdesystem.jl +++ b/test/sdesystem.jl @@ -874,7 +874,7 @@ end @parameters p d @variables X(t)::Int64 @brownian z - eq2 = D(X) ~ p - d*X + z + eq2 = D(X) ~ p - d * X + z @test_throws ArgumentError @mtkbuild ssys = System([eq2], t) noiseeq = [1] @test_throws ArgumentError @named ssys = SDESystem([eq2], [noiseeq], t) From b107e129ccb99b26ee493131d45d175af294a753 Mon Sep 17 00:00:00 2001 From: Herman Sletmoen Date: Sun, 2 Feb 2025 11:56:16 +0100 Subject: [PATCH 0705/1337] Add @description to @mtkmodel --- docs/src/basics/MTKLanguage.md | 8 ++++++++ docs/src/tutorials/acausal_components.md | 2 ++ src/systems/model_parsing.jl | 16 ++++++++++++++-- test/model_parsing.jl | 3 ++- 4 files changed, 26 insertions(+), 3 deletions(-) diff --git a/docs/src/basics/MTKLanguage.md b/docs/src/basics/MTKLanguage.md index 06250f90b4..2bcec99b6a 100644 --- a/docs/src/basics/MTKLanguage.md +++ b/docs/src/basics/MTKLanguage.md @@ -23,6 +23,7 @@ equations. `@mtkmodel` definition contains begin blocks of + - `@description`: for describing the whole system with a human-readable string - `@components`: for listing sub-components of the system - `@constants`: for declaring constants - `@defaults`: for passing `defaults` to ODESystem @@ -42,6 +43,7 @@ using ModelingToolkit using ModelingToolkit: t @mtkmodel ModelA begin + @description "A component with parameters `k` and `k_array`." @parameters begin k k_array[1:2] @@ -49,6 +51,7 @@ using ModelingToolkit: t end @mtkmodel ModelB begin + @description "A component with parameters `p1` and `p2`." @parameters begin p1 = 1.0, [description = "Parameter of ModelB"] p2 = 1.0, [description = "Parameter of ModelB"] @@ -56,6 +59,7 @@ end end @mtkmodel ModelC begin + @description "A bigger system that contains many more things." @icon "https://github.com/SciML/SciMLDocs/blob/main/docs/src/assets/logo.png" @constants begin c::Int = 1, [description = "Example constant."] @@ -91,6 +95,10 @@ end end ``` +#### `@description` + +A documenting `String` that summarizes and explains what the model is. + #### `@icon` An icon can be embedded in 3 ways: diff --git a/docs/src/tutorials/acausal_components.md b/docs/src/tutorials/acausal_components.md index 096b576d29..b97500a3e9 100644 --- a/docs/src/tutorials/acausal_components.md +++ b/docs/src/tutorials/acausal_components.md @@ -84,6 +84,7 @@ end end @mtkmodel RCModel begin + @description "A circuit with a constant voltage source, resistor and capacitor connected in series." @components begin resistor = Resistor(R = 1.0) capacitor = Capacitor(C = 1.0) @@ -251,6 +252,7 @@ make all of our parameter values 1.0. As `resistor`, `capacitor`, `source` lists ```@example acausal @mtkmodel RCModel begin + @description "A circuit with a constant voltage source, resistor and capacitor connected in series." @components begin resistor = Resistor(R = 1.0) capacitor = Capacitor(C = 1.0) diff --git a/src/systems/model_parsing.jl b/src/systems/model_parsing.jl index 52e46597bf..1976e89aa2 100644 --- a/src/systems/model_parsing.jl +++ b/src/systems/model_parsing.jl @@ -109,11 +109,13 @@ function _model_macro(mod, name, expr, isconnector) gui_metadata = isassigned(icon) > 0 ? GUIMetadata(GlobalRef(mod, name), icon[]) : GUIMetadata(GlobalRef(mod, name)) + description = get(dict, :description, "") + @inline pop_structure_dict!.( Ref(dict), [:constants, :defaults, :kwargs, :structural_parameters]) sys = :($ODESystem($(flatten_equations)(equations), $iv, variables, parameters; - name, systems, gui_metadata = $gui_metadata, defaults)) + name, description = $description, systems, gui_metadata = $gui_metadata, defaults)) if length(ext) == 0 push!(exprs.args, :(var"#___sys___" = $sys)) @@ -598,7 +600,9 @@ function parse_model!(exprs, comps, ext, eqs, icon, vs, ps, sps, c_evts, d_evts, dict, mod, arg, kwargs, where_types) mname = arg.args[1] body = arg.args[end] - if mname == Symbol("@components") + if mname == Symbol("@description") + parse_description!(body, dict) + elseif mname == Symbol("@components") parse_components!(exprs, comps, dict, body, kwargs) elseif mname == Symbol("@extend") parse_extend!(exprs, ext, dict, mod, body, kwargs) @@ -1156,6 +1160,14 @@ function parse_icon!(body::Symbol, dict, icon, mod) parse_icon!(getfield(mod, body), dict, icon, mod) end +function parse_description!(body, dict) + if body isa String + dict[:description] = body + else + error("Invalid description string $body") + end +end + ### Parsing Components: function component_args!(a, b, varexpr, kwargs; index_name = nothing) diff --git a/test/model_parsing.jl b/test/model_parsing.jl index baaa4651bf..559992d7c5 100644 --- a/test/model_parsing.jl +++ b/test/model_parsing.jl @@ -127,6 +127,7 @@ end end @mtkmodel RC begin + @description "An RC circuit." @structural_parameters begin R_val = 10u"Ω" C_val = 10u"F" @@ -139,7 +140,6 @@ end constant = Constant(; k = k_val) ground = MyMockModule.Ground() end - @equations begin connect(constant.output, source.V) connect(source.p, resistor.p) @@ -157,6 +157,7 @@ sol = solve(prob) defs = ModelingToolkit.defaults(rc) @test sol[rc.capacitor.v, end] ≈ defs[rc.constant.k] resistor = getproperty(rc, :resistor; namespace = false) +@test ModelingToolkit.description(rc) == "An RC circuit." @test getname(rc.resistor) === getname(resistor) @test getname(rc.resistor.R) === getname(resistor.R) @test getname(rc.resistor.v) === getname(resistor.v) From d90dda3482a3747d46c5ae77a0432336c9a83b09 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 30 Jan 2025 11:55:21 +0530 Subject: [PATCH 0706/1337] feat: allow specifying observed equations to `observed_equations_used_by` --- src/utils.jl | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/utils.jl b/src/utils.jl index 5edd8fe792..1f573ca0e8 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -1060,10 +1060,9 @@ Keyword arguments: variables which will be explored to find dependencies on observed equations. Typically, providing this keyword is not necessary and is only useful to avoid repeatedly calling `vars(exprs)` +- `obs`: the list of observed equations. """ -function observed_equations_used_by(sys::AbstractSystem, exprs; involved_vars = vars(exprs)) - obs = observed(sys) - +function observed_equations_used_by(sys::AbstractSystem, exprs; involved_vars = vars(exprs), obs = observed(sys)) obsvars = getproperty.(obs, :lhs) graph = observed_dependency_graph(obs) From cf406210f78fd7f0ea1785e79f1fc059ac011c0e Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 30 Jan 2025 11:41:43 +0530 Subject: [PATCH 0707/1337] fix: fix `observed_equations_used_by` for `DiscreteSystem` --- src/utils.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/utils.jl b/src/utils.jl index 1f573ca0e8..e7259cc898 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -1062,7 +1062,8 @@ Keyword arguments: `vars(exprs)` - `obs`: the list of observed equations. """ -function observed_equations_used_by(sys::AbstractSystem, exprs; involved_vars = vars(exprs), obs = observed(sys)) +function observed_equations_used_by(sys::AbstractSystem, exprs; + involved_vars = vars(exprs; op = Union{Shift, Differential}), obs = observed(sys)) obsvars = getproperty.(obs, :lhs) graph = observed_dependency_graph(obs) From b50cb27963e6e85277c1903e6cfbe73b51df01f9 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 30 Jan 2025 11:55:44 +0530 Subject: [PATCH 0708/1337] feat: add `build_function_wrapper` for centralized codegen --- src/ModelingToolkit.jl | 1 + src/systems/codegen_utils.jl | 88 ++++++++++++++++++++++++++++++++++++ 2 files changed, 89 insertions(+) create mode 100644 src/systems/codegen_utils.jl diff --git a/src/ModelingToolkit.jl b/src/ModelingToolkit.jl index 2710d7d1e4..107f98f0ab 100644 --- a/src/ModelingToolkit.jl +++ b/src/ModelingToolkit.jl @@ -150,6 +150,7 @@ 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("systems/nonlinear/nonlinearsystem.jl") diff --git a/src/systems/codegen_utils.jl b/src/systems/codegen_utils.jl new file mode 100644 index 0000000000..35a7c579a2 --- /dev/null +++ b/src/systems/codegen_utils.jl @@ -0,0 +1,88 @@ +function generated_argument_name(i::Int) + return Symbol(:__mtk_arg_, i) +end + +function array_variable_assignments(args...) + var_to_arridxs = Dict{BasicSymbolic, Array{Tuple{Int, Int}}}() + for (i, arg) in enumerate(args) + symbolic_type(arg) == NotSymbolic() || continue + arg isa AbstractArray || continue + + for (j, var) in enumerate(arg) + var = unwrap(var) + iscall(var) || continue + operation(var) == getindex || continue + arrvar = arguments(var)[1] + idxbuffer = get!(() -> map(Returns((0, 0)), eachindex(arrvar)), var_to_arridxs, arrvar) + idxbuffer[arguments(var)[2:end]...] = (i, j) + end + end + + assignments = Assignment[] + for (arrvar, idxs) in var_to_arridxs + any(iszero ∘ first, idxs) && continue + + if allequal(Iterators.map(first, idxs)) + buffer_idx = first(first(idxs)) + idxs = map(last, idxs) + if first(idxs) < last(idxs) && vec(idxs) == first(idxs):last(idxs) + idxs = first(idxs):last(idxs) + elseif vec(idxs) == last(idxs):-1:first(idxs) + idxs = last(idxs):-1:first(idxs) + else + idxs = SArray{Tuple{size(idxs)...}}(idxs) + end + push!(assignments, arrvar ← term(reshape, term(view, generated_argument_name(buffer_idx), idxs), size(arrvar))) + else + elems = map(idxs) do idx + i, j = idx + term(getindex, generated_argument_name(i), j) + end + push!(assignments, arrvar ← MakeArray(elems, SArray)) + end + end + + return assignments +end + +function build_function_wrapper(sys::AbstractSystem, expr, args...; p_start = 2, p_end = is_time_dependent(sys) ? length(args) - 1 : length(args), wrap_delays = is_dde(sys), wrap_code = identity, kwargs...) + isscalar = !(expr isa AbstractArray || symbolic_type(expr) == ArraySymbolic()) + + obs = observed(sys) + pdeps = parameter_dependencies(sys) + + cmap, _ = get_cmap(sys) + obsidxs = observed_equations_used_by(sys, expr) + pdepidxs = observed_equations_used_by(sys, expr; obs = pdeps) + + assignments = array_variable_assignments(args...) + + for eq in Iterators.flatten((cmap, pdeps[pdepidxs], obs[obsidxs])) + push!(assignments, eq.lhs ← eq.rhs) + end + + args = ntuple(Val(length(args))) do i + arg = args[i] + if symbolic_type(arg) == NotSymbolic() && arg isa AbstractArray + DestructuredArgs(arg, generated_argument_name(i)) + else + arg + end + end + + if is_split(sys) + if p_start > p_end + args = (args[1:p_start-1]..., MTKPARAMETERS_ARG, args[p_end+1:end]...) + else + args = (args[1:p_start-1]..., DestructuredArgs(collect(args[p_start:p_end]), MTKPARAMETERS_ARG), args[p_end+1:end]...) + end + end + + wrap_code = wrap_code .∘ wrap_assignments(isscalar, assignments) + + if wrap_delays + expr = delay_to_function(sys, expr; history_arg = is_split(sys) ? MTKPARAMETERS_ARG : generated_argument_name(p_start)) + end + + return build_function(expr, args...; wrap_code, kwargs...) +end From 9166c74f0db371dd7bc9e1048e26e32954f32b4b Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 30 Jan 2025 11:56:11 +0530 Subject: [PATCH 0709/1337] refactor: use `build_function_wrapper` in `generate_tgrad` --- src/systems/diffeqs/abstractodesystem.jl | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index 6571aed9cc..222a36b5ac 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -106,24 +106,13 @@ end function generate_tgrad( sys::AbstractODESystem, dvs = unknowns(sys), ps = parameters(sys); - simplify = false, wrap_code = identity, kwargs...) + simplify = false, kwargs...) tgrad = calculate_tgrad(sys, simplify = simplify) - pre = get_preprocess_constants(tgrad) - p = if has_index_cache(sys) && get_index_cache(sys) !== nothing - reorder_parameters(get_index_cache(sys), ps) - elseif ps isa Tuple - ps - else - (ps,) - end - wrap_code = wrap_code .∘ wrap_array_vars(sys, tgrad; dvs, ps) .∘ - wrap_parameter_dependencies(sys, !(tgrad isa AbstractArray)) - return build_function(tgrad, + p = reorder_parameters(sys, ps) + return build_function_wrapper(sys, tgrad, dvs, p..., get_iv(sys); - postprocess_fbody = pre, - wrap_code, kwargs...) end From 2887ca4bc17774637ac202b8cfb73c1504d7d3bd Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 30 Jan 2025 11:56:26 +0530 Subject: [PATCH 0710/1337] refactor: use `build_function` in `generate_jacobian(::AbstractODESystem)` --- src/systems/diffeqs/abstractodesystem.jl | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index 222a36b5ac..ea891e8557 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -118,22 +118,13 @@ end function generate_jacobian(sys::AbstractODESystem, dvs = unknowns(sys), ps = parameters(sys); - simplify = false, sparse = false, wrap_code = identity, kwargs...) + simplify = false, sparse = false, kwargs...) jac = calculate_jacobian(sys; simplify = simplify, sparse = sparse) - pre = get_preprocess_constants(jac) - p = if has_index_cache(sys) && get_index_cache(sys) !== nothing - reorder_parameters(get_index_cache(sys), ps) - else - (ps,) - end - wrap_code = wrap_code .∘ wrap_array_vars(sys, jac; dvs, ps) .∘ - wrap_parameter_dependencies(sys, false) - return build_function(jac, + p = reorder_parameters(sys, ps) + return build_function_wrapper(sys, jac, dvs, p..., get_iv(sys); - postprocess_fbody = pre, - wrap_code, kwargs...) end From 3574e3f45e9d5647b0418fb7595bfe5c35348b49 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 30 Jan 2025 11:57:11 +0530 Subject: [PATCH 0711/1337] refactor: use `build_function_wrapper` in `generate_control_jacobian` --- src/systems/diffeqs/abstractodesystem.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index ea891e8557..4f36a3c100 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -133,7 +133,7 @@ function generate_control_jacobian(sys::AbstractODESystem, dvs = unknowns(sys), simplify = false, sparse = false, kwargs...) jac = calculate_control_jacobian(sys; simplify = simplify, sparse = sparse) p = reorder_parameters(sys, ps) - return build_function(jac, dvs, p..., get_iv(sys); kwargs...) + return build_function_wrapper(sys, jac, dvs, p..., get_iv(sys); kwargs...) end function generate_dae_jacobian(sys::AbstractODESystem, dvs = unknowns(sys), From f183e8ebfa763e2070e096ff6ea633318b0fba75 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 30 Jan 2025 11:57:25 +0530 Subject: [PATCH 0712/1337] refactor: use `build_function_wrapper` in `generate_dae_jacobian` --- src/systems/diffeqs/abstractodesystem.jl | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index 4f36a3c100..9d57761af0 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -147,13 +147,9 @@ function generate_dae_jacobian(sys::AbstractODESystem, dvs = unknowns(sys), @variables ˍ₋gamma jac = ˍ₋gamma * jac_du + jac_u pre = get_preprocess_constants(jac) - p = if has_index_cache(sys) && get_index_cache(sys) !== nothing - reorder_parameters(get_index_cache(sys), ps) - else - (ps,) - end - return build_function(jac, derivatives, dvs, p..., ˍ₋gamma, get_iv(sys); - postprocess_fbody = pre, kwargs...) + p = reorder_parameters(sys, ps) + return build_function_wrapper(sys, jac, derivatives, dvs, p..., ˍ₋gamma, get_iv(sys); + p_start = 3, p_end = 2 + length(p), kwargs...) end function generate_function(sys::AbstractODESystem, dvs = unknowns(sys), From d162999bde58707eaf22f7d4da6afc53e533f13e Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 30 Jan 2025 11:57:59 +0530 Subject: [PATCH 0713/1337] refactor: use `build_function_wrapper` in `generate_function(::AbstractODESystem)` --- src/systems/diffeqs/abstractodesystem.jl | 42 ++++-------------------- 1 file changed, 6 insertions(+), 36 deletions(-) diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index 9d57761af0..e422404b44 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -158,55 +158,25 @@ function generate_function(sys::AbstractODESystem, dvs = unknowns(sys), ddvs = implicit_dae ? map(Differential(get_iv(sys)), dvs) : nothing, isdde = false, - wrap_code = identity, kwargs...) - if isdde - issplit = has_index_cache(sys) && get_index_cache(sys) !== nothing - eqs = delay_to_function( - sys; history_arg = issplit ? MTKPARAMETERS_ARG : DEFAULT_PARAMS_ARG) - else - eqs = [eq for eq in equations(sys)] - end + eqs = [eq for eq in equations(sys)] if !implicit_dae check_operator_variables(eqs, Differential) check_lhs(eqs, Differential, Set(dvs)) end - # substitute constants in - eqs = map(subs_constants, eqs) - - # substitute x(t) by just x rhss = implicit_dae ? [_iszero(eq.lhs) ? eq.rhs : eq.rhs - eq.lhs for eq in eqs] : [eq.rhs for eq in eqs] # TODO: add an optional check on the ordering of observed equations - 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)) + u = dvs + p = reorder_parameters(sys, ps) t = get_iv(sys) - if isdde - build_function(rhss, u, DDE_HISTORY_FUN, p..., t; kwargs..., - wrap_code = wrap_code .∘ wrap_mtkparameters(sys, false, 3) .∘ - wrap_array_vars(sys, rhss; dvs, ps, history = true) .∘ - wrap_parameter_dependencies(sys, false)) + if implicit_dae + build_function_wrapper(sys, rhss, ddvs, u, p..., t; p_start = 3, kwargs...) else - pre, sol_states = get_substitutions_and_solved_unknowns(sys) - - if implicit_dae - # inputs = [] makes `wrap_array_vars` offset by 1 since there is an extra - # argument - build_function(rhss, ddvs, u, p..., t; postprocess_fbody = pre, - states = sol_states, - wrap_code = wrap_code .∘ wrap_array_vars(sys, rhss; dvs, ps, inputs = []) .∘ - wrap_parameter_dependencies(sys, false), - kwargs...) - else - build_function(rhss, u, p..., t; postprocess_fbody = pre, - states = sol_states, - wrap_code = wrap_code .∘ wrap_array_vars(sys, rhss; dvs, ps) .∘ - wrap_parameter_dependencies(sys, false), - kwargs...) - end + build_function_wrapper(sys, rhss, u, p..., t; kwargs...) end end From 7010d12d0cb2ede57830b677588eac518701433b Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 30 Jan 2025 11:58:19 +0530 Subject: [PATCH 0714/1337] refactor: use new codegen in `ODEFunction` --- src/systems/diffeqs/abstractodesystem.jl | 24 +++--------------------- 1 file changed, 3 insertions(+), 21 deletions(-) diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index e422404b44..86049ba303 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -317,12 +317,6 @@ function DiffEqBase.ODEFunction{iip, specialize}(sys::AbstractODESystem, f(u, p, t) = f_oop(u, p, t) f(du, u, p, t) = f_iip(du, u, p, t) - f(u, p::Tuple{Vararg{Number}}, t) = f_oop(u, p, t) - f(du, u, p::Tuple{Vararg{Number}}, t) = f_iip(du, u, p, t) - f(u, p::Tuple, t) = f_oop(u, p..., t) - f(du, u, p::Tuple, t) = f_iip(du, u, p..., t) - f(u, p::MTKParameters, t) = f_oop(u, p..., t) - f(du, u, p::MTKParameters, t) = f_iip(du, u, p..., t) if specialize === SciMLBase.FunctionWrapperSpecialize && iip if u0 === nothing || p === nothing || t === nothing @@ -339,15 +333,9 @@ function DiffEqBase.ODEFunction{iip, specialize}(sys::AbstractODESystem, checkbounds = checkbounds, kwargs...) tgrad_oop, tgrad_iip = eval_or_rgf.(tgrad_gen; eval_expression, eval_module) - if p isa Tuple - __tgrad(u, p, t) = tgrad_oop(u, p..., t) - __tgrad(J, u, p, t) = tgrad_iip(J, u, p..., t) - _tgrad = __tgrad - else - ___tgrad(u, p, t) = tgrad_oop(u, p, t) - ___tgrad(J, u, p, t) = tgrad_iip(J, u, p, t) - _tgrad = ___tgrad - end + ___tgrad(u, p, t) = tgrad_oop(u, p, t) + ___tgrad(J, u, p, t) = tgrad_iip(J, u, p, t) + _tgrad = ___tgrad else _tgrad = nothing end @@ -362,12 +350,6 @@ function DiffEqBase.ODEFunction{iip, specialize}(sys::AbstractODESystem, _jac(u, p, t) = jac_oop(u, p, t) _jac(J, u, p, t) = jac_iip(J, u, p, t) - _jac(u, p::Tuple{Vararg{Number}}, t) = jac_oop(u, p, t) - _jac(J, u, p::Tuple{Vararg{Number}}, t) = jac_iip(J, u, p, t) - _jac(u, p::Tuple, t) = jac_oop(u, p..., t) - _jac(J, u, p::Tuple, t) = jac_iip(J, u, p..., t) - _jac(u, p::MTKParameters, t) = jac_oop(u, p..., t) - _jac(J, u, p::MTKParameters, t) = jac_iip(J, u, p..., t) else _jac = nothing end From 92364b8afb1c6de301f5151bbc99809ee890ff9f Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 30 Jan 2025 11:58:46 +0530 Subject: [PATCH 0715/1337] refactor: use new codegen in `DAEFunction` --- src/systems/diffeqs/abstractodesystem.jl | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index 86049ba303..faa78ef6ff 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -430,9 +430,7 @@ function DiffEqBase.DAEFunction{iip}(sys::AbstractODESystem, dvs = unknowns(sys) kwargs...) f_oop, f_iip = eval_or_rgf.(f_gen; eval_expression, eval_module) f(du, u, p, t) = f_oop(du, u, p, t) - f(du, u, p::MTKParameters, t) = f_oop(du, u, p..., t) f(out, du, u, p, t) = f_iip(out, du, u, p, t) - f(out, du, u, p::MTKParameters, t) = f_iip(out, du, u, p..., t) if jac jac_gen = generate_dae_jacobian(sys, dvs, ps; @@ -443,10 +441,7 @@ function DiffEqBase.DAEFunction{iip}(sys::AbstractODESystem, dvs = unknowns(sys) jac_oop, jac_iip = eval_or_rgf.(jac_gen; eval_expression, eval_module) _jac(du, u, p, ˍ₋gamma, t) = jac_oop(du, u, p, ˍ₋gamma, t) - _jac(du, u, p::MTKParameters, ˍ₋gamma, t) = jac_oop(du, u, p..., ˍ₋gamma, t) - _jac(J, du, u, p, ˍ₋gamma, t) = jac_iip(J, du, u, p, ˍ₋gamma, t) - _jac(J, du, u, p::MTKParameters, ˍ₋gamma, t) = jac_iip(J, du, u, p..., ˍ₋gamma, t) else _jac = nothing end From 141fcabd414dfebedddcaa1110f008f24686f85b Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 30 Jan 2025 11:58:59 +0530 Subject: [PATCH 0716/1337] refactor: use new codegen in `ODEFunctionClosure`, `DAEFunctionClosure` --- src/systems/diffeqs/abstractodesystem.jl | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index faa78ef6ff..4861677fa8 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -549,8 +549,6 @@ struct ODEFunctionClosure{O, I} <: Function end (f::ODEFunctionClosure)(u, p, t) = f.f_oop(u, p, t) (f::ODEFunctionClosure)(du, u, p, t) = f.f_iip(du, u, p, t) -(f::ODEFunctionClosure)(u, p::MTKParameters, t) = f.f_oop(u, p..., t) -(f::ODEFunctionClosure)(du, u, p::MTKParameters, t) = f.f_iip(du, u, p..., t) function ODEFunctionExpr{iip}(sys::AbstractODESystem, dvs = unknowns(sys), ps = parameters(sys), u0 = nothing; @@ -645,8 +643,6 @@ struct DAEFunctionClosure{O, I} <: Function end (f::DAEFunctionClosure)(du, u, p, t) = f.f_oop(du, u, p, t) (f::DAEFunctionClosure)(out, du, u, p, t) = f.f_iip(out, du, u, p, t) -(f::DAEFunctionClosure)(du, u, p::MTKParameters, t) = f.f_oop(du, u, p..., t) -(f::DAEFunctionClosure)(out, du, u, p::MTKParameters, t) = f.f_iip(out, du, u, p..., t) function DAEFunctionExpr{iip}(sys::AbstractODESystem, dvs = unknowns(sys), ps = parameters(sys), u0 = nothing; From a3ff92f41a188a262428a7edaf1b7ce9831fd13b Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 30 Jan 2025 11:59:15 +0530 Subject: [PATCH 0717/1337] refactor: use `build_function_wrapper` in `generate_diffusion_function` --- src/systems/diffeqs/sdesystem.jl | 20 ++------------------ 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/src/systems/diffeqs/sdesystem.jl b/src/systems/diffeqs/sdesystem.jl index 0426a77858..a017ebc6b1 100644 --- a/src/systems/diffeqs/sdesystem.jl +++ b/src/systems/diffeqs/sdesystem.jl @@ -411,24 +411,8 @@ end function generate_diffusion_function(sys::SDESystem, dvs = unknowns(sys), ps = parameters(sys); isdde = false, kwargs...) eqs = get_noiseeqs(sys) - if isdde - eqs = delay_to_function(sys, eqs) - end - u = map(x -> time_varying_as_func(value(x), sys), dvs) - p = if has_index_cache(sys) && get_index_cache(sys) !== nothing - reorder_parameters(get_index_cache(sys), ps) - else - (map(x -> time_varying_as_func(value(x), sys), ps),) - end - if isdde - return build_function(eqs, u, DDE_HISTORY_FUN, p..., get_iv(sys); kwargs..., - wrap_code = get(kwargs, :wrap_code, identity) .∘ - wrap_mtkparameters(sys, false, 3) .∘ - wrap_array_vars(sys, eqs; dvs, ps, history = true) .∘ - wrap_parameter_dependencies(sys, false)) - else - return build_function(eqs, u, p..., get_iv(sys); kwargs...) - end + p = reorder_parameters(sys, ps) + return build_function_wrapper(sys, eqs, dvs, p..., get_iv(sys); kwargs...) end """ From b0fdacfabb63d527f5bba2c459bb981f21bb8945 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 30 Jan 2025 11:59:33 +0530 Subject: [PATCH 0718/1337] refactor: use new codegen in `SDEFunction` --- src/systems/diffeqs/sdesystem.jl | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/systems/diffeqs/sdesystem.jl b/src/systems/diffeqs/sdesystem.jl index a017ebc6b1..212e20d743 100644 --- a/src/systems/diffeqs/sdesystem.jl +++ b/src/systems/diffeqs/sdesystem.jl @@ -596,13 +596,9 @@ function DiffEqBase.SDEFunction{iip, specialize}(sys::SDESystem, dvs = unknowns( g_oop, g_iip = eval_or_rgf.(g_gen; eval_expression, eval_module) f(u, p, t) = f_oop(u, p, t) - f(u, p::MTKParameters, t) = f_oop(u, p..., t) f(du, u, p, t) = f_iip(du, u, p, t) - f(du, u, p::MTKParameters, t) = f_iip(du, u, p..., t) g(u, p, t) = g_oop(u, p, t) - g(u, p::MTKParameters, t) = g_oop(u, p..., t) g(du, u, p, t) = g_iip(du, u, p, t) - g(du, u, p::MTKParameters, t) = g_iip(du, u, p..., t) if tgrad tgrad_gen = generate_tgrad(sys, dvs, ps; expression = Val{true}, @@ -610,9 +606,7 @@ function DiffEqBase.SDEFunction{iip, specialize}(sys::SDESystem, dvs = unknowns( tgrad_oop, tgrad_iip = eval_or_rgf.(tgrad_gen; eval_expression, eval_module) _tgrad(u, p, t) = tgrad_oop(u, p, t) - _tgrad(u, p::MTKParameters, t) = tgrad_oop(u, p..., t) _tgrad(J, u, p, t) = tgrad_iip(J, u, p, t) - _tgrad(J, u, p::MTKParameters, t) = tgrad_iip(J, u, p..., t) else _tgrad = nothing end @@ -623,9 +617,7 @@ function DiffEqBase.SDEFunction{iip, specialize}(sys::SDESystem, dvs = unknowns( jac_oop, jac_iip = eval_or_rgf.(jac_gen; eval_expression, eval_module) _jac(u, p, t) = jac_oop(u, p, t) - _jac(u, p::MTKParameters, t) = jac_oop(u, p..., t) _jac(J, u, p, t) = jac_iip(J, u, p, t) - _jac(J, u, p::MTKParameters, t) = jac_iip(J, u, p..., t) else _jac = nothing end @@ -637,13 +629,9 @@ function DiffEqBase.SDEFunction{iip, specialize}(sys::SDESystem, dvs = unknowns( Wfact_oop_t, Wfact_iip_t = eval_or_rgf.(tmp_Wfact_t; eval_expression, eval_module) _Wfact(u, p, dtgamma, t) = Wfact_oop(u, p, dtgamma, t) - _Wfact(u, p::MTKParameters, dtgamma, t) = Wfact_oop(u, p..., dtgamma, t) _Wfact(W, u, p, dtgamma, t) = Wfact_iip(W, u, p, dtgamma, t) - _Wfact(W, u, p::MTKParameters, dtgamma, t) = Wfact_iip(W, u, p..., dtgamma, t) _Wfact_t(u, p, dtgamma, t) = Wfact_oop_t(u, p, dtgamma, t) - _Wfact_t(u, p::MTKParameters, dtgamma, t) = Wfact_oop_t(u, p..., dtgamma, t) _Wfact_t(W, u, p, dtgamma, t) = Wfact_iip_t(W, u, p, dtgamma, t) - _Wfact_t(W, u, p::MTKParameters, dtgamma, t) = Wfact_iip_t(W, u, p..., dtgamma, t) else _Wfact, _Wfact_t = nothing, nothing end From cff74a9f5b8a496bb53b0e4ef90b6989397eb13f Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 30 Jan 2025 11:59:51 +0530 Subject: [PATCH 0719/1337] refactor: use `build_function_wrapper` in `generate_jacobian(::NonlinearSystem)` --- src/systems/nonlinear/nonlinearsystem.jl | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/systems/nonlinear/nonlinearsystem.jl b/src/systems/nonlinear/nonlinearsystem.jl index 58f61d3b22..6ffac624b2 100644 --- a/src/systems/nonlinear/nonlinearsystem.jl +++ b/src/systems/nonlinear/nonlinearsystem.jl @@ -249,14 +249,10 @@ end function generate_jacobian( sys::NonlinearSystem, vs = unknowns(sys), ps = parameters(sys); - sparse = false, simplify = false, wrap_code = identity, kwargs...) + sparse = false, simplify = false, kwargs...) jac = calculate_jacobian(sys, sparse = sparse, simplify = simplify) - pre, sol_states = get_substitutions_and_solved_unknowns(sys) p = reorder_parameters(sys, ps) - wrap_code = wrap_code .∘ wrap_array_vars(sys, jac; dvs = vs, ps) .∘ - wrap_parameter_dependencies(sys, false) - return build_function( - jac, vs, p...; postprocess_fbody = pre, states = sol_states, wrap_code, kwargs...) + return build_function_wrapper(sys, jac, vs, p...; kwargs...) end function calculate_hessian(sys::NonlinearSystem; sparse = false, simplify = false) From 517c1a51f21fc9ecf7e60b2eed6be5530b672eaa Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 30 Jan 2025 12:00:08 +0530 Subject: [PATCH 0720/1337] refactor: use `build_function_wrapper` in `generate_hessian(::NonlinearSystem)` --- src/systems/nonlinear/nonlinearsystem.jl | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/systems/nonlinear/nonlinearsystem.jl b/src/systems/nonlinear/nonlinearsystem.jl index 6ffac624b2..7a0ba020ae 100644 --- a/src/systems/nonlinear/nonlinearsystem.jl +++ b/src/systems/nonlinear/nonlinearsystem.jl @@ -269,13 +269,10 @@ end function generate_hessian( sys::NonlinearSystem, vs = unknowns(sys), ps = parameters(sys); - sparse = false, simplify = false, wrap_code = identity, kwargs...) + sparse = false, simplify = false, kwargs...) hess = calculate_hessian(sys, sparse = sparse, simplify = simplify) - pre = get_preprocess_constants(hess) p = reorder_parameters(sys, ps) - wrap_code = wrap_code .∘ wrap_array_vars(sys, hess; dvs = vs, ps) .∘ - wrap_parameter_dependencies(sys, false) - return build_function(hess, vs, p...; postprocess_fbody = pre, wrap_code, kwargs...) + return build_function_wrapper(sys, hess, vs, p...; kwargs...) end function generate_function( From 4c7cffd4042e1d8b7bb1f06558f2ce08b0503c25 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 30 Jan 2025 12:00:33 +0530 Subject: [PATCH 0721/1337] refactor: use `build_function_wrapper` in `generate_function(::NonlinearSystem)` --- src/systems/nonlinear/nonlinearsystem.jl | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/systems/nonlinear/nonlinearsystem.jl b/src/systems/nonlinear/nonlinearsystem.jl index 7a0ba020ae..e7eeffb801 100644 --- a/src/systems/nonlinear/nonlinearsystem.jl +++ b/src/systems/nonlinear/nonlinearsystem.jl @@ -277,19 +277,15 @@ end function generate_function( sys::NonlinearSystem, dvs = unknowns(sys), ps = parameters(sys); - wrap_code = identity, scalar = false, kwargs...) + scalar = false, kwargs...) rhss = [deq.rhs for deq in equations(sys)] dvs′ = value.(dvs) if scalar rhss = only(rhss) dvs′ = only(dvs) end - pre, sol_states = get_substitutions_and_solved_unknowns(sys) - wrap_code = wrap_code .∘ wrap_array_vars(sys, rhss; dvs, ps) .∘ - wrap_parameter_dependencies(sys, scalar) p = reorder_parameters(sys, value.(ps)) - return build_function(rhss, dvs′, p...; postprocess_fbody = pre, - states = sol_states, wrap_code, kwargs...) + return build_function_wrapper(sys, rhss, dvs′, p...; kwargs...) end function jacobian_sparsity(sys::NonlinearSystem) From 0bc240d3994509606cbb875ea740e79384f3bcd4 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 30 Jan 2025 12:00:50 +0530 Subject: [PATCH 0722/1337] refactor: use new codegen in `NonlinearFunction`, `IntervalNonlinearFunction` --- src/systems/nonlinear/nonlinearsystem.jl | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/systems/nonlinear/nonlinearsystem.jl b/src/systems/nonlinear/nonlinearsystem.jl index e7eeffb801..08ede84033 100644 --- a/src/systems/nonlinear/nonlinearsystem.jl +++ b/src/systems/nonlinear/nonlinearsystem.jl @@ -341,9 +341,7 @@ function SciMLBase.NonlinearFunction{iip}(sys::NonlinearSystem, dvs = unknowns(s f_gen = generate_function(sys, dvs, ps; expression = Val{true}, kwargs...) f_oop, f_iip = eval_or_rgf.(f_gen; eval_expression, eval_module) f(u, p) = f_oop(u, p) - f(u, p::MTKParameters) = f_oop(u, p...) f(du, u, p) = f_iip(du, u, p) - f(du, u, p::MTKParameters) = f_iip(du, u, p...) if jac jac_gen = generate_jacobian(sys, dvs, ps; @@ -351,9 +349,7 @@ function SciMLBase.NonlinearFunction{iip}(sys::NonlinearSystem, dvs = unknowns(s expression = Val{true}, kwargs...) jac_oop, jac_iip = eval_or_rgf.(jac_gen; eval_expression, eval_module) _jac(u, p) = jac_oop(u, p) - _jac(u, p::MTKParameters) = jac_oop(u, p...) _jac(J, u, p) = jac_iip(J, u, p) - _jac(J, u, p::MTKParameters) = jac_iip(J, u, p...) else _jac = nothing end @@ -397,9 +393,7 @@ function SciMLBase.IntervalNonlinearFunction( f_gen = generate_function( sys, dvs, ps; expression = Val{true}, scalar = true, kwargs...) - f_oop = eval_or_rgf(f_gen; eval_expression, eval_module) - f(u, p) = f_oop(u, p) - f(u, p::MTKParameters) = f_oop(u, p...) + f = eval_or_rgf(f_gen; eval_expression, eval_module) observedfun = ObservedFunctionCache( sys; eval_expression, eval_module, checkbounds = get(kwargs, :checkbounds, false)) From de83f7056bd7ce04c72bca92f7fbb59f566eca05 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 30 Jan 2025 11:43:35 +0530 Subject: [PATCH 0723/1337] fix: handle extra constants in `build_function_wrapper` --- src/systems/codegen_utils.jl | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/systems/codegen_utils.jl b/src/systems/codegen_utils.jl index 35a7c579a2..6ccdc77034 100644 --- a/src/systems/codegen_utils.jl +++ b/src/systems/codegen_utils.jl @@ -52,7 +52,13 @@ function build_function_wrapper(sys::AbstractSystem, expr, args...; p_start = 2, pdeps = parameter_dependencies(sys) cmap, _ = get_cmap(sys) - obsidxs = observed_equations_used_by(sys, expr) + extra_constants = collect_constants(expr) + filter!(extra_constants) do c + !any(x -> isequal(c, x.lhs), cmap) + end + for c in extra_constants + push!(cmap, c ~ getdefault(c)) + end pdepidxs = observed_equations_used_by(sys, expr; obs = pdeps) assignments = array_variable_assignments(args...) From a845c2bd3a56e1ea1a7cc128cc512d2f25d50597 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 30 Jan 2025 11:45:12 +0530 Subject: [PATCH 0724/1337] feat: better observed handling, optional `DestructuredArgs` binding in `build_function_wrapper` --- src/systems/codegen_utils.jl | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/systems/codegen_utils.jl b/src/systems/codegen_utils.jl index 6ccdc77034..c06a6d1462 100644 --- a/src/systems/codegen_utils.jl +++ b/src/systems/codegen_utils.jl @@ -45,10 +45,10 @@ function array_variable_assignments(args...) return assignments end -function build_function_wrapper(sys::AbstractSystem, expr, args...; p_start = 2, p_end = is_time_dependent(sys) ? length(args) - 1 : length(args), wrap_delays = is_dde(sys), wrap_code = identity, kwargs...) +function build_function_wrapper(sys::AbstractSystem, expr, args...; p_start = 2, p_end = is_time_dependent(sys) ? length(args) - 1 : length(args), wrap_delays = is_dde(sys), wrap_code = identity, add_observed = true, filter_observed = Returns(true), create_bindings = true, kwargs...) isscalar = !(expr isa AbstractArray || symbolic_type(expr) == ArraySymbolic()) - obs = observed(sys) + obs = filter(filter_observed, observed(sys)) pdeps = parameter_dependencies(sys) cmap, _ = get_cmap(sys) @@ -59,6 +59,11 @@ function build_function_wrapper(sys::AbstractSystem, expr, args...; p_start = 2, for c in extra_constants push!(cmap, c ~ getdefault(c)) end + if add_observed + obsidxs = observed_equations_used_by(sys, expr) + else + obsidxs = Int[] + end pdepidxs = observed_equations_used_by(sys, expr; obs = pdeps) assignments = array_variable_assignments(args...) @@ -70,7 +75,7 @@ function build_function_wrapper(sys::AbstractSystem, expr, args...; p_start = 2, args = ntuple(Val(length(args))) do i arg = args[i] if symbolic_type(arg) == NotSymbolic() && arg isa AbstractArray - DestructuredArgs(arg, generated_argument_name(i)) + DestructuredArgs(arg, generated_argument_name(i); create_bindings) else arg end @@ -80,7 +85,10 @@ function build_function_wrapper(sys::AbstractSystem, expr, args...; p_start = 2, if p_start > p_end args = (args[1:p_start-1]..., MTKPARAMETERS_ARG, args[p_end+1:end]...) else - args = (args[1:p_start-1]..., DestructuredArgs(collect(args[p_start:p_end]), MTKPARAMETERS_ARG), args[p_end+1:end]...) + # cannot apply `create_bindings` here since it doesn't nest + args = (args[1:(p_start - 1)]..., + DestructuredArgs(collect(args[p_start:p_end]), MTKPARAMETERS_ARG), + args[(p_end + 1):end]...) end end From 60d501bb1995b11a29d83bc3ac5ee094499fab66 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 30 Jan 2025 11:45:30 +0530 Subject: [PATCH 0725/1337] fix: use `time_varying_as_func` in `build_function_wrapper` --- src/systems/codegen_utils.jl | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/systems/codegen_utils.jl b/src/systems/codegen_utils.jl index c06a6d1462..97ba818083 100644 --- a/src/systems/codegen_utils.jl +++ b/src/systems/codegen_utils.jl @@ -74,6 +74,14 @@ function build_function_wrapper(sys::AbstractSystem, expr, args...; p_start = 2, args = ntuple(Val(length(args))) do i arg = args[i] + if is_time_dependent(sys) + arg = if symbolic_type(arg) == NotSymbolic() + arg isa AbstractArray ? + map(x -> time_varying_as_func(unwrap(x), sys), arg) : arg + else + time_varying_as_func(unwrap(arg), sys) + end + end if symbolic_type(arg) == NotSymbolic() && arg isa AbstractArray DestructuredArgs(arg, generated_argument_name(i); create_bindings) else From 1cab1c319392a5f923455d2ba8cfc406c6afaa8a Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 30 Jan 2025 11:46:43 +0530 Subject: [PATCH 0726/1337] feat: better delay handling, preface handling in `build_function_wrapper` --- src/systems/codegen_utils.jl | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/systems/codegen_utils.jl b/src/systems/codegen_utils.jl index 97ba818083..35526c3c0e 100644 --- a/src/systems/codegen_utils.jl +++ b/src/systems/codegen_utils.jl @@ -49,6 +49,16 @@ function build_function_wrapper(sys::AbstractSystem, expr, args...; p_start = 2, isscalar = !(expr isa AbstractArray || symbolic_type(expr) == ArraySymbolic()) obs = filter(filter_observed, observed(sys)) + if wrap_delays + history_arg = is_split(sys) ? MTKPARAMETERS_ARG : generated_argument_name(p_start) + obs = map(obs) do eq + delay_to_function(sys, eq; history_arg) + end + expr = delay_to_function(sys, expr; history_arg) + args = (args[1:p_start-1]..., DDE_HISTORY_FUN, args[p_start:end]...) + p_start += 1 + p_end += 1 + end pdeps = parameter_dependencies(sys) cmap, _ = get_cmap(sys) @@ -100,11 +110,11 @@ function build_function_wrapper(sys::AbstractSystem, expr, args...; p_start = 2, end end - wrap_code = wrap_code .∘ wrap_assignments(isscalar, assignments) - - if wrap_delays - expr = delay_to_function(sys, expr; history_arg = is_split(sys) ? MTKPARAMETERS_ARG : generated_argument_name(p_start)) + if has_preface(sys) && (pref = preface(sys)) !== nothing + append!(assignments, pref) end + wrap_code = wrap_code .∘ wrap_assignments(isscalar, assignments) + return build_function(expr, args...; wrap_code, kwargs...) end From 106b05a356ee501ea6d40a0825474479e9a4b091 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 30 Jan 2025 11:49:14 +0530 Subject: [PATCH 0727/1337] refactor: use `build_function_wrapper` in callbacks --- src/systems/callbacks.jl | 88 ++++++++-------------------------------- 1 file changed, 17 insertions(+), 71 deletions(-) diff --git a/src/systems/callbacks.jl b/src/systems/callbacks.jl index 492b5dac8c..a58ff3f8ec 100644 --- a/src/systems/callbacks.jl +++ b/src/systems/callbacks.jl @@ -519,70 +519,19 @@ end # handles ensuring that affect! functions work with integrator arguments function add_integrator_header( sys::AbstractSystem, integrator = gensym(:MTKIntegrator), out = :u) - if has_index_cache(sys) && get_index_cache(sys) !== nothing - function (expr) - p = gensym(:p) - Func( - [ - DestructuredArgs([expr.args[1], p, expr.args[end]], - integrator, inds = [:u, :p, :t]) - ], - [], - Let( - [DestructuredArgs([arg.name for arg in expr.args[2:(end - 1)]], p), - expr.args[2:(end - 1)]...], - expr.body, - false) - ) - end, - function (expr) - p = gensym(:p) - Func( - [ - DestructuredArgs([expr.args[1], expr.args[2], p, expr.args[end]], - integrator, inds = [out, :u, :p, :t]) - ], - [], - Let( - [DestructuredArgs([arg.name for arg in expr.args[3:(end - 1)]], p), - expr.args[3:(end - 1)]...], - expr.body, - false) - ) - end - else - 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 + 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 function condition_header(sys::AbstractSystem, integrator = gensym(:MTKIntegrator)) - if has_index_cache(sys) && get_index_cache(sys) !== nothing - function (expr) - p = gensym(:p) - res = Func( - [expr.args[1], expr.args[2], - DestructuredArgs([p], integrator, inds = [:p])], - [], - Let( - [ - DestructuredArgs([arg.name for arg in expr.args[3:end]], p), - expr.args[3:end]... - ], expr.body, false - ) - ) - return res - end - else - expr -> Func( - [expr.args[1], expr.args[2], - DestructuredArgs(expr.args[3:end], integrator, inds = [:p])], - [], - expr.body) - end + expr -> Func( + [expr.args[1], expr.args[2], + DestructuredArgs(expr.args[3:end], integrator, inds = [:p])], + [], + expr.body) end function callback_save_header(sys::AbstractSystem, cb) @@ -628,11 +577,10 @@ function compile_condition(cb::SymbolicDiscreteCallback, sys, dvs, ps; cmap = map(x -> x => getdefault(x), cs) condit = substitute(condit, cmap) end - expr = build_function( + expr = build_function_wrapper(sys, condit, u, t, p...; expression = Val{true}, - wrap_code = condition_header(sys) .∘ - wrap_array_vars(sys, condit; dvs, ps, inputs = true) .∘ - wrap_parameter_dependencies(sys, !(condit isa AbstractArray)), + p_start = 3, p_end = length(p) + 2, + wrap_code = condition_header(sys), kwargs...) if expression == Val{true} return expr @@ -715,14 +663,12 @@ function compile_affect(eqs::Vector{Equation}, cb, sys, dvs, ps; outputidxs = no end t = get_iv(sys) integ = gensym(:MTKIntegrator) - pre = get_preprocess_constants(rhss) - rf_oop, rf_ip = build_function(rhss, u, p..., t; expression = Val{true}, + 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) .∘ - wrap_array_vars(sys, rhss; dvs, ps = _ps) .∘ - wrap_parameter_dependencies(sys, false), + add_integrator_header(sys, integ, outvar), outputidxs = update_inds, - postprocess_fbody = pre, + create_bindings = false, kwargs...) # applied user-provided function to the generated expression if postprocess_affect_expr! !== nothing From d6607f91b3604566f0c96ea99940e4d0d4928e7c Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 30 Jan 2025 11:49:43 +0530 Subject: [PATCH 0728/1337] refactor: use `build_function_wrapper` in `generate_custom_function` --- src/systems/abstractsystem.jl | 26 +++----------------------- 1 file changed, 3 insertions(+), 23 deletions(-) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index eddcdc6266..e63364cbf0 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -161,7 +161,7 @@ time-independent systems. If `split=true` (the default) was passed to [`complete object. """ function generate_custom_function(sys::AbstractSystem, exprs, dvs = unknowns(sys), - ps = parameters(sys); wrap_code = nothing, postprocess_fbody = nothing, states = nothing, + ps = parameters(sys); expression = Val{true}, eval_expression = false, eval_module = @__MODULE__, cachesyms::Tuple = (), kwargs...) if !iscomplete(sys) @@ -169,39 +169,19 @@ function generate_custom_function(sys::AbstractSystem, exprs, dvs = unknowns(sys end p = (reorder_parameters(sys, unwrap.(ps))..., cachesyms...) isscalar = !(exprs isa AbstractArray) - if wrap_code === nothing - wrap_code = isscalar ? identity : (identity, identity) - end - pre, sol_states = get_substitutions_and_solved_unknowns(sys, isscalar ? [exprs] : exprs) - if postprocess_fbody === nothing - postprocess_fbody = pre - end - if states === nothing - states = sol_states - end fnexpr = if is_time_dependent(sys) - build_function(exprs, + build_function_wrapper(sys, exprs, dvs, p..., get_iv(sys); kwargs..., - postprocess_fbody, - states, - wrap_code = wrap_code .∘ wrap_mtkparameters(sys, isscalar) .∘ - wrap_array_vars(sys, exprs; dvs, cachesyms) .∘ - wrap_parameter_dependencies(sys, isscalar), expression = Val{true} ) else - build_function(exprs, + build_function_wrapper(sys, exprs, dvs, p...; kwargs..., - postprocess_fbody, - states, - wrap_code = wrap_code .∘ wrap_mtkparameters(sys, isscalar) .∘ - wrap_array_vars(sys, exprs; dvs, cachesyms) .∘ - wrap_parameter_dependencies(sys, isscalar), expression = Val{true} ) end From f9a50274dacb77dcbb9d5350d833811421607aef Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 30 Jan 2025 11:51:21 +0530 Subject: [PATCH 0729/1337] fix: don't consider parameters as delays --- src/systems/diffeqs/abstractodesystem.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index 4861677fa8..a80e5db5b2 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -183,6 +183,7 @@ end function isdelay(var, iv) iv === nothing && return false isvariable(var) || return false + isparameter(var) && return false if iscall(var) && !ModelingToolkit.isoperator(var, Symbolics.Operator) args = arguments(var) length(args) == 1 || return false From 5637a0cafb037e42023759f37c48caab152636f8 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 30 Jan 2025 11:51:48 +0530 Subject: [PATCH 0730/1337] refactor: use `build_function_wrapper` in `SymbolicTstops` --- src/systems/diffeqs/abstractodesystem.jl | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index a80e5db5b2..749cef2766 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -675,7 +675,7 @@ struct SymbolicTstops{F} end function (st::SymbolicTstops)(p, tspan) - unique!(sort!(reduce(vcat, st.fn(p..., tspan...)))) + unique!(sort!(reduce(vcat, st.fn(p, tspan...)))) end function SymbolicTstops( @@ -692,13 +692,12 @@ function SymbolicTstops( end end rps = reorder_parameters(sys, parameters(sys)) - tstops, _ = build_function(tstops, + tstops, _ = build_function_wrapper(sys, tstops, rps..., t0, t1; expression = Val{true}, - wrap_code = wrap_array_vars(sys, tstops; dvs = nothing) .∘ - wrap_parameter_dependencies(sys, false)) + p_start = 1, p_end = length(rps), add_observed = false, force_SA = true) tstops = eval_or_rgf(tstops; eval_expression, eval_module) return SymbolicTstops(tstops) end From 6491c171e3dcd98ee1e186fee570e63bab226dc5 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 30 Jan 2025 11:52:08 +0530 Subject: [PATCH 0731/1337] refactor: use `build_function_wrapper` for history functions --- src/systems/diffeqs/abstractodesystem.jl | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index 749cef2766..d9b8fb31ab 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -818,7 +818,9 @@ end function generate_history(sys::AbstractODESystem, u0; expression = Val{false}, kwargs...) p = reorder_parameters(sys, parameters(sys)) - build_function(u0, p..., get_iv(sys); expression, kwargs...) + build_function_wrapper( + sys, u0, p..., get_iv(sys); expression, p_start = 1, p_end = length(p), + similarto = typeof(u0), wrap_delays = false, kwargs...) end function DiffEqBase.DDEProblem(sys::AbstractODESystem, args...; kwargs...) @@ -842,8 +844,7 @@ function DiffEqBase.DDEProblem{iip}(sys::AbstractODESystem, u0map = [], check_length, eval_expression, eval_module, kwargs...) h_gen = generate_history(sys, u0; expression = Val{true}) h_oop, h_iip = eval_or_rgf.(h_gen; eval_expression, eval_module) - h(p, t) = h_oop(p, t) - h(p::MTKParameters, t) = h_oop(p..., t) + h = h_oop u0 = float.(h(p, tspan[1])) if u0 !== nothing u0 = u0_constructor(u0) @@ -881,10 +882,7 @@ function DiffEqBase.SDDEProblem{iip}(sys::AbstractODESystem, u0map = [], check_length, kwargs...) h_gen = generate_history(sys, u0; expression = Val{true}) h_oop, h_iip = eval_or_rgf.(h_gen; eval_expression, eval_module) - h(out, p, t) = h_iip(out, p, t) - h(p, t) = h_oop(p, t) - h(p::MTKParameters, t) = h_oop(p..., t) - h(out, p::MTKParameters, t) = h_iip(out, p..., t) + h = h_oop u0 = h(p, tspan[1]) if u0 !== nothing u0 = u0_constructor(u0) From ead141adca0250c35b2aea16f58cb605fa279984 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 30 Jan 2025 11:52:58 +0530 Subject: [PATCH 0732/1337] refactor: refactor `generate_function(::DiscreteSystem)` to use new codegen --- src/systems/discrete_system/discrete_system.jl | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/systems/discrete_system/discrete_system.jl b/src/systems/discrete_system/discrete_system.jl index bd5c72eec7..bd81255706 100644 --- a/src/systems/discrete_system/discrete_system.jl +++ b/src/systems/discrete_system/discrete_system.jl @@ -260,9 +260,7 @@ end function generate_function( sys::DiscreteSystem, dvs = unknowns(sys), ps = parameters(sys); wrap_code = identity, kwargs...) exprs = [eq.rhs for eq in equations(sys)] - wrap_code = wrap_code .∘ wrap_array_vars(sys, exprs) .∘ - wrap_parameter_dependencies(sys, false) - generate_custom_function(sys, exprs, dvs, ps; wrap_code, kwargs...) + generate_custom_function(sys, exprs, dvs, ps; kwargs...) end function shift_u0map_forward(sys::DiscreteSystem, u0map, defs) From dc099099827f3f03bbf4ca0717fd9c591daf5c56 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 30 Jan 2025 11:53:21 +0530 Subject: [PATCH 0733/1337] refactor: use `build_function_wrapper` in `JumpSystem` codegen --- src/systems/jumps/jumpsystem.jl | 20 +++++--------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/src/systems/jumps/jumpsystem.jl b/src/systems/jumps/jumpsystem.jl index f39b5fa46a..beaa78e1e3 100644 --- a/src/systems/jumps/jumpsystem.jl +++ b/src/systems/jumps/jumpsystem.jl @@ -283,10 +283,8 @@ function generate_rate_function(js::JumpSystem, rate) rate = substitute(rate, csubs) end p = reorder_parameters(js, parameters(js)) - rf = build_function(rate, unknowns(js), p..., + rf = build_function_wrapper(js, rate, unknowns(js), p..., get_iv(js), - wrap_code = wrap_array_vars(js, rate; dvs = unknowns(js), ps = parameters(js)) .∘ - wrap_parameter_dependencies(js, !(rate isa AbstractArray)), expression = Val{true}) end @@ -303,9 +301,7 @@ end function assemble_vrj( js, vrj, unknowntoid; eval_expression = false, eval_module = @__MODULE__) - _rate = eval_or_rgf(generate_rate_function(js, vrj.rate); eval_expression, eval_module) - rate(u, p, t) = _rate(u, p, t) - rate(u, p::MTKParameters, t) = _rate(u, p..., t) + rate = eval_or_rgf(generate_rate_function(js, vrj.rate); eval_expression, eval_module) outputvars = (value(affect.lhs) for affect in vrj.affect!) outputidxs = [unknowntoid[var] for var in outputvars] @@ -320,9 +316,7 @@ function assemble_vrj_expr(js, vrj, unknowntoid) outputidxs = ((unknowntoid[var] for var in outputvars)...,) affect = generate_affect_function(js, vrj.affect!, outputidxs) quote - _rate = $rate - rate(u, p, t) = _rate(u, p, t) - rate(u, p::MTKParameters, t) = _rate(u, p..., t) + rate = $rate affect = $affect VariableRateJump(rate, affect) @@ -331,9 +325,7 @@ end function assemble_crj( js, crj, unknowntoid; eval_expression = false, eval_module = @__MODULE__) - _rate = eval_or_rgf(generate_rate_function(js, crj.rate); eval_expression, eval_module) - rate(u, p, t) = _rate(u, p, t) - rate(u, p::MTKParameters, t) = _rate(u, p..., t) + rate = eval_or_rgf(generate_rate_function(js, crj.rate); eval_expression, eval_module) outputvars = (value(affect.lhs) for affect in crj.affect!) outputidxs = [unknowntoid[var] for var in outputvars] @@ -348,9 +340,7 @@ function assemble_crj_expr(js, crj, unknowntoid) outputidxs = ((unknowntoid[var] for var in outputvars)...,) affect = generate_affect_function(js, crj.affect!, outputidxs) quote - _rate = $rate - rate(u, p, t) = _rate(u, p, t) - rate(u, p::MTKParameters, t) = _rate(u, p..., t) + rate = $rate affect = $affect ConstantRateJump(rate, affect) From 95a75ab21dfebfb5ed5e12f7cca91c522ffc6148 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 30 Jan 2025 11:53:37 +0530 Subject: [PATCH 0734/1337] refactor: fix `linearize_symbolic` to respect new codegen --- src/systems/abstractsystem.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index e63364cbf0..aaa87fc02d 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -2558,7 +2558,7 @@ function linearize_symbolic(sys::AbstractSystem, inputs, fun_expr = generate_function(sys, sts, ps; expression = Val{true})[1] fun = eval_or_rgf(fun_expr; eval_expression, eval_module) - dx = fun(sts, p..., t) + dx = fun(sts, p, t) h = build_explicit_observed_function(sys, outputs; eval_expression, eval_module) y = h(sts, p, t) From 16000134b9615927c5457811b354fc45734f9071 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 30 Jan 2025 11:53:49 +0530 Subject: [PATCH 0735/1337] fix: refactor `modelingtoolkitize` to use new codegen --- src/systems/diffeqs/modelingtoolkitize.jl | 10 +++++----- src/systems/nonlinear/modelingtoolkitize.jl | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/systems/diffeqs/modelingtoolkitize.jl b/src/systems/diffeqs/modelingtoolkitize.jl index 2e0623bea1..7ab68e3bee 100644 --- a/src/systems/diffeqs/modelingtoolkitize.jl +++ b/src/systems/diffeqs/modelingtoolkitize.jl @@ -62,9 +62,9 @@ function modelingtoolkitize( fill!(rhs, 0) if prob.f isa ODEFunction && prob.f.f isa FunctionWrappersWrappers.FunctionWrappersWrapper - prob.f.f.fw[1].obj[](rhs, vars, params, t) + prob.f.f.fw[1].obj[](rhs, vars, p isa MTKParameters ? (params,) : params, t) else - prob.f(rhs, vars, params, t) + prob.f(rhs, vars, p isa MTKParameters ? (params,) : params, t) end else rhs = prob.f(vars, params, t) @@ -253,14 +253,14 @@ function modelingtoolkitize(prob::DiffEqBase.SDEProblem; kwargs...) if DiffEqBase.isinplace(prob) lhs = similar(vars, Any) - prob.f(lhs, vars, params, t) + prob.f(lhs, vars, p isa MTKParameters ? (params,) : params, t) if DiffEqBase.is_diagonal_noise(prob) neqs = similar(vars, Any) - prob.g(neqs, vars, params, t) + prob.g(neqs, vars, p isa MTKParameters ? (params,) : params, t) else neqs = similar(vars, Any, size(prob.noise_rate_prototype)) - prob.g(neqs, vars, params, t) + prob.g(neqs, vars, p isa MTKParameters ? (params,) : params, t) end else lhs = prob.f(vars, params, t) diff --git a/src/systems/nonlinear/modelingtoolkitize.jl b/src/systems/nonlinear/modelingtoolkitize.jl index 2f12157884..ec35318e3e 100644 --- a/src/systems/nonlinear/modelingtoolkitize.jl +++ b/src/systems/nonlinear/modelingtoolkitize.jl @@ -45,7 +45,7 @@ function modelingtoolkitize( eqs = vcat([0.0 ~ rhs[i] for i in 1:length(prob.f.resid_prototype)]...) else rhs = ArrayInterface.restructure(prob.u0, similar(vars, Num)) - prob.f(rhs, vars, params) + prob.f(rhs, vars, p isa MTKParameters ? (params,) : params) eqs = vcat([0.0 ~ rhs[i] for i in 1:length(rhs)]...) end From a42cec7a39dd5702cd2dd257879098e476b581a4 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 30 Jan 2025 11:54:03 +0530 Subject: [PATCH 0736/1337] test: use new codegen in index reduction test --- test/structural_transformation/index_reduction.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/structural_transformation/index_reduction.jl b/test/structural_transformation/index_reduction.jl index b94362bedc..d7f19e1fa2 100644 --- a/test/structural_transformation/index_reduction.jl +++ b/test/structural_transformation/index_reduction.jl @@ -62,7 +62,7 @@ first_order_idx1_pendulum = complete(ode_order_lowering(idx1_pendulum)) using OrdinaryDiffEq using LinearAlgebra -prob = ODEProblem(ODEFunction(first_order_idx1_pendulum), +prob = ODEProblem(first_order_idx1_pendulum, # [x, y, w, z, xˍt, yˍt, T] [1, 0, 0, 0, 0, 0, 0.0],# 0, 0, 0, 0], (0, 10.0), From 5e7b5838d93056ce0168ecff97faa843731ab985 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 30 Jan 2025 11:54:13 +0530 Subject: [PATCH 0737/1337] test: use new codegen in distributed test --- test/distributed.jl | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/distributed.jl b/test/distributed.jl index 3a53b9951e..0b75b8eeb3 100644 --- a/test/distributed.jl +++ b/test/distributed.jl @@ -15,12 +15,11 @@ addprocs(2) @everywhere @named de = ODESystem(eqs, t) @everywhere de = complete(de) -@everywhere ode_func = ODEFunction(de, [x, y, z], [σ, ρ, β]) @everywhere u0 = [19.0, 20.0, 50.0] @everywhere params = [16.0, 45.92, 4] -@everywhere ode_prob = ODEProblem(ode_func, u0, (0.0, 10.0), params) +@everywhere ode_prob = ODEProblem(de, u0, (0.0, 10.0), params) @everywhere begin using OrdinaryDiffEq From 8ce0eb2e4de2ab5decea8c02e458934691cd6821 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 30 Jan 2025 11:54:22 +0530 Subject: [PATCH 0738/1337] test: use new codegen in jumpsystem test --- test/jumpsystem.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/jumpsystem.jl b/test/jumpsystem.jl index 0fd6dc4af0..4cd5135939 100644 --- a/test/jumpsystem.jl +++ b/test/jumpsystem.jl @@ -104,8 +104,8 @@ sol = solve(jprob, SSAStepper(); saveat = 1.0) rate2(u, p, t) = 0.01u[2] jump2 = ConstantRateJump(rate2, affect2!) mtjumps = jprob.discrete_jump_aggregation -@test abs(mtjumps.rates[1](u, p, tf) - jump1.rate(u, p, tf)) < 10 * eps() -@test abs(mtjumps.rates[2](u, p, tf) - jump2.rate(u, p, tf)) < 10 * eps() +@test abs(mtjumps.rates[1](u, (p,), tf) - jump1.rate(u, p, tf)) < 10 * eps() +@test abs(mtjumps.rates[2](u, (p,), tf) - jump2.rate(u, p, tf)) < 10 * eps() mtjumps.affects![1](mtintegrator) jump1.affect!(integrator) @test all(integrator.u .== mtintegrator.u) From f9069b1e957347936c1e5aeb8431d14fd45443c7 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 30 Jan 2025 11:54:29 +0530 Subject: [PATCH 0739/1337] test: use new codegen in odesystem test --- test/odesystem.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/odesystem.jl b/test/odesystem.jl index 30465d2975..87c192afc2 100644 --- a/test/odesystem.jl +++ b/test/odesystem.jl @@ -216,7 +216,7 @@ end prob = ODEProblem(ODEFunction{false}(lotka), [1.0, 1.0], (0.0, 1.0), [1.5, 1.0, 3.0, 1.0]) de = complete(modelingtoolkitize(prob)) -ODEFunction(de)(similar(prob.u0), prob.u0, prob.p, 0.1) +ODEFunction(de)(similar(prob.u0), prob.u0, (prob.p,), 0.1) function lotka(du, u, p, t) x = u[1] @@ -228,7 +228,7 @@ end prob = ODEProblem(lotka, [1.0, 1.0], (0.0, 1.0), [1.5, 1.0, 3.0, 1.0]) de = complete(modelingtoolkitize(prob)) -ODEFunction(de)(similar(prob.u0), prob.u0, prob.p, 0.1) +ODEFunction(de)(similar(prob.u0), prob.u0, (prob.p,), 0.1) # automatic unknown detection for DAEs @parameters k₁ k₂ k₃ @@ -1269,7 +1269,7 @@ end t, [u..., x..., o...], [p...]) sys1, = structural_simplify(sys, ([x...], [])) fn1, = ModelingToolkit.generate_function(sys1; expression = Val{false}) - @test_nowarn fn1(ones(4), 2ones(2), 3ones(2, 2), 4.0) + @test_nowarn fn1(ones(4), (2ones(2), 3ones(2, 2)), 4.0) sys2, = structural_simplify(sys, ([x...], []); split = false) fn2, = ModelingToolkit.generate_function(sys2; expression = Val{false}) @test_nowarn fn2(ones(4), 2ones(6), 4.0) From ddce985c692df4ac11620d9d823c5a17acfd11e6 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 30 Jan 2025 11:54:37 +0530 Subject: [PATCH 0740/1337] test: use new codegen in sdesystem test --- test/sdesystem.jl | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/test/sdesystem.jl b/test/sdesystem.jl index 40b8747d39..0a30982bac 100644 --- a/test/sdesystem.jl +++ b/test/sdesystem.jl @@ -24,14 +24,14 @@ noiseeqs = [0.1 * x, @named de = SDESystem(eqs, noiseeqs, tt, [x, y, z], [σ, ρ, β], tspan = (0.0, 10.0)) de = complete(de) f = eval(generate_diffusion_function(de)[1]) -@test f(ones(3), rand(3), nothing) == 0.1ones(3) +@test f(ones(3), (rand(3),), nothing) == 0.1ones(3) f = SDEFunction(de) -prob = SDEProblem(SDEFunction(de), [1.0, 0.0, 0.0], (0.0, 100.0), (10.0, 26.0, 2.33)) +prob = SDEProblem(de, [1.0, 0.0, 0.0], (0.0, 100.0), [10.0, 26.0, 2.33]) sol = solve(prob, SRIW1(), seed = 1) -probexpr = SDEProblem(SDEFunction(de), [1.0, 0.0, 0.0], (0.0, 100.0), - (10.0, 26.0, 2.33)) +probexpr = SDEProblem(de, [1.0, 0.0, 0.0], (0.0, 100.0), + [10.0, 26.0, 2.33]) solexpr = solve(eval(probexpr), SRIW1(), seed = 1) @test all(x -> x == 0, Array(sol - solexpr)) @@ -43,13 +43,13 @@ noiseeqs_nd = [0.01*x 0.01*x*y 0.02*x*z de = complete(de) f = eval(generate_diffusion_function(de)[1]) p = MTKParameters(de, [σ => 0.1, ρ => 0.2, β => 0.3]) -@test f([1, 2, 3.0], p..., nothing) == [0.01*1 0.01*1*2 0.02*1*3 +@test f([1, 2, 3.0], p, nothing) == [0.01*1 0.01*1*2 0.02*1*3 0.1 0.01*2 0.02*1*3 0.2 0.3 0.01*3] f = eval(generate_diffusion_function(de)[2]) du = ones(3, 3) -f(du, [1, 2, 3.0], p..., nothing) +f(du, [1, 2, 3.0], p, nothing) @test du == [0.01*1 0.01*1*2 0.02*1*3 0.1 0.01*2 0.02*1*3 0.2 0.3 0.01*3] @@ -86,7 +86,7 @@ function test_SDEFunction_no_eval() # Need to test within a function scope to trigger world age issues f = SDEFunction(de, eval_expression = false) p = MTKParameters(de, [σ => 10.0, ρ => 26.0, β => 2.33]) - @test f([1.0, 0.0, 0.0], p..., (0.0, 100.0)) ≈ [-10.0, 26.0, 0.0] + @test f([1.0, 0.0, 0.0], p, (0.0, 100.0)) ≈ [-10.0, 26.0, 0.0] end test_SDEFunction_no_eval() From b8d9de5fad889863b5e10a82872c9eeeabf0b5e7 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 30 Jan 2025 12:59:29 +0530 Subject: [PATCH 0741/1337] test: use new codegen in labelledarrays tests --- test/labelledarrays.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/labelledarrays.jl b/test/labelledarrays.jl index c9ee7ee50b..75d5de6d56 100644 --- a/test/labelledarrays.jl +++ b/test/labelledarrays.jl @@ -19,7 +19,7 @@ ff = ODEFunction(de, [x, y, z], [σ, ρ, β], jac = true) a = @SVector [1.0, 2.0, 3.0] b = SLVector(x = 1.0, y = 2.0, z = 3.0) c = [1.0, 2.0, 3.0] -p = SLVector(σ = 10.0, ρ = 26.0, β = 8 / 3) +p = (SLVector(σ = 10.0, ρ = 26.0, β = 8 / 3),) @test ff(a, p, 0.0) isa SVector @test typeof(ff(b, p, 0.0)) <: SLArray @test ff(c, p, 0.0) isa Vector From 5fb0effe815bd9b7b697957aadd829ad0e9c3563 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 31 Jan 2025 12:59:30 +0530 Subject: [PATCH 0742/1337] fix: add `collect_constants!` fallback for `Symbol` --- src/utils.jl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/utils.jl b/src/utils.jl index e7259cc898..ec5323cba6 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -636,6 +636,8 @@ function collect_constants(x) return constants end +collect_constants!(::Any, ::Symbol) = nothing + function collect_constants!(constants, arr::AbstractArray) for el in arr collect_constants!(constants, el) From 3e33d96fa18d2b8e6b8b74d2b76b0fd1080eb196 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 31 Jan 2025 12:59:40 +0530 Subject: [PATCH 0743/1337] feat: support `get_cmap(::OptimizationSystem)` --- src/utils.jl | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/utils.jl b/src/utils.jl index ec5323cba6..9e90299a6c 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -739,7 +739,12 @@ end function get_cmap(sys, exprs = nothing) #Inject substitutions for constants => values - cs = collect_constants([collect(get_eqs(sys)); get_observed(sys)]) #ctrls? what else? + buffer = [] + has_eqs(sys) && append!(buffer, collect(get_eqs(sys))) + has_observed(sys) && append!(buffer, collect(get_observed(sys))) + has_op(sys) && push!(buffer, get_op(sys)) + has_constraints(sys) && append!(buffer, get_constraints(sys)) + cs = collect_constants(buffer) #ctrls? what else? if !empty_substitutions(sys) cs = [cs; collect_constants(get_substitutions(sys).subs)] end From 29ef316f9c97a0a64504996d89e327367eac30ae Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 31 Jan 2025 13:00:09 +0530 Subject: [PATCH 0744/1337] fix: handle array symbolics in `observed_equations_used_by` --- src/utils.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/utils.jl b/src/utils.jl index 9e90299a6c..cf49d9f445 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -1076,7 +1076,8 @@ function observed_equations_used_by(sys::AbstractSystem, exprs; obsidxs = BitSet() for sym in involved_vars - idx = findfirst(isequal(sym), obsvars) + arrsym = iscall(sym) && operation(sym) === getindex ? arguments(sym)[1] : nothing + idx = findfirst(v -> isequal(v, sym) || isequal(v, arrsym), obsvars) idx === nothing && continue idx in obsidxs && continue parents = dfs_parents(graph, idx) From 322b5db4b31fcf7b44c1f75e1f1684f95f98b78a Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 31 Jan 2025 13:00:39 +0530 Subject: [PATCH 0745/1337] feat: add output type support to `build_function_wrapper` --- src/systems/codegen_utils.jl | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/systems/codegen_utils.jl b/src/systems/codegen_utils.jl index 35526c3c0e..dae9f07292 100644 --- a/src/systems/codegen_utils.jl +++ b/src/systems/codegen_utils.jl @@ -45,7 +45,7 @@ function array_variable_assignments(args...) return assignments end -function build_function_wrapper(sys::AbstractSystem, expr, args...; p_start = 2, p_end = is_time_dependent(sys) ? length(args) - 1 : length(args), wrap_delays = is_dde(sys), wrap_code = identity, add_observed = true, filter_observed = Returns(true), create_bindings = true, kwargs...) +function build_function_wrapper(sys::AbstractSystem, expr, args...; p_start = 2, p_end = is_time_dependent(sys) ? length(args) - 1 : length(args), wrap_delays = is_dde(sys), wrap_code = identity, add_observed = true, filter_observed = Returns(true), create_bindings = true, output_type = nothing, mkarray = nothing, kwargs...) isscalar = !(expr isa AbstractArray || symbolic_type(expr) == ArraySymbolic()) obs = filter(filter_observed, observed(sys)) @@ -116,5 +116,19 @@ function build_function_wrapper(sys::AbstractSystem, expr, args...; p_start = 2, wrap_code = wrap_code .∘ wrap_assignments(isscalar, assignments) - return build_function(expr, args...; wrap_code, kwargs...) + similarto = nothing + if output_type === Tuple + expr = MakeTuple(Tuple(expr)) + wrap_code = wrap_code[1] + elseif mkarray === nothing + similarto = output_type + else + expr = mkarray(expr, output_type) + wrap_code = wrap_code[2] + end + + if wrap_code isa Tuple && symbolic_type(expr) == ScalarSymbolic() + wrap_code = wrap_code[1] + end + return build_function(expr, args...; wrap_code, similarto, kwargs...) end From 07f82061c5212d186e56be47f6f4801eb4562c5a Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 31 Jan 2025 13:00:53 +0530 Subject: [PATCH 0746/1337] fix: fix `all_symbols` not returning dependent parameters --- src/systems/abstractsystem.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index aaa87fc02d..0444877432 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -824,7 +824,7 @@ end function SymbolicIndexingInterface.all_symbols(sys::AbstractSystem) syms = all_variable_symbols(sys) - for other in (parameter_symbols(sys), independent_variable_symbols(sys)) + for other in (full_parameters(sys), independent_variable_symbols(sys)) isempty(other) || (syms = vcat(syms, other)) end return syms From 0774860a624b882b0e3f543aa1b815aed4e5734b Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 31 Jan 2025 13:02:08 +0530 Subject: [PATCH 0747/1337] feat: use `build_function_wrapper` in `build_explicit_observed_function` --- src/systems/diffeqs/odesystem.jl | 245 ++++++++----------------------- 1 file changed, 62 insertions(+), 183 deletions(-) diff --git a/src/systems/diffeqs/odesystem.jl b/src/systems/diffeqs/odesystem.jl index c95e9f19db..c735b52c37 100644 --- a/src/systems/diffeqs/odesystem.jl +++ b/src/systems/diffeqs/odesystem.jl @@ -429,215 +429,94 @@ function build_explicit_observed_function(sys, ts; param_only = false, op = Operator, throw = true, - mkarray = MakeArray) + mkarray = nothing) is_tuple = ts isa Tuple if is_tuple ts = collect(ts) + output_type = Tuple end - if (isscalar = symbolic_type(ts) !== NotSymbolic()) - ts = [ts] - end - ts = unwrap.(ts) - issplit = has_index_cache(sys) && get_index_cache(sys) !== nothing - if is_dde(sys) - if issplit - ts = map( - x -> delay_to_function( - sys, x; history_arg = issplit ? MTKPARAMETERS_ARG : DEFAULT_PARAMS_ARG), - ts) - else - ts = map(x -> delay_to_function(sys, x), ts) - end - end - - vars = Set() - foreach(v -> vars!(vars, v; op), ts) - ivs = independent_variables(sys) - dep_vars = scalarize(setdiff(vars, ivs)) - obs = observed(sys) - if param_only - if has_index_cache(sys) && (ic = get_index_cache(sys)) !== nothing - obs = filter(obs) do eq - !(ContinuousTimeseries() in ic.observed_syms_to_timeseries[eq.lhs]) - end - else - obs = Equation[] + allsyms = all_symbols(sys) + function symbol_to_symbolic(sym) + sym isa Symbol || return sym + idx = findfirst(x -> (hasname(x) ? getname(x) : Symbol(x)) == sym, allsyms) + idx === nothing && return sym + sym = allsyms[idx] + if iscall(sym) && operation(sym) == getindex + sym = arguments(sym)[1] end + return sym end - - cs = collect_constants(obs) - if !isempty(cs) > 0 - cmap = map(x -> x => getdefault(x), cs) - obs = map(x -> x.lhs ~ substitute(x.rhs, cmap), obs) + if symbolic_type(ts) == NotSymbolic() && ts isa AbstractArray + ts = map(symbol_to_symbolic, ts) + else + ts = symbol_to_symbolic(ts) end - sts = param_only ? Set() : Set(unknowns(sys)) - sts = param_only ? Set() : - union(sts, - Set(arguments(st)[1] for st in sts if iscall(st) && operation(st) === getindex)) - - observed_idx = Dict(x.lhs => i for (i, x) in enumerate(obs)) - param_set = Set(full_parameters(sys)) - param_set = union(param_set, - Set(arguments(p)[1] for p in param_set if iscall(p) && operation(p) === getindex)) - param_set_ns = Set(unknowns(sys, p) for p in full_parameters(sys)) - param_set_ns = union(param_set_ns, - Set(arguments(p)[1] - for p in param_set_ns if iscall(p) && operation(p) === getindex)) - namespaced_to_obs = Dict(unknowns(sys, x.lhs) => x.lhs for x in obs) - namespaced_to_sts = param_only ? Dict() : - Dict(unknowns(sys, x) => x for x in unknowns(sys)) - - # FIXME: This is a rather rough estimate of dependencies. We assume - # the expression depends on everything before the `maxidx`. - subs = Dict() - maxidx = 0 - for s in dep_vars - if s in param_set || s in param_set_ns || - iscall(s) && - operation(s) === getindex && - (arguments(s)[1] in param_set || arguments(s)[1] in param_set_ns) - continue + vs = ModelingToolkit.vars(ts; op) + namespace_subs = Dict() + ns_map = Dict{Any, Any}(renamespace(sys, eq.lhs) => eq.lhs for eq in observed(sys)) + for sym in unknowns(sys) + ns_map[renamespace(sys, sym)] = sym + if iscall(sym) && operation(sym) === getindex + ns_map[renamespace(sys, arguments(sym)[1])] = arguments(sym)[1] end - idx = get(observed_idx, s, nothing) - if idx !== nothing - idx > maxidx && (maxidx = idx) - else - s′ = get(namespaced_to_obs, s, nothing) - if s′ !== nothing - subs[s] = s′ - s = s′ - idx = get(observed_idx, s, nothing) - end - if idx !== nothing - idx > maxidx && (maxidx = idx) - elseif !(s in sts) - s′ = get(namespaced_to_sts, s, nothing) - if s′ !== nothing - subs[s] = s′ - continue - end - if throw - Base.throw(ArgumentError("$s is neither an observed nor an unknown variable.")) - else - # TODO: return variables that don't exist in the system. - return nothing - end - end - continue + end + for sym in full_parameters(sys) + ns_map[renamespace(sys, sym)] = sym + if iscall(sym) && operation(sym) === getindex + ns_map[renamespace(sys, arguments(sym)[1])] = arguments(sym)[1] end end - ts = map(t -> substitute(t, subs), ts) - obsexprs = [] - - for i in 1:maxidx - eq = obs[i] - if is_dde(sys) - eq = delay_to_function( - sys, eq; history_arg = issplit ? MTKPARAMETERS_ARG : DEFAULT_PARAMS_ARG) + allsyms = Set(all_symbols(sys)) + for var in vs + var = unwrap(var) + newvar = get(ns_map, var, nothing) + if newvar !== nothing + namespace_subs[var] = newvar end - lhs = eq.lhs - rhs = eq.rhs - push!(obsexprs, lhs ← rhs) end + ts = fast_substitute(ts, namespace_subs) - if inputs !== nothing - ps = setdiff(ps, inputs) # Inputs have been converted to parameters by io_preprocessing, remove those from the parameter list - end - _ps = ps - if ps isa Tuple - ps = DestructuredArgs.(unwrap.(ps), inbounds = !checkbounds) - elseif has_index_cache(sys) && get_index_cache(sys) !== nothing - ps = DestructuredArgs.(reorder_parameters(get_index_cache(sys), unwrap.(ps))) - if isempty(ps) && inputs !== nothing - ps = (:EMPTY,) + obsfilter = if param_only + if is_split(sys) + let ic = get_index_cache(sys) + eq -> !(ContinuousTimeseries() in ic.observed_syms_to_timeseries[eq.lhs]) + end + else + Returns(false) end else - ps = (DestructuredArgs(unwrap.(ps), inbounds = !checkbounds),) + Returns(true) end - dvs = DestructuredArgs(unknowns(sys), inbounds = !checkbounds) - if is_dde(sys) - dvs = (dvs, DDE_HISTORY_FUN) + dvs = if param_only + () else - dvs = (dvs,) + (unknowns(sys),) end - p_start = param_only ? 1 : (length(dvs) + 1) if inputs === nothing - args = param_only ? [ps..., ivs...] : [dvs..., ps..., ivs...] + inputs = () else - inputs = unwrap.(inputs) - ipts = DestructuredArgs(inputs, inbounds = !checkbounds) - args = param_only ? [ipts, ps..., ivs...] : [dvs..., ipts, ps..., ivs...] - p_start += 1 - end - pre = get_postprocess_fbody(sys) - - array_wrapper = if param_only - wrap_array_vars(sys, ts; ps = _ps, dvs = nothing, inputs, history = is_dde(sys)) .∘ - wrap_parameter_dependencies(sys, isscalar) - else - wrap_array_vars(sys, ts; ps = _ps, inputs, history = is_dde(sys)) .∘ - wrap_parameter_dependencies(sys, isscalar) - end - mtkparams_wrapper = wrap_mtkparameters(sys, isscalar, p_start) - if mtkparams_wrapper isa Tuple - oop_mtkp_wrapper = mtkparams_wrapper[1] - else - oop_mtkp_wrapper = mtkparams_wrapper + ps = setdiff(ps, inputs) # Inputs have been converted to parameters by io_preprocessing, remove those from the parameter list + inputs = (inputs,) end - - # Need to keep old method of building the function since it uses `output_type`, - # which can't be provided to `build_function` - return_value = if isscalar - ts[1] - elseif is_tuple - MakeTuple(Tuple(ts)) + ps = reorder_parameters(sys, ps) + iv = if is_time_dependent(sys) + (get_iv(sys),) else - mkarray(ts, output_type) - end - oop_fn = Func(args, [], - pre(Let(obsexprs, - return_value, - false)), [Expr(:meta, :propagate_inbounds)]) |> array_wrapper[1] |> - oop_mtkp_wrapper |> toexpr - - if !checkbounds - oop_fn.args[end] = quote - @inbounds begin - $(oop_fn.args[end]) - end - end - end - oop_fn = expression ? oop_fn : eval_or_rgf(oop_fn; eval_expression, eval_module) - - if !isscalar - iip_fn = build_function(ts, - args...; - postprocess_fbody = pre, - wrap_code = mtkparams_wrapper .∘ array_wrapper .∘ - wrap_assignments(isscalar, obsexprs), - expression = Val{true})[2] - if !checkbounds - iip_fn.args[end] = quote - @inbounds begin - $(iip_fn.args[end]) - end - end - end - iip_fn.args[end] = quote - $(Expr(:meta, :propagate_inbounds)) - $(iip_fn.args[end]) - end - - if !expression - iip_fn = eval_or_rgf(iip_fn; eval_expression, eval_module) - end + () end - if isscalar || !return_inplace - return oop_fn + args = (dvs..., inputs..., ps..., iv...) + p_start = length(dvs) + length(inputs) + 1 + p_end = length(dvs) + length(inputs) + length(ps) + fns = build_function_wrapper( + sys, ts, args...; p_start, p_end, filter_observed = obsfilter, + output_type, mkarray, try_namespaced = true, expression = Val{true}) + if fns isa Tuple + oop, iip = eval_or_rgf.(fns; eval_expression, eval_module) + return return_inplace ? (oop, iip) : oop else - return oop_fn, iip_fn + return eval_or_rgf(fns; eval_expression, eval_module) end end From 14020bd0119cfc5425fa9e6ce2d8d8f73a14c709 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 31 Jan 2025 14:42:11 +0530 Subject: [PATCH 0748/1337] feat: allow opting out of destructuring `MTKParameters` in `build_function_wrapper` --- src/systems/codegen_utils.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/systems/codegen_utils.jl b/src/systems/codegen_utils.jl index dae9f07292..c0aa9b018b 100644 --- a/src/systems/codegen_utils.jl +++ b/src/systems/codegen_utils.jl @@ -45,7 +45,7 @@ function array_variable_assignments(args...) return assignments end -function build_function_wrapper(sys::AbstractSystem, expr, args...; p_start = 2, p_end = is_time_dependent(sys) ? length(args) - 1 : length(args), wrap_delays = is_dde(sys), wrap_code = identity, add_observed = true, filter_observed = Returns(true), create_bindings = true, output_type = nothing, mkarray = nothing, kwargs...) +function build_function_wrapper(sys::AbstractSystem, expr, args...; p_start = 2, p_end = is_time_dependent(sys) ? length(args) - 1 : length(args), wrap_delays = is_dde(sys), wrap_code = identity, add_observed = true, filter_observed = Returns(true), create_bindings = true, output_type = nothing, mkarray = nothing, wrap_mtkparameters = true, kwargs...) isscalar = !(expr isa AbstractArray || symbolic_type(expr) == ArraySymbolic()) obs = filter(filter_observed, observed(sys)) @@ -99,7 +99,7 @@ function build_function_wrapper(sys::AbstractSystem, expr, args...; p_start = 2, end end - if is_split(sys) + if is_split(sys) && wrap_mtkparameters if p_start > p_end args = (args[1:p_start-1]..., MTKPARAMETERS_ARG, args[p_end+1:end]...) else From 4686893b94509837937194081c62d5420ef3f8bc Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 31 Jan 2025 14:42:31 +0530 Subject: [PATCH 0749/1337] refactor: use `build_function_wrapper` in `generate_control_function` --- src/inputoutput.jl | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/src/inputoutput.jl b/src/inputoutput.jl index 6bdcac6dd4..7d2fa875b1 100644 --- a/src/inputoutput.jl +++ b/src/inputoutput.jl @@ -249,17 +249,8 @@ function generate_control_function(sys::AbstractODESystem, inputs = unbound_inpu ddvs = map(Differential(get_iv(sys)), dvs) args = (ddvs, args...) end - process = get_postprocess_fbody(sys) - wrapped_arrays_vars = disturbance_argument ? - wrap_array_vars( - sys, rhss; dvs, ps, inputs, extra_args = (disturbance_inputs,)) : - wrap_array_vars(sys, rhss; dvs, ps, inputs) - f = build_function(rhss, args...; postprocess_fbody = process, - expression = Val{true}, wrap_code = wrap_mtkparameters( - sys, false, 3, Int(disturbance_argument) + 1) .∘ - wrapped_arrays_vars .∘ - wrap_parameter_dependencies(sys, false), - kwargs...) + f = build_function_wrapper(sys, rhss, args...; p_start = 3 + implicit_dae, + p_end = length(p) + 2 + implicit_dae) f = eval_or_rgf.(f; eval_expression, eval_module) (; f, dvs, ps, io_sys = sys) end From f28df16df7ade0185c7b77fdbea52882b188de2c Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 31 Jan 2025 14:42:50 +0530 Subject: [PATCH 0750/1337] fix: refactor `modelingtoolkitize(::OptimizationProblem)` to use new codegen --- src/systems/optimization/modelingtoolkitize.jl | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/systems/optimization/modelingtoolkitize.jl b/src/systems/optimization/modelingtoolkitize.jl index b66f113f6c..0228a944cc 100644 --- a/src/systems/optimization/modelingtoolkitize.jl +++ b/src/systems/optimization/modelingtoolkitize.jl @@ -79,7 +79,11 @@ function modelingtoolkitize(prob::DiffEqBase.OptimizationProblem; if DiffEqBase.isinplace(prob) && !isnothing(prob.f.cons) lhs = Array{Num}(undef, num_cons) - prob.f.cons(lhs, vars, params) + if p isa MTKParameters + prob.f.cons(lhs, vars, params...) + else + prob.f.cons(lhs, vars, params) + end cons = Union{Equation, Inequality}[] if !isnothing(prob.lcons) @@ -108,7 +112,8 @@ function modelingtoolkitize(prob::DiffEqBase.OptimizationProblem; or pass the lower and upper bounds for inequality constraints.")) end elseif !isnothing(prob.f.cons) - cons = prob.f.cons(vars, params) + cons = p isa MTKParameters ? prob.f.cons(vars, params...) : + prob.f.cons(vars, params) else cons = [] end From cff02c9fd90e0ef659d1f547f65631101e726b5c Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 31 Jan 2025 14:43:24 +0530 Subject: [PATCH 0751/1337] refactor: use `build_function_wrapper` in `OptimizationProblem` codegen --- .../optimization/constraints_system.jl | 19 +++----- .../optimization/optimizationsystem.jl | 45 ++++++------------- 2 files changed, 19 insertions(+), 45 deletions(-) diff --git a/src/systems/optimization/constraints_system.jl b/src/systems/optimization/constraints_system.jl index 03225fc900..275079297a 100644 --- a/src/systems/optimization/constraints_system.jl +++ b/src/systems/optimization/constraints_system.jl @@ -172,12 +172,10 @@ end function generate_jacobian( sys::ConstraintsSystem, vs = unknowns(sys), ps = parameters(sys); - sparse = false, simplify = false, wrap_code = identity, kwargs...) + sparse = false, simplify = false, kwargs...) jac = calculate_jacobian(sys, sparse = sparse, simplify = simplify) p = reorder_parameters(sys, ps) - wrap_code = wrap_code .∘ wrap_array_vars(sys, jac; dvs = vs, ps) .∘ - wrap_parameter_dependencies(sys, false) - return build_function(jac, vs, p...; wrap_code, kwargs...) + return build_function_wrapper(sys, jac, vs, p...; kwargs...) end function calculate_hessian(sys::ConstraintsSystem; sparse = false, simplify = false) @@ -193,25 +191,18 @@ end function generate_hessian( sys::ConstraintsSystem, vs = unknowns(sys), ps = parameters(sys); - sparse = false, simplify = false, wrap_code = identity, kwargs...) + sparse = false, simplify = false, kwargs...) hess = calculate_hessian(sys, sparse = sparse, simplify = simplify) p = reorder_parameters(sys, ps) - wrap_code = wrap_code .∘ wrap_array_vars(sys, hess; dvs = vs, ps) .∘ - wrap_parameter_dependencies(sys, false) - return build_function(hess, vs, p...; wrap_code, kwargs...) + return build_function_wrapper(sys, hess, vs, p...; kwargs...) end function generate_function(sys::ConstraintsSystem, dvs = unknowns(sys), ps = parameters(sys); - wrap_code = identity, kwargs...) lhss = generate_canonical_form_lhss(sys) - pre, sol_states = get_substitutions_and_solved_unknowns(sys) p = reorder_parameters(sys, value.(ps)) - wrap_code = wrap_code .∘ wrap_array_vars(sys, lhss; dvs, ps) .∘ - wrap_parameter_dependencies(sys, false) - func = build_function(lhss, value.(dvs), p...; postprocess_fbody = pre, - states = sol_states, wrap_code, kwargs...) + func = build_function_wrapper(sys, lhss, value.(dvs), p...; kwargs...) cstr = constraints(sys) lcons = fill(-Inf, length(cstr)) diff --git a/src/systems/optimization/optimizationsystem.jl b/src/systems/optimization/optimizationsystem.jl index 860e063e35..3ae98be084 100644 --- a/src/systems/optimization/optimizationsystem.jl +++ b/src/systems/optimization/optimizationsystem.jl @@ -194,16 +194,10 @@ function calculate_gradient(sys::OptimizationSystem) end function generate_gradient(sys::OptimizationSystem, vs = unknowns(sys), - ps = parameters(sys); - wrap_code = identity, - kwargs...) + ps = parameters(sys); kwargs...) grad = calculate_gradient(sys) - pre = get_preprocess_constants(grad) p = reorder_parameters(sys, ps) - wrap_code = wrap_code .∘ wrap_array_vars(sys, grad; dvs = vs, ps) .∘ - wrap_parameter_dependencies(sys, !(grad isa AbstractArray)) - return build_function(grad, vs, p...; postprocess_fbody = pre, wrap_code, - kwargs...) + return build_function_wrapper(sys, grad, vs, p...; kwargs...) end function calculate_hessian(sys::OptimizationSystem) @@ -212,34 +206,22 @@ end function generate_hessian( sys::OptimizationSystem, vs = unknowns(sys), ps = parameters(sys); - sparse = false, wrap_code = identity, kwargs...) + sparse = false, kwargs...) if sparse hess = sparsehessian(objective(sys), unknowns(sys)) else hess = calculate_hessian(sys) end - pre = get_preprocess_constants(hess) p = reorder_parameters(sys, ps) - wrap_code = wrap_code .∘ wrap_array_vars(sys, hess; dvs = vs, ps) .∘ - wrap_parameter_dependencies(sys, false) - return build_function(hess, vs, p...; postprocess_fbody = pre, wrap_code, - kwargs...) + return build_function_wrapper(sys, hess, vs, p...; kwargs...) end function generate_function(sys::OptimizationSystem, vs = unknowns(sys), ps = parameters(sys); - wrap_code = identity, - kwargs...) - eqs = subs_constants(objective(sys)) - p = if has_index_cache(sys) - reorder_parameters(get_index_cache(sys), ps) - else - (ps,) - end - wrap_code = wrap_code .∘ wrap_array_vars(sys, eqs; dvs = vs, ps) .∘ - wrap_parameter_dependencies(sys, !(eqs isa AbstractArray)) - return build_function(eqs, vs, p...; wrap_code, kwargs...) + eqs = objective(sys) + p = reorder_parameters(sys, ps) + return build_function_wrapper(sys, eqs, vs, p...; kwargs...) end function namespace_objective(sys::AbstractSystem) @@ -368,7 +350,7 @@ function DiffEqBase.OptimizationProblem{iip}(sys::OptimizationSystem, u0map, f = let _f = eval_or_rgf( generate_function( sys, checkbounds = checkbounds, linenumbers = linenumbers, - expression = Val{true}); + expression = Val{true}, wrap_mtkparameters = false); eval_expression, eval_module) __f(u, p) = _f(u, p) @@ -382,7 +364,8 @@ function DiffEqBase.OptimizationProblem{iip}(sys::OptimizationSystem, u0map, generate_gradient( sys, checkbounds = checkbounds, linenumbers = linenumbers, - parallel = parallel, expression = Val{true}); + parallel = parallel, expression = Val{true}, + wrap_mtkparameters = false); eval_expression, eval_module) _grad(u, p) = grad_oop(u, p) @@ -401,7 +384,7 @@ function DiffEqBase.OptimizationProblem{iip}(sys::OptimizationSystem, u0map, sys, checkbounds = checkbounds, linenumbers = linenumbers, sparse = sparse, parallel = parallel, - expression = Val{true}); + expression = Val{true}, wrap_mtkparameters = false); eval_expression, eval_module) _hess(u, p) = hess_oop(u, p) @@ -427,7 +410,7 @@ function DiffEqBase.OptimizationProblem{iip}(sys::OptimizationSystem, u0map, cons_sys = complete(cons_sys) cons, lcons_, ucons_ = generate_function(cons_sys, checkbounds = checkbounds, linenumbers = linenumbers, - expression = Val{true}) + expression = Val{true}; wrap_mtkparameters = false) cons = let (cons_oop, cons_iip) = eval_or_rgf.(cons; eval_expression, eval_module) _cons(u, p) = cons_oop(u, p) _cons(resid, u, p) = cons_iip(resid, u, p) @@ -440,7 +423,7 @@ function DiffEqBase.OptimizationProblem{iip}(sys::OptimizationSystem, u0map, checkbounds = checkbounds, linenumbers = linenumbers, parallel = parallel, expression = Val{true}, - sparse = cons_sparse); + sparse = cons_sparse, wrap_mtkparameters = false); eval_expression, eval_module) _cons_j(u, p) = cons_jac_oop(u, p) @@ -458,7 +441,7 @@ function DiffEqBase.OptimizationProblem{iip}(sys::OptimizationSystem, u0map, cons_sys, checkbounds = checkbounds, linenumbers = linenumbers, sparse = cons_sparse, parallel = parallel, - expression = Val{true}); + expression = Val{true}, wrap_mtkparameters = false); eval_expression, eval_module) _cons_h(u, p) = cons_hess_oop(u, p) From 4274ec9bb9d741e5767f514c92014159546df6c3 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 31 Jan 2025 15:03:45 +0530 Subject: [PATCH 0752/1337] docs: add docstring for code generation utils --- src/systems/codegen_utils.jl | 92 +++++++++++++++++++++++++++++++++--- 1 file changed, 86 insertions(+), 6 deletions(-) diff --git a/src/systems/codegen_utils.jl b/src/systems/codegen_utils.jl index c0aa9b018b..1bf0830e7d 100644 --- a/src/systems/codegen_utils.jl +++ b/src/systems/codegen_utils.jl @@ -1,43 +1,71 @@ +""" + $(TYPEDSIGNATURES) + +Return the name for the `i`th argument in a function generated by `build_function_wrapper`. +""" function generated_argument_name(i::Int) return Symbol(:__mtk_arg_, i) end +""" + $(TYPEDSIGNATURES) + +Given the arguments to `build_function_wrapper`, return a list of assignments which +reconstruct array variables if they are present scalarized in `args`. +""" function array_variable_assignments(args...) + # map array symbolic to an identically sized array where each element is (buffer_idx, idx_in_buffer) var_to_arridxs = Dict{BasicSymbolic, Array{Tuple{Int, Int}}}() for (i, arg) in enumerate(args) + # filter out non-arrays + # any element of args which is not an array is assumed to not contain a + # scalarized array symbolic. This works because the only non-array element + # is the independent variable symbolic_type(arg) == NotSymbolic() || continue arg isa AbstractArray || continue + # go through symbolics for (j, var) in enumerate(arg) var = unwrap(var) + # filter out non-array-symbolics iscall(var) || continue operation(var) == getindex || continue arrvar = arguments(var)[1] - idxbuffer = get!(() -> map(Returns((0, 0)), eachindex(arrvar)), var_to_arridxs, arrvar) + # get and/or construct the buffer storing indexes + idxbuffer = get!( + () -> map(Returns((0, 0)), eachindex(arrvar)), var_to_arridxs, arrvar) idxbuffer[arguments(var)[2:end]...] = (i, j) end end assignments = Assignment[] for (arrvar, idxs) in var_to_arridxs + # all elements of the array need to be present in `args` to form the + # reconstructing assignment any(iszero ∘ first, idxs) && continue + # if they are all in the same buffer, we can take a shortcut and `view` into it if allequal(Iterators.map(first, idxs)) buffer_idx = first(first(idxs)) idxs = map(last, idxs) + # if all the elements are contiguous and ordered, turn the array of indexes into a range + # to help reduce allocations if first(idxs) < last(idxs) && vec(idxs) == first(idxs):last(idxs) idxs = first(idxs):last(idxs) elseif vec(idxs) == last(idxs):-1:first(idxs) idxs = last(idxs):-1:first(idxs) else + # Otherwise, turn the indexes into an `SArray` so they're stack-allocated idxs = SArray{Tuple{size(idxs)...}}(idxs) end + # view and reshape push!(assignments, arrvar ← term(reshape, term(view, generated_argument_name(buffer_idx), idxs), size(arrvar))) else elems = map(idxs) do idx i, j = idx term(getindex, generated_argument_name(i), j) end + # use `MakeArray` and generate a stack-allocated array push!(assignments, arrvar ← MakeArray(elems, SArray)) end end @@ -45,22 +73,63 @@ function array_variable_assignments(args...) return assignments end +""" + $(TYPEDSIGNATURES) + +A wrapper around `build_function` which performs the necessary transformations for +code generation of all types of systems. `expr` is the expression returned from the +generated functions, and `args` are the arguments. + +# Keyword Arguments + +- `p_start`, `p_end`: Denotes the indexes in `args` where the buffers of the splatted + `MTKParameters` object are present. These are collapsed into a single argument and + destructured inside the function. `p_start` must also be provided for non-split systems + since it is used by `wrap_delays`. +- `wrap_delays`: Whether to transform delayed unknowns of `sys` present in `expr` into + calls to a history function. The history function is added to the list of arguments + right before parameters, at the index `p_start`. +- `wrap_code`: Forwarded to `build_function`. +- `add_observed`: Whether to add assignment statements for observed equations in the + generated code. +- `filter_observed`: A predicate function to filter out observed equations which should + not be added to the generated code. +- `create_bindings`: Whether to explicitly destructure arrays of symbolics present in + `args` in the generated code. If `false`, all usages of the individual symbolics will + instead call `getindex` on the relevant argument. This is useful if the generated + function writes to one of its arguments and expects subsequent code to use the new + values. Note that the collapsed `MTKParameters` argument will always be explicitly + destructured regardless of this keyword argument. +- `output_type`: The type of the output buffer. If `mkarray` (see below) is `nothing`, + this will be passed to the `similarto` argument of `build_function`. If `output_type` + is `Tuple`, `expr` will be wrapped in `SymbolicUtils.Code.MakeTuple` (regardless of + whether it is scalar or an array). +- `mkarray`: A function which accepts `expr` and `output_type` and returns a code + generation object similar to `MakeArray` or `MakeTuple` to be used to generate + code for `expr`. +- `wrap_mtkparameters`: Whether to collapse parameter buffers for a split system into a + argument. + +All other keyword arguments are forwarded to `build_function`. +""" function build_function_wrapper(sys::AbstractSystem, expr, args...; p_start = 2, p_end = is_time_dependent(sys) ? length(args) - 1 : length(args), wrap_delays = is_dde(sys), wrap_code = identity, add_observed = true, filter_observed = Returns(true), create_bindings = true, output_type = nothing, mkarray = nothing, wrap_mtkparameters = true, kwargs...) isscalar = !(expr isa AbstractArray || symbolic_type(expr) == ArraySymbolic()) - + # filter observed equations obs = filter(filter_observed, observed(sys)) + # turn delayed unknowns into calls to the history function if wrap_delays history_arg = is_split(sys) ? MTKPARAMETERS_ARG : generated_argument_name(p_start) obs = map(obs) do eq delay_to_function(sys, eq; history_arg) end expr = delay_to_function(sys, expr; history_arg) - args = (args[1:p_start-1]..., DDE_HISTORY_FUN, args[p_start:end]...) + # add extra argument + args = (args[1:(p_start - 1)]..., DDE_HISTORY_FUN, args[p_start:end]...) p_start += 1 p_end += 1 end pdeps = parameter_dependencies(sys) - + # get the constants to add to the code cmap, _ = get_cmap(sys) extra_constants = collect_constants(expr) filter!(extra_constants) do c @@ -69,13 +138,15 @@ function build_function_wrapper(sys::AbstractSystem, expr, args...; p_start = 2, for c in extra_constants push!(cmap, c ~ getdefault(c)) end + # only get the necessary observed equations, avoiding extra computation if add_observed obsidxs = observed_equations_used_by(sys, expr) else obsidxs = Int[] end + # similarly for parameter dependency equations pdepidxs = observed_equations_used_by(sys, expr; obs = pdeps) - + # assignments for reconstructing scalarized array symbolics assignments = array_variable_assignments(args...) for eq in Iterators.flatten((cmap, pdeps[pdepidxs], obs[obsidxs])) @@ -84,6 +155,9 @@ function build_function_wrapper(sys::AbstractSystem, expr, args...; p_start = 2, args = ntuple(Val(length(args))) do i arg = args[i] + # for time-dependent systems, all arguments are passed through `time_varying_as_func` + # TODO: This is legacy behavior and a candidate for removal in v10 since we have callable + # parameters now. if is_time_dependent(sys) arg = if symbolic_type(arg) == NotSymbolic() arg isa AbstractArray ? @@ -92,6 +166,7 @@ function build_function_wrapper(sys::AbstractSystem, expr, args...; p_start = 2, time_varying_as_func(unwrap(arg), sys) end end + # Make sure to use the proper names for arguments if symbolic_type(arg) == NotSymbolic() && arg isa AbstractArray DestructuredArgs(arg, generated_argument_name(i); create_bindings) else @@ -99,9 +174,11 @@ function build_function_wrapper(sys::AbstractSystem, expr, args...; p_start = 2, end end + # wrap into a single MTKParameters argument if is_split(sys) && wrap_mtkparameters if p_start > p_end - args = (args[1:p_start-1]..., MTKPARAMETERS_ARG, args[p_end+1:end]...) + # In case there are no parameter buffers, still insert an argument + args = (args[1:(p_start - 1)]..., MTKPARAMETERS_ARG, args[(p_end + 1):end]...) else # cannot apply `create_bindings` here since it doesn't nest args = (args[1:(p_start - 1)]..., @@ -110,12 +187,14 @@ function build_function_wrapper(sys::AbstractSystem, expr, args...; p_start = 2, end end + # add preface assignments if has_preface(sys) && (pref = preface(sys)) !== nothing append!(assignments, pref) end wrap_code = wrap_code .∘ wrap_assignments(isscalar, assignments) + # handling of `output_type` and `mkarray` similarto = nothing if output_type === Tuple expr = MakeTuple(Tuple(expr)) @@ -127,6 +206,7 @@ function build_function_wrapper(sys::AbstractSystem, expr, args...; p_start = 2, wrap_code = wrap_code[2] end + # scalar `build_function` only accepts a single function for `wrap_code`. if wrap_code isa Tuple && symbolic_type(expr) == ScalarSymbolic() wrap_code = wrap_code[1] end From dfc49b469414aa3ab86abf50707f75d8d6bc81fd Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 31 Jan 2025 15:21:12 +0530 Subject: [PATCH 0753/1337] feat: add `extra_assignments` to `build_function_wrapper` --- src/systems/codegen_utils.jl | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/systems/codegen_utils.jl b/src/systems/codegen_utils.jl index 1bf0830e7d..3db890e0c3 100644 --- a/src/systems/codegen_utils.jl +++ b/src/systems/codegen_utils.jl @@ -59,7 +59,10 @@ function array_variable_assignments(args...) idxs = SArray{Tuple{size(idxs)...}}(idxs) end # view and reshape - push!(assignments, arrvar ← term(reshape, term(view, generated_argument_name(buffer_idx), idxs), size(arrvar))) + push!(assignments, + arrvar ← + term(reshape, term(view, generated_argument_name(buffer_idx), idxs), + size(arrvar))) else elems = map(idxs) do idx i, j = idx @@ -109,10 +112,17 @@ generated functions, and `args` are the arguments. code for `expr`. - `wrap_mtkparameters`: Whether to collapse parameter buffers for a split system into a argument. +- `extra_assignments`: Extra `Assignment` statements to prefix to `expr`, after all other + assignments. All other keyword arguments are forwarded to `build_function`. """ -function build_function_wrapper(sys::AbstractSystem, expr, args...; p_start = 2, p_end = is_time_dependent(sys) ? length(args) - 1 : length(args), wrap_delays = is_dde(sys), wrap_code = identity, add_observed = true, filter_observed = Returns(true), create_bindings = true, output_type = nothing, mkarray = nothing, wrap_mtkparameters = true, kwargs...) +function build_function_wrapper(sys::AbstractSystem, expr, args...; p_start = 2, + p_end = is_time_dependent(sys) ? length(args) - 1 : length(args), + wrap_delays = is_dde(sys), wrap_code = identity, + add_observed = true, filter_observed = Returns(true), + create_bindings = true, output_type = nothing, mkarray = nothing, + wrap_mtkparameters = true, extra_assignments = Assignment[], kwargs...) isscalar = !(expr isa AbstractArray || symbolic_type(expr) == ArraySymbolic()) # filter observed equations obs = filter(filter_observed, observed(sys)) @@ -152,6 +162,7 @@ function build_function_wrapper(sys::AbstractSystem, expr, args...; p_start = 2, for eq in Iterators.flatten((cmap, pdeps[pdepidxs], obs[obsidxs])) push!(assignments, eq.lhs ← eq.rhs) end + append!(assignments, extra_assignments) args = ntuple(Val(length(args))) do i arg = args[i] From 884a0fb2bf8911786bdde71a7546e7a07a7dcb6a Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 31 Jan 2025 15:21:23 +0530 Subject: [PATCH 0754/1337] refactor: use `build_function_wrapper` in `SCCNonlinearProblem` --- src/systems/nonlinear/nonlinearsystem.jl | 35 ++++++++---------------- 1 file changed, 11 insertions(+), 24 deletions(-) diff --git a/src/systems/nonlinear/nonlinearsystem.jl b/src/systems/nonlinear/nonlinearsystem.jl index 08ede84033..b5256ca4ca 100644 --- a/src/systems/nonlinear/nonlinearsystem.jl +++ b/src/systems/nonlinear/nonlinearsystem.jl @@ -563,7 +563,7 @@ struct CacheWriter{F} end function (cw::CacheWriter)(p, sols) - cw.fn(p.caches, sols, p...) + cw.fn(p.caches, sols, p) end function CacheWriter(sys::AbstractSystem, buffer_types::Vector{TypeT}, @@ -572,22 +572,15 @@ function CacheWriter(sys::AbstractSystem, buffer_types::Vector{TypeT}, ps = parameters(sys) rps = reorder_parameters(sys, ps) obs_assigns = [eq.lhs ← eq.rhs for eq in obseqs] - cmap, cs = get_cmap(sys) - cmap_assigns = [eq.lhs ← eq.rhs for eq in cmap] - - outsyms = [Symbol(:out, i) for i in eachindex(buffer_types)] body = map(eachindex(buffer_types), buffer_types) do i, T Symbol(:tmp, i) ← SetArray(true, :(out[$i]), get(exprs, T, [])) end - fn = Func( - [:out, DestructuredArgs(DestructuredArgs.(solsyms)), - DestructuredArgs.(rps)...], - [], - Let(body, :()) - ) |> wrap_assignments(false, obs_assigns)[2] |> - wrap_parameter_dependencies(sys, false)[2] |> - wrap_array_vars(sys, []; dvs = nothing, inputs = [])[2] |> - wrap_assignments(false, cmap_assigns)[2] |> toexpr + + fn = build_function_wrapper( + sys, nothing, :out, DestructuredArgs(DestructuredArgs.(solsyms)), + DestructuredArgs.(rps)...; p_start = 3, p_end = length(rps) + 2, + expression = Val{true}, add_observed = false, + extra_assignments = [obs_assigns; body]) return CacheWriter(eval_or_rgf(fn; eval_expression, eval_module)) end @@ -601,21 +594,15 @@ function SCCNonlinearFunction{iip}( obs_assignments = [eq.lhs ← eq.rhs for eq in _obs] - cmap, cs = get_cmap(sys) - cmap_assignments = [eq.lhs ← eq.rhs for eq in cmap] rhss = [eq.rhs - eq.lhs for eq in _eqs] - wrap_code = wrap_assignments(false, cmap_assignments) .∘ - (wrap_array_vars(sys, rhss; dvs = _dvs, cachesyms)) .∘ - wrap_parameter_dependencies(sys, false) .∘ - wrap_assignments(false, obs_assignments) - f_gen = build_function( - rhss, _dvs, rps..., cachesyms...; wrap_code, expression = Val{true}) + f_gen = build_function_wrapper(sys, + rhss, _dvs, rps..., cachesyms...; p_start = 2, + p_end = length(rps) + length(cachesyms) + 1, add_observed = false, + extra_assignments = obs_assignments, expression = Val{true}) f_oop, f_iip = eval_or_rgf.(f_gen; eval_expression, eval_module) f(u, p) = f_oop(u, p) - f(u, p::MTKParameters) = f_oop(u, p...) f(resid, u, p) = f_iip(resid, u, p) - f(resid, u, p::MTKParameters) = f_iip(resid, u, p...) subsys = NonlinearSystem(_eqs, _dvs, ps; observed = _obs, parameter_dependencies = parameter_dependencies(sys), name = nameof(sys)) From 4c39175fe33bd380e1b009e048cecaa0541a4d70 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 3 Feb 2025 12:21:33 +0530 Subject: [PATCH 0755/1337] fix: search through filtered observed equations in `build_function_wrapper` --- src/systems/codegen_utils.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/systems/codegen_utils.jl b/src/systems/codegen_utils.jl index 3db890e0c3..fd54ef01ca 100644 --- a/src/systems/codegen_utils.jl +++ b/src/systems/codegen_utils.jl @@ -149,8 +149,8 @@ function build_function_wrapper(sys::AbstractSystem, expr, args...; p_start = 2, push!(cmap, c ~ getdefault(c)) end # only get the necessary observed equations, avoiding extra computation - if add_observed - obsidxs = observed_equations_used_by(sys, expr) + if add_observed && !isempty(obs) + obsidxs = observed_equations_used_by(sys, expr; obs) else obsidxs = Int[] end From 1fc081f3ca7ff08c7e36e3cad34e1484e91e885e Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 3 Feb 2025 12:23:32 +0530 Subject: [PATCH 0756/1337] build: bump Symbolics compat --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index e4b2238170..c069b49b5a 100644 --- a/Project.toml +++ b/Project.toml @@ -147,7 +147,7 @@ StochasticDiffEq = "6.72.1" StochasticDelayDiffEq = "1.8.1" SymbolicIndexingInterface = "0.3.37" SymbolicUtils = "3.10.1" -Symbolics = "6.25" +Symbolics = "6.27" URIs = "1" UnPack = "0.1, 1.0" Unitful = "1.1" From b5305950778d256c70462e7858f4cb3b79ff835e Mon Sep 17 00:00:00 2001 From: vyudu Date: Mon, 3 Feb 2025 07:58:14 -0500 Subject: [PATCH 0757/1337] working simplification --- .../symbolics_tearing.jl | 58 +++++++++++++++++-- src/structural_transformation/utils.jl | 18 +++--- src/systems/systemstructure.jl | 30 +++++++--- 3 files changed, 88 insertions(+), 18 deletions(-) diff --git a/src/structural_transformation/symbolics_tearing.jl b/src/structural_transformation/symbolics_tearing.jl index d95951c41e..8c802d9765 100644 --- a/src/structural_transformation/symbolics_tearing.jl +++ b/src/structural_transformation/symbolics_tearing.jl @@ -240,7 +240,7 @@ end function tearing_reassemble(state::TearingState, var_eq_matching, full_var_eq_matching = nothing; simplify = false, mm = nothing, cse_hack = true, array_hack = true) @unpack fullvars, sys, structure = state - @unpack solvable_graph, var_to_diff, eq_to_diff, graph = structure + @unpack solvable_graph, var_to_diff, eq_to_diff, graph, lowest_shift = structure extra_vars = Int[] if full_var_eq_matching !== nothing for v in 𝑑vertices(state.structure.graph) @@ -279,6 +279,7 @@ function tearing_reassemble(state::TearingState, var_eq_matching, iv = D = nothing end diff_to_var = invview(var_to_diff) + dummy_sub = Dict() for var in 1:length(fullvars) dv = var_to_diff[var] @@ -310,7 +311,10 @@ function tearing_reassemble(state::TearingState, var_eq_matching, diff_to_var[dv] = nothing end end + @show neweqs + println("Post state selection.") + # `SelectedState` information is no longer needed past here. State selection # is done. All non-differentiated variables are algebraic variables, and all # variables that appear differentiated are differential variables. @@ -331,10 +335,28 @@ function tearing_reassemble(state::TearingState, var_eq_matching, order += 1 dv = dv′ end + println("Order") + @show fullvars[dv] + is_only_discrete(state.structure) && begin + var = fullvars[dv] + key = operation(var) isa Shift ? only(arguments(var)) : var + order = -get(lowest_shift, key, 0) - order + end order, dv end end + lower_name = is_only_discrete(state.structure) ? lower_varname_withshift : lower_varname_with_unit + # is_only_discrete(state.structure) && for v in 1:length(fullvars) + # var = fullvars[v] + # op = operation(var) + # if op isa Shift + # x = only(arguments(var)) + # lowest_shift_idxs[v] + # op.steps == lowest_shift[x] && (fullvars[v] = lower_varname_withshift(var, iv, -op.steps)) + # end + # end + #retear = BitSet() # There are three cases where we want to generate new variables to convert # the system into first order (semi-implicit) ODEs. @@ -384,9 +406,28 @@ function tearing_reassemble(state::TearingState, var_eq_matching, eq_var_matching = invview(var_eq_matching) linear_eqs = mm === nothing ? Dict{Int, Int}() : Dict(reverse(en) for en in enumerate(mm.nzrows)) + for v in 1:length(var_to_diff) - dv = var_to_diff[v] + println() + @show fullvars + @show diff_to_var + is_highest_discrete = begin + var = fullvars[v] + op = operation(var) + if (!is_only_discrete(state.structure) || op isa Shift) + false + elseif !haskey(lowest_shift, var) + false + else + low = lowest_shift[var] + idx = findfirst(x -> isequal(x, Shift(iv, low)(var)), fullvars) + true + end + end + dv = is_highest_discrete ? idx : var_to_diff[v] + @show (v, fullvars[v], dv) dv isa Int || continue + solved = var_eq_matching[dv] isa Int solved && continue # check if there's `D(x) = x_t` already @@ -404,17 +445,19 @@ function tearing_reassemble(state::TearingState, var_eq_matching, diff_to_var[v_t] === nothing) @assert dv in rvs dummy_eq = eq + @show "FOUND DUMMY EQ" @goto FOUND_DUMMY_EQ end end dx = fullvars[dv] # add `x_t` - order, lv = var_order(dv) - x_t = lower_varname_withshift(fullvars[lv], iv, order) + @show order, lv = var_order(dv) + x_t = lower_name(fullvars[lv], iv, order) push!(fullvars, simplify_shifts(x_t)) v_t = length(fullvars) v_t_idx = add_vertex!(var_to_diff) add_vertex!(graph, DST) + @show x_t, dx # TODO: do we care about solvable_graph? We don't use them after # `dummy_derivative_graph`. add_vertex!(solvable_graph, DST) @@ -433,10 +476,16 @@ function tearing_reassemble(state::TearingState, var_eq_matching, add_edge!(solvable_graph, dummy_eq, dv) @assert nsrcs(graph) == nsrcs(solvable_graph) == dummy_eq @label FOUND_DUMMY_EQ + @show is_highest_discrete + @show diff_to_var + @show v_t, dv + # If var = x with no shift, then + is_highest_discrete && (lowest_shift[x_t] = lowest_shift[fullvars[v]]) var_to_diff[v_t] = var_to_diff[dv] var_eq_matching[dv] = unassigned eq_var_matching[dummy_eq] = dv end + @show neweqs # Will reorder equations and unknowns to be: # [diffeqs; ...] @@ -537,6 +586,7 @@ function tearing_reassemble(state::TearingState, var_eq_matching, deps = Vector{Int}[i == 1 ? Int[] : collect(1:(i - 1)) for i in 1:length(solved_equations)] + # Contract the vertices in the structure graph to make the structure match # the new reality of the system we've just created. graph = contract_variables(graph, var_eq_matching, varsperm, eqsperm, diff --git a/src/structural_transformation/utils.jl b/src/structural_transformation/utils.jl index 29c0f66756..dd063c0b07 100644 --- a/src/structural_transformation/utils.jl +++ b/src/structural_transformation/utils.jl @@ -451,18 +451,22 @@ end function lower_varname_withshift(var, iv, order) order == 0 && return var + ds = "$iv-$order" + d_separator = 'ˍ' + if ModelingToolkit.isoperator(var, ModelingToolkit.Shift) O = only(arguments(var)) oldop = operation(O) - ds = "$iv-$order" - d_separator = 'ˍ' newname = Symbol(string(nameof(oldop)), d_separator, ds) - - newvar = maketerm(typeof(O), Symbolics.rename(oldop, newname), Symbolics.children(O), Symbolics.metadata(O)) - setmetadata(newvar, Symbolics.VariableSource, (:variables, newname)) - return ModelingToolkit._with_unit(identity, newvar, iv) + else + O = var + oldop = operation(var) + varname = split(string(nameof(oldop)), d_separator)[1] + newname = Symbol(varname, d_separator, ds) end - return lower_varname_with_unit(var, iv, order) + newvar = maketerm(typeof(O), Symbolics.rename(oldop, newname), Symbolics.children(O), Symbolics.metadata(O)) + setmetadata(newvar, Symbolics.VariableSource, (:variables, newname)) + return ModelingToolkit._with_unit(identity, newvar, iv) end function isdoubleshift(var) diff --git a/src/systems/systemstructure.jl b/src/systems/systemstructure.jl index 1bdc11f06a..44558ade6c 100644 --- a/src/systems/systemstructure.jl +++ b/src/systems/systemstructure.jl @@ -140,17 +140,21 @@ get_fullvars(ts::TransformationState) = ts.fullvars has_equations(::TransformationState) = true Base.@kwdef mutable struct SystemStructure - # Maps the (index of) a variable to the (index of) the variable describing - # its derivative. + """Maps the (index of) a variable to the (index of) the variable describing its derivative.""" var_to_diff::DiffGraph + """Maps the (index of) a """ eq_to_diff::DiffGraph # Can be access as # `graph` to automatically look at the bipartite graph # or as `torn` to assert that tearing has run. + """Incidence graph of the system of equations. An edge from equation x to variable y exists if variable y appears in equation x.""" graph::BipartiteGraph{Int, Nothing} + """.""" solvable_graph::Union{BipartiteGraph{Int, Nothing}, Nothing} var_types::Union{Vector{VariableType}, Nothing} + """Whether the system is discrete.""" only_discrete::Bool + lowest_shift::Union{Dict, Nothing} end function Base.copy(structure::SystemStructure) @@ -346,6 +350,8 @@ function TearingState(sys; quick_cancel = false, check = true) eqs[i] = eqs[i].lhs ~ rhs end end + + ### Handle discrete variables lowest_shift = Dict() for var in fullvars if ModelingToolkit.isoperator(var, ModelingToolkit.Shift) @@ -430,10 +436,10 @@ function TearingState(sys; quick_cancel = false, check = true) ts = TearingState(sys, fullvars, SystemStructure(complete(var_to_diff), complete(eq_to_diff), - complete(graph), nothing, var_types, sys isa DiscreteSystem), + complete(graph), nothing, var_types, sys isa DiscreteSystem, lowest_shift), Any[]) if sys isa DiscreteSystem - ts = shift_discrete_system(ts) + ts = shift_discrete_system(ts, lowest_shift) end return ts end @@ -456,17 +462,27 @@ function lower_order_var(dervar, t) diffvar end -function shift_discrete_system(ts::TearingState) +""" + Shift variable x by the largest shift s such that x(k-s) appears in the system of equations. + The lowest-shift term will have. +""" +function shift_discrete_system(ts::TearingState, lowest_shift) @unpack fullvars, sys = ts + return ts discvars = OrderedSet() eqs = equations(sys) + for eq in eqs vars!(discvars, eq; op = Union{Sample, Hold}) end iv = get_iv(sys) - discmap = Dict(k => StructuralTransformations.simplify_shifts(Shift(iv, 1)(k)) + discmap = Dict(k => StructuralTransformations.simplify_shifts(Shift(iv, -get(lowest_shift, k, 0))(k)) for k in discvars - if any(isequal(k), fullvars) && !isa(operation(k), Union{Sample, Hold})) + if any(isequal(k), fullvars) && !isa(operation(k), Union{Sample, Hold})) + + discmap = Dict(k => StructuralTransformations.simplify_shifts(Shift(iv, 1)(k)) for k in discvars + if any(isequal(k), fullvars) && !isa(operation(k), Union{Sample, Hold})) + for i in eachindex(fullvars) fullvars[i] = StructuralTransformations.simplify_shifts(fast_substitute( fullvars[i], discmap; operator = Union{Sample, Hold})) From fe372865a7dd79d1e576f19d50677b1977e70fd1 Mon Sep 17 00:00:00 2001 From: vyudu Date: Mon, 3 Feb 2025 14:21:03 -0500 Subject: [PATCH 0758/1337] solving equations --- .../symbolics_tearing.jl | 64 ++++++++++--------- 1 file changed, 34 insertions(+), 30 deletions(-) diff --git a/src/structural_transformation/symbolics_tearing.jl b/src/structural_transformation/symbolics_tearing.jl index 8c802d9765..599a75f4f7 100644 --- a/src/structural_transformation/symbolics_tearing.jl +++ b/src/structural_transformation/symbolics_tearing.jl @@ -237,6 +237,18 @@ function check_diff_graph(var_to_diff, fullvars) end =# +function state_selection() + +end + +function create_new_deriv_variables() + +end + +function solve_solvable_equations() + +end + function tearing_reassemble(state::TearingState, var_eq_matching, full_var_eq_matching = nothing; simplify = false, mm = nothing, cse_hack = true, array_hack = true) @unpack fullvars, sys, structure = state @@ -323,6 +335,11 @@ function tearing_reassemble(state::TearingState, var_eq_matching, is_solvable = let solvable_graph = solvable_graph (eq, iv) -> eq isa Int && iv isa Int && BipartiteEdge(eq, iv) in solvable_graph end + idx_to_lowest_shift = Dict{Int, Int}(var => 0 for var in 1:length(fullvars)) + for (i,var) in enumerate(fullvars) + key = operation(var) isa Shift ? only(arguments(var)) : var + idx_to_lowest_shift[i] = get(lowest_shift, key, 0) + end # if var is like D(x) isdervar = let diff_to_var = diff_to_var @@ -335,27 +352,11 @@ function tearing_reassemble(state::TearingState, var_eq_matching, order += 1 dv = dv′ end - println("Order") - @show fullvars[dv] - is_only_discrete(state.structure) && begin - var = fullvars[dv] - key = operation(var) isa Shift ? only(arguments(var)) : var - order = -get(lowest_shift, key, 0) - order - end + is_only_discrete(state.structure) && (order = -idx_to_lowest_shift[dv] - order - 1) order, dv end end - lower_name = is_only_discrete(state.structure) ? lower_varname_withshift : lower_varname_with_unit - # is_only_discrete(state.structure) && for v in 1:length(fullvars) - # var = fullvars[v] - # op = operation(var) - # if op isa Shift - # x = only(arguments(var)) - # lowest_shift_idxs[v] - # op.steps == lowest_shift[x] && (fullvars[v] = lower_varname_withshift(var, iv, -op.steps)) - # end - # end #retear = BitSet() # There are three cases where we want to generate new variables to convert @@ -408,9 +409,6 @@ function tearing_reassemble(state::TearingState, var_eq_matching, Dict(reverse(en) for en in enumerate(mm.nzrows)) for v in 1:length(var_to_diff) - println() - @show fullvars - @show diff_to_var is_highest_discrete = begin var = fullvars[v] op = operation(var) @@ -425,7 +423,6 @@ function tearing_reassemble(state::TearingState, var_eq_matching, end end dv = is_highest_discrete ? idx : var_to_diff[v] - @show (v, fullvars[v], dv) dv isa Int || continue solved = var_eq_matching[dv] isa Int @@ -445,19 +442,17 @@ function tearing_reassemble(state::TearingState, var_eq_matching, diff_to_var[v_t] === nothing) @assert dv in rvs dummy_eq = eq - @show "FOUND DUMMY EQ" @goto FOUND_DUMMY_EQ end end dx = fullvars[dv] # add `x_t` - @show order, lv = var_order(dv) + order, lv = var_order(dv) x_t = lower_name(fullvars[lv], iv, order) push!(fullvars, simplify_shifts(x_t)) v_t = length(fullvars) v_t_idx = add_vertex!(var_to_diff) add_vertex!(graph, DST) - @show x_t, dx # TODO: do we care about solvable_graph? We don't use them after # `dummy_derivative_graph`. add_vertex!(solvable_graph, DST) @@ -476,16 +471,12 @@ function tearing_reassemble(state::TearingState, var_eq_matching, add_edge!(solvable_graph, dummy_eq, dv) @assert nsrcs(graph) == nsrcs(solvable_graph) == dummy_eq @label FOUND_DUMMY_EQ - @show is_highest_discrete - @show diff_to_var - @show v_t, dv # If var = x with no shift, then - is_highest_discrete && (lowest_shift[x_t] = lowest_shift[fullvars[v]]) + is_only_discrete(state.structure) && (idx_to_lowest_shift[v_t] = idx_to_lowest_shift[dv]) var_to_diff[v_t] = var_to_diff[dv] var_eq_matching[dv] = unassigned eq_var_matching[dummy_eq] = dv end - @show neweqs # Will reorder equations and unknowns to be: # [diffeqs; ...] @@ -501,9 +492,16 @@ function tearing_reassemble(state::TearingState, var_eq_matching, subeqs = Equation[] solved_equations = Int[] solved_variables = Int[] + # Solve solvable equations + println() + println("SOLVING SOLVABLE EQUATIONS.") + @show eq_var_matching toporder = topological_sort(DiCMOBiGraph{false}(graph, var_eq_matching)) eqs = Iterators.reverse(toporder) + @show eqs + @show neweqs + @show fullvars total_sub = Dict() idep = iv for ieq in eqs @@ -516,12 +514,18 @@ function tearing_reassemble(state::TearingState, var_eq_matching, isnothing(D) && error("Differential found in a non-differential system. Likely this is a bug in the construction of an initialization system. Please report this issue with a reproducible example. Offending equation: $(equations(sys)[ieq])") order, lv = var_order(iv) - dx = D(simplify_shifts(lower_varname_withshift( + @show fullvars[iv] + @show (order, lv) + dx = D(simplify_shifts(lower_name( fullvars[lv], idep, order - 1))) + @show dx + @show neweqs[ieq] eq = dx ~ simplify_shifts(Symbolics.fixpoint_sub( Symbolics.symbolic_linear_solve(neweqs[ieq], fullvars[iv]), total_sub; operator = ModelingToolkit.Shift)) + @show total_sub + @show eq for e in 𝑑neighbors(graph, iv) e == ieq && continue for v in 𝑠neighbors(graph, e) From 13a242c35ed4ec7c19143ccde26f3a8e6edddf3f Mon Sep 17 00:00:00 2001 From: vyudu Date: Mon, 3 Feb 2025 15:34:05 -0500 Subject: [PATCH 0759/1337] update to use updated codegen --- src/systems/diffeqs/abstractodesystem.jl | 33 ++++++++---------------- test/bvproblem.jl | 11 ++++---- 2 files changed, 17 insertions(+), 27 deletions(-) diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index f017cb902f..95e966a461 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -881,18 +881,23 @@ function SciMLBase.BVProblem{iip, specialize}(sys::AbstractODESystem, u0map = [] stidxmap = Dict([v => i for (i, v) in enumerate(sts)]) u0_idxs = has_alg_eqs(sys) ? collect(1:length(sts)) : [stidxmap[k] for (k,v) in u0map] - bc = generate_function_bc(sys, u0, u0_idxs, tspan, iip) + fns = generate_function_bc(sys, u0, u0_idxs, tspan) + bc_oop, bc_iip = eval_or_rgf.(fns; eval_expression, eval_module) + # bc(sol, p, t) = bc_oop(sol, p, t) + bc(resid, u, p, t) = bc_iip(resid, u, p, t) + return BVProblem{iip}(f, bc, u0, tspan, p; kwargs...) end get_callback(prob::BVProblem) = error("BVP solvers do not support callbacks.") """ - generate_function_bc(sys::ODESystem, u0, u0_idxs, tspan, iip) + generate_function_bc(sys::ODESystem, u0, u0_idxs, tspan) Given an ODESystem with constraints, generate the boundary condition function to pass to boundary value problem solvers. + Expression uses the constraints and the provided initial conditions. """ -function generate_function_bc(sys::ODESystem, u0, u0_idxs, tspan, iip) +function generate_function_bc(sys::ODESystem, u0, u0_idxs, tspan; kwargs...) iv = get_iv(sys) sts = get_unknowns(sys) ps = get_ps(sys) @@ -915,19 +920,6 @@ function generate_function_bc(sys::ODESystem, u0, u0_idxs, tspan, iip) cons = map(c -> Symbolics.substitute(c, Dict(x(t) => sol(t)[idx])), cons) end - - for var in parameters(conssys) - if iscall(var) - x = operation(var) - t = only(arguments(var)) - idx = pidxmap[x] - - cons = map(c -> Symbolics.substitute(c, Dict(x(t) => p[idx])), cons) - else - idx = pidxmap[var] - cons = map(c -> Symbolics.substitute(c, Dict(var => p[idx])), cons) - end - end end init_conds = Any[] @@ -937,12 +929,9 @@ function generate_function_bc(sys::ODESystem, u0, u0_idxs, tspan, iip) end exprs = vcat(init_conds, cons) - bcs = Symbolics.build_function(exprs, sol, p, expression = Val{false}) - if iip - return (resid, u, p, t) -> bcs[2](resid, u, p) - else - return (u, p, t) -> bcs[1](u, p) - end + _p = reorder_parameters(sys, ps) + + build_function_wrapper(sys, exprs, sol, _p..., t; kwargs...) end """ diff --git a/test/bvproblem.jl b/test/bvproblem.jl index 069372aef7..cedd0eef8d 100644 --- a/test/bvproblem.jl +++ b/test/bvproblem.jl @@ -1,6 +1,7 @@ ### TODO: update when BoundaryValueDiffEqAscher is updated to use the normal boundary condition conventions -using BoundaryValueDiffEq, OrdinaryDiffEq, BoundaryValueDiffEqAscher +using OrdinaryDiffEqVerner +using BoundaryValueDiffEqMIRK, BoundaryValueDiffEqAscher using BenchmarkTools using ModelingToolkit using SciMLBase @@ -207,22 +208,22 @@ let u0map = [] tspan = (0.0, 1.0) - guesses = [x(t) => 4.0, y(t) => 2.] + guess = [x(t) => 4.0, y(t) => 2.0] constr = [x(.6) ~ 3.5, x(.3) ~ 7.] @mtkbuild lksys = ODESystem(eqs, t; constraints = constr) - bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(lksys, u0map, tspan; guesses) + bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(lksys, u0map, tspan; guesses = guess) test_solvers(solvers, bvp, u0map, constr; dt = 0.05) # Testing that more complicated constraints give correct solutions. constr = [y(.2) + x(.8) ~ 3., y(.3) ~ 2.] @mtkbuild lksys = ODESystem(eqs, t; constraints = constr) - bvp = SciMLBase.BVProblem{false, SciMLBase.FullSpecialize}(lksys, u0map, tspan; guesses) + bvp = SciMLBase.BVProblem{false, SciMLBase.FullSpecialize}(lksys, u0map, tspan; guesses = guess) test_solvers(solvers, bvp, u0map, constr; dt = 0.05) constr = [α * β - x(.6) ~ 0.0, y(.2) ~ 3.] @mtkbuild lksys = ODESystem(eqs, t; constraints = constr) - bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(lksys, u0map, tspan; guesses) + bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(lksys, u0map, tspan; guesses = guess) test_solvers(solvers, bvp, u0map, constr) end From 2fcb9c930c0e4d607d509f1f13a01cef11df6b12 Mon Sep 17 00:00:00 2001 From: vyudu Date: Mon, 3 Feb 2025 15:34:37 -0500 Subject: [PATCH 0760/1337] up --- src/systems/diffeqs/abstractodesystem.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index 95e966a461..056ecd80e4 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -883,7 +883,7 @@ function SciMLBase.BVProblem{iip, specialize}(sys::AbstractODESystem, u0map = [] fns = generate_function_bc(sys, u0, u0_idxs, tspan) bc_oop, bc_iip = eval_or_rgf.(fns; eval_expression, eval_module) - # bc(sol, p, t) = bc_oop(sol, p, t) + bc(sol, p, t) = bc_oop(sol, p, t) bc(resid, u, p, t) = bc_iip(resid, u, p, t) return BVProblem{iip}(f, bc, u0, tspan, p; kwargs...) From 25b56d77859cb4afd8c33bf2c48771e81591859e Mon Sep 17 00:00:00 2001 From: vyudu Date: Mon, 3 Feb 2025 22:03:33 -0500 Subject: [PATCH 0761/1337] working codegen --- src/systems/diffeqs/abstractodesystem.jl | 8 ++++---- src/systems/diffeqs/odesystem.jl | 2 +- src/systems/optimization/constraints_system.jl | 2 +- test/odesystem.jl | 12 ++++++++++++ 4 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index 056ecd80e4..8d8dd80d47 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -899,14 +899,14 @@ get_callback(prob::BVProblem) = error("BVP solvers do not support callbacks.") """ function generate_function_bc(sys::ODESystem, u0, u0_idxs, tspan; kwargs...) iv = get_iv(sys) - sts = get_unknowns(sys) - ps = get_ps(sys) + sts = unknowns(sys) + ps = parameters(sys) np = length(ps) ns = length(sts) stidxmap = Dict([v => i for (i, v) in enumerate(sts)]) pidxmap = Dict([v => i for (i, v) in enumerate(ps)]) - @variables sol(..)[1:ns] p[1:np] + @variables sol(..)[1:ns] conssys = get_constraintsystem(sys) cons = Any[] @@ -931,7 +931,7 @@ function generate_function_bc(sys::ODESystem, u0, u0_idxs, tspan; kwargs...) exprs = vcat(init_conds, cons) _p = reorder_parameters(sys, ps) - build_function_wrapper(sys, exprs, sol, _p..., t; kwargs...) + build_function_wrapper(sys, exprs, sol, _p..., t; output_type = Array, kwargs...) end """ diff --git a/src/systems/diffeqs/odesystem.jl b/src/systems/diffeqs/odesystem.jl index c38f20c235..44cc7df46b 100644 --- a/src/systems/diffeqs/odesystem.jl +++ b/src/systems/diffeqs/odesystem.jl @@ -687,7 +687,6 @@ function process_constraint_system(constraints::Vector{Equation}, sts, ps, iv; c constraintsts = OrderedSet() constraintps = OrderedSet() - # Hack? to extract parameters from callable variables in constraints. for cons in constraints collect_vars!(constraintsts, constraintps, cons, iv) end @@ -712,5 +711,6 @@ function process_constraint_system(constraints::Vector{Equation}, sts, ps, iv; c end end + @show constraints ConstraintsSystem(constraints, collect(constraintsts), collect(constraintps); name = consname) end diff --git a/src/systems/optimization/constraints_system.jl b/src/systems/optimization/constraints_system.jl index 275079297a..ecdbafa044 100644 --- a/src/systems/optimization/constraints_system.jl +++ b/src/systems/optimization/constraints_system.jl @@ -123,7 +123,7 @@ function ConstraintsSystem(constraints, unknowns, ps; name === nothing && throw(ArgumentError("The `name` keyword must be provided. Please consider using the `@named` macro")) - cstr = value.(Symbolics.canonical_form.(scalarize(constraints))) + cstr = value.(Symbolics.canonical_form.(vcat(scalarize(constraints)...))) unknowns′ = value.(scalarize(unknowns)) ps′ = value.(ps) diff --git a/test/odesystem.jl b/test/odesystem.jl index 98ad6dd87a..97073457b3 100644 --- a/test/odesystem.jl +++ b/test/odesystem.jl @@ -1670,4 +1670,16 @@ end @test_throws ArgumentError @mtkbuild sys = ODESystem(eqs, t; constraints = cons) cons = [x(t) * v ~ 3] @test_throws ArgumentError @mtkbuild sys = ODESystem(eqs, t; constraints = cons) # Need time argument. + + # Test array variables + @variables x(..)[1:5] + mat = [1 2 0 3 2 + 0 0 3 2 0 + 0 1 3 0 4 + 2 0 0 2 1 + 0 0 2 0 5] + eqs = D(x(t)) ~ mat * x(t) + cons = [x(3) ~ [2,3,3,5,4]] + @mtkbuild ode = ODESystem(D(x(t)) ~ mat * x(t), t; constraints = cons) + @test length(constraints(ModelingToolkit.get_constraintsystem(ode))) == 5 end From dd71d2349f0d8eca81b04fc212209045bfb18018 Mon Sep 17 00:00:00 2001 From: vyudu Date: Mon, 3 Feb 2025 22:10:22 -0500 Subject: [PATCH 0762/1337] add docs --- docs/src/systems/DiscreteSystem.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 docs/src/systems/DiscreteSystem.md diff --git a/docs/src/systems/DiscreteSystem.md b/docs/src/systems/DiscreteSystem.md new file mode 100644 index 0000000000..b6a8061e50 --- /dev/null +++ b/docs/src/systems/DiscreteSystem.md @@ -0,0 +1,28 @@ +# DiscreteSystem + +## System Constructors + +```@docs +DiscreteSystem +``` + +## Composition and Accessor Functions + + - `get_eqs(sys)` or `equations(sys)`: The equations that define the discrete system. + - `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 + +```@docs; canonical=false +structural_simplify +``` + +## Problem Constructors + +```@docs; canonical=false +DiscreteProblem(sys::DiscreteSystem, u0map, tspan) +DiscreteFunction(sys::DiscreteSystem, u0map, tspan) +``` From 4475a5a6ca7b61a897f3c90041895ed23b3df5d4 Mon Sep 17 00:00:00 2001 From: vyudu Date: Mon, 3 Feb 2025 22:37:56 -0500 Subject: [PATCH 0763/1337] up --- .../symbolics_tearing.jl | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/src/structural_transformation/symbolics_tearing.jl b/src/structural_transformation/symbolics_tearing.jl index 599a75f4f7..059c7ca9e4 100644 --- a/src/structural_transformation/symbolics_tearing.jl +++ b/src/structural_transformation/symbolics_tearing.jl @@ -237,18 +237,6 @@ function check_diff_graph(var_to_diff, fullvars) end =# -function state_selection() - -end - -function create_new_deriv_variables() - -end - -function solve_solvable_equations() - -end - function tearing_reassemble(state::TearingState, var_eq_matching, full_var_eq_matching = nothing; simplify = false, mm = nothing, cse_hack = true, array_hack = true) @unpack fullvars, sys, structure = state @@ -323,7 +311,7 @@ function tearing_reassemble(state::TearingState, var_eq_matching, diff_to_var[dv] = nothing end end - @show neweqs + @show var_eq_matching println("Post state selection.") @@ -337,7 +325,7 @@ function tearing_reassemble(state::TearingState, var_eq_matching, end idx_to_lowest_shift = Dict{Int, Int}(var => 0 for var in 1:length(fullvars)) for (i,var) in enumerate(fullvars) - key = operation(var) isa Shift ? only(arguments(var)) : var + key = (operation(var) isa Shift) ? only(arguments(var)) : var idx_to_lowest_shift[i] = get(lowest_shift, key, 0) end @@ -345,6 +333,8 @@ function tearing_reassemble(state::TearingState, var_eq_matching, isdervar = let diff_to_var = diff_to_var var -> diff_to_var[var] !== nothing end + # For discrete variables, we want the substitution to turn + # Shift(t, k)(x(t)) => x_t-k(t) var_order = let diff_to_var = diff_to_var dv -> begin order = 0 @@ -358,7 +348,6 @@ function tearing_reassemble(state::TearingState, var_eq_matching, end lower_name = is_only_discrete(state.structure) ? lower_varname_withshift : lower_varname_with_unit - #retear = BitSet() # There are three cases where we want to generate new variables to convert # the system into first order (semi-implicit) ODEs. # From c35b7974f2d96584c96cfbffa2de87a390d92d8d Mon Sep 17 00:00:00 2001 From: vyudu Date: Mon, 3 Feb 2025 22:43:39 -0500 Subject: [PATCH 0764/1337] revert to OrdinaryDiffEqDefault --- test/bvproblem.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/bvproblem.jl b/test/bvproblem.jl index cedd0eef8d..311a17e172 100644 --- a/test/bvproblem.jl +++ b/test/bvproblem.jl @@ -1,6 +1,6 @@ ### TODO: update when BoundaryValueDiffEqAscher is updated to use the normal boundary condition conventions -using OrdinaryDiffEqVerner +using OrdinaryDiffEqDefault using BoundaryValueDiffEqMIRK, BoundaryValueDiffEqAscher using BenchmarkTools using ModelingToolkit @@ -24,7 +24,7 @@ let @mtkbuild lotkavolterra = ODESystem(eqs, t) op = ODEProblem(lotkavolterra, u0map, tspan, parammap) - osol = solve(op, Vern9()) + osol = solve(op) bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}( lotkavolterra, u0map, tspan, parammap; eval_expression = true) @@ -61,7 +61,7 @@ let tspan = (0.0, 6.0) op = ODEProblem(pend, u0map, tspan, parammap) - osol = solve(op, Vern9()) + osol = solve(op) bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(pend, u0map, tspan, parammap) for solver in solvers From a964dd7e1660f599afdbf0e9b2c9ddbfa717ea83 Mon Sep 17 00:00:00 2001 From: vyudu Date: Mon, 3 Feb 2025 23:01:48 -0500 Subject: [PATCH 0765/1337] up --- src/structural_transformation/symbolics_tearing.jl | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/structural_transformation/symbolics_tearing.jl b/src/structural_transformation/symbolics_tearing.jl index 059c7ca9e4..a80bebd436 100644 --- a/src/structural_transformation/symbolics_tearing.jl +++ b/src/structural_transformation/symbolics_tearing.jl @@ -237,6 +237,10 @@ function check_diff_graph(var_to_diff, fullvars) end =# +function substitute_lower_order!(state::TearingState) + +end + function tearing_reassemble(state::TearingState, var_eq_matching, full_var_eq_matching = nothing; simplify = false, mm = nothing, cse_hack = true, array_hack = true) @unpack fullvars, sys, structure = state From 26a545dbaaf4ced4b59ae45cc02377523dec5f10 Mon Sep 17 00:00:00 2001 From: vyudu Date: Mon, 3 Feb 2025 23:07:07 -0500 Subject: [PATCH 0766/1337] up --- src/structural_transformation/symbolics_tearing.jl | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/structural_transformation/symbolics_tearing.jl b/src/structural_transformation/symbolics_tearing.jl index a80bebd436..e612369498 100644 --- a/src/structural_transformation/symbolics_tearing.jl +++ b/src/structural_transformation/symbolics_tearing.jl @@ -241,6 +241,7 @@ function substitute_lower_order!(state::TearingState) end +import ModelingToolkit: Shift function tearing_reassemble(state::TearingState, var_eq_matching, full_var_eq_matching = nothing; simplify = false, mm = nothing, cse_hack = true, array_hack = true) @unpack fullvars, sys, structure = state @@ -255,6 +256,8 @@ function tearing_reassemble(state::TearingState, var_eq_matching, end neweqs = collect(equations(state)) + lower_name = is_only_discrete(state.structure) ? lower_varname_withshift : lower_varname_with_unit + # Terminology and Definition: # # A general DAE is in the form of `F(u'(t), u(t), p, t) == 0`. We can @@ -350,7 +353,6 @@ function tearing_reassemble(state::TearingState, var_eq_matching, order, dv end end - lower_name = is_only_discrete(state.structure) ? lower_varname_withshift : lower_varname_with_unit # There are three cases where we want to generate new variables to convert # the system into first order (semi-implicit) ODEs. @@ -464,7 +466,6 @@ function tearing_reassemble(state::TearingState, var_eq_matching, add_edge!(solvable_graph, dummy_eq, dv) @assert nsrcs(graph) == nsrcs(solvable_graph) == dummy_eq @label FOUND_DUMMY_EQ - # If var = x with no shift, then is_only_discrete(state.structure) && (idx_to_lowest_shift[v_t] = idx_to_lowest_shift[dv]) var_to_diff[v_t] = var_to_diff[dv] var_eq_matching[dv] = unassigned From 25e84dbf39a92c569dd3a8f4027c9b33dd99f34e Mon Sep 17 00:00:00 2001 From: vyudu Date: Mon, 3 Feb 2025 23:08:12 -0500 Subject: [PATCH 0767/1337] use MIRK --- Project.toml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Project.toml b/Project.toml index c3646021ff..41c5ad0730 100644 --- a/Project.toml +++ b/Project.toml @@ -85,6 +85,7 @@ BifurcationKit = "0.4" BlockArrays = "1.1" BoundaryValueDiffEq = "5.12.0" BoundaryValueDiffEqAscher = "1.1.0" +BoundaryValueDiffEqMIRK = "1.4.0" ChainRulesCore = "1" Combinatorics = "1" CommonSolve = "0.2.4" @@ -106,11 +107,11 @@ DynamicQuantities = "^0.11.2, 0.12, 0.13, 1" EnumX = "1.0.4" ExprTools = "0.1.10" Expronicon = "0.8" +FMI = "0.14" FindFirstFunctions = "1" ForwardDiff = "0.10.3" FunctionWrappers = "1.1" FunctionWrappersWrappers = "0.1" -FMI = "0.14" Graphs = "1.5.2" HomotopyContinuation = "2.11" InfiniteOpt = "0.5" @@ -157,7 +158,7 @@ julia = "1.9" [extras] AmplNLWriter = "7c4d4715-977e-5154-bfe0-e096adeac482" BenchmarkTools = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf" -BoundaryValueDiffEq = "764a87c0-6b3e-53db-9096-fe964310641d" +BoundaryValueDiffEqMIRK = "1a22d4ce-7765-49ea-b6f2-13c8438986a6" BoundaryValueDiffEqAscher = "7227322d-7511-4e07-9247-ad6ff830280e" ControlSystemsBase = "aaaaaaaa-a6ca-5380-bf3e-84a91bcd477e" DataInterpolations = "82cc6244-b520-54b8-b5a6-8a565e85f1d0" @@ -190,4 +191,4 @@ Sundials = "c3572dad-4567-51f8-b174-8c6c989267f4" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [targets] -test = ["AmplNLWriter", "BenchmarkTools", "BoundaryValueDiffEq", "BoundaryValueDiffEqAscher", "ControlSystemsBase", "DataInterpolations", "DelayDiffEq", "NonlinearSolve", "ForwardDiff", "Ipopt", "Ipopt_jll", "ModelingToolkitStandardLibrary", "Optimization", "OptimizationOptimJL", "OptimizationMOI", "OrdinaryDiffEq", "OrdinaryDiffEqCore", "OrdinaryDiffEqDefault", "REPL", "Random", "ReferenceTests", "SafeTestsets", "StableRNGs", "Statistics", "SteadyStateDiffEq", "Test", "StochasticDiffEq", "Sundials", "StochasticDelayDiffEq", "Pkg", "JET", "OrdinaryDiffEqNonlinearSolve"] +test = ["AmplNLWriter", "BenchmarkTools", "BoundaryValueDiffEqMIRK", "BoundaryValueDiffEqAscher", "ControlSystemsBase", "DataInterpolations", "DelayDiffEq", "NonlinearSolve", "ForwardDiff", "Ipopt", "Ipopt_jll", "ModelingToolkitStandardLibrary", "Optimization", "OptimizationOptimJL", "OptimizationMOI", "OrdinaryDiffEq", "OrdinaryDiffEqCore", "OrdinaryDiffEqDefault", "REPL", "Random", "ReferenceTests", "SafeTestsets", "StableRNGs", "Statistics", "SteadyStateDiffEq", "Test", "StochasticDiffEq", "Sundials", "StochasticDelayDiffEq", "Pkg", "JET", "OrdinaryDiffEqNonlinearSolve"] From e6a6932d192ef72bc46be8ed6124965372e87f26 Mon Sep 17 00:00:00 2001 From: vyudu Date: Mon, 3 Feb 2025 23:09:04 -0500 Subject: [PATCH 0768/1337] up --- Project.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/Project.toml b/Project.toml index 41c5ad0730..cd2c2c4380 100644 --- a/Project.toml +++ b/Project.toml @@ -83,7 +83,6 @@ AbstractTrees = "0.3, 0.4" ArrayInterface = "6, 7" BifurcationKit = "0.4" BlockArrays = "1.1" -BoundaryValueDiffEq = "5.12.0" BoundaryValueDiffEqAscher = "1.1.0" BoundaryValueDiffEqMIRK = "1.4.0" ChainRulesCore = "1" From 5e5c24c071f00a8d50b6e7441a141ca9cbeb1681 Mon Sep 17 00:00:00 2001 From: vyudu Date: Mon, 3 Feb 2025 23:30:44 -0500 Subject: [PATCH 0769/1337] revert to OrdinaryDiffEq --- test/bvproblem.jl | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/test/bvproblem.jl b/test/bvproblem.jl index 311a17e172..1d7d4b3793 100644 --- a/test/bvproblem.jl +++ b/test/bvproblem.jl @@ -1,6 +1,6 @@ ### TODO: update when BoundaryValueDiffEqAscher is updated to use the normal boundary condition conventions -using OrdinaryDiffEqDefault +using OrdinaryDiffEq using BoundaryValueDiffEqMIRK, BoundaryValueDiffEqAscher using BenchmarkTools using ModelingToolkit @@ -24,10 +24,9 @@ let @mtkbuild lotkavolterra = ODESystem(eqs, t) op = ODEProblem(lotkavolterra, u0map, tspan, parammap) - osol = solve(op) + osol = solve(op, Vern9()) - bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}( - lotkavolterra, u0map, tspan, parammap; eval_expression = true) + bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(lotkavolterra, u0map, tspan, parammap) for solver in solvers sol = solve(bvp, solver(), dt = 0.01) @@ -36,8 +35,7 @@ let end # Test out of place - bvp2 = SciMLBase.BVProblem{false, SciMLBase.AutoSpecialize}( - lotkavolterra, u0map, tspan, parammap; eval_expression = true) + bvp2 = SciMLBase.BVProblem{false, SciMLBase.AutoSpecialize}(lotkavolterra, u0map, tspan, parammap) for solver in solvers sol = solve(bvp2, solver(), dt = 0.01) From 5338d4f6b60704ebcc1df0c425a0a721f1a46138 Mon Sep 17 00:00:00 2001 From: vyudu Date: Tue, 4 Feb 2025 00:08:54 -0500 Subject: [PATCH 0770/1337] tests passing --- test/bvproblem.jl | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/bvproblem.jl b/test/bvproblem.jl index 1d7d4b3793..b4032e2927 100644 --- a/test/bvproblem.jl +++ b/test/bvproblem.jl @@ -59,7 +59,7 @@ let tspan = (0.0, 6.0) op = ODEProblem(pend, u0map, tspan, parammap) - osol = solve(op) + osol = solve(op, Vern9()) bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(pend, u0map, tspan, parammap) for solver in solvers @@ -111,8 +111,8 @@ let end u0 = [1., 2.]; p = [1.5, 1., 1., 3.] - genbc_iip = ModelingToolkit.generate_function_bc(lksys, u0, [1, 2], tspan, true) - genbc_oop = ModelingToolkit.generate_function_bc(lksys, u0, [1, 2], tspan, false) + fns = ModelingToolkit.generate_function_bc(lksys, u0, [1, 2], tspan) + genbc_oop, genbc_iip = ModelingToolkit.eval_or_rgf.(fns) bvpi1 = SciMLBase.BVProblem(lotkavolterra!, bc!, [1.,2.], tspan, p) bvpi2 = SciMLBase.BVProblem(lotkavolterra!, genbc_iip, [1.,2.], tspan, p) @@ -141,8 +141,8 @@ let end u0 = [1, 1.] - genbc_iip = ModelingToolkit.generate_function_bc(lksys, u0, [1], tspan, true) - genbc_oop = ModelingToolkit.generate_function_bc(lksys, u0, [1], tspan, false) + fns = ModelingToolkit.generate_function_bc(lksys, u0, [1], tspan) + genbc_oop, genbc_iip = ModelingToolkit.eval_or_rgf.(fns) bvpi1 = SciMLBase.BVProblem(lotkavolterra!, bc!, u0, tspan, p) bvpi2 = SciMLBase.BVProblem(lotkavolterra!, genbc_iip, u0, tspan, p) From 62766354d7fac2166d76e3e3e022f35129b3f7fd Mon Sep 17 00:00:00 2001 From: Christopher Rackauckas Date: Mon, 3 Feb 2025 21:14:59 -0800 Subject: [PATCH 0771/1337] Update docs/src/basics/Variable_metadata.md --- docs/src/basics/Variable_metadata.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/basics/Variable_metadata.md b/docs/src/basics/Variable_metadata.md index d36c91697e..bcfe90bab4 100644 --- a/docs/src/basics/Variable_metadata.md +++ b/docs/src/basics/Variable_metadata.md @@ -191,7 +191,7 @@ isirreducible(important_value) When a model is structurally simplified, the algorithm will try to ensure that the variables with higher state priority become states of the system. A variable's state priority is a number set using the `state_priority` metadata. ```@example metadata -@variable important_dof [state_priority = 10] unimportant_dof [state_priority = 2] +@variable important_dof [state_priority = 10] unimportant_dof [state_priority = -2] state_priority(important_dof) ``` From 79b07ceb12d88b212cd3dfb8e851b317eb216e24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20B=C4=99czkowski?= Date: Tue, 4 Feb 2025 08:43:50 +0100 Subject: [PATCH 0772/1337] latexify equation --- docs/src/tutorials/initialization.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/src/tutorials/initialization.md b/docs/src/tutorials/initialization.md index c01310eb06..26dbb58899 100644 --- a/docs/src/tutorials/initialization.md +++ b/docs/src/tutorials/initialization.md @@ -14,8 +14,10 @@ principles of initialization of DAE systems. Take a DAE written in semi-explicit form: ```math -x' = f(x,y,t)\\ -0 = g(x,y,t) +\begin{aligned} + x^\prime &= f(x,y,t) \\ + 0 &= g(x,y,t) +\end{aligned} ``` where ``x`` are the differential variables and ``y`` are the algebraic variables. From 1656277f45e7a12229d8ed0545a979cb8bddb273 Mon Sep 17 00:00:00 2001 From: vyudu Date: Tue, 4 Feb 2025 02:49:11 -0500 Subject: [PATCH 0773/1337] maadd comments --- .../symbolics_tearing.jl | 68 ++++++++++++++----- src/structural_transformation/utils.jl | 3 +- src/systems/systemstructure.jl | 3 - 3 files changed, 52 insertions(+), 22 deletions(-) diff --git a/src/structural_transformation/symbolics_tearing.jl b/src/structural_transformation/symbolics_tearing.jl index e612369498..cf6736b419 100644 --- a/src/structural_transformation/symbolics_tearing.jl +++ b/src/structural_transformation/symbolics_tearing.jl @@ -241,6 +241,32 @@ function substitute_lower_order!(state::TearingState) end +# Documenting the differences to structural simplification for discrete systems: +# In discrete systems the lowest-order term is x_k-i, instead of x(t). In order +# to substitute dummy variables for x_k-1, x_k-2, ... instead you need to reverse +# the order. So for discrete systems `var_order` is defined a little differently. +# +# The orders will also be off by one. The reason this is is that the dynamics of +# the system should be given in terms of Shift(t, 1)(x(t), x(t-1), ...). But +# having the observables be indexed by the next time step is not so nice. So we +# handle the shifts in the renaming, rather than explicitly. +# +# The substitution should look like the following: +# x(t) -> Shift(t, 1)(x(t)) +# x(k-1) -> x(t) +# x(k-2) -> x_{t-1}(t) +# x(k-3) -> x_{t-2}(t) +# and so on... +# +# In the implicit discrete case this shouldn't happen. The simplification should +# look like a NonlinearSystem. +# +# For discrete systems Shift(t, 2)(x(t)) is not equivalent to Shift(t, 1)(Shift(t,1)(x(t)) +# This is different from the continuous case where D(D(x)) can be substituted for +# by iteratively substituting x_t ~ D(x), then x_tt ~ D(x_t). For this reason the +# total_sub dict is updated at the time that the renamed variables are written, +# inside the loop where new variables are generated. + import ModelingToolkit: Shift function tearing_reassemble(state::TearingState, var_eq_matching, full_var_eq_matching = nothing; simplify = false, mm = nothing, cse_hack = true, array_hack = true) @@ -318,9 +344,6 @@ function tearing_reassemble(state::TearingState, var_eq_matching, diff_to_var[dv] = nothing end end - @show var_eq_matching - - println("Post state selection.") # `SelectedState` information is no longer needed past here. State selection # is done. All non-differentiated variables are algebraic variables, and all @@ -330,18 +353,19 @@ function tearing_reassemble(state::TearingState, var_eq_matching, is_solvable = let solvable_graph = solvable_graph (eq, iv) -> eq isa Int && iv isa Int && BipartiteEdge(eq, iv) in solvable_graph end - idx_to_lowest_shift = Dict{Int, Int}(var => 0 for var in 1:length(fullvars)) - for (i,var) in enumerate(fullvars) - key = (operation(var) isa Shift) ? only(arguments(var)) : var - idx_to_lowest_shift[i] = get(lowest_shift, key, 0) + + if is_only_discrete(state.structure) + idx_to_lowest_shift = Dict{Int, Int}(var => 0 for var in 1:length(fullvars)) + for (i,var) in enumerate(fullvars) + key = (operation(var) isa Shift) ? only(arguments(var)) : var + idx_to_lowest_shift[i] = get(lowest_shift, key, 0) + end end # if var is like D(x) isdervar = let diff_to_var = diff_to_var var -> diff_to_var[var] !== nothing end - # For discrete variables, we want the substitution to turn - # Shift(t, k)(x(t)) => x_t-k(t) var_order = let diff_to_var = diff_to_var dv -> begin order = 0 @@ -349,7 +373,7 @@ function tearing_reassemble(state::TearingState, var_eq_matching, order += 1 dv = dv′ end - is_only_discrete(state.structure) && (order = -idx_to_lowest_shift[dv] - order - 1) + is_only_discrete(state.structure) && (order = -idx_to_lowest_shift[dv] - order) order, dv end end @@ -403,6 +427,7 @@ function tearing_reassemble(state::TearingState, var_eq_matching, linear_eqs = mm === nothing ? Dict{Int, Int}() : Dict(reverse(en) for en in enumerate(mm.nzrows)) + total_sub = Dict() for v in 1:length(var_to_diff) is_highest_discrete = begin var = fullvars[v] @@ -442,8 +467,13 @@ function tearing_reassemble(state::TearingState, var_eq_matching, end dx = fullvars[dv] # add `x_t` - order, lv = var_order(dv) + println() + @show order, lv = var_order(dv) x_t = lower_name(fullvars[lv], iv, order) + @show fullvars[v] + @show fullvars[dv] + @show fullvars[lv] + @show dx, x_t push!(fullvars, simplify_shifts(x_t)) v_t = length(fullvars) v_t_idx = add_vertex!(var_to_diff) @@ -466,11 +496,16 @@ function tearing_reassemble(state::TearingState, var_eq_matching, add_edge!(solvable_graph, dummy_eq, dv) @assert nsrcs(graph) == nsrcs(solvable_graph) == dummy_eq @label FOUND_DUMMY_EQ - is_only_discrete(state.structure) && (idx_to_lowest_shift[v_t] = idx_to_lowest_shift[dv]) + is_only_discrete(state.structure) && begin + idx_to_lowest_shift[v_t] = idx_to_lowest_shift[dv] + operation(dx) isa Shift && (total_sub[dx] = x_t) + order == 1 && (total_sub[x_t] = fullvars[var_to_diff[dv]]) + end var_to_diff[v_t] = var_to_diff[dv] var_eq_matching[dv] = unassigned eq_var_matching[dummy_eq] = dv end + @show total_sub # Will reorder equations and unknowns to be: # [diffeqs; ...] @@ -490,15 +525,13 @@ function tearing_reassemble(state::TearingState, var_eq_matching, # Solve solvable equations println() println("SOLVING SOLVABLE EQUATIONS.") - @show eq_var_matching toporder = topological_sort(DiCMOBiGraph{false}(graph, var_eq_matching)) eqs = Iterators.reverse(toporder) - @show eqs - @show neweqs - @show fullvars - total_sub = Dict() idep = iv + @show eq_var_matching + for ieq in eqs + println() iv = eq_var_matching[ieq] if is_solvable(ieq, iv) # We don't solve differential equations, but we will need to try to @@ -513,7 +546,6 @@ function tearing_reassemble(state::TearingState, var_eq_matching, dx = D(simplify_shifts(lower_name( fullvars[lv], idep, order - 1))) @show dx - @show neweqs[ieq] eq = dx ~ simplify_shifts(Symbolics.fixpoint_sub( Symbolics.symbolic_linear_solve(neweqs[ieq], fullvars[iv]), diff --git a/src/structural_transformation/utils.jl b/src/structural_transformation/utils.jl index dd063c0b07..707a57097b 100644 --- a/src/structural_transformation/utils.jl +++ b/src/structural_transformation/utils.jl @@ -451,7 +451,8 @@ end function lower_varname_withshift(var, iv, order) order == 0 && return var - ds = "$iv-$order" + #order == -1 && return Shift(iv, 1)(var) + ds = "$iv-$(order-1)" d_separator = 'ˍ' if ModelingToolkit.isoperator(var, ModelingToolkit.Shift) diff --git a/src/systems/systemstructure.jl b/src/systems/systemstructure.jl index 44558ade6c..89fcebf549 100644 --- a/src/systems/systemstructure.jl +++ b/src/systems/systemstructure.jl @@ -476,9 +476,6 @@ function shift_discrete_system(ts::TearingState, lowest_shift) vars!(discvars, eq; op = Union{Sample, Hold}) end iv = get_iv(sys) - discmap = Dict(k => StructuralTransformations.simplify_shifts(Shift(iv, -get(lowest_shift, k, 0))(k)) - for k in discvars - if any(isequal(k), fullvars) && !isa(operation(k), Union{Sample, Hold})) discmap = Dict(k => StructuralTransformations.simplify_shifts(Shift(iv, 1)(k)) for k in discvars if any(isequal(k), fullvars) && !isa(operation(k), Union{Sample, Hold})) From 810d4fa26af98e0231da9323bb61053f02fd13ce Mon Sep 17 00:00:00 2001 From: vyudu Date: Tue, 4 Feb 2025 02:58:11 -0500 Subject: [PATCH 0774/1337] remove problematic tests, codegen assumes MTKParameters --- test/bvproblem.jl | 44 ++------------------------------------------ 1 file changed, 2 insertions(+), 42 deletions(-) diff --git a/test/bvproblem.jl b/test/bvproblem.jl index b4032e2927..edc85b4cbd 100644 --- a/test/bvproblem.jl +++ b/test/bvproblem.jl @@ -100,33 +100,6 @@ let function lotkavolterra(u, p, t) [p[1]*u[1] - p[2]*u[1]*u[2], -p[4]*u[2] + p[3]*u[1]*u[2]] end - # Compare the built bc function to the actual constructed one. - function bc!(resid, u, p, t) - resid[1] = u[1][1] - 1. - resid[2] = u[1][2] - 2. - nothing - end - function bc(u, p, t) - [u[1][1] - 1., u[1][2] - 2.] - end - - u0 = [1., 2.]; p = [1.5, 1., 1., 3.] - fns = ModelingToolkit.generate_function_bc(lksys, u0, [1, 2], tspan) - genbc_oop, genbc_iip = ModelingToolkit.eval_or_rgf.(fns) - - bvpi1 = SciMLBase.BVProblem(lotkavolterra!, bc!, [1.,2.], tspan, p) - bvpi2 = SciMLBase.BVProblem(lotkavolterra!, genbc_iip, [1.,2.], tspan, p) - - sol1 = solve(bvpi1, MIRK4(), dt = 0.05) - sol2 = solve(bvpi2, MIRK4(), dt = 0.05) - @test sol1 ≈ sol2 - - bvpo1 = BVProblem(lotkavolterra, bc, [1,2], tspan, p) - bvpo2 = BVProblem(lotkavolterra, genbc_oop, [1,2], tspan, p) - - sol1 = solve(bvpo1, MIRK4(), dt = 0.05) - sol2 = solve(bvpo2, MIRK4(), dt = 0.05) - @test sol1 ≈ sol2 # Test with a constraint. constr = [y(0.5) ~ 2.] @@ -140,29 +113,16 @@ let [u(0.0)[1] - 1., u(0.5)[2] - 2.] end - u0 = [1, 1.] - fns = ModelingToolkit.generate_function_bc(lksys, u0, [1], tspan) - genbc_oop, genbc_iip = ModelingToolkit.eval_or_rgf.(fns) - bvpi1 = SciMLBase.BVProblem(lotkavolterra!, bc!, u0, tspan, p) - bvpi2 = SciMLBase.BVProblem(lotkavolterra!, genbc_iip, u0, tspan, p) + bvpi1 = SciMLBase.BVProblem(lotkavolterra, bc, u0, tspan, p) bvpi3 = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(lksys, [x(t) => 1.], tspan; guesses = [y(t) => 1.]) - bvpi4 = SciMLBase.BVProblem{true, SciMLBase.FullSpecialize}(lksys, [x(t) => 1.], tspan; guesses = [y(t) => 1.]) + bvpi4 = SciMLBase.BVProblem{false, SciMLBase.FullSpecialize}(lksys, [x(t) => 1.], tspan; guesses = [y(t) => 1.]) sol1 = @btime solve($bvpi1, MIRK4(), dt = 0.01) sol2 = @btime solve($bvpi2, MIRK4(), dt = 0.01) sol3 = @btime solve($bvpi3, MIRK4(), dt = 0.01) sol4 = @btime solve($bvpi4, MIRK4(), dt = 0.01) @test sol1 ≈ sol2 ≈ sol3 ≈ sol4 # don't get true equality here, not sure why - - bvpo1 = BVProblem(lotkavolterra, bc, u0, tspan, p) - bvpo2 = BVProblem(lotkavolterra, genbc_oop, u0, tspan, p) - bvpo3 = SciMLBase.BVProblem{false, SciMLBase.FullSpecialize}(lksys, [x(t) => 1.], tspan; guesses = [y(t) => 1.]) - - sol1 = @btime solve($bvpo1, MIRK4(), dt = 0.05) - sol2 = @btime solve($bvpo2, MIRK4(), dt = 0.05) - sol3 = @btime solve($bvpo3, MIRK4(), dt = 0.05) - @test sol1 ≈ sol2 ≈ sol3 end function test_solvers(solvers, prob, u0map, constraints, equations = []; dt = 0.05, atol = 1e-2) From 16ea18bbde45a0c50db93d0922375a5f54312fe9 Mon Sep 17 00:00:00 2001 From: vyudu Date: Tue, 4 Feb 2025 03:27:27 -0500 Subject: [PATCH 0775/1337] begin refactor --- .../symbolics_tearing.jl | 129 +++++++++--------- 1 file changed, 66 insertions(+), 63 deletions(-) diff --git a/src/structural_transformation/symbolics_tearing.jl b/src/structural_transformation/symbolics_tearing.jl index cf6736b419..6340087003 100644 --- a/src/structural_transformation/symbolics_tearing.jl +++ b/src/structural_transformation/symbolics_tearing.jl @@ -237,8 +237,49 @@ function check_diff_graph(var_to_diff, fullvars) end =# -function substitute_lower_order!(state::TearingState) - + +function substitute_dummy_derivatives!(fullvars, var_to_diff, diff_to_var, var_eq_matching, neweqs) + # Dummy derivatives may determine that some differential variables are + # algebraic variables in disguise. The derivative of such variables are + # called dummy derivatives. + + # Step 1: + # Replace derivatives of non-selected unknown variables by dummy derivatives + + dummy_sub = Dict() + for var in 1:length(fullvars) + dv = var_to_diff[var] + dv === nothing && continue + if var_eq_matching[var] !== SelectedState() + dd = fullvars[dv] + v_t = setio(diff2term_with_unit(unwrap(dd), unwrap(iv)), false, false) + for eq in 𝑑neighbors(graph, dv) + dummy_sub[dd] = v_t + neweqs[eq] = fast_substitute(neweqs[eq], dd => v_t) + end + fullvars[dv] = v_t + # If we have: + # x -> D(x) -> D(D(x)) + # We need to to transform it to: + # x x_t -> D(x_t) + # update the structural information + dx = dv + x_t = v_t + while (ddx = var_to_diff[dx]) !== nothing + dx_t = D(x_t) + for eq in 𝑑neighbors(graph, ddx) + neweqs[eq] = fast_substitute(neweqs[eq], fullvars[ddx] => dx_t) + end + fullvars[ddx] = dx_t + dx = ddx + x_t = dx_t + end + diff_to_var[dv] = nothing + end + end +end + +function generate_derivative_variables!() end # Documenting the differences to structural simplification for discrete systems: @@ -281,88 +322,50 @@ function tearing_reassemble(state::TearingState, var_eq_matching, end end - neweqs = collect(equations(state)) - lower_name = is_only_discrete(state.structure) ? lower_varname_withshift : lower_varname_with_unit - - # Terminology and Definition: - # - # A general DAE is in the form of `F(u'(t), u(t), p, t) == 0`. We can - # characterize variables in `u(t)` into two classes: differential variables - # (denoted `v(t)`) and algebraic variables (denoted `z(t)`). Differential - # variables are marked as `SelectedState` and they are differentiated in the - # DAE system, i.e. `v'(t)` are all the variables in `u'(t)` that actually - # appear in the system. Algebraic variables are variables that are not - # differential variables. - # - # Dummy derivatives may determine that some differential variables are - # algebraic variables in disguise. The derivative of such variables are - # called dummy derivatives. - - # Step 1: - # Replace derivatives of non-selected unknown variables by dummy derivatives - if ModelingToolkit.has_iv(state.sys) iv = get_iv(state.sys) if is_only_discrete(state.structure) D = Shift(iv, 1) + lower_name = lower_varname_withshift + idx_to_lowest_shift = Dict{Int, Int}(var => 0 for var in 1:length(fullvars)) + for (i,var) in enumerate(fullvars) + key = (operation(var) isa Shift) ? only(arguments(var)) : var + idx_to_lowest_shift[i] = get(lowest_shift, key, 0) + end else D = Differential(iv) + lower_name = lower_varname_with_unit end else iv = D = nothing + lower_name = lower_varname_with_unit end + diff_to_var = invview(var_to_diff) + neweqs = collect(equations(state)) - dummy_sub = Dict() - for var in 1:length(fullvars) - dv = var_to_diff[var] - dv === nothing && continue - if var_eq_matching[var] !== SelectedState() - dd = fullvars[dv] - v_t = setio(diff2term_with_unit(unwrap(dd), unwrap(iv)), false, false) - for eq in 𝑑neighbors(graph, dv) - dummy_sub[dd] = v_t - neweqs[eq] = fast_substitute(neweqs[eq], dd => v_t) - end - fullvars[dv] = v_t - # If we have: - # x -> D(x) -> D(D(x)) - # We need to to transform it to: - # x x_t -> D(x_t) - # update the structural information - dx = dv - x_t = v_t - while (ddx = var_to_diff[dx]) !== nothing - dx_t = D(x_t) - for eq in 𝑑neighbors(graph, ddx) - neweqs[eq] = fast_substitute(neweqs[eq], fullvars[ddx] => dx_t) - end - fullvars[ddx] = dx_t - dx = ddx - x_t = dx_t - end - diff_to_var[dv] = nothing - end - end + # Terminology and Definition: + # + # A general DAE is in the form of `F(u'(t), u(t), p, t) == 0`. We can + # characterize variables in `u(t)` into two classes: differential variables + # (denoted `v(t)`) and algebraic variables (denoted `z(t)`). Differential + # variables are marked as `SelectedState` and they are differentiated in the + # DAE system, i.e. `v'(t)` are all the variables in `u'(t)` that actually + # appear in the system. Algebraic variables are variables that are not + # differential variables. + substitute_dummy_derivatives!(fullvars, var_to_diff, diff_to_var, var_eq_matching, neweqs) + # `SelectedState` information is no longer needed past here. State selection # is done. All non-differentiated variables are algebraic variables, and all # variables that appear differentiated are differential variables. - ### extract partition information + ### Extract partition information is_solvable = let solvable_graph = solvable_graph (eq, iv) -> eq isa Int && iv isa Int && BipartiteEdge(eq, iv) in solvable_graph end - if is_only_discrete(state.structure) - idx_to_lowest_shift = Dict{Int, Int}(var => 0 for var in 1:length(fullvars)) - for (i,var) in enumerate(fullvars) - key = (operation(var) isa Shift) ? only(arguments(var)) : var - idx_to_lowest_shift[i] = get(lowest_shift, key, 0) - end - end - - # if var is like D(x) + # if var is like D(x) or Shift(t, 1)(x) isdervar = let diff_to_var = diff_to_var var -> diff_to_var[var] !== nothing end From 6740b8c89b6992e3ddfc1b004f74fe632ef2295d Mon Sep 17 00:00:00 2001 From: vyudu Date: Tue, 4 Feb 2025 03:31:53 -0500 Subject: [PATCH 0776/1337] test fix --- test/bvproblem.jl | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/bvproblem.jl b/test/bvproblem.jl index edc85b4cbd..f05be90281 100644 --- a/test/bvproblem.jl +++ b/test/bvproblem.jl @@ -113,8 +113,11 @@ let [u(0.0)[1] - 1., u(0.5)[2] - 2.] end + u0 = [1., 1.] + tspan = (0., 1.) + p = [1.5, 1., 1., 3.] bvpi1 = SciMLBase.BVProblem(lotkavolterra!, bc!, u0, tspan, p) - bvpi1 = SciMLBase.BVProblem(lotkavolterra, bc, u0, tspan, p) + bvpi2 = SciMLBase.BVProblem(lotkavolterra, bc, u0, tspan, p) bvpi3 = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(lksys, [x(t) => 1.], tspan; guesses = [y(t) => 1.]) bvpi4 = SciMLBase.BVProblem{false, SciMLBase.FullSpecialize}(lksys, [x(t) => 1.], tspan; guesses = [y(t) => 1.]) From 2c1997b864c068caaf8af6195e26655a317ed2bb Mon Sep 17 00:00:00 2001 From: Benjamin Chung Date: Tue, 4 Feb 2025 02:47:00 -0800 Subject: [PATCH 0777/1337] Fix parameter array handling in ImperativeAffect --- src/systems/imperative_affect.jl | 7 +++- test/symbolic_events.jl | 57 ++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/src/systems/imperative_affect.jl b/src/systems/imperative_affect.jl index aee95fd051..4c9ff3d248 100644 --- a/src/systems/imperative_affect.jl +++ b/src/systems/imperative_affect.jl @@ -75,7 +75,12 @@ func(f::ImperativeAffect) = f.f context(a::ImperativeAffect) = a.ctx observed(a::ImperativeAffect) = a.obs observed_syms(a::ImperativeAffect) = a.obs_syms -discretes(a::ImperativeAffect) = filter(ModelingToolkit.isparameter, a.modified) +function discretes(a::ImperativeAffect) + Iterators.filter(ModelingToolkit.isparameter, + Iterators.flatten(Iterators.map( + x -> symbolic_type(x) == NotSymbolic() && x isa AbstractArray ? x : [x], + a.modified))) +end modified(a::ImperativeAffect) = a.modified modified_syms(a::ImperativeAffect) = a.mod_syms diff --git a/test/symbolic_events.jl b/test/symbolic_events.jl index ccf0a17a40..3fb66081a2 100644 --- a/test/symbolic_events.jl +++ b/test/symbolic_events.jl @@ -1377,3 +1377,60 @@ end prob = ODEProblem(decay, [], (0.0, 10.0), []) @test_nowarn solve(prob, Tsit5(), tstops = [1.0]) end + +@testset "Array parameter updates in ImperativeEffect" begin + function weird1(max_time; name) + params = @parameters begin + θ(t) = 0.0 + end + vars = @variables begin + x(t) = 0.0 + end + eqs = reduce(vcat, Symbolics.scalarize.([ + D(x) ~ 1.0 + ])) + reset = ModelingToolkit.ImperativeAffect( + modified = (; x, θ)) do m, o, _, i + @set! m.θ = 0.0 + @set! m.x = 0.0 + return m + end + return ODESystem(eqs, t, vars, params; name = name, + continuous_events = [[x ~ max_time] => reset]) + end + + function weird2(max_time; name) + params = @parameters begin + θ(t) = 0.0 + end + vars = @variables begin + x(t) = 0.0 + end + eqs = reduce(vcat, Symbolics.scalarize.([ + D(x) ~ 1.0 + ])) + return ODESystem(eqs, t, vars, params; name = name) # note no event + end + + @named wd1 = weird1(0.021) + @named wd2 = weird2(0.021) + + sys1 = structural_simplify(ODESystem([], t; name = :parent, + discrete_events = [0.01 => ModelingToolkit.ImperativeAffect( + modified = (; θs = reduce(vcat, [[wd1.θ]])), ctx = [1]) do m, o, c, i + @set! m.θs[1] = c[] += 1 + end], + systems = [wd1])) + sys2 = structural_simplify(ODESystem([], t; name = :parent, + discrete_events = [0.01 => ModelingToolkit.ImperativeAffect( + modified = (; θs = reduce(vcat, [[wd2.θ]])), ctx = [1]) do m, o, c, i + @set! m.θs[1] = c[] += 1 + end], + systems = [wd2])) + + sol1 = solve(ODEProblem(sys1, [], (0.0, 1.0)), Tsit5()) + @test 100.0 ∈ sol1[sys1.wd1.θ] + + sol2 = solve(ODEProblem(sys2, [], (0.0, 1.0)), Tsit5()) + @test 100.0 ∈ sol2[sys2.wd2.θ] +end From bf3cd3394212acd34678e1927f79b958b5320bcd Mon Sep 17 00:00:00 2001 From: vyudu Date: Tue, 4 Feb 2025 14:52:43 -0500 Subject: [PATCH 0778/1337] reorganization into functions --- .../symbolics_tearing.jl | 376 ++++++++++-------- 1 file changed, 208 insertions(+), 168 deletions(-) diff --git a/src/structural_transformation/symbolics_tearing.jl b/src/structural_transformation/symbolics_tearing.jl index 6340087003..cf70914a51 100644 --- a/src/structural_transformation/symbolics_tearing.jl +++ b/src/structural_transformation/symbolics_tearing.jl @@ -237,16 +237,22 @@ function check_diff_graph(var_to_diff, fullvars) end =# +""" +Replace derivatives of non-selected unknown variables by dummy derivatives. -function substitute_dummy_derivatives!(fullvars, var_to_diff, diff_to_var, var_eq_matching, neweqs) - # Dummy derivatives may determine that some differential variables are - # algebraic variables in disguise. The derivative of such variables are - # called dummy derivatives. +State selection may determine that some differential variables are +algebraic variables in disguise. The derivative of such variables are +called dummy derivatives. - # Step 1: - # Replace derivatives of non-selected unknown variables by dummy derivatives +`SelectedState` information is no longer needed past here. State selection +is done. All non-differentiated variables are algebraic variables, and all +variables that appear differentiated are differential variables. +""" +function substitute_dummy_derivatives!(ts::TearingState, neweqs, dummy_sub, var_eq_matching) + @unpack fullvars, sys, structure = ts + @unpack solvable_graph, var_to_diff, eq_to_diff, graph, lowest_shift = structure + diff_to_var = invview(var_to_diff) - dummy_sub = Dict() for var in 1:length(fullvars) dv = var_to_diff[var] dv === nothing && continue @@ -279,163 +285,73 @@ function substitute_dummy_derivatives!(fullvars, var_to_diff, diff_to_var, var_e end end -function generate_derivative_variables!() -end +""" +Generate new derivative variables for the system. -# Documenting the differences to structural simplification for discrete systems: -# In discrete systems the lowest-order term is x_k-i, instead of x(t). In order -# to substitute dummy variables for x_k-1, x_k-2, ... instead you need to reverse -# the order. So for discrete systems `var_order` is defined a little differently. -# -# The orders will also be off by one. The reason this is is that the dynamics of -# the system should be given in terms of Shift(t, 1)(x(t), x(t-1), ...). But -# having the observables be indexed by the next time step is not so nice. So we -# handle the shifts in the renaming, rather than explicitly. -# -# The substitution should look like the following: -# x(t) -> Shift(t, 1)(x(t)) -# x(k-1) -> x(t) -# x(k-2) -> x_{t-1}(t) -# x(k-3) -> x_{t-2}(t) -# and so on... -# -# In the implicit discrete case this shouldn't happen. The simplification should -# look like a NonlinearSystem. -# -# For discrete systems Shift(t, 2)(x(t)) is not equivalent to Shift(t, 1)(Shift(t,1)(x(t)) -# This is different from the continuous case where D(D(x)) can be substituted for -# by iteratively substituting x_t ~ D(x), then x_tt ~ D(x_t). For this reason the -# total_sub dict is updated at the time that the renamed variables are written, -# inside the loop where new variables are generated. +There are three cases where we want to generate new variables to convert +the system into first order (semi-implicit) ODEs. + +1. To first order: +Whenever higher order differentiated variable like `D(D(D(x)))` appears, +we introduce new variables `x_t`, `x_tt`, and `x_ttt` and new equations +``` +D(x_tt) = x_ttt +D(x_t) = x_tt +D(x) = x_t +``` +and replace `D(x)` to `x_t`, `D(D(x))` to `x_tt`, and `D(D(D(x)))` to +`x_ttt`. + +2. To implicit to semi-implicit ODEs: +2.1: Unsolvable derivative: +If one derivative variable `D(x)` is unsolvable in all the equations it +appears in, then we introduce a new variable `x_t`, a new equation +``` +D(x) ~ x_t +``` +and replace all other `D(x)` to `x_t`. + +2.2: Solvable derivative: +If one derivative variable `D(x)` is solvable in at least one of the +equations it appears in, then we introduce a new variable `x_t`. One of +the solvable equations must be in the form of `0 ~ L(D(x), u...)` and +there exists a function `l` such that `D(x) ~ l(u...)`. We should replace +it to +``` +0 ~ x_t - l(u...) +D(x) ~ x_t +``` +and replace all other `D(x)` to `x_t`. + +Observe that we don't need to actually introduce a new variable `x_t`, as +the above equations can be lowered to +``` +x_t := l(u...) +D(x) ~ x_t +``` +where `:=` denotes assignment. + +As a final note, in all the above cases where we need to introduce new +variables and equations, don't add them when they already exist. +""" +function generate_derivative_variables!(ts::TearingState, neweqs, total_sub, var_eq_matching, var_order; + is_discrete = false, mm = nothing) -import ModelingToolkit: Shift -function tearing_reassemble(state::TearingState, var_eq_matching, - full_var_eq_matching = nothing; simplify = false, mm = nothing, cse_hack = true, array_hack = true) - @unpack fullvars, sys, structure = state + @unpack fullvars, sys, structure = ts @unpack solvable_graph, var_to_diff, eq_to_diff, graph, lowest_shift = structure - extra_vars = Int[] - if full_var_eq_matching !== nothing - for v in 𝑑vertices(state.structure.graph) - eq = full_var_eq_matching[v] - eq isa Int && continue - push!(extra_vars, v) - end - end - - if ModelingToolkit.has_iv(state.sys) - iv = get_iv(state.sys) - if is_only_discrete(state.structure) - D = Shift(iv, 1) - lower_name = lower_varname_withshift - idx_to_lowest_shift = Dict{Int, Int}(var => 0 for var in 1:length(fullvars)) - for (i,var) in enumerate(fullvars) - key = (operation(var) isa Shift) ? only(arguments(var)) : var - idx_to_lowest_shift[i] = get(lowest_shift, key, 0) - end - else - D = Differential(iv) - lower_name = lower_varname_with_unit - end - else - iv = D = nothing - lower_name = lower_varname_with_unit - end - + eq_var_matching = invview(var_eq_matching) diff_to_var = invview(var_to_diff) - neweqs = collect(equations(state)) - - # Terminology and Definition: - # - # A general DAE is in the form of `F(u'(t), u(t), p, t) == 0`. We can - # characterize variables in `u(t)` into two classes: differential variables - # (denoted `v(t)`) and algebraic variables (denoted `z(t)`). Differential - # variables are marked as `SelectedState` and they are differentiated in the - # DAE system, i.e. `v'(t)` are all the variables in `u'(t)` that actually - # appear in the system. Algebraic variables are variables that are not - # differential variables. - - substitute_dummy_derivatives!(fullvars, var_to_diff, diff_to_var, var_eq_matching, neweqs) - - # `SelectedState` information is no longer needed past here. State selection - # is done. All non-differentiated variables are algebraic variables, and all - # variables that appear differentiated are differential variables. - - ### Extract partition information - is_solvable = let solvable_graph = solvable_graph - (eq, iv) -> eq isa Int && iv isa Int && BipartiteEdge(eq, iv) in solvable_graph - end - - # if var is like D(x) or Shift(t, 1)(x) - isdervar = let diff_to_var = diff_to_var - var -> diff_to_var[var] !== nothing - end - var_order = let diff_to_var = diff_to_var - dv -> begin - order = 0 - while (dv′ = diff_to_var[dv]) !== nothing - order += 1 - dv = dv′ - end - is_only_discrete(state.structure) && (order = -idx_to_lowest_shift[dv] - order) - order, dv - end - end + iv = ModelingToolkit.has_iv(sys) ? ModelingToolkit.get_iv(sys) : nothing + lower_name = is_discrete ? lower_name_withshift : lower_name_with_unit - # There are three cases where we want to generate new variables to convert - # the system into first order (semi-implicit) ODEs. - # - # 1. To first order: - # Whenever higher order differentiated variable like `D(D(D(x)))` appears, - # we introduce new variables `x_t`, `x_tt`, and `x_ttt` and new equations - # ``` - # D(x_tt) = x_ttt - # D(x_t) = x_tt - # D(x) = x_t - # ``` - # and replace `D(x)` to `x_t`, `D(D(x))` to `x_tt`, and `D(D(D(x)))` to - # `x_ttt`. - # - # 2. To implicit to semi-implicit ODEs: - # 2.1: Unsolvable derivative: - # If one derivative variable `D(x)` is unsolvable in all the equations it - # appears in, then we introduce a new variable `x_t`, a new equation - # ``` - # D(x) ~ x_t - # ``` - # and replace all other `D(x)` to `x_t`. - # - # 2.2: Solvable derivative: - # If one derivative variable `D(x)` is solvable in at least one of the - # equations it appears in, then we introduce a new variable `x_t`. One of - # the solvable equations must be in the form of `0 ~ L(D(x), u...)` and - # there exists a function `l` such that `D(x) ~ l(u...)`. We should replace - # it to - # ``` - # 0 ~ x_t - l(u...) - # D(x) ~ x_t - # ``` - # and replace all other `D(x)` to `x_t`. - # - # Observe that we don't need to actually introduce a new variable `x_t`, as - # the above equations can be lowered to - # ``` - # x_t := l(u...) - # D(x) ~ x_t - # ``` - # where `:=` denotes assignment. - # - # As a final note, in all the above cases where we need to introduce new - # variables and equations, don't add them when they already exist. - eq_var_matching = invview(var_eq_matching) linear_eqs = mm === nothing ? Dict{Int, Int}() : Dict(reverse(en) for en in enumerate(mm.nzrows)) - total_sub = Dict() for v in 1:length(var_to_diff) is_highest_discrete = begin var = fullvars[v] op = operation(var) - if (!is_only_discrete(state.structure) || op isa Shift) + if (!is_discrete || op isa Shift) false elseif !haskey(lowest_shift, var) false @@ -450,7 +366,8 @@ function tearing_reassemble(state::TearingState, var_eq_matching, solved = var_eq_matching[dv] isa Int solved && continue - # check if there's `D(x) = x_t` already + + # Check if there's `D(x) = x_t` already local v_t, dummy_eq for eq in 𝑑neighbors(solvable_graph, dv) mi = get(linear_eqs, eq, 0) @@ -468,6 +385,7 @@ function tearing_reassemble(state::TearingState, var_eq_matching, @goto FOUND_DUMMY_EQ end end + dx = fullvars[dv] # add `x_t` println() @@ -499,7 +417,7 @@ function tearing_reassemble(state::TearingState, var_eq_matching, add_edge!(solvable_graph, dummy_eq, dv) @assert nsrcs(graph) == nsrcs(solvable_graph) == dummy_eq @label FOUND_DUMMY_EQ - is_only_discrete(state.structure) && begin + is_discrete && begin idx_to_lowest_shift[v_t] = idx_to_lowest_shift[dv] operation(dx) isa Shift && (total_sub[dx] = x_t) order == 1 && (total_sub[x_t] = fullvars[var_to_diff[dv]]) @@ -509,13 +427,51 @@ function tearing_reassemble(state::TearingState, var_eq_matching, eq_var_matching[dummy_eq] = dv end @show total_sub +end + +""" +Solve the solvable equations of the system and generate differential (or discrete) +equations in terms of the selected states. + +Will reorder equations and unknowns to be: + [diffeqs; ...] + [diffvars; ...] +such that the mass matrix is: + [I 0 + 0 0]. + +Update the state to account for the new ordering and equations. +""" +function solve_and_generate_equations!(state::TearingState, neweqs, total_sub, var_eq_matching, var_order) + @unpack fullvars, sys, structure = state + @unpack solvable_graph, var_to_diff, eq_to_diff, graph, lowest_shift = structure + eq_var_matching = invview(var_eq_matching) + diff_to_var = invview(var_to_diff) + + if ModelingToolkit.has_iv(sys) + iv = get_iv(sys) + if is_only_discrete(structure) + D = Shift(iv, 1) + lower_name = lower_varname_withshift + else + D = Differential(iv) + lower_name = lower_varname_with_unit + end + else + iv = D = nothing + lower_name = lower_varname_with_unit + end + + # if var is like D(x) or Shift(t, 1)(x) + isdervar = let diff_to_var = diff_to_var + var -> diff_to_var[var] !== nothing + end + + ### Extract partition information + is_solvable = let solvable_graph = solvable_graph + (eq, iv) -> eq isa Int && iv isa Int && BipartiteEdge(eq, iv) in solvable_graph + end - # Will reorder equations and unknowns to be: - # [diffeqs; ...] - # [diffvars; ...] - # such that the mass matrix is: - # [I 0 - # 0 0]. diffeq_idxs = Int[] algeeq_idxs = Int[] diff_eqs = Equation[] @@ -525,14 +481,11 @@ function tearing_reassemble(state::TearingState, var_eq_matching, solved_equations = Int[] solved_variables = Int[] - # Solve solvable equations - println() - println("SOLVING SOLVABLE EQUATIONS.") toporder = topological_sort(DiCMOBiGraph{false}(graph, var_eq_matching)) eqs = Iterators.reverse(toporder) - idep = iv - @show eq_var_matching + idep = ModelingToolkit.has_iv(sys) ? ModelingToolkit.get_iv(sys) : nothing + # Generate differential equations in terms of the new variables. for ieq in eqs println() iv = eq_var_matching[ieq] @@ -597,6 +550,7 @@ function tearing_reassemble(state::TearingState, var_eq_matching, push!(algeeq_idxs, ieq) end end + # TODO: BLT sorting neweqs = [diff_eqs; alge_eqs] inveqsperm = [diffeq_idxs; algeeq_idxs] @@ -625,7 +579,6 @@ function tearing_reassemble(state::TearingState, var_eq_matching, graph = contract_variables(graph, var_eq_matching, varsperm, eqsperm, length(solved_variables), length(solved_variables_set)) - # Update system new_var_to_diff = complete(DiffGraph(length(invvarsperm))) for (v, d) in enumerate(var_to_diff) v′ = varsperm[v] @@ -641,6 +594,92 @@ function tearing_reassemble(state::TearingState, var_eq_matching, new_eq_to_diff[v′] = d′ > 0 ? d′ : nothing end + fullvars[invvarsperm], new_var_to_diff, new_eq_to_diff, neweqs, subeqs +end + +# Documenting the differences to structural simplification for discrete systems: +# In discrete systems the lowest-order term is x_k-i, instead of x(t). In order +# to substitute dummy variables for x_k-1, x_k-2, ... instead you need to reverse +# the order. So for discrete systems `var_order` is defined a little differently. +# +# The orders will also be off by one. The reason this is is that the dynamics of +# the system should be given in terms of Shift(t, 1)(x(t), x(t-1), ...). But +# having the observables be indexed by the next time step is not so nice. So we +# handle the shifts in the renaming, rather than explicitly. +# +# The substitution should look like the following: +# x(t) -> Shift(t, 1)(x(t)) +# x(k-1) -> x(t) +# x(k-2) -> x_{t-1}(t) +# x(k-3) -> x_{t-2}(t) +# and so on... +# +# In the implicit discrete case this shouldn't happen. The simplification should +# look like a NonlinearSystem. +# +# For discrete systems Shift(t, 2)(x(t)) is not equivalent to Shift(t, 1)(Shift(t,1)(x(t)) +# This is different from the continuous case where D(D(x)) can be substituted for +# by iteratively substituting x_t ~ D(x), then x_tt ~ D(x_t). For this reason the +# total_sub dict is updated at the time that the renamed variables are written, +# inside the loop where new variables are generated. + + +# Terminology and Definition: +# +# A general DAE is in the form of `F(u'(t), u(t), p, t) == 0`. We can +# characterize variables in `u(t)` into two classes: differential variables +# (denoted `v(t)`) and algebraic variables (denoted `z(t)`). Differential +# variables are marked as `SelectedState` and they are differentiated in the +# DAE system, i.e. `v'(t)` are all the variables in `u'(t)` that actually +# appear in the system. Algebraic variables are variables that are not +# differential variables. + +import ModelingToolkit: Shift + +function tearing_reassemble(state::TearingState, var_eq_matching, + full_var_eq_matching = nothing; simplify = false, mm = nothing, cse_hack = true, array_hack = true) + @unpack fullvars, sys, structure = state + @unpack solvable_graph, var_to_diff, eq_to_diff, graph, lowest_shift = structure + extra_vars = Int[] + if full_var_eq_matching !== nothing + for v in 𝑑vertices(state.structure.graph) + eq = full_var_eq_matching[v] + eq isa Int && continue + push!(extra_vars, v) + end + end + neweqs = collect(equations(state)) + diff_to_var = invview(var_to_diff) + total_sub = Dict() + dummy_sub = Dict() + is_discrete = is_only_discrete(state.structure) + + if is_discrete + idx_to_lowest_shift = Dict{Int, Int}(var => 0 for var in 1:length(fullvars)) + for (i,var) in enumerate(fullvars) + key = (operation(var) isa Shift) ? only(arguments(var)) : var + idx_to_lowest_shift[i] = get(lowest_shift, key, 0) + end + end + var_order = let diff_to_var = diff_to_var + dv -> begin + order = 0 + while (dv′ = diff_to_var[dv]) !== nothing + order += 1 + dv = dv′ + end + is_discrete && (order = -idx_to_lowest_shift[dv] - order) + order, dv + end + end + + # Structural simplification + substitute_dummy_derivatives!(state, neweqs, dummy_sub, var_eq_matching) + generate_derivative_variables!(state, neweqs, total_sub, var_eq_matching, var_order; + is_discrete, mm) + new_fullvars, new_var_to_diff, new_eq_to_diff, neweqs, subeqs = solve_and_generate_equations!(state, neweqs, total_sub, var_eq_matching, var_order) + + # Update system var_to_diff = new_var_to_diff eq_to_diff = new_eq_to_diff diff_to_var = invview(var_to_diff) @@ -649,7 +688,7 @@ function tearing_reassemble(state::TearingState, var_eq_matching, @set! state.structure.graph = complete(graph) @set! state.structure.var_to_diff = var_to_diff @set! state.structure.eq_to_diff = eq_to_diff - @set! state.fullvars = fullvars = fullvars[invvarsperm] + @set! state.fullvars = fullvars = new_fullvars ispresent = let var_to_diff = var_to_diff, graph = graph i -> (!isempty(𝑑neighbors(graph, i)) || (var_to_diff[i] !== nothing && !isempty(𝑑neighbors(graph, var_to_diff[i])))) @@ -657,11 +696,13 @@ function tearing_reassemble(state::TearingState, var_eq_matching, sys = state.sys + @show dummy_sub obs_sub = dummy_sub for eq in neweqs isdiffeq(eq) || continue obs_sub[eq.lhs] = eq.rhs end + @show obs_sub # TODO: compute the dependency correctly so that we don't have to do this obs = [fast_substitute(observed(sys), obs_sub); subeqs] @@ -680,7 +721,6 @@ function tearing_reassemble(state::TearingState, var_eq_matching, @set! sys.eqs = neweqs @set! sys.observed = obs - @set! sys.substitutions = Substitutions(subeqs, deps) # Only makes sense for time-dependent From 7fa4324e41136d42b7e5b5f8818c4ab796ae55ad Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 5 Feb 2025 15:28:35 +0530 Subject: [PATCH 0779/1337] fix: fix `@mtkbuild` macro keyword parsing --- src/systems/abstractsystem.jl | 15 ++++++++++----- test/if_lifting.jl | 14 ++++++++++++++ 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index 0444877432..dc47457825 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -2248,15 +2248,20 @@ macro mtkbuild(exprs...) expr = exprs[1] named_expr = ModelingToolkit.named_expr(expr) name = named_expr.args[1] - kwargs = if length(exprs) > 1 - NamedTuple{Tuple(ex.args[1] for ex in Base.tail(exprs))}(Tuple(ex.args[2] - for ex in Base.tail(exprs))) + kwargs = Base.tail(exprs) + kwargs = map(kwargs) do ex + @assert ex.head == :(=) + Expr(:kw, ex.args[1], ex.args[2]) + end + if isempty(kwargs) + kwargs = () else - (;) + kwargs = (Expr(:parameters, kwargs...),) end + call_expr = Expr(:call, structural_simplify, kwargs..., name) esc(quote $named_expr - $name = $structural_simplify($name; $(kwargs)...) + $name = $call_expr end) end diff --git a/test/if_lifting.jl b/test/if_lifting.jl index b72b468bf1..9c58e676d0 100644 --- a/test/if_lifting.jl +++ b/test/if_lifting.jl @@ -110,3 +110,17 @@ end @test operation(eq.rhs) === ifelse end end + +@testset "`@mtkbuild` macro accepts `additional_passes`" begin + @mtkmodel SimpleAbs begin + @variables begin + x(t) + y(t) + end + @equations begin + D(x) ~ abs(y) + y ~ sin(t) + end + end + @test_nowarn @mtkbuild sys=SimpleAbs() additional_passes=[IfLifting] +end From 9012d9bd8f7baca0713fef47ee3894a34dc8c38b Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 5 Feb 2025 15:28:39 +0530 Subject: [PATCH 0780/1337] refactor: format --- docs/src/basics/Variable_metadata.md | 6 ++++-- src/variables.jl | 9 ++++++--- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/docs/src/basics/Variable_metadata.md b/docs/src/basics/Variable_metadata.md index bcfe90bab4..44dfb30327 100644 --- a/docs/src/basics/Variable_metadata.md +++ b/docs/src/basics/Variable_metadata.md @@ -56,6 +56,7 @@ using ModelingToolkit: t_nounits as t, D_nounits as D @variables k(t) [connect = Stream] hasconnect(i) ``` + ```@example connect getconnect(k) ``` @@ -197,12 +198,13 @@ state_priority(important_dof) ## Units -Units for variables can be designated using symbolic metadata. For more information, please see the [model validation and units](@ref units) section of the docs. Note that `getunit` is not equivalent to `get_unit` - the former is a metadata getter for individual variables (and is provided so the same interface function for `unit` exists like other metadata), while the latter is used to handle more general symbolic expressions. +Units for variables can be designated using symbolic metadata. For more information, please see the [model validation and units](@ref units) section of the docs. Note that `getunit` is not equivalent to `get_unit` - the former is a metadata getter for individual variables (and is provided so the same interface function for `unit` exists like other metadata), while the latter is used to handle more general symbolic expressions. ```@example metadata -@variable speed [unit=u"m/s"] +@variable speed [unit = u"m/s"] hasunit(speed) ``` + ```@example metadata getunit(speed) ``` diff --git a/src/variables.jl b/src/variables.jl index 049dac790b..536119f107 100644 --- a/src/variables.jl +++ b/src/variables.jl @@ -28,7 +28,8 @@ ModelingToolkit.dump_variable_metadata(p) """ function dump_variable_metadata(var) uvar = unwrap(var) - variable_source, name = Symbolics.getmetadata(uvar, VariableSource, (:unknown, :unknown)) + variable_source, name = Symbolics.getmetadata( + uvar, VariableSource, (:unknown, :unknown)) type = symtype(uvar) if type <: AbstractArray shape = Symbolics.shape(var) @@ -102,7 +103,9 @@ getconnect(x::Symbolic) = Symbolics.getmetadata(x, VariableConnectType, nothing) Determine whether variable `x` has a connect type. See also [`getconnect`](@ref). """ hasconnect(x) = getconnect(x) !== nothing -setconnect(x, t::Type{T}) where T <: AbstractConnectType = setmetadata(x, VariableConnectType, t) +function setconnect(x, t::Type{T}) where {T <: AbstractConnectType} + setmetadata(x, VariableConnectType, t) +end ### Input, Output, Irreducible isvarkind(m, x::Union{Num, Symbolics.Arr}) = isvarkind(m, value(x)) @@ -581,7 +584,7 @@ metadata associated with it. See also [`getmisc(x)`](@ref). """ hasmisc(x) = getmisc(x) !== nothing -setmisc(x, miscdata) = setmetadata(x, VariableMisc, miscdata) +setmisc(x, miscdata) = setmetadata(x, VariableMisc, miscdata) ## Units ====================================================================== """ From de6f54d3d41865acd8d01e11e140f3f08f06d4f5 Mon Sep 17 00:00:00 2001 From: David Widmann Date: Wed, 5 Feb 2025 12:16:17 +0100 Subject: [PATCH 0781/1337] Support specialization in `ODEFunctionExpr` --- src/systems/diffeqs/abstractodesystem.jl | 48 +++++++++++++----------- test/odesystem.jl | 19 ++++++++++ 2 files changed, 45 insertions(+), 22 deletions(-) diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index d9b8fb31ab..ef48efd8e9 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -542,7 +542,7 @@ Create a Julia expression for an `ODEFunction` from the [`ODESystem`](@ref). The arguments `dvs` and `ps` are used to set the order of the dependent variable and parameter vectors, respectively. """ -struct ODEFunctionExpr{iip} end +struct ODEFunctionExpr{iip,specialize} end struct ODEFunctionClosure{O, I} <: Function f_oop::O @@ -551,7 +551,7 @@ end (f::ODEFunctionClosure)(u, p, t) = f.f_oop(u, p, t) (f::ODEFunctionClosure)(du, u, p, t) = f.f_iip(du, u, p, t) -function ODEFunctionExpr{iip}(sys::AbstractODESystem, dvs = unknowns(sys), +function ODEFunctionExpr{iip,specialize}(sys::AbstractODESystem, dvs = unknowns(sys), ps = parameters(sys), u0 = nothing; version = nothing, tgrad = false, jac = false, p = nothing, @@ -560,14 +560,12 @@ function ODEFunctionExpr{iip}(sys::AbstractODESystem, dvs = unknowns(sys), steady_state = false, sparsity = false, observedfun_exp = nothing, - kwargs...) where {iip} + kwargs...) where {iip,specialize} if !iscomplete(sys) error("A completed system is required. Call `complete` or `structural_simplify` on the system before creating an `ODEFunctionExpr`") end f_oop, f_iip = generate_function(sys, dvs, ps; expression = Val{true}, kwargs...) - dict = Dict() - fsym = gensym(:f) _f = :($fsym = $ODEFunctionClosure($f_oop, $f_iip)) tgradsym = gensym(:tgrad) @@ -590,30 +588,28 @@ function ODEFunctionExpr{iip}(sys::AbstractODESystem, dvs = unknowns(sys), _jac = :($jacsym = nothing) end + Msym = gensym(:M) M = calculate_massmatrix(sys) - - _M = if sparse && !(u0 === nothing || M === I) - SparseArrays.sparse(M) + if sparse && !(u0 === nothing || M === I) + _M = :($Msym = $(SparseArrays.sparse(M))) elseif u0 === nothing || M === I - M + _M = :($Msym = $M) else - ArrayInterface.restructure(u0 .* u0', M) + _M = :($Msym = $(ArrayInterface.restructure(u0 .* u0', M))) end jp_expr = sparse ? :($similar($(get_jac(sys)[]), Float64)) : :nothing ex = quote - $_f - $_tgrad - $_jac - M = $_M - ODEFunction{$iip}($fsym, - sys = $sys, - jac = $jacsym, - tgrad = $tgradsym, - mass_matrix = M, - jac_prototype = $jp_expr, - sparsity = $(sparsity ? jacobian_sparsity(sys) : nothing), - observed = $observedfun_exp) + let $_f, $_tgrad, $_jac, $_M + ODEFunction{$iip,$specialize}($fsym, + sys = $sys, + jac = $jacsym, + tgrad = $tgradsym, + mass_matrix = $Msym, + jac_prototype = $jp_expr, + sparsity = $(sparsity ? jacobian_sparsity(sys) : nothing), + observed = $observedfun_exp) + end end !linenumbers ? Base.remove_linenums!(ex) : ex end @@ -622,6 +618,14 @@ function ODEFunctionExpr(sys::AbstractODESystem, args...; kwargs...) ODEFunctionExpr{true}(sys, args...; kwargs...) end +function ODEFunctionExpr{true}(sys::AbstractODESystem, args...; kwargs...) + return ODEFunctionExpr{true, SciMLBase.AutoSpecialize}(sys, args...; kwargs...) +end + +function ODEFunctionExpr{false}(sys::AbstractODESystem, args...; kwargs...) + return ODEFunctionExpr{false, SciMLBase.FullSpecialize}(sys, args...; kwargs...) +end + """ ```julia DAEFunctionExpr{iip}(sys::AbstractODESystem, dvs = unknowns(sys), diff --git a/test/odesystem.jl b/test/odesystem.jl index 87c192afc2..ea6f22f33b 100644 --- a/test/odesystem.jl +++ b/test/odesystem.jl @@ -97,6 +97,25 @@ f.f(du, u, p, 0.1) @test du == [4, 0, -16] @test_throws ArgumentError f.f(u, p, 0.1) +#check iip +f = eval(ODEFunctionExpr(de, [x, y, z], [σ, ρ, β])) +f2 = ODEFunction(de, [x, y, z], [σ, ρ, β]) +@test SciMLBase.isinplace(f) === SciMLBase.isinplace(f2) +@test SciMLBase.specialization(f) === SciMLBase.specialization(f2) +for iip in (true, false) + f = eval(ODEFunctionExpr{iip}(de, [x, y, z], [σ, ρ, β])) + f2 = ODEFunction{iip}(de, [x, y, z], [σ, ρ, β]) + @test SciMLBase.isinplace(f) === SciMLBase.isinplace(f2) === iip + @test SciMLBase.specialization(f) === SciMLBase.specialization(f2) + + for specialize in (SciMLBase.AutoSpecialize, SciMLBase.FullSpecialize) + f = eval(ODEFunctionExpr{iip,specialize}(de, [x, y, z], [σ, ρ, β])) + f2 = ODEFunction{iip,specialize}(de, [x, y, z], [σ, ρ, β]) + @test SciMLBase.isinplace(f) === SciMLBase.isinplace(f2) === iip + @test SciMLBase.specialization(f) === SciMLBase.specialization(f2) === specialize + end +end + #check sparsity f = eval(ODEFunctionExpr(de, [x, y, z], [σ, ρ, β], sparsity = true)) @test f.sparsity == ModelingToolkit.jacobian_sparsity(de) From 58dde099686862ca0dd3116667866b7a234c8162 Mon Sep 17 00:00:00 2001 From: David Widmann Date: Wed, 5 Feb 2025 12:28:22 +0100 Subject: [PATCH 0782/1337] Fix format Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- src/systems/diffeqs/abstractodesystem.jl | 8 ++++---- test/odesystem.jl | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index ef48efd8e9..8d0720a1d2 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -542,7 +542,7 @@ Create a Julia expression for an `ODEFunction` from the [`ODESystem`](@ref). The arguments `dvs` and `ps` are used to set the order of the dependent variable and parameter vectors, respectively. """ -struct ODEFunctionExpr{iip,specialize} end +struct ODEFunctionExpr{iip, specialize} end struct ODEFunctionClosure{O, I} <: Function f_oop::O @@ -551,7 +551,7 @@ end (f::ODEFunctionClosure)(u, p, t) = f.f_oop(u, p, t) (f::ODEFunctionClosure)(du, u, p, t) = f.f_iip(du, u, p, t) -function ODEFunctionExpr{iip,specialize}(sys::AbstractODESystem, dvs = unknowns(sys), +function ODEFunctionExpr{iip, specialize}(sys::AbstractODESystem, dvs = unknowns(sys), ps = parameters(sys), u0 = nothing; version = nothing, tgrad = false, jac = false, p = nothing, @@ -560,7 +560,7 @@ function ODEFunctionExpr{iip,specialize}(sys::AbstractODESystem, dvs = unknowns( steady_state = false, sparsity = false, observedfun_exp = nothing, - kwargs...) where {iip,specialize} + kwargs...) where {iip, specialize} if !iscomplete(sys) error("A completed system is required. Call `complete` or `structural_simplify` on the system before creating an `ODEFunctionExpr`") end @@ -601,7 +601,7 @@ function ODEFunctionExpr{iip,specialize}(sys::AbstractODESystem, dvs = unknowns( jp_expr = sparse ? :($similar($(get_jac(sys)[]), Float64)) : :nothing ex = quote let $_f, $_tgrad, $_jac, $_M - ODEFunction{$iip,$specialize}($fsym, + ODEFunction{$iip, $specialize}($fsym, sys = $sys, jac = $jacsym, tgrad = $tgradsym, diff --git a/test/odesystem.jl b/test/odesystem.jl index ea6f22f33b..8281fafe93 100644 --- a/test/odesystem.jl +++ b/test/odesystem.jl @@ -109,8 +109,8 @@ for iip in (true, false) @test SciMLBase.specialization(f) === SciMLBase.specialization(f2) for specialize in (SciMLBase.AutoSpecialize, SciMLBase.FullSpecialize) - f = eval(ODEFunctionExpr{iip,specialize}(de, [x, y, z], [σ, ρ, β])) - f2 = ODEFunction{iip,specialize}(de, [x, y, z], [σ, ρ, β]) + f = eval(ODEFunctionExpr{iip, specialize}(de, [x, y, z], [σ, ρ, β])) + f2 = ODEFunction{iip, specialize}(de, [x, y, z], [σ, ρ, β]) @test SciMLBase.isinplace(f) === SciMLBase.isinplace(f2) === iip @test SciMLBase.specialization(f) === SciMLBase.specialization(f2) === specialize end From 5a77f4347b1b9259cb6da56128146af254b827a2 Mon Sep 17 00:00:00 2001 From: vyudu Date: Thu, 6 Feb 2025 17:22:49 -0500 Subject: [PATCH 0783/1337] explicit case --- .../symbolics_tearing.jl | 180 ++++++++++-------- src/structural_transformation/utils.jl | 7 +- src/systems/systems.jl | 8 +- 3 files changed, 104 insertions(+), 91 deletions(-) diff --git a/src/structural_transformation/symbolics_tearing.jl b/src/structural_transformation/symbolics_tearing.jl index cf70914a51..76bba2a303 100644 --- a/src/structural_transformation/symbolics_tearing.jl +++ b/src/structural_transformation/symbolics_tearing.jl @@ -333,6 +333,32 @@ where `:=` denotes assignment. As a final note, in all the above cases where we need to introduce new variables and equations, don't add them when they already exist. + +###### DISCRETE SYSTEMS ####### + +Documenting the differences to structural simplification for discrete systems: +In discrete systems the lowest-order term is x_k-i, instead of x(t). + +The orders will also be off by one. The reason this is is that the dynamics of +the system should be given in terms of Shift(t, 1)(x(t), x(t-1), ...). But +having the observables be indexed by the next time step is not so nice. So we +handle the shifts in the renaming, rather than explicitly. + +The substitution should look like the following: + x(t) -> Shift(t, 1)(x(t)) + Shift(t, -1)(x(t)) -> x(t) + Shift(t, -2)(x(t)) -> x_{t-1}(t) + Shift(t, -3)(x(t)) -> x_{t-2}(t) + and so on... + +In the implicit discrete case this shouldn't happen. The simplification should +look like a NonlinearSystem. + +For discrete systems Shift(t, 2)(x(t)) is not equivalent to Shift(t, 1)(Shift(t,1)(x(t)) +This is different from the continuous case where D(D(x)) can be substituted for +by iteratively substituting x_t ~ D(x), then x_tt ~ D(x_t). For this reason the +total_sub dict is updated at the time that the renamed variables are written, +inside the loop where new variables are generated. """ function generate_derivative_variables!(ts::TearingState, neweqs, total_sub, var_eq_matching, var_order; is_discrete = false, mm = nothing) @@ -342,26 +368,41 @@ function generate_derivative_variables!(ts::TearingState, neweqs, total_sub, var eq_var_matching = invview(var_eq_matching) diff_to_var = invview(var_to_diff) iv = ModelingToolkit.has_iv(sys) ? ModelingToolkit.get_iv(sys) : nothing - lower_name = is_discrete ? lower_name_withshift : lower_name_with_unit + lower_name = is_discrete ? lower_varname_withshift : lower_varname_with_unit + + # index v gets mapped to the lowest shift and the index of the unshifted variable + if is_discrete + idx_to_lowest_shift = Dict{Int, Tuple{Int, Int}}(var => (0,0) for var in 1:length(fullvars)) + var_to_unshiftedidx = Dict{Any, Int}(var => findfirst(x -> isequal(x, var), fullvars) for var in keys(lowest_shift)) + + for (i,var) in enumerate(fullvars) + key = (operation(var) isa Shift) ? only(arguments(var)) : var + idx_to_lowest_shift[i] = (get(lowest_shift, key, 0), get(var_to_unshiftedidx, key, i)) + end + end linear_eqs = mm === nothing ? Dict{Int, Int}() : Dict(reverse(en) for en in enumerate(mm.nzrows)) + # v is the index of the current variable, x = fullvars[v] + # dv is the index of the derivative dx = D(x), x_t is the substituted variable + # + # For ODESystems: lv is the index of the lowest-order variable (x(t)) + # For DiscreteSystems: + # - lv is the index of the lowest-order variable (Shift(t, k)(x(t))) + # - uv is the index of the highest-order variable (x(t)) for v in 1:length(var_to_diff) - is_highest_discrete = begin - var = fullvars[v] - op = operation(var) - if (!is_discrete || op isa Shift) - false - elseif !haskey(lowest_shift, var) - false - else - low = lowest_shift[var] - idx = findfirst(x -> isequal(x, Shift(iv, low)(var)), fullvars) - true + dv = var_to_diff[v] + if is_discrete + x = fullvars[v] + op = operation(x) + (low, uv) = idx_to_lowest_shift[v] + + # If v is unshifted (i.e. x(t)), then substitute the lowest-shift variable + if !(op isa Shift) && (low != 0) + dv = findfirst(_x -> isequal(_x, Shift(iv, low)(x)), fullvars) end end - dv = is_highest_discrete ? idx : var_to_diff[v] dv isa Int || continue solved = var_eq_matching[dv] isa Int @@ -388,13 +429,9 @@ function generate_derivative_variables!(ts::TearingState, neweqs, total_sub, var dx = fullvars[dv] # add `x_t` - println() - @show order, lv = var_order(dv) - x_t = lower_name(fullvars[lv], iv, order) - @show fullvars[v] - @show fullvars[dv] - @show fullvars[lv] - @show dx, x_t + order, lv = var_order(dv) + x_t = is_discrete ? lower_name(fullvars[lv], iv, -low-order-1; unshifted = fullvars[uv]) : + lower_name(fullvars[lv], iv, order) push!(fullvars, simplify_shifts(x_t)) v_t = length(fullvars) v_t_idx = add_vertex!(var_to_diff) @@ -402,11 +439,27 @@ function generate_derivative_variables!(ts::TearingState, neweqs, total_sub, var # TODO: do we care about solvable_graph? We don't use them after # `dummy_derivative_graph`. add_vertex!(solvable_graph, DST) - # var_eq_matching is a bit odd. - # length(var_eq_matching) == length(invview(var_eq_matching)) push!(var_eq_matching, unassigned) @assert v_t_idx == ndsts(graph) == ndsts(solvable_graph) == length(fullvars) == length(var_eq_matching) + + # Add the substitutions to total_sub directly. + is_discrete && begin + idx_to_lowest_shift[v_t] = idx_to_lowest_shift[dv] + @show dx + if operation(dx) isa Shift + total_sub[dx] = x_t + for e in 𝑑neighbors(graph, dv) + add_edge!(graph, e, v_t) + rem_edge!(graph, e, dv) + end + @show graph + !(operation(x) isa Shift) && begin + var_to_diff[v_t] = var_to_diff[dv] + continue + end + end + end # add `D(x) - x_t ~ 0` push!(neweqs, 0 ~ x_t - dx) add_vertex!(graph, SRC) @@ -417,16 +470,10 @@ function generate_derivative_variables!(ts::TearingState, neweqs, total_sub, var add_edge!(solvable_graph, dummy_eq, dv) @assert nsrcs(graph) == nsrcs(solvable_graph) == dummy_eq @label FOUND_DUMMY_EQ - is_discrete && begin - idx_to_lowest_shift[v_t] = idx_to_lowest_shift[dv] - operation(dx) isa Shift && (total_sub[dx] = x_t) - order == 1 && (total_sub[x_t] = fullvars[var_to_diff[dv]]) - end var_to_diff[v_t] = var_to_diff[dv] var_eq_matching[dv] = unassigned eq_var_matching[dummy_eq] = dv end - @show total_sub end """ @@ -442,7 +489,7 @@ such that the mass matrix is: Update the state to account for the new ordering and equations. """ -function solve_and_generate_equations!(state::TearingState, neweqs, total_sub, var_eq_matching, var_order) +function solve_and_generate_equations!(state::TearingState, neweqs, total_sub, var_eq_matching, var_order; simplify = false) @unpack fullvars, sys, structure = state @unpack solvable_graph, var_to_diff, eq_to_diff, graph, lowest_shift = structure eq_var_matching = invview(var_eq_matching) @@ -467,7 +514,7 @@ function solve_and_generate_equations!(state::TearingState, neweqs, total_sub, v var -> diff_to_var[var] !== nothing end - ### Extract partition information + # Extract partition information is_solvable = let solvable_graph = solvable_graph (eq, iv) -> eq isa Int && iv isa Int && BipartiteEdge(eq, iv) in solvable_graph end @@ -485,9 +532,12 @@ function solve_and_generate_equations!(state::TearingState, neweqs, total_sub, v eqs = Iterators.reverse(toporder) idep = ModelingToolkit.has_iv(sys) ? ModelingToolkit.get_iv(sys) : nothing - # Generate differential equations in terms of the new variables. + @show eq_var_matching + @show fullvars + @show neweqs + + # Equation ieq is solved for the RHS of iv for ieq in eqs - println() iv = eq_var_matching[ieq] if is_solvable(ieq, iv) # We don't solve differential equations, but we will need to try to @@ -497,23 +547,14 @@ function solve_and_generate_equations!(state::TearingState, neweqs, total_sub, v isnothing(D) && error("Differential found in a non-differential system. Likely this is a bug in the construction of an initialization system. Please report this issue with a reproducible example. Offending equation: $(equations(sys)[ieq])") order, lv = var_order(iv) - @show fullvars[iv] - @show (order, lv) - dx = D(simplify_shifts(lower_name( - fullvars[lv], idep, order - 1))) - @show dx + dx = D(fullvars[lv]) eq = dx ~ simplify_shifts(Symbolics.fixpoint_sub( Symbolics.symbolic_linear_solve(neweqs[ieq], fullvars[iv]), total_sub; operator = ModelingToolkit.Shift)) - @show total_sub - @show eq for e in 𝑑neighbors(graph, iv) - e == ieq && continue - for v in 𝑠neighbors(graph, e) - add_edge!(graph, e, v) - end rem_edge!(graph, e, iv) + add_edge!(graph, e, lv) end push!(diff_eqs, eq) total_sub[simplify_shifts(eq.lhs)] = eq.rhs @@ -594,38 +635,10 @@ function solve_and_generate_equations!(state::TearingState, neweqs, total_sub, v new_eq_to_diff[v′] = d′ > 0 ? d′ : nothing end - fullvars[invvarsperm], new_var_to_diff, new_eq_to_diff, neweqs, subeqs + fullvars[invvarsperm], new_var_to_diff, new_eq_to_diff, neweqs, subeqs, graph end -# Documenting the differences to structural simplification for discrete systems: -# In discrete systems the lowest-order term is x_k-i, instead of x(t). In order -# to substitute dummy variables for x_k-1, x_k-2, ... instead you need to reverse -# the order. So for discrete systems `var_order` is defined a little differently. -# -# The orders will also be off by one. The reason this is is that the dynamics of -# the system should be given in terms of Shift(t, 1)(x(t), x(t-1), ...). But -# having the observables be indexed by the next time step is not so nice. So we -# handle the shifts in the renaming, rather than explicitly. -# -# The substitution should look like the following: -# x(t) -> Shift(t, 1)(x(t)) -# x(k-1) -> x(t) -# x(k-2) -> x_{t-1}(t) -# x(k-3) -> x_{t-2}(t) -# and so on... -# -# In the implicit discrete case this shouldn't happen. The simplification should -# look like a NonlinearSystem. -# -# For discrete systems Shift(t, 2)(x(t)) is not equivalent to Shift(t, 1)(Shift(t,1)(x(t)) -# This is different from the continuous case where D(D(x)) can be substituted for -# by iteratively substituting x_t ~ D(x), then x_tt ~ D(x_t). For this reason the -# total_sub dict is updated at the time that the renamed variables are written, -# inside the loop where new variables are generated. - - # Terminology and Definition: -# # A general DAE is in the form of `F(u'(t), u(t), p, t) == 0`. We can # characterize variables in `u(t)` into two classes: differential variables # (denoted `v(t)`) and algebraic variables (denoted `z(t)`). Differential @@ -654,13 +667,6 @@ function tearing_reassemble(state::TearingState, var_eq_matching, dummy_sub = Dict() is_discrete = is_only_discrete(state.structure) - if is_discrete - idx_to_lowest_shift = Dict{Int, Int}(var => 0 for var in 1:length(fullvars)) - for (i,var) in enumerate(fullvars) - key = (operation(var) isa Shift) ? only(arguments(var)) : var - idx_to_lowest_shift[i] = get(lowest_shift, key, 0) - end - end var_order = let diff_to_var = diff_to_var dv -> begin order = 0 @@ -668,7 +674,6 @@ function tearing_reassemble(state::TearingState, var_eq_matching, order += 1 dv = dv′ end - is_discrete && (order = -idx_to_lowest_shift[dv] - order) order, dv end end @@ -677,7 +682,7 @@ function tearing_reassemble(state::TearingState, var_eq_matching, substitute_dummy_derivatives!(state, neweqs, dummy_sub, var_eq_matching) generate_derivative_variables!(state, neweqs, total_sub, var_eq_matching, var_order; is_discrete, mm) - new_fullvars, new_var_to_diff, new_eq_to_diff, neweqs, subeqs = solve_and_generate_equations!(state, neweqs, total_sub, var_eq_matching, var_order) + new_fullvars, new_var_to_diff, new_eq_to_diff, neweqs, subeqs, graph = solve_and_generate_equations!(state, neweqs, total_sub, var_eq_matching, var_order; simplify) # Update system var_to_diff = new_var_to_diff @@ -693,16 +698,25 @@ function tearing_reassemble(state::TearingState, var_eq_matching, i -> (!isempty(𝑑neighbors(graph, i)) || (var_to_diff[i] !== nothing && !isempty(𝑑neighbors(graph, var_to_diff[i])))) end + @show graph + println() + println("Shift test...") + @show neweqs + @show fullvars + @show 𝑑neighbors(graph, 5) sys = state.sys - - @show dummy_sub obs_sub = dummy_sub for eq in neweqs isdiffeq(eq) || continue obs_sub[eq.lhs] = eq.rhs end + is_discrete && for eq in subeqs + obs_sub[eq.rhs] = eq.lhs + end + @show obs_sub + @show observed(sys) # TODO: compute the dependency correctly so that we don't have to do this obs = [fast_substitute(observed(sys), obs_sub); subeqs] diff --git a/src/structural_transformation/utils.jl b/src/structural_transformation/utils.jl index 707a57097b..8df2d4177b 100644 --- a/src/structural_transformation/utils.jl +++ b/src/structural_transformation/utils.jl @@ -449,10 +449,9 @@ end ### Misc ### -function lower_varname_withshift(var, iv, order) - order == 0 && return var - #order == -1 && return Shift(iv, 1)(var) - ds = "$iv-$(order-1)" +function lower_varname_withshift(var, iv, backshift; unshifted = nothing) + backshift == 0 && return unshifted + ds = "$iv-$backshift" d_separator = 'ˍ' if ModelingToolkit.isoperator(var, ModelingToolkit.Shift) diff --git a/src/systems/systems.jl b/src/systems/systems.jl index 9c8c272c5c..0a7e4264e4 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. This is \ - not yet supported. - """) + # error(""" + # Encountered algebraic equations when simplifying discrete system. This is \ + # not yet supported. + # """) end for pass in additional_passes newsys = pass(newsys) From 91acf912e7651b1482a5e29552026774ed5fe5f0 Mon Sep 17 00:00:00 2001 From: vyudu Date: Thu, 6 Feb 2025 22:48:29 -0500 Subject: [PATCH 0784/1337] beginning implicit equation --- .../symbolics_tearing.jl | 76 +++++++++---------- src/structural_transformation/utils.jl | 1 + 2 files changed, 37 insertions(+), 40 deletions(-) diff --git a/src/structural_transformation/symbolics_tearing.jl b/src/structural_transformation/symbolics_tearing.jl index 76bba2a303..fb1765964e 100644 --- a/src/structural_transformation/symbolics_tearing.jl +++ b/src/structural_transformation/symbolics_tearing.jl @@ -360,9 +360,8 @@ by iteratively substituting x_t ~ D(x), then x_tt ~ D(x_t). For this reason the total_sub dict is updated at the time that the renamed variables are written, inside the loop where new variables are generated. """ -function generate_derivative_variables!(ts::TearingState, neweqs, total_sub, var_eq_matching, var_order; +function generate_derivative_variables!(ts::TearingState, neweqs, total_sub, var_eq_matching; is_discrete = false, mm = nothing) - @unpack fullvars, sys, structure = ts @unpack solvable_graph, var_to_diff, eq_to_diff, graph, lowest_shift = structure eq_var_matching = invview(var_eq_matching) @@ -386,13 +385,14 @@ function generate_derivative_variables!(ts::TearingState, neweqs, total_sub, var # v is the index of the current variable, x = fullvars[v] # dv is the index of the derivative dx = D(x), x_t is the substituted variable - # # For ODESystems: lv is the index of the lowest-order variable (x(t)) # For DiscreteSystems: # - lv is the index of the lowest-order variable (Shift(t, k)(x(t))) # - uv is the index of the highest-order variable (x(t)) for v in 1:length(var_to_diff) dv = var_to_diff[v] + println() + @show (v, dv) if is_discrete x = fullvars[v] op = operation(x) @@ -405,6 +405,9 @@ function generate_derivative_variables!(ts::TearingState, neweqs, total_sub, var end dv isa Int || continue + @show dv + @show var_eq_matching[dv] + @show fullvars solved = var_eq_matching[dv] isa Int solved && continue @@ -429,9 +432,10 @@ function generate_derivative_variables!(ts::TearingState, neweqs, total_sub, var dx = fullvars[dv] # add `x_t` - order, lv = var_order(dv) + order, lv = var_order(diff_to_var, dv) x_t = is_discrete ? lower_name(fullvars[lv], iv, -low-order-1; unshifted = fullvars[uv]) : lower_name(fullvars[lv], iv, order) + @show dx, x_t push!(fullvars, simplify_shifts(x_t)) v_t = length(fullvars) v_t_idx = add_vertex!(var_to_diff) @@ -443,23 +447,23 @@ function generate_derivative_variables!(ts::TearingState, neweqs, total_sub, var @assert v_t_idx == ndsts(graph) == ndsts(solvable_graph) == length(fullvars) == length(var_eq_matching) - # Add the substitutions to total_sub directly. + # Add discrete substitutions to total_sub directly. is_discrete && begin idx_to_lowest_shift[v_t] = idx_to_lowest_shift[dv] - @show dx if operation(dx) isa Shift total_sub[dx] = x_t for e in 𝑑neighbors(graph, dv) add_edge!(graph, e, v_t) rem_edge!(graph, e, dv) end - @show graph + # Do not add the lowest-order substitution as an equation, just substitute !(operation(x) isa Shift) && begin var_to_diff[v_t] = var_to_diff[dv] continue end end end + # add `D(x) - x_t ~ 0` push!(neweqs, 0 ~ x_t - dx) add_vertex!(graph, SRC) @@ -489,7 +493,7 @@ such that the mass matrix is: Update the state to account for the new ordering and equations. """ -function solve_and_generate_equations!(state::TearingState, neweqs, total_sub, var_eq_matching, var_order; simplify = false) +function solve_and_generate_equations!(state::TearingState, neweqs, total_sub, var_eq_matching; simplify = false) @unpack fullvars, sys, structure = state @unpack solvable_graph, var_to_diff, eq_to_diff, graph, lowest_shift = structure eq_var_matching = invview(var_eq_matching) @@ -530,13 +534,11 @@ function solve_and_generate_equations!(state::TearingState, neweqs, total_sub, v toporder = topological_sort(DiCMOBiGraph{false}(graph, var_eq_matching)) eqs = Iterators.reverse(toporder) - idep = ModelingToolkit.has_iv(sys) ? ModelingToolkit.get_iv(sys) : nothing - - @show eq_var_matching - @show fullvars - @show neweqs + idep = iv - # Equation ieq is solved for the RHS of iv + # Generate differential equations. + # fullvars[iv] is a differential variable of the form D^n(x), and neweqs[ieq] + # is solved to give the RHS. for ieq in eqs iv = eq_var_matching[ieq] if is_solvable(ieq, iv) @@ -546,7 +548,7 @@ function solve_and_generate_equations!(state::TearingState, neweqs, total_sub, v if isdervar(iv) isnothing(D) && error("Differential found in a non-differential system. Likely this is a bug in the construction of an initialization system. Please report this issue with a reproducible example. Offending equation: $(equations(sys)[ieq])") - order, lv = var_order(iv) + order, lv = var_order(diff_to_var, iv) dx = D(fullvars[lv]) eq = dx ~ simplify_shifts(Symbolics.fixpoint_sub( Symbolics.symbolic_linear_solve(neweqs[ieq], @@ -634,8 +636,9 @@ function solve_and_generate_equations!(state::TearingState, neweqs, total_sub, v d′ = eqsperm[d] new_eq_to_diff[v′] = d′ > 0 ? d′ : nothing end + new_fullvars = fullvars[invvarsperm] - fullvars[invvarsperm], new_var_to_diff, new_eq_to_diff, neweqs, subeqs, graph + new_fullvars, new_var_to_diff, new_eq_to_diff, neweqs, subeqs, graph end # Terminology and Definition: @@ -649,6 +652,16 @@ end import ModelingToolkit: Shift +# Give the order of the variable indexed by dv +function var_order(diff_to_var, dv) + order = 0 + while (dv′ = diff_to_var[dv]) !== nothing + order += 1 + dv = dv′ + end + order, dv +end + function tearing_reassemble(state::TearingState, var_eq_matching, full_var_eq_matching = nothing; simplify = false, mm = nothing, cse_hack = true, array_hack = true) @unpack fullvars, sys, structure = state @@ -667,22 +680,12 @@ function tearing_reassemble(state::TearingState, var_eq_matching, dummy_sub = Dict() is_discrete = is_only_discrete(state.structure) - var_order = let diff_to_var = diff_to_var - dv -> begin - order = 0 - while (dv′ = diff_to_var[dv]) !== nothing - order += 1 - dv = dv′ - end - order, dv - end - end - # Structural simplification substitute_dummy_derivatives!(state, neweqs, dummy_sub, var_eq_matching) - generate_derivative_variables!(state, neweqs, total_sub, var_eq_matching, var_order; + generate_derivative_variables!(state, neweqs, total_sub, var_eq_matching; is_discrete, mm) - new_fullvars, new_var_to_diff, new_eq_to_diff, neweqs, subeqs, graph = solve_and_generate_equations!(state, neweqs, total_sub, var_eq_matching, var_order; simplify) + new_fullvars, new_var_to_diff, new_eq_to_diff, neweqs, subeqs, graph = + solve_and_generate_equations!(state, neweqs, total_sub, var_eq_matching; simplify) # Update system var_to_diff = new_var_to_diff @@ -698,25 +701,18 @@ function tearing_reassemble(state::TearingState, var_eq_matching, i -> (!isempty(𝑑neighbors(graph, i)) || (var_to_diff[i] !== nothing && !isempty(𝑑neighbors(graph, var_to_diff[i])))) end - @show graph - println() - println("Shift test...") - @show neweqs - @show fullvars + @show ispresent.(collect(1:length(fullvars))) @show 𝑑neighbors(graph, 5) + @show var_to_diff[5] + @show neweqs + @show fullvars sys = state.sys obs_sub = dummy_sub for eq in neweqs isdiffeq(eq) || continue obs_sub[eq.lhs] = eq.rhs end - is_discrete && for eq in subeqs - obs_sub[eq.rhs] = eq.lhs - end - - @show obs_sub - @show observed(sys) # TODO: compute the dependency correctly so that we don't have to do this obs = [fast_substitute(observed(sys), obs_sub); subeqs] diff --git a/src/structural_transformation/utils.jl b/src/structural_transformation/utils.jl index 8df2d4177b..b2d67dc547 100644 --- a/src/structural_transformation/utils.jl +++ b/src/structural_transformation/utils.jl @@ -450,6 +450,7 @@ end ### function lower_varname_withshift(var, iv, backshift; unshifted = nothing) + backshift < 0 && return Shift(iv, -backshift)(var) backshift == 0 && return unshifted ds = "$iv-$backshift" d_separator = 'ˍ' From 56829f7c05e94a6048c55b482af8c1752e9cbad7 Mon Sep 17 00:00:00 2001 From: vyudu Date: Fri, 7 Feb 2025 02:26:13 -0500 Subject: [PATCH 0785/1337] up --- .../symbolics_tearing.jl | 112 ++++++++++-------- src/structural_transformation/utils.jl | 13 +- 2 files changed, 73 insertions(+), 52 deletions(-) diff --git a/src/structural_transformation/symbolics_tearing.jl b/src/structural_transformation/symbolics_tearing.jl index fb1765964e..020c85b05f 100644 --- a/src/structural_transformation/symbolics_tearing.jl +++ b/src/structural_transformation/symbolics_tearing.jl @@ -252,6 +252,7 @@ function substitute_dummy_derivatives!(ts::TearingState, neweqs, dummy_sub, var_ @unpack fullvars, sys, structure = ts @unpack solvable_graph, var_to_diff, eq_to_diff, graph, lowest_shift = structure diff_to_var = invview(var_to_diff) + iv = ModelingToolkit.has_iv(sys) ? ModelingToolkit.get_iv(sys) : nothing for var in 1:length(fullvars) dv = var_to_diff[var] @@ -337,16 +338,17 @@ variables and equations, don't add them when they already exist. ###### DISCRETE SYSTEMS ####### Documenting the differences to structural simplification for discrete systems: -In discrete systems the lowest-order term is x_k-i, instead of x(t). + +1. In discrete systems the lowest-order term is Shift(t, k)(x(t)), instead of x(t). We need to substitute the k-1 lowest order terms instead of the k-1 highest order terms. The orders will also be off by one. The reason this is is that the dynamics of the system should be given in terms of Shift(t, 1)(x(t), x(t-1), ...). But having the observables be indexed by the next time step is not so nice. So we -handle the shifts in the renaming, rather than explicitly. +handle the shifts in the renaming. The substitution should look like the following: x(t) -> Shift(t, 1)(x(t)) - Shift(t, -1)(x(t)) -> x(t) + Shift(t, -1)(x(t)) -> Shift(t, 0)(x(t)) Shift(t, -2)(x(t)) -> x_{t-1}(t) Shift(t, -3)(x(t)) -> x_{t-2}(t) and so on... @@ -354,14 +356,14 @@ The substitution should look like the following: In the implicit discrete case this shouldn't happen. The simplification should look like a NonlinearSystem. -For discrete systems Shift(t, 2)(x(t)) is not equivalent to Shift(t, 1)(Shift(t,1)(x(t)) +2. For discrete systems Shift(t, 2)(x(t)) cannot be substituted as Shift(t, 1)(Shift(t,1)(x(t)). This is different from the continuous case where D(D(x)) can be substituted for by iteratively substituting x_t ~ D(x), then x_tt ~ D(x_t). For this reason the -total_sub dict is updated at the time that the renamed variables are written, +shift_sub dict is updated at the time that the renamed variables are written, inside the loop where new variables are generated. """ -function generate_derivative_variables!(ts::TearingState, neweqs, total_sub, var_eq_matching; - is_discrete = false, mm = nothing) +function generate_derivative_variables!(ts::TearingState, neweqs, var_eq_matching; + is_discrete = false, mm = nothing, shift_sub = nothing) @unpack fullvars, sys, structure = ts @unpack solvable_graph, var_to_diff, eq_to_diff, graph, lowest_shift = structure eq_var_matching = invview(var_eq_matching) @@ -391,23 +393,24 @@ function generate_derivative_variables!(ts::TearingState, neweqs, total_sub, var # - uv is the index of the highest-order variable (x(t)) for v in 1:length(var_to_diff) dv = var_to_diff[v] - println() - @show (v, dv) + if is_discrete x = fullvars[v] op = operation(x) (low, uv) = idx_to_lowest_shift[v] # If v is unshifted (i.e. x(t)), then substitute the lowest-shift variable - if !(op isa Shift) && (low != 0) + if !(op isa Shift) dv = findfirst(_x -> isequal(_x, Shift(iv, low)(x)), fullvars) end + dx = fullvars[dv] + order, lv = var_order(diff_to_var, dv) + @show fullvars[uv] + x_t = lower_name(fullvars[lv], iv, -low-order-1; unshifted = fullvars[uv]) + shift_sub[dx] = x_t + (var_eq_matching[dv] isa Int) ? continue : @goto DISCRETE_VARIABLE end dv isa Int || continue - - @show dv - @show var_eq_matching[dv] - @show fullvars solved = var_eq_matching[dv] isa Int solved && continue @@ -430,13 +433,13 @@ function generate_derivative_variables!(ts::TearingState, neweqs, total_sub, var end end - dx = fullvars[dv] # add `x_t` + dx = fullvars[dv] order, lv = var_order(diff_to_var, dv) - x_t = is_discrete ? lower_name(fullvars[lv], iv, -low-order-1; unshifted = fullvars[uv]) : - lower_name(fullvars[lv], iv, order) - @show dx, x_t - push!(fullvars, simplify_shifts(x_t)) + x_t = lower_name(fullvars[lv], iv, order) + + @label DISCRETE_VARIABLE + push!(fullvars, x_t) v_t = length(fullvars) v_t_idx = add_vertex!(var_to_diff) add_vertex!(graph, DST) @@ -444,23 +447,18 @@ function generate_derivative_variables!(ts::TearingState, neweqs, total_sub, var # `dummy_derivative_graph`. add_vertex!(solvable_graph, DST) push!(var_eq_matching, unassigned) - @assert v_t_idx == ndsts(graph) == ndsts(solvable_graph) == length(fullvars) == - length(var_eq_matching) # Add discrete substitutions to total_sub directly. is_discrete && begin idx_to_lowest_shift[v_t] = idx_to_lowest_shift[dv] - if operation(dx) isa Shift - total_sub[dx] = x_t - for e in 𝑑neighbors(graph, dv) - add_edge!(graph, e, v_t) - rem_edge!(graph, e, dv) - end - # Do not add the lowest-order substitution as an equation, just substitute - !(operation(x) isa Shift) && begin - var_to_diff[v_t] = var_to_diff[dv] - continue - end + for e in 𝑑neighbors(graph, dv) + add_edge!(graph, e, v_t) + rem_edge!(graph, e, dv) + end + # Do not add the lowest-order substitution as an equation, just substitute + !(operation(x) isa Shift) && begin + var_to_diff[v_t] = var_to_diff[dv] + continue end end @@ -472,7 +470,7 @@ function generate_derivative_variables!(ts::TearingState, neweqs, total_sub, var add_edge!(graph, dummy_eq, v_t) add_vertex!(solvable_graph, SRC) add_edge!(solvable_graph, dummy_eq, dv) - @assert nsrcs(graph) == nsrcs(solvable_graph) == dummy_eq + @label FOUND_DUMMY_EQ var_to_diff[v_t] = var_to_diff[dv] var_eq_matching[dv] = unassigned @@ -480,6 +478,14 @@ function generate_derivative_variables!(ts::TearingState, neweqs, total_sub, var end end +function add_solvable_variable!() + +end + +function add_solvable_equation!() + +end + """ Solve the solvable equations of the system and generate differential (or discrete) equations in terms of the selected states. @@ -492,21 +498,27 @@ such that the mass matrix is: 0 0]. Update the state to account for the new ordering and equations. + +####### DISCRETE CASE +- only substitute Shift(t, -2) """ -function solve_and_generate_equations!(state::TearingState, neweqs, total_sub, var_eq_matching; simplify = false) +function solve_and_generate_equations!(state::TearingState, neweqs, var_eq_matching; simplify = false, shift_sub = Dict()) @unpack fullvars, sys, structure = state @unpack solvable_graph, var_to_diff, eq_to_diff, graph, lowest_shift = structure eq_var_matching = invview(var_eq_matching) diff_to_var = invview(var_to_diff) + dx_sub = Dict() if ModelingToolkit.has_iv(sys) iv = get_iv(sys) if is_only_discrete(structure) D = Shift(iv, 1) lower_name = lower_varname_withshift + total_sub = shift_sub else D = Differential(iv) lower_name = lower_varname_with_unit + total_sub = dx_sub end else iv = D = nothing @@ -540,6 +552,7 @@ function solve_and_generate_equations!(state::TearingState, neweqs, total_sub, v # fullvars[iv] is a differential variable of the form D^n(x), and neweqs[ieq] # is solved to give the RHS. for ieq in eqs + println() iv = eq_var_matching[ieq] if is_solvable(ieq, iv) # We don't solve differential equations, but we will need to try to @@ -549,7 +562,9 @@ function solve_and_generate_equations!(state::TearingState, neweqs, total_sub, v isnothing(D) && error("Differential found in a non-differential system. Likely this is a bug in the construction of an initialization system. Please report this issue with a reproducible example. Offending equation: $(equations(sys)[ieq])") order, lv = var_order(diff_to_var, iv) - dx = D(fullvars[lv]) + @show fullvars[lv] + @show simplify_shifts(fullvars[lv]) + dx = D(simplify_shifts(fullvars[lv])) eq = dx ~ simplify_shifts(Symbolics.fixpoint_sub( Symbolics.symbolic_linear_solve(neweqs[ieq], fullvars[iv]), @@ -560,6 +575,9 @@ function solve_and_generate_equations!(state::TearingState, neweqs, total_sub, v end push!(diff_eqs, eq) total_sub[simplify_shifts(eq.lhs)] = eq.rhs + dx_sub[simplify_shifts(eq.lhs)] = eq.rhs + @show total_sub + @show eq push!(diffeq_idxs, ieq) push!(diff_vars, diff_to_var[iv]) continue @@ -575,10 +593,10 @@ function solve_and_generate_equations!(state::TearingState, neweqs, total_sub, v @warn "Tearing: solving $eq for $var is singular!" else rhs = -b / a - neweq = var ~ Symbolics.fixpoint_sub( + neweq = var ~ simplify_shifts(Symbolics.fixpoint_sub( simplify ? Symbolics.simplify(rhs) : rhs, - total_sub; operator = ModelingToolkit.Shift) + dx_sub; operator = ModelingToolkit.Shift)) push!(subeqs, neweq) push!(solved_equations, ieq) push!(solved_variables, iv) @@ -589,7 +607,7 @@ function solve_and_generate_equations!(state::TearingState, neweqs, total_sub, v if !(eq.lhs isa Number && eq.lhs == 0) rhs = eq.rhs - eq.lhs end - push!(alge_eqs, 0 ~ Symbolics.fixpoint_sub(rhs, total_sub)) + push!(alge_eqs, 0 ~ simplify_shifts(Symbolics.fixpoint_sub(rhs, total_sub))) push!(algeeq_idxs, ieq) end end @@ -676,16 +694,19 @@ function tearing_reassemble(state::TearingState, var_eq_matching, end neweqs = collect(equations(state)) diff_to_var = invview(var_to_diff) - total_sub = Dict() - dummy_sub = Dict() is_discrete = is_only_discrete(state.structure) + shift_sub = Dict() + # Structural simplification + dummy_sub = Dict() substitute_dummy_derivatives!(state, neweqs, dummy_sub, var_eq_matching) - generate_derivative_variables!(state, neweqs, total_sub, var_eq_matching; - is_discrete, mm) + + generate_derivative_variables!(state, neweqs, var_eq_matching; + is_discrete, mm, shift_sub) + new_fullvars, new_var_to_diff, new_eq_to_diff, neweqs, subeqs, graph = - solve_and_generate_equations!(state, neweqs, total_sub, var_eq_matching; simplify) + solve_and_generate_equations!(state, neweqs, var_eq_matching; simplify, shift_sub) # Update system var_to_diff = new_var_to_diff @@ -701,12 +722,7 @@ function tearing_reassemble(state::TearingState, var_eq_matching, i -> (!isempty(𝑑neighbors(graph, i)) || (var_to_diff[i] !== nothing && !isempty(𝑑neighbors(graph, var_to_diff[i])))) end - @show ispresent.(collect(1:length(fullvars))) - @show 𝑑neighbors(graph, 5) - @show var_to_diff[5] - @show neweqs - @show fullvars sys = state.sys obs_sub = dummy_sub for eq in neweqs diff --git a/src/structural_transformation/utils.jl b/src/structural_transformation/utils.jl index b2d67dc547..c202556906 100644 --- a/src/structural_transformation/utils.jl +++ b/src/structural_transformation/utils.jl @@ -449,10 +449,9 @@ end ### Misc ### -function lower_varname_withshift(var, iv, backshift; unshifted = nothing) - backshift < 0 && return Shift(iv, -backshift)(var) - backshift == 0 && return unshifted - ds = "$iv-$backshift" +function lower_varname_withshift(var, iv, backshift; unshifted = nothing, allow_zero = true) + backshift <= 0 && return Shift(iv, -backshift)(unshifted, allow_zero) + ds = backshift > 0 ? "$iv-$backshift" : "$iv+$(-backshift)" d_separator = 'ˍ' if ModelingToolkit.isoperator(var, ModelingToolkit.Shift) @@ -475,9 +474,15 @@ function isdoubleshift(var) ModelingToolkit.isoperator(arguments(var)[1], ModelingToolkit.Shift) end +### Rules +# 1. x(t) -> x(t) +# 2. Shift(t, 0)(x(t)) -> x(t) +# 3. Shift(t, 1)(x + z) -> Shift(t, 1)(x) + Shift(t, 1)(z) + function simplify_shifts(var) ModelingToolkit.hasshift(var) || return var var isa Equation && return simplify_shifts(var.lhs) ~ simplify_shifts(var.rhs) + ((op = operation(var)) isa Shift) && op.steps == 0 && return simplify_shifts(arguments(var)[1]) if isdoubleshift(var) op1 = operation(var) vv1 = arguments(var)[1] From 1c578c3535fe0d7af1d82d08285d90fd244ce2af Mon Sep 17 00:00:00 2001 From: vyudu Date: Fri, 7 Feb 2025 14:46:09 -0500 Subject: [PATCH 0786/1337] rename functions --- .../symbolics_tearing.jl | 37 +++++++----- src/structural_transformation/utils.jl | 60 ++++++++++++++++++- test/structural_transformation/utils.jl | 17 ++++++ 3 files changed, 98 insertions(+), 16 deletions(-) diff --git a/src/structural_transformation/symbolics_tearing.jl b/src/structural_transformation/symbolics_tearing.jl index 020c85b05f..778473907e 100644 --- a/src/structural_transformation/symbolics_tearing.jl +++ b/src/structural_transformation/symbolics_tearing.jl @@ -254,7 +254,9 @@ function substitute_dummy_derivatives!(ts::TearingState, neweqs, dummy_sub, var_ diff_to_var = invview(var_to_diff) iv = ModelingToolkit.has_iv(sys) ? ModelingToolkit.get_iv(sys) : nothing + @show neweqs for var in 1:length(fullvars) + #@show neweqs dv = var_to_diff[var] dv === nothing && continue if var_eq_matching[var] !== SelectedState() @@ -286,9 +288,7 @@ function substitute_dummy_derivatives!(ts::TearingState, neweqs, dummy_sub, var_ end end -""" -Generate new derivative variables for the system. - +#= There are three cases where we want to generate new variables to convert the system into first order (semi-implicit) ODEs. @@ -361,6 +361,16 @@ This is different from the continuous case where D(D(x)) can be substituted for by iteratively substituting x_t ~ D(x), then x_tt ~ D(x_t). For this reason the shift_sub dict is updated at the time that the renamed variables are written, inside the loop where new variables are generated. +=# +""" +Generate new derivative variables for the system. + +Effects on the state: +- fullvars: add the new derivative variables x_t +- neweqs: add the identity equations for the new variables, D(x) ~ x_t +- graph: update graph with the new equations and variables, and their connections +- solvable_graph: +- var_eq_matching: solvable equations """ function generate_derivative_variables!(ts::TearingState, neweqs, var_eq_matching; is_discrete = false, mm = nothing, shift_sub = nothing) @@ -406,7 +416,7 @@ function generate_derivative_variables!(ts::TearingState, neweqs, var_eq_matchin dx = fullvars[dv] order, lv = var_order(diff_to_var, dv) @show fullvars[uv] - x_t = lower_name(fullvars[lv], iv, -low-order-1; unshifted = fullvars[uv]) + x_t = lower_name(fullvars[lv], iv, -low-order-1; unshifted = fullvars[uv], allow_zero = true) shift_sub[dx] = x_t (var_eq_matching[dv] isa Int) ? continue : @goto DISCRETE_VARIABLE end @@ -439,7 +449,7 @@ function generate_derivative_variables!(ts::TearingState, neweqs, var_eq_matchin x_t = lower_name(fullvars[lv], iv, order) @label DISCRETE_VARIABLE - push!(fullvars, x_t) + push!(fullvars, simplify_shifts(x_t)) v_t = length(fullvars) v_t_idx = add_vertex!(var_to_diff) add_vertex!(graph, DST) @@ -448,7 +458,6 @@ function generate_derivative_variables!(ts::TearingState, neweqs, var_eq_matchin add_vertex!(solvable_graph, DST) push!(var_eq_matching, unassigned) - # Add discrete substitutions to total_sub directly. is_discrete && begin idx_to_lowest_shift[v_t] = idx_to_lowest_shift[dv] for e in 𝑑neighbors(graph, dv) @@ -463,7 +472,7 @@ function generate_derivative_variables!(ts::TearingState, neweqs, var_eq_matchin end # add `D(x) - x_t ~ 0` - push!(neweqs, 0 ~ x_t - dx) + push!(neweqs, 0 ~ dx - x_t) add_vertex!(graph, SRC) dummy_eq = length(neweqs) add_edge!(graph, dummy_eq, dv) @@ -478,12 +487,11 @@ function generate_derivative_variables!(ts::TearingState, neweqs, var_eq_matchin end end -function add_solvable_variable!() +function add_solvable_variable!(state::TearingState) end -function add_solvable_equation!() - +function add_solvable_equation!(s::SystemStructure, neweqs, eq) end """ @@ -500,7 +508,8 @@ such that the mass matrix is: Update the state to account for the new ordering and equations. ####### DISCRETE CASE -- only substitute Shift(t, -2) +- Differential equations: substitute variables with everything shifted forward one timestep. +- Algebraic and observable equations: substitute variables with everything shifted back one timestep. """ function solve_and_generate_equations!(state::TearingState, neweqs, var_eq_matching; simplify = false, shift_sub = Dict()) @unpack fullvars, sys, structure = state @@ -562,8 +571,6 @@ function solve_and_generate_equations!(state::TearingState, neweqs, var_eq_match isnothing(D) && error("Differential found in a non-differential system. Likely this is a bug in the construction of an initialization system. Please report this issue with a reproducible example. Offending equation: $(equations(sys)[ieq])") order, lv = var_order(diff_to_var, iv) - @show fullvars[lv] - @show simplify_shifts(fullvars[lv]) dx = D(simplify_shifts(fullvars[lv])) eq = dx ~ simplify_shifts(Symbolics.fixpoint_sub( Symbolics.symbolic_linear_solve(neweqs[ieq], @@ -576,8 +583,6 @@ function solve_and_generate_equations!(state::TearingState, neweqs, var_eq_match push!(diff_eqs, eq) total_sub[simplify_shifts(eq.lhs)] = eq.rhs dx_sub[simplify_shifts(eq.lhs)] = eq.rhs - @show total_sub - @show eq push!(diffeq_idxs, ieq) push!(diff_vars, diff_to_var[iv]) continue @@ -611,6 +616,8 @@ function solve_and_generate_equations!(state::TearingState, neweqs, var_eq_match push!(algeeq_idxs, ieq) end end + @show neweqs + @show subeqs # TODO: BLT sorting neweqs = [diff_eqs; alge_eqs] diff --git a/src/structural_transformation/utils.jl b/src/structural_transformation/utils.jl index c202556906..b947fa4269 100644 --- a/src/structural_transformation/utils.jl +++ b/src/structural_transformation/utils.jl @@ -271,6 +271,8 @@ end function find_solvables!(state::TearingState; kwargs...) @assert state.structure.solvable_graph === nothing + println("in find_solvables") + @show eqs eqs = equations(state) graph = state.structure.graph state.structure.solvable_graph = BipartiteGraph(nsrcs(graph), ndsts(graph)) @@ -278,6 +280,7 @@ function find_solvables!(state::TearingState; kwargs...) for ieq in 1:length(eqs) find_eq_solvables!(state, ieq, to_rm; kwargs...) end + @show eqs return nothing end @@ -477,7 +480,7 @@ end ### Rules # 1. x(t) -> x(t) # 2. Shift(t, 0)(x(t)) -> x(t) -# 3. Shift(t, 1)(x + z) -> Shift(t, 1)(x) + Shift(t, 1)(z) +# 3. Shift(t, 3)(Shift(t, 2)(x(t)) -> Shift(t, 5)(x(t)) function simplify_shifts(var) ModelingToolkit.hasshift(var) || return var @@ -498,3 +501,58 @@ function simplify_shifts(var) unwrap(var).metadata) end end + +""" +Power expand the shifts. Used for substitution. + +Shift(t, -3)(x(t)) -> Shift(t, -1)(Shift(t, -1)(Shift(t, -1)(x))) +""" +function expand_shifts(var) + ModelingToolkit.hasshift(var) || return var + var = ModelingToolkit.value(var) + + var isa Equation && return expand_shifts(var.lhs) ~ expand_shifts(var.rhs) + op = operation(var) + s = sign(op.steps) + arg = only(arguments(var)) + + if ModelingToolkit.isvariable(arg) && (ModelingToolkit.getvariabletype(arg) === VARIABLE) && isequal(op.t, only(arguments(arg))) + out = arg + for i in 1:op.steps + out = Shift(op.t, s)(out) + end + return out + elseif iscall(arg) + return maketerm(typeof(var), operation(var), expand_shifts.(arguments(var)), + unwrap(var).metadata) + else + return arg + end +end + +""" +Shift(t, 1)(x + z) -> Shift(t, 1)(x) + Shift(t, 1)(z) +""" +function distribute_shift(var) + ModelingToolkit.hasshift(var) || return var + var isa Equation && return distribute_shift(var.lhs) ~ distribute_shift(var.rhs) + shift = operation(var) + expr = only(arguments(var)) + _distribute_shift(expr, shift) +end + +function _distribute_shift(expr, shift) + op = operation(expr) + args = arguments(expr) + + if length(args) == 1 + if ModelingToolkit.isvariable(only(args)) && isequal(op.t, only(args)) + return shift(only(args)) + else + return only(args) + end + else iscall(op) + return maketerm(typeof(expr), operation(expr), _distribute_shift.(args, shift), + unwrap(var).metadata) + end +end diff --git a/test/structural_transformation/utils.jl b/test/structural_transformation/utils.jl index 621aa8b8a8..98d986ebd4 100644 --- a/test/structural_transformation/utils.jl +++ b/test/structural_transformation/utils.jl @@ -4,6 +4,7 @@ using Graphs using SparseArrays using UnPack using ModelingToolkit: t_nounits as t, D_nounits as D +const ST = StructuralTransformations # Define some variables @parameters L g @@ -161,3 +162,19 @@ end structural_simplify(sys; additional_passes = [pass]) @test value[] == 1 end + +@testset "Shift simplification" begin + @variables x(t) y(t) z(t) + @parameters a b c + + # Expand shifts + @test isequal(ST.expand_shifts(Shift(t, -3)(x)), Shift(t, -1)(Shift(t, -1)(Shift(t, -1)(x)))) + expr = a * Shift(t, -2)(x) + Shift(t, 2)(y) + b + @test isequal(ST.expand_shifts(expr), + a * Shift(t, -1)(Shift(t, -1)(x)) + Shift(t, 1)(Shift(t, 1)(y)) + b) + @test isequal(ST.expand_shifts(Shift(t, 2)(Shift(t, 1)(a))), a) + + + # Distribute shifts + +end From 5f8cda3a59aa3333c9f1144562ff7642f82bd4b8 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 3 Feb 2025 16:27:12 +0530 Subject: [PATCH 0787/1337] feat: add `assertions` field to `ODESystem` --- src/systems/abstractsystem.jl | 24 ++++++++++++++++++++++++ src/systems/diffeqs/odesystem.jl | 14 +++++++++++--- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index dc47457825..ad46e4529e 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -983,6 +983,7 @@ for prop in [:eqs :gui_metadata :discrete_subsystems :parameter_dependencies + :assertions :solved_unknowns :split_idxs :parent @@ -1468,6 +1469,24 @@ end """ $(TYPEDSIGNATURES) +Get the assertions for a system `sys` and its subsystems. +""" +function assertions(sys::AbstractSystem) + has_assertions(sys) || return Dict{BasicSymbolic, String}() + + asserts = get_assertions(sys) + systems = get_systems(sys) + namespaced_asserts = mapreduce( + merge!, systems; init = Dict{BasicSymbolic, String}()) do subsys + Dict{BasicSymbolic, String}(namespace_expr(k, subsys) => v + for (k, v) in assertions(subsys)) + end + return merge(asserts, namespaced_asserts) +end + +""" +$(TYPEDSIGNATURES) + Get the guesses for variables in the initialization system of the system `sys` and its subsystems. See also [`initialization_equations`](@ref) and [`ModelingToolkit.get_guesses`](@ref). @@ -3036,6 +3055,11 @@ function extend(sys::AbstractSystem, basesys::AbstractSystem; kwargs = merge(kwargs, (initialization_eqs = ieqs, guesses = guesses)) end + if has_assertions(basesys) + kwargs = merge( + kwargs, (; assertions = merge(get_assertions(basesys), get_assertions(sys)))) + end + return T(args...; kwargs...) end diff --git a/src/systems/diffeqs/odesystem.jl b/src/systems/diffeqs/odesystem.jl index c735b52c37..e32b43bd2a 100644 --- a/src/systems/diffeqs/odesystem.jl +++ b/src/systems/diffeqs/odesystem.jl @@ -137,6 +137,11 @@ struct ODESystem <: AbstractODESystem """ parameter_dependencies::Vector{Equation} """ + Mapping of conditions which should be true throughout the solve to corresponding error + messages. These will be added to the equations when calling `debug_system`. + """ + assertions::Dict{BasicSymbolic, String} + """ Metadata for the system, to be used by downstream packages. """ metadata::Any @@ -190,7 +195,7 @@ struct ODESystem <: AbstractODESystem jac, ctrl_jac, Wfact, Wfact_t, name, description, systems, defaults, guesses, torn_matching, initializesystem, initialization_eqs, schedule, connector_type, preface, cevents, - devents, parameter_dependencies, + devents, parameter_dependencies, assertions = Dict{BasicSymbolic, String}(), metadata = nothing, gui_metadata = nothing, is_dde = false, tstops = [], tearing_state = nothing, substitutions = nothing, complete = false, index_cache = nothing, @@ -210,7 +215,7 @@ struct ODESystem <: AbstractODESystem new(tag, deqs, iv, dvs, ps, tspan, var_to_name, ctrls, observed, tgrad, jac, ctrl_jac, Wfact, Wfact_t, name, description, systems, defaults, guesses, torn_matching, initializesystem, initialization_eqs, schedule, connector_type, preface, - cevents, devents, parameter_dependencies, metadata, + cevents, devents, parameter_dependencies, assertions, metadata, gui_metadata, is_dde, tstops, tearing_state, substitutions, complete, index_cache, discrete_subsystems, solved_unknowns, split_idxs, parent) end @@ -235,6 +240,7 @@ function ODESystem(deqs::AbstractVector{<:Equation}, iv, dvs, ps; continuous_events = nothing, discrete_events = nothing, parameter_dependencies = Equation[], + assertions = Dict(), checks = true, metadata = nothing, gui_metadata = nothing, @@ -286,12 +292,13 @@ function ODESystem(deqs::AbstractVector{<:Equation}, iv, dvs, ps; if is_dde === nothing is_dde = _check_if_dde(deqs, iv′, systems) end + assertions = Dict{BasicSymbolic, Any}(unwrap(k) => v for (k, v) in assertions) ODESystem(Threads.atomic_add!(SYSTEM_COUNT, UInt(1)), deqs, iv′, dvs′, ps′, tspan, var_to_name, ctrl′, observed, tgrad, jac, ctrl_jac, Wfact, Wfact_t, name, description, systems, defaults, guesses, nothing, initializesystem, initialization_eqs, schedule, connector_type, preface, cont_callbacks, - disc_callbacks, parameter_dependencies, + disc_callbacks, parameter_dependencies, assertions, metadata, gui_metadata, is_dde, tstops, checks = checks) end @@ -364,6 +371,7 @@ function flatten(sys::ODESystem, noeqs = false) name = nameof(sys), description = description(sys), initialization_eqs = initialization_equations(sys), + assertions = assertions(sys), is_dde = is_dde(sys), tstops = symbolic_tstops(sys), metadata = get_metadata(sys), From aa6f3402a5050f3d4c92eea45de8fbacdc98cc96 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 3 Feb 2025 16:27:39 +0530 Subject: [PATCH 0788/1337] feat: add assertions support to `debug_system` --- src/debugging.jl | 18 ++++++++++++++++++ src/systems/abstractsystem.jl | 10 +++++++++- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/src/debugging.jl b/src/debugging.jl index a1a168d8dd..b30a1ac2ac 100644 --- a/src/debugging.jl +++ b/src/debugging.jl @@ -42,3 +42,21 @@ function debug_sub(ex, funcs; kw...) f in funcs ? logged_fun(f, args...; kw...) : maketerm(typeof(ex), f, args, metadata(ex)) end + +function _debug_assertion(expr::Bool, message::String, log::Bool) + expr && return 0.0 + log && @error message + return NaN +end + +@register_symbolic _debug_assertion(expr::Bool, message::String, log::Bool) + +const ASSERTION_LOG_VARIABLE = only(@parameters __log_assertions_ₘₜₖ::Bool = false) + +function get_assertions_expr(assertions::Dict{BasicSymbolic, String}) + term = 0 + for (k, v) in assertions + term += _debug_assertion(k, "Assertion $k failed:\n$v", ASSERTION_LOG_VARIABLE) + end + return term +end diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index ad46e4529e..80b27e4e84 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -2312,7 +2312,15 @@ function debug_system( error("debug_system(sys) only works on systems with no sub-systems! Consider flattening it with flatten(sys) or structural_simplify(sys) first.") end if has_eqs(sys) - @set! sys.eqs = debug_sub.(equations(sys), Ref(functions); kw...) + eqs = debug_sub.(equations(sys), Ref(functions); kw...) + expr = get_assertions_expr(assertions(sys)) + eqs[end] = eqs[end].lhs ~ eqs[end].rhs + expr + @set! sys.eqs = eqs + @set! sys.ps = unique!([get_ps(sys); ASSERTION_LOG_VARIABLE]) + @set! sys.defaults = merge(get_defaults(sys), Dict(ASSERTION_LOG_VARIABLE => true)) + if iscomplete(sys) + sys = complete(sys; split = is_split(sys)) + end end if has_observed(sys) @set! sys.observed = debug_sub.(observed(sys), Ref(functions); kw...) From b5e6dd93f57c5e9bc51dfb1278c0411d8425724c Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 3 Feb 2025 17:13:22 +0530 Subject: [PATCH 0789/1337] feat: add `assertions` field to `SDESystem` --- src/systems/diffeqs/sdesystem.jl | 19 ++++++++++++++----- src/systems/systems.jl | 2 +- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/systems/diffeqs/sdesystem.jl b/src/systems/diffeqs/sdesystem.jl index 212e20d743..8b7bc8b0d8 100644 --- a/src/systems/diffeqs/sdesystem.jl +++ b/src/systems/diffeqs/sdesystem.jl @@ -126,6 +126,11 @@ struct SDESystem <: AbstractODESystem """ parameter_dependencies::Vector{Equation} """ + Mapping of conditions which should be true throughout the solve to corresponding error + messages. These will be added to the equations when calling `debug_system`. + """ + assertions::Dict{BasicSymbolic, String} + """ Metadata for the system, to be used by downstream packages. """ metadata::Any @@ -159,7 +164,9 @@ struct SDESystem <: AbstractODESystem function SDESystem(tag, deqs, neqs, iv, dvs, ps, tspan, var_to_name, ctrls, observed, tgrad, jac, ctrl_jac, Wfact, Wfact_t, name, description, systems, defaults, guesses, initializesystem, initialization_eqs, connector_type, - cevents, devents, parameter_dependencies, metadata = nothing, gui_metadata = nothing, + cevents, devents, parameter_dependencies, assertions = Dict{ + BasicSymbolic, Nothing}, + metadata = nothing, gui_metadata = nothing, complete = false, index_cache = nothing, parent = nothing, is_scalar_noise = false, is_dde = false, isscheduled = false; @@ -185,9 +192,8 @@ struct SDESystem <: AbstractODESystem new(tag, deqs, neqs, iv, dvs, ps, tspan, var_to_name, ctrls, observed, tgrad, jac, ctrl_jac, Wfact, Wfact_t, name, description, systems, defaults, guesses, initializesystem, initialization_eqs, connector_type, cevents, - devents, - parameter_dependencies, metadata, gui_metadata, complete, index_cache, parent, is_scalar_noise, - is_dde, isscheduled) + devents, parameter_dependencies, assertions, metadata, gui_metadata, complete, + index_cache, parent, is_scalar_noise, is_dde, isscheduled) end end @@ -209,6 +215,7 @@ function SDESystem(deqs::AbstractVector{<:Equation}, neqs::AbstractArray, iv, dv continuous_events = nothing, discrete_events = nothing, parameter_dependencies = Equation[], + assertions = Dict{BasicSymbolic, String}(), metadata = nothing, gui_metadata = nothing, complete = false, @@ -261,11 +268,12 @@ function SDESystem(deqs::AbstractVector{<:Equation}, neqs::AbstractArray, iv, dv if is_dde === nothing is_dde = _check_if_dde(deqs, iv′, systems) end + assertions = Dict{BasicSymbolic, Any}(unwrap(k) => v for (k, v) in assertions) SDESystem(Threads.atomic_add!(SYSTEM_COUNT, UInt(1)), deqs, neqs, iv′, dvs′, ps′, tspan, var_to_name, ctrl′, observed, tgrad, jac, ctrl_jac, Wfact, Wfact_t, name, description, systems, defaults, guesses, initializesystem, initialization_eqs, connector_type, - cont_callbacks, disc_callbacks, parameter_dependencies, metadata, gui_metadata, + cont_callbacks, disc_callbacks, parameter_dependencies, assertions, metadata, gui_metadata, complete, index_cache, parent, is_scalar_noise, is_dde; checks = checks) end @@ -378,6 +386,7 @@ function ODESystem(sys::SDESystem) newsys = ODESystem(neweqs, get_iv(sys), unknowns(sys), parameters(sys); parameter_dependencies = parameter_dependencies(sys), defaults = defaults(sys), continuous_events = continuous_events(sys), discrete_events = discrete_events(sys), + assertions = assertions(sys), name = nameof(sys), description = description(sys), metadata = get_metadata(sys)) @set newsys.parent = sys end diff --git a/src/systems/systems.jl b/src/systems/systems.jl index 9c8c272c5c..f8630f2d20 100644 --- a/src/systems/systems.jl +++ b/src/systems/systems.jl @@ -165,7 +165,7 @@ function __structural_simplify(sys::AbstractSystem, io = nothing; simplify = fal return SDESystem(Vector{Equation}(full_equations(ode_sys)), noise_eqs, 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), + parameter_dependencies = parameter_dependencies(sys), assertions = assertions(sys), guesses = guesses(sys), initialization_eqs = initialization_equations(sys)) end end From 8e23967fd7c7e18a6d621252672f58753a07c06b Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 3 Feb 2025 17:13:31 +0530 Subject: [PATCH 0790/1337] test: test new assertions functionality --- test/debugging.jl | 39 +++++++++++++++++++++++++++++++++++++++ test/runtests.jl | 1 + 2 files changed, 40 insertions(+) create mode 100644 test/debugging.jl diff --git a/test/debugging.jl b/test/debugging.jl new file mode 100644 index 0000000000..f127df6cf8 --- /dev/null +++ b/test/debugging.jl @@ -0,0 +1,39 @@ +using ModelingToolkit, OrdinaryDiffEq, StochasticDiffEq, SymbolicIndexingInterface +using ModelingToolkit: t_nounits as t, D_nounits as D, ASSERTION_LOG_VARIABLE + +@variables x(t) +@brownian a +@named inner_ode = ODESystem(D(x) ~ -sqrt(x), t; assertions = [(x > 0) => "ohno"]) +@named inner_sde = System([D(x) ~ -sqrt(x) + a], t; assertions = [(x > 0) => "ohno"]) +sys_ode = structural_simplify(inner_ode) +sys_sde = structural_simplify(inner_sde) + +@testset "`debug_system` adds assertions" begin + @testset "$(typeof(sys))" for (Problem, sys, alg) in [ + (ODEProblem, sys_ode, Tsit5()), (SDEProblem, sys_sde, ImplicitEM())] + dsys = debug_system(sys; functions = []) + @test is_parameter(dsys, ASSERTION_LOG_VARIABLE) + prob = Problem(dsys, [x => 1.0], (0.0, 5.0)) + sol = solve(prob, alg) + @test !SciMLBase.successful_retcode(sol) + prob.ps[ASSERTION_LOG_VARIABLE] = true + sol = @test_logs (:error, r"ohno") match_mode=:any solve(prob, alg) + @test !SciMLBase.successful_retcode(sol) + end +end + +@testset "Hierarchical system" begin + @testset "$(typeof(inner))" for (ctor, Problem, inner, alg) in [ + (ODESystem, ODEProblem, inner_ode, Tsit5()), + (System, SDEProblem, inner_sde, ImplicitEM())] + @mtkbuild outer = ctor(Equation[], t; systems = [inner]) + dsys = debug_system(outer; functions = []) + @test is_parameter(dsys, ASSERTION_LOG_VARIABLE) + prob = Problem(dsys, [inner.x => 1.0], (0.0, 5.0)) + sol = solve(prob, alg) + @test !SciMLBase.successful_retcode(sol) + prob.ps[ASSERTION_LOG_VARIABLE] = true + sol = @test_logs (:error, r"ohno") match_mode=:any solve(prob, alg) + @test !SciMLBase.successful_retcode(sol) + end +end diff --git a/test/runtests.jl b/test/runtests.jl index 52875fdae5..9537b1b44e 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -92,6 +92,7 @@ end @safetestset "IfLifting Test" include("if_lifting.jl") @safetestset "Analysis Points Test" include("analysis_points.jl") @safetestset "Causal Variables Connection Test" include("causal_variables_connection.jl") + @safetestset "Debugging Test" include("debugging.jl") end end From 5d2e8ede6355369b7f064864eb83852fcb5aec2c Mon Sep 17 00:00:00 2001 From: Christopher Rackauckas Date: Mon, 3 Feb 2025 04:37:21 -0800 Subject: [PATCH 0791/1337] Update src/systems/diffeqs/odesystem.jl Co-authored-by: Fredrik Bagge Carlson --- src/systems/diffeqs/odesystem.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/systems/diffeqs/odesystem.jl b/src/systems/diffeqs/odesystem.jl index e32b43bd2a..96fd8534ae 100644 --- a/src/systems/diffeqs/odesystem.jl +++ b/src/systems/diffeqs/odesystem.jl @@ -137,7 +137,7 @@ struct ODESystem <: AbstractODESystem """ parameter_dependencies::Vector{Equation} """ - Mapping of conditions which should be true throughout the solve to corresponding error + Mapping of conditions which should be true throughout the solution process to corresponding error messages. These will be added to the equations when calling `debug_system`. """ assertions::Dict{BasicSymbolic, String} From f9b8f52ee8254400849e435f65ed64eea09c29a4 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 3 Feb 2025 18:09:03 +0530 Subject: [PATCH 0792/1337] Update src/systems/diffeqs/sdesystem.jl Co-authored-by: Fredrik Bagge Carlson --- src/systems/diffeqs/sdesystem.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/systems/diffeqs/sdesystem.jl b/src/systems/diffeqs/sdesystem.jl index 8b7bc8b0d8..92a8ddd710 100644 --- a/src/systems/diffeqs/sdesystem.jl +++ b/src/systems/diffeqs/sdesystem.jl @@ -126,7 +126,7 @@ struct SDESystem <: AbstractODESystem """ parameter_dependencies::Vector{Equation} """ - Mapping of conditions which should be true throughout the solve to corresponding error + Mapping of conditions which should be true throughout the solution process to corresponding error messages. These will be added to the equations when calling `debug_system`. """ assertions::Dict{BasicSymbolic, String} From 41a43fdf506ce291241a9e0077a68570c662cf57 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 3 Feb 2025 18:19:22 +0530 Subject: [PATCH 0793/1337] docs: add documentation for assertions functionality --- docs/src/basics/Debugging.md | 17 +++++++++++++++++ src/debugging.jl | 19 ++++++++++++++++++- src/systems/abstractsystem.jl | 8 ++++++++ 3 files changed, 43 insertions(+), 1 deletion(-) diff --git a/docs/src/basics/Debugging.md b/docs/src/basics/Debugging.md index d5c51ec0c1..b2524832d9 100644 --- a/docs/src/basics/Debugging.md +++ b/docs/src/basics/Debugging.md @@ -35,6 +35,23 @@ dsol = solve(dprob, Tsit5()); Now we see that it crashed because `u1` decreased so much that it became negative and outside the domain of the `√` function. We could have figured that out ourselves, but it is not always so obvious for more complex models. +Suppose we also want to validate that `u1 + u2 >= 2.0`. We can do this via the assertions functionality. + +```@example debug +@mtkbuild sys = ODESystem(eqs, t; defaults, assertions = [(u1 + u2 >= 2.0) => "Oh no!"]) +``` + +The assertions must be an iterable of pairs, where the first element is the symbolic condition and +the second is the message to be logged when the condition fails. + +```@repl debug +dsys = debug_system(sys; functions = []); +dprob = ODEProblem(dsys, [], (0.0, 10.0)); +dsol = solve(dprob, Tsit5()); +``` + +Note the messages containing the failed assertion and corresponding message. + ```@docs debug_system ``` diff --git a/src/debugging.jl b/src/debugging.jl index b30a1ac2ac..266819d60b 100644 --- a/src/debugging.jl +++ b/src/debugging.jl @@ -43,6 +43,13 @@ function debug_sub(ex, funcs; kw...) maketerm(typeof(ex), f, args, metadata(ex)) end +""" + $(TYPEDSIGNATURES) + +A function which takes a condition `expr` and returns `NaN` if it is false, +and zero if it is true. In case the condition is false and `log == true`, +`message` will be logged as an `@error`. +""" function _debug_assertion(expr::Bool, message::String, log::Bool) expr && return 0.0 log && @error message @@ -51,9 +58,19 @@ end @register_symbolic _debug_assertion(expr::Bool, message::String, log::Bool) +""" +Boolean parameter added to models returned from `debug_system` to control logging of +assertions. +""" const ASSERTION_LOG_VARIABLE = only(@parameters __log_assertions_ₘₜₖ::Bool = false) -function get_assertions_expr(assertions::Dict{BasicSymbolic, String}) +""" + $(TYPEDSIGNATURES) + +Get a symbolic expression as per the requirement of `debug_system` for all the assertions +in `assertions`. `is_split` denotes whether the corresponding system is a split system. +""" +function get_assertions_expr(assertions::Dict{BasicSymbolic, String}, is_split::Bool) term = 0 for (k, v) in assertions term += _debug_assertion(k, "Assertion $k failed:\n$v", ASSERTION_LOG_VARIABLE) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index 80b27e4e84..7857cc4c74 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -2302,6 +2302,14 @@ ERROR: Function /(1, sin(P(t))) output non-finite value Inf with input 1 => 1 sin(P(t)) => 0.0 ``` + +Additionally, all assertions in the system are validated in the equations. If any of +the conditions are false, the right hand side of at least one of the equations of +the system will evaluate to `NaN`. A new parameter is also added to the system which +controls whether the message associated with each assertion will be logged when the +assertion fails. This parameter defaults to `true` and can be toggled by +symbolic indexing with `ModelingToolkit.ASSERTION_LOG_VARIABLE`. For example, +`prob.ps[ModelingToolkit.ASSERTION_LOG_VARIABLE] = false` will disable logging. """ function debug_system( sys::AbstractSystem; functions = [log, sqrt, (^), /, inv, asin, acos], kw...) From b968b9244d2f9d4d99f7ed304f64a76eabcda171 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 10 Feb 2025 13:21:34 +0530 Subject: [PATCH 0794/1337] feat: add assertions to function regardless of `debug_system` add logging in `debug_system` --- src/debugging.jl | 35 +++++++++++++++++++----- src/systems/abstractsystem.jl | 19 ++++++------- src/systems/diffeqs/abstractodesystem.jl | 4 +++ test/debugging.jl | 17 ++++++++++-- 4 files changed, 54 insertions(+), 21 deletions(-) diff --git a/src/debugging.jl b/src/debugging.jl index 266819d60b..c16b47c2e3 100644 --- a/src/debugging.jl +++ b/src/debugging.jl @@ -43,6 +43,17 @@ function debug_sub(ex, funcs; kw...) maketerm(typeof(ex), f, args, metadata(ex)) end +""" + $(TYPEDSIGNATURES) + +A function which returns `NaN` if `condition` fails, and `0.0` otherwise. +""" +function _nan_condition(condition::Bool) + condition ? 0.0 : NaN +end + +@register_symbolic _nan_condition(condition::Bool) + """ $(TYPEDSIGNATURES) @@ -51,9 +62,10 @@ and zero if it is true. In case the condition is false and `log == true`, `message` will be logged as an `@error`. """ function _debug_assertion(expr::Bool, message::String, log::Bool) - expr && return 0.0 + value = _nan_condition(expr) + isnan(value) || return value log && @error message - return NaN + return value end @register_symbolic _debug_assertion(expr::Bool, message::String, log::Bool) @@ -67,13 +79,22 @@ const ASSERTION_LOG_VARIABLE = only(@parameters __log_assertions_ₘₜₖ::Bool """ $(TYPEDSIGNATURES) -Get a symbolic expression as per the requirement of `debug_system` for all the assertions -in `assertions`. `is_split` denotes whether the corresponding system is a split system. +Get a symbolic expression for all the assertions in `sys`. The expression returns `NaN` +if any of the assertions fail, and `0.0` otherwise. If `ASSERTION_LOG_VARIABLE` is a +parameter in the system, it will control whether the message associated with each +assertion is logged when it fails. """ -function get_assertions_expr(assertions::Dict{BasicSymbolic, String}, is_split::Bool) +function get_assertions_expr(sys::AbstractSystem) + asserts = assertions(sys) term = 0 - for (k, v) in assertions - term += _debug_assertion(k, "Assertion $k failed:\n$v", ASSERTION_LOG_VARIABLE) + if is_parameter(sys, ASSERTION_LOG_VARIABLE) + for (k, v) in asserts + term += _debug_assertion(k, "Assertion $k failed:\n$v", ASSERTION_LOG_VARIABLE) + end + else + for (k, v) in asserts + term += _nan_condition(k) + end end return term end diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index 7857cc4c74..8213b8f241 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -2303,12 +2303,11 @@ ERROR: Function /(1, sin(P(t))) output non-finite value Inf with input sin(P(t)) => 0.0 ``` -Additionally, all assertions in the system are validated in the equations. If any of -the conditions are false, the right hand side of at least one of the equations of -the system will evaluate to `NaN`. A new parameter is also added to the system which -controls whether the message associated with each assertion will be logged when the -assertion fails. This parameter defaults to `true` and can be toggled by -symbolic indexing with `ModelingToolkit.ASSERTION_LOG_VARIABLE`. For example, +Additionally, all assertions in the system are optionally logged when they fail. +A new parameter is also added to the system which controls whether the message associated +with each assertion will be logged when the assertion fails. This parameter defaults to +`true` and can be toggled by symbolic indexing with +`ModelingToolkit.ASSERTION_LOG_VARIABLE`. For example, `prob.ps[ModelingToolkit.ASSERTION_LOG_VARIABLE] = false` will disable logging. """ function debug_system( @@ -2321,18 +2320,16 @@ function debug_system( end if has_eqs(sys) eqs = debug_sub.(equations(sys), Ref(functions); kw...) - expr = get_assertions_expr(assertions(sys)) - eqs[end] = eqs[end].lhs ~ eqs[end].rhs + expr @set! sys.eqs = eqs @set! sys.ps = unique!([get_ps(sys); ASSERTION_LOG_VARIABLE]) @set! sys.defaults = merge(get_defaults(sys), Dict(ASSERTION_LOG_VARIABLE => true)) - if iscomplete(sys) - sys = complete(sys; split = is_split(sys)) - end end if has_observed(sys) @set! sys.observed = debug_sub.(observed(sys), Ref(functions); kw...) end + if iscomplete(sys) + sys = complete(sys; split = is_split(sys)) + end return sys end diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index 8d0720a1d2..ea55dce388 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -168,6 +168,10 @@ function generate_function(sys::AbstractODESystem, dvs = unknowns(sys), rhss = implicit_dae ? [_iszero(eq.lhs) ? eq.rhs : eq.rhs - eq.lhs for eq in eqs] : [eq.rhs for eq in eqs] + if !isempty(assertions(sys)) + rhss[end] += unwrap(get_assertions_expr(sys)) + end + # TODO: add an optional check on the ordering of observed equations u = dvs p = reorder_parameters(sys, ps) diff --git a/test/debugging.jl b/test/debugging.jl index f127df6cf8..aad36eb995 100644 --- a/test/debugging.jl +++ b/test/debugging.jl @@ -8,12 +8,23 @@ using ModelingToolkit: t_nounits as t, D_nounits as D, ASSERTION_LOG_VARIABLE sys_ode = structural_simplify(inner_ode) sys_sde = structural_simplify(inner_sde) -@testset "`debug_system` adds assertions" begin +@testset "assertions are present in generated `f`" begin + @testset "$(typeof(sys))" for (Problem, sys, alg) in [ + (ODEProblem, sys_ode, Tsit5()), (SDEProblem, sys_sde, ImplicitEM())] + @test !is_parameter(sys, ASSERTION_LOG_VARIABLE) + prob = Problem(sys, [x => 0.1], (0.0, 5.0)) + sol = solve(prob, alg) + @test !SciMLBase.successful_retcode(sol) + @test isnan(prob.f.f([0.0], prob.p, sol.t[end])[1]) + end +end + +@testset "`debug_system` adds logging" begin @testset "$(typeof(sys))" for (Problem, sys, alg) in [ (ODEProblem, sys_ode, Tsit5()), (SDEProblem, sys_sde, ImplicitEM())] dsys = debug_system(sys; functions = []) @test is_parameter(dsys, ASSERTION_LOG_VARIABLE) - prob = Problem(dsys, [x => 1.0], (0.0, 5.0)) + prob = Problem(dsys, [x => 0.1], (0.0, 5.0)) sol = solve(prob, alg) @test !SciMLBase.successful_retcode(sol) prob.ps[ASSERTION_LOG_VARIABLE] = true @@ -29,7 +40,7 @@ end @mtkbuild outer = ctor(Equation[], t; systems = [inner]) dsys = debug_system(outer; functions = []) @test is_parameter(dsys, ASSERTION_LOG_VARIABLE) - prob = Problem(dsys, [inner.x => 1.0], (0.0, 5.0)) + prob = Problem(dsys, [inner.x => 0.1], (0.0, 5.0)) sol = solve(prob, alg) @test !SciMLBase.successful_retcode(sol) prob.ps[ASSERTION_LOG_VARIABLE] = true From 6493175547aef97c06172cdd58288350acae9ab6 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 10 Feb 2025 14:48:09 +0530 Subject: [PATCH 0795/1337] test: update reference tests --- test/latexify/20.tex | 2 +- test/latexify/30.tex | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/latexify/20.tex b/test/latexify/20.tex index 0af631162c..ce6f5067c2 100644 --- a/test/latexify/20.tex +++ b/test/latexify/20.tex @@ -1,5 +1,5 @@ \begin{align} \frac{\mathrm{d} u\left( t \right)_{1}}{\mathrm{d}t} &= p_{3} \left( - u\left( t \right)_{1} + u\left( t \right)_{2} \right) \\ 0 &= - u\left( t \right)_{2} + \frac{1}{10} \left( p_{1} - u\left( t \right)_{1} \right) p_{2} p_{3} u\left( t \right)_{1} \\ -\frac{\mathrm{d} u\left( t \right)_{3}}{\mathrm{d}t} &= u\left( t \right)_{2}^{\frac{2}{3}} u\left( t \right)_{1} - p_{3} u\left( t \right)_{3} +\frac{\mathrm{d} u\left( t \right)_{3}}{\mathrm{d}t} &= \left( u\left( t \right)_{2} \right)^{\frac{2}{3}} u\left( t \right)_{1} - p_{3} u\left( t \right)_{3} \end{align} diff --git a/test/latexify/30.tex b/test/latexify/30.tex index 5cd2394374..d206c48d47 100644 --- a/test/latexify/30.tex +++ b/test/latexify/30.tex @@ -1,5 +1,5 @@ \begin{align} \frac{\mathrm{d} u\left( t \right)_{1}}{\mathrm{d}t} &= p_{3} \left( - u\left( t \right)_{1} + u\left( t \right)_{2} \right) \\ \frac{\mathrm{d} u\left( t \right)_{2}}{\mathrm{d}t} &= - u\left( t \right)_{2} + \frac{1}{10} \left( p_{1} - u\left( t \right)_{1} \right) p_{2} p_{3} u\left( t \right)_{1} \\ -\frac{\mathrm{d} u\left( t \right)_{3}}{\mathrm{d}t} &= u\left( t \right)_{2}^{\frac{2}{3}} u\left( t \right)_{1} - p_{3} u\left( t \right)_{3} +\frac{\mathrm{d} u\left( t \right)_{3}}{\mathrm{d}t} &= \left( u\left( t \right)_{2} \right)^{\frac{2}{3}} u\left( t \right)_{1} - p_{3} u\left( t \right)_{3} \end{align} From 3ca004e4b11e2513121c4e68f3d56ad6918effa2 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 10 Feb 2025 15:11:59 +0530 Subject: [PATCH 0796/1337] docs: update assertions documentation --- docs/src/basics/Debugging.md | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/docs/src/basics/Debugging.md b/docs/src/basics/Debugging.md index b2524832d9..b432810bd0 100644 --- a/docs/src/basics/Debugging.md +++ b/docs/src/basics/Debugging.md @@ -42,7 +42,17 @@ Suppose we also want to validate that `u1 + u2 >= 2.0`. We can do this via the a ``` The assertions must be an iterable of pairs, where the first element is the symbolic condition and -the second is the message to be logged when the condition fails. +the second is a message to be logged when the condition fails. All assertions are added to the +generated code and will cause the solver to reject steps that fail the assertions. For systems such +as the above where the assertion is guaranteed to eventually fail, the solver will likely exit +with a `dtmin` failure.. + +```@example debug +prob = ODEProblem(sys, [], (0.0, 10.0)) +sol = solve(prob, Tsit5()) +``` + +We can use `debug_system` to log the failing assertions in each call to the RHS function. ```@repl debug dsys = debug_system(sys; functions = []); @@ -50,7 +60,13 @@ dprob = ODEProblem(dsys, [], (0.0, 10.0)); dsol = solve(dprob, Tsit5()); ``` -Note the messages containing the failed assertion and corresponding message. +Note the logs containing the failed assertion and corresponding message. To temporarily disable +logging in a system returned from `debug_system`, use `ModelingToolkit.ASSERTION_LOG_VARIABLE`. + +```@repl debug +dprob[ModelingToolkit.ASSERTION_LOG_VARIABLE] = false; +solve(drob, Tsit5()); +``` ```@docs debug_system From 8fe1e957d5a1239bab24b219bc0cdab842659334 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 10 Feb 2025 16:42:16 +0530 Subject: [PATCH 0797/1337] docs: fix broken example blocks --- docs/src/basics/Variable_metadata.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/src/basics/Variable_metadata.md b/docs/src/basics/Variable_metadata.md index 44dfb30327..b2cc472f2f 100644 --- a/docs/src/basics/Variable_metadata.md +++ b/docs/src/basics/Variable_metadata.md @@ -183,7 +183,7 @@ A variable can be marked `irreducible` to prevent it from being moved to an it can be accessed in [callbacks](@ref events) ```@example metadata -@variable important_value [irreducible = true] +@variables important_value [irreducible = true] isirreducible(important_value) ``` @@ -192,7 +192,7 @@ isirreducible(important_value) When a model is structurally simplified, the algorithm will try to ensure that the variables with higher state priority become states of the system. A variable's state priority is a number set using the `state_priority` metadata. ```@example metadata -@variable important_dof [state_priority = 10] unimportant_dof [state_priority = -2] +@variables important_dof [state_priority = 10] unimportant_dof [state_priority = -2] state_priority(important_dof) ``` @@ -201,7 +201,7 @@ state_priority(important_dof) Units for variables can be designated using symbolic metadata. For more information, please see the [model validation and units](@ref units) section of the docs. Note that `getunit` is not equivalent to `get_unit` - the former is a metadata getter for individual variables (and is provided so the same interface function for `unit` exists like other metadata), while the latter is used to handle more general symbolic expressions. ```@example metadata -@variable speed [unit = u"m/s"] +@variables speed [unit = u"m/s"] hasunit(speed) ``` From 7cd399f82b0be3552789c441b8268769ccc4bb68 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 10 Feb 2025 18:13:54 +0530 Subject: [PATCH 0798/1337] feat: export `state_priority` --- src/ModelingToolkit.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ModelingToolkit.jl b/src/ModelingToolkit.jl index ed99fb2772..09b59c4ed6 100644 --- a/src/ModelingToolkit.jl +++ b/src/ModelingToolkit.jl @@ -249,7 +249,7 @@ export isinput, isoutput, getbounds, hasbounds, getguess, hasguess, isdisturbanc istunable, getdist, hasdist, tunable_parameters, isirreducible, getdescription, hasdescription, hasunit, getunit, hasconnect, getconnect, - hasmisc, getmisc + hasmisc, getmisc, state_priority export ode_order_lowering, dae_order_lowering, liouville_transform export PDESystem export Differential, expand_derivatives, @derivatives From 603c894044cd6589a0a15a7f8765369393ed9762 Mon Sep 17 00:00:00 2001 From: Christopher Rackauckas Date: Mon, 10 Feb 2025 06:10:32 -0800 Subject: [PATCH 0799/1337] Update src/systems/diffeqs/odesystem.jl --- src/systems/diffeqs/odesystem.jl | 1 - 1 file changed, 1 deletion(-) diff --git a/src/systems/diffeqs/odesystem.jl b/src/systems/diffeqs/odesystem.jl index 44cc7df46b..5b8041ba33 100644 --- a/src/systems/diffeqs/odesystem.jl +++ b/src/systems/diffeqs/odesystem.jl @@ -711,6 +711,5 @@ function process_constraint_system(constraints::Vector{Equation}, sts, ps, iv; c end end - @show constraints ConstraintsSystem(constraints, collect(constraintsts), collect(constraintps); name = consname) end From ed143a5d85d4b4660d478718fc9bcd9d79ec810c Mon Sep 17 00:00:00 2001 From: vyudu Date: Mon, 10 Feb 2025 15:18:05 -0500 Subject: [PATCH 0800/1337] refactor: refactor tearing_assemble into functions --- .../symbolics_tearing.jl | 503 ++++++++---------- src/structural_transformation/utils.jl | 71 +-- src/systems/systemstructure.jl | 6 +- 3 files changed, 239 insertions(+), 341 deletions(-) diff --git a/src/structural_transformation/symbolics_tearing.jl b/src/structural_transformation/symbolics_tearing.jl index 778473907e..b99e6ec21a 100644 --- a/src/structural_transformation/symbolics_tearing.jl +++ b/src/structural_transformation/symbolics_tearing.jl @@ -244,19 +244,17 @@ State selection may determine that some differential variables are algebraic variables in disguise. The derivative of such variables are called dummy derivatives. -`SelectedState` information is no longer needed past here. State selection -is done. All non-differentiated variables are algebraic variables, and all -variables that appear differentiated are differential variables. +`SelectedState` information is no longer needed after this function is called. +State selection is done. All non-differentiated variables are algebraic +variables, and all variables that appear differentiated are differential variables. """ -function substitute_dummy_derivatives!(ts::TearingState, neweqs, dummy_sub, var_eq_matching) +function substitute_derivatives_algevars!(ts::TearingState, neweqs, dummy_sub, var_eq_matching) @unpack fullvars, sys, structure = ts - @unpack solvable_graph, var_to_diff, eq_to_diff, graph, lowest_shift = structure + @unpack solvable_graph, var_to_diff, eq_to_diff, graph = structure diff_to_var = invview(var_to_diff) iv = ModelingToolkit.has_iv(sys) ? ModelingToolkit.get_iv(sys) : nothing - @show neweqs for var in 1:length(fullvars) - #@show neweqs dv = var_to_diff[var] dv === nothing && continue if var_eq_matching[var] !== SelectedState() @@ -339,199 +337,137 @@ variables and equations, don't add them when they already exist. Documenting the differences to structural simplification for discrete systems: -1. In discrete systems the lowest-order term is Shift(t, k)(x(t)), instead of x(t). We need to substitute the k-1 lowest order terms instead of the k-1 highest order terms. +In discrete systems the lowest-order term is Shift(t, k)(x(t)), instead of x(t). +We want to substitute the k-1 lowest order terms instead of the k-1 highest order terms. -The orders will also be off by one. The reason this is is that the dynamics of -the system should be given in terms of Shift(t, 1)(x(t), x(t-1), ...). But -having the observables be indexed by the next time step is not so nice. So we -handle the shifts in the renaming. - -The substitution should look like the following: - x(t) -> Shift(t, 1)(x(t)) - Shift(t, -1)(x(t)) -> Shift(t, 0)(x(t)) - Shift(t, -2)(x(t)) -> x_{t-1}(t) - Shift(t, -3)(x(t)) -> x_{t-2}(t) - and so on... - -In the implicit discrete case this shouldn't happen. The simplification should -look like a NonlinearSystem. - -2. For discrete systems Shift(t, 2)(x(t)) cannot be substituted as Shift(t, 1)(Shift(t,1)(x(t)). -This is different from the continuous case where D(D(x)) can be substituted for -by iteratively substituting x_t ~ D(x), then x_tt ~ D(x_t). For this reason the -shift_sub dict is updated at the time that the renamed variables are written, -inside the loop where new variables are generated. +In the system x(k) ~ x(k-1) + x(k-2), we want to lower +Shift(t, -1)(x(t)) -> x\_{t-1}(t) =# """ Generate new derivative variables for the system. -Effects on the state: +Effects on the system structure: - fullvars: add the new derivative variables x_t - neweqs: add the identity equations for the new variables, D(x) ~ x_t - graph: update graph with the new equations and variables, and their connections - solvable_graph: -- var_eq_matching: solvable equations +- var_eq_matching: match D(x) to the added identity equation """ function generate_derivative_variables!(ts::TearingState, neweqs, var_eq_matching; - is_discrete = false, mm = nothing, shift_sub = nothing) + is_discrete = false, mm = nothing) @unpack fullvars, sys, structure = ts - @unpack solvable_graph, var_to_diff, eq_to_diff, graph, lowest_shift = structure + @unpack solvable_graph, var_to_diff, eq_to_diff, graph = structure eq_var_matching = invview(var_eq_matching) diff_to_var = invview(var_to_diff) iv = ModelingToolkit.has_iv(sys) ? ModelingToolkit.get_iv(sys) : nothing lower_name = is_discrete ? lower_varname_withshift : lower_varname_with_unit - # index v gets mapped to the lowest shift and the index of the unshifted variable - if is_discrete - idx_to_lowest_shift = Dict{Int, Tuple{Int, Int}}(var => (0,0) for var in 1:length(fullvars)) - var_to_unshiftedidx = Dict{Any, Int}(var => findfirst(x -> isequal(x, var), fullvars) for var in keys(lowest_shift)) - - for (i,var) in enumerate(fullvars) - key = (operation(var) isa Shift) ? only(arguments(var)) : var - idx_to_lowest_shift[i] = (get(lowest_shift, key, 0), get(var_to_unshiftedidx, key, i)) - end - end - linear_eqs = mm === nothing ? Dict{Int, Int}() : Dict(reverse(en) for en in enumerate(mm.nzrows)) - # v is the index of the current variable, x = fullvars[v] - # dv is the index of the derivative dx = D(x), x_t is the substituted variable - # For ODESystems: lv is the index of the lowest-order variable (x(t)) - # For DiscreteSystems: - # - lv is the index of the lowest-order variable (Shift(t, k)(x(t))) - # - uv is the index of the highest-order variable (x(t)) + # Generate new derivative variables for all unsolved variables that have a derivative in the system for v in 1:length(var_to_diff) + # Check if a derivative 1) exists and 2) is unsolved for dv = var_to_diff[v] - - if is_discrete - x = fullvars[v] - op = operation(x) - (low, uv) = idx_to_lowest_shift[v] - - # If v is unshifted (i.e. x(t)), then substitute the lowest-shift variable - if !(op isa Shift) - dv = findfirst(_x -> isequal(_x, Shift(iv, low)(x)), fullvars) - end - dx = fullvars[dv] - order, lv = var_order(diff_to_var, dv) - @show fullvars[uv] - x_t = lower_name(fullvars[lv], iv, -low-order-1; unshifted = fullvars[uv], allow_zero = true) - shift_sub[dx] = x_t - (var_eq_matching[dv] isa Int) ? continue : @goto DISCRETE_VARIABLE - end dv isa Int || continue solved = var_eq_matching[dv] isa Int solved && continue - # Check if there's `D(x) = x_t` already - local v_t, dummy_eq - for eq in 𝑑neighbors(solvable_graph, dv) - mi = get(linear_eqs, eq, 0) - iszero(mi) && continue - row = @view mm[mi, :] - nzs = nonzeros(row) - rvs = SparseArrays.nonzeroinds(row) - # note that `v_t` must not be differentiated - if length(nzs) == 2 && - (abs(nzs[1]) == 1 && nzs[1] == -nzs[2]) && - (v_t = rvs[1] == dv ? rvs[2] : rvs[1]; - diff_to_var[v_t] === nothing) - @assert dv in rvs - dummy_eq = eq - @goto FOUND_DUMMY_EQ - end + # If there's `D(x) = x_t` already, update mappings and continue without + # adding new equations/variables + dd = find_duplicate_dd(dv, lineareqs, mm) + + if !isnothing(dd) + dummy_eq, v_t = dd + var_to_diff[v_t] = var_to_diff[dv] + var_eq_matching[dv] = unassigned + eq_var_matching[dummy_eq] = dv + continue end - # add `x_t` dx = fullvars[dv] order, lv = var_order(diff_to_var, dv) - x_t = lower_name(fullvars[lv], iv, order) + x_t = is_discrete ? lower_name(fullvars[lv], iv) + : lower_name(fullvars[lv], iv, order) - @label DISCRETE_VARIABLE - push!(fullvars, simplify_shifts(x_t)) - v_t = length(fullvars) - v_t_idx = add_vertex!(var_to_diff) - add_vertex!(graph, DST) - # TODO: do we care about solvable_graph? We don't use them after - # `dummy_derivative_graph`. - add_vertex!(solvable_graph, DST) - push!(var_eq_matching, unassigned) - - is_discrete && begin - idx_to_lowest_shift[v_t] = idx_to_lowest_shift[dv] - for e in 𝑑neighbors(graph, dv) - add_edge!(graph, e, v_t) - rem_edge!(graph, e, dv) - end - # Do not add the lowest-order substitution as an equation, just substitute - !(operation(x) isa Shift) && begin - var_to_diff[v_t] = var_to_diff[dv] - continue - end - end + # Add `x_t` to the graph + add_dd_variable!(structure, x_t, dv) + # Add `D(x) - x_t ~ 0` to the graph + add_dd_equation!(structure, neweqs, 0 ~ dx - x_t, dv) - # add `D(x) - x_t ~ 0` - push!(neweqs, 0 ~ dx - x_t) - add_vertex!(graph, SRC) - dummy_eq = length(neweqs) - add_edge!(graph, dummy_eq, dv) - add_edge!(graph, dummy_eq, v_t) - add_vertex!(solvable_graph, SRC) - add_edge!(solvable_graph, dummy_eq, dv) - - @label FOUND_DUMMY_EQ - var_to_diff[v_t] = var_to_diff[dv] + # Update matching + push!(var_eq_matching, unassigned) var_eq_matching[dv] = unassigned eq_var_matching[dummy_eq] = dv end end -function add_solvable_variable!(state::TearingState) - +""" +Check if there's `D(x) = x_t` already. +""" +function find_duplicate_dd(dv, lineareqs, mm) + for eq in 𝑑neighbors(solvable_graph, dv) + mi = get(linear_eqs, eq, 0) + iszero(mi) && continue + row = @view mm[mi, :] + nzs = nonzeros(row) + rvs = SparseArrays.nonzeroinds(row) + # note that `v_t` must not be differentiated + if length(nzs) == 2 && + (abs(nzs[1]) == 1 && nzs[1] == -nzs[2]) && + (v_t = rvs[1] == dv ? rvs[2] : rvs[1]; + diff_to_var[v_t] === nothing) + @assert dv in rvs + return eq, v_t + end + end + return nothing +end + +function add_dd_variable!(s::SystemStructure, x_t, dv) + push!(s.fullvars, simplify_shifts(x_t)) + v_t_idx = add_vertex!(s.var_to_diff) + @assert v_t_idx == ndsts(graph) == ndsts(solvable_graph) == length(fullvars) == + length(var_eq_matching) + add_vertex!(s.graph, DST) + # TODO: do we care about solvable_graph? We don't use them after + # `dummy_derivative_graph`. + add_vertex!(s.solvable_graph, DST) + var_to_diff[v_t] = var_to_diff[dv] end -function add_solvable_equation!(s::SystemStructure, neweqs, eq) +# dv = index of D(x), v_t = index of x_t +function add_dd_equation!(s::SystemStructure, neweqs, eq, dv) + push!(neweqs, eq) + add_vertex!(s.graph, SRC) + v_t = length(s.fullvars) + dummy_eq = length(neweqs) + add_edge!(s.graph, dummy_eq, dv) + add_edge!(s.graph, dummy_eq, v_t) + add_vertex!(s.solvable_graph, SRC) + add_edge!(s.solvable_graph, dummy_eq, dv) end """ Solve the solvable equations of the system and generate differential (or discrete) equations in terms of the selected states. - -Will reorder equations and unknowns to be: - [diffeqs; ...] - [diffvars; ...] -such that the mass matrix is: - [I 0 - 0 0]. - -Update the state to account for the new ordering and equations. - -####### DISCRETE CASE -- Differential equations: substitute variables with everything shifted forward one timestep. -- Algebraic and observable equations: substitute variables with everything shifted back one timestep. """ -function solve_and_generate_equations!(state::TearingState, neweqs, var_eq_matching; simplify = false, shift_sub = Dict()) +function generate_system_equations!(state::TearingState, neweqs, var_eq_matching; simplify = false) @unpack fullvars, sys, structure = state - @unpack solvable_graph, var_to_diff, eq_to_diff, graph, lowest_shift = structure + @unpack solvable_graph, var_to_diff, eq_to_diff, graph = structure eq_var_matching = invview(var_eq_matching) diff_to_var = invview(var_to_diff) - dx_sub = Dict() + total_sub = Dict() if ModelingToolkit.has_iv(sys) iv = get_iv(sys) if is_only_discrete(structure) D = Shift(iv, 1) - lower_name = lower_varname_withshift - total_sub = shift_sub else D = Differential(iv) - lower_name = lower_varname_with_unit - total_sub = dx_sub end else iv = D = nothing - lower_name = lower_varname_with_unit end # if var is like D(x) or Shift(t, 1)(x) @@ -544,14 +480,14 @@ function solve_and_generate_equations!(state::TearingState, neweqs, var_eq_match (eq, iv) -> eq isa Int && iv isa Int && BipartiteEdge(eq, iv) in solvable_graph end - diffeq_idxs = Int[] - algeeq_idxs = Int[] diff_eqs = Equation[] - alge_eqs = Equation[] + diffeq_idxs = Int[] diff_vars = Int[] - subeqs = Equation[] - solved_equations = Int[] - solved_variables = Int[] + alge_eqs = Equation[] + algeeq_idxs = Int[] + solved_eqs = Equation[] + solvedeq_idxs = Int[] + solved_vars = Int[] toporder = topological_sort(DiCMOBiGraph{false}(graph, var_eq_matching)) eqs = Iterators.reverse(toporder) @@ -561,100 +497,126 @@ function solve_and_generate_equations!(state::TearingState, neweqs, var_eq_match # fullvars[iv] is a differential variable of the form D^n(x), and neweqs[ieq] # is solved to give the RHS. for ieq in eqs - println() iv = eq_var_matching[ieq] if is_solvable(ieq, iv) - # We don't solve differential equations, but we will need to try to - # convert it into the mass matrix form. - # We cannot solve the differential variable like D(x) if isdervar(iv) isnothing(D) && error("Differential found in a non-differential system. Likely this is a bug in the construction of an initialization system. Please report this issue with a reproducible example. Offending equation: $(equations(sys)[ieq])") - order, lv = var_order(diff_to_var, iv) - dx = D(simplify_shifts(fullvars[lv])) - eq = dx ~ simplify_shifts(Symbolics.fixpoint_sub( - Symbolics.symbolic_linear_solve(neweqs[ieq], - fullvars[iv]), - total_sub; operator = ModelingToolkit.Shift)) - for e in 𝑑neighbors(graph, iv) - rem_edge!(graph, e, iv) - add_edge!(graph, e, lv) - end - push!(diff_eqs, eq) - total_sub[simplify_shifts(eq.lhs)] = eq.rhs - dx_sub[simplify_shifts(eq.lhs)] = eq.rhs - push!(diffeq_idxs, ieq) - push!(diff_vars, diff_to_var[iv]) - continue - end - eq = neweqs[ieq] - var = fullvars[iv] - residual = eq.lhs - eq.rhs - a, b, islinear = linear_expansion(residual, var) - @assert islinear - # 0 ~ a * var + b - # var ~ -b/a - if ModelingToolkit._iszero(a) - @warn "Tearing: solving $eq for $var is singular!" + add_differential_equation!(structure, iv, neweqs, ieq, + diff_vars, diff_eqs, diffeq_idxs, total_sub) else - rhs = -b / a - neweq = var ~ simplify_shifts(Symbolics.fixpoint_sub( - simplify ? - Symbolics.simplify(rhs) : rhs, - dx_sub; operator = ModelingToolkit.Shift)) - push!(subeqs, neweq) - push!(solved_equations, ieq) - push!(solved_variables, iv) + add_solved_equation!(structure, iv, neweqs, ieq, + solved_vars, solved_eqs, solvedeq_idxs, total_sub) end else - eq = neweqs[ieq] - rhs = eq.rhs - if !(eq.lhs isa Number && eq.lhs == 0) - rhs = eq.rhs - eq.lhs - end - push!(alge_eqs, 0 ~ simplify_shifts(Symbolics.fixpoint_sub(rhs, total_sub))) - push!(algeeq_idxs, ieq) + add_algebraic_equation!(structure, neweqs, ieq, + alge_eqs, algeeq_idxs, total_sub) end end - @show neweqs - @show subeqs - # TODO: BLT sorting + # Generate new equations and orderings neweqs = [diff_eqs; alge_eqs] - inveqsperm = [diffeq_idxs; algeeq_idxs] - eqsperm = zeros(Int, nsrcs(graph)) - for (i, v) in enumerate(inveqsperm) - eqsperm[v] = i - end + eq_ordering = [diffeq_idxs; algeeq_idxs] diff_vars_set = BitSet(diff_vars) if length(diff_vars_set) != length(diff_vars) error("Tearing internal error: lowering DAE into semi-implicit ODE failed!") end - solved_variables_set = BitSet(solved_variables) - invvarsperm = [diff_vars; + solved_vars_set = BitSet(solved_vars) + var_ordering = [diff_vars; setdiff!(setdiff(1:ndsts(graph), diff_vars_set), - solved_variables_set)] + solved_vars_set)] + + return neweqs, solved_eqs, eq_ordering, var_ordering, length(solved_vars), length(solved_vars_set) +end + +function add_differential_equation!(s::SystemStructure, iv, neweqs, ieq, diff_vars, diff_eqs, diffeqs_idxs, total_sub) + diff_to_var = invview(s.var_to_diff) + + order, lv = var_order(diff_to_var, iv) + dx = D(simplify_shifts(fullvars[lv])) + eq = dx ~ simplify_shifts(Symbolics.fixpoint_sub( + Symbolics.symbolic_linear_solve(neweqs[ieq], + fullvars[iv]), + total_sub; operator = ModelingToolkit.Shift)) + for e in 𝑑neighbors(s.graph, iv) + e == ieq && continue + rem_edge!(s.graph, e, iv) + end + + push!(diff_eqs, eq) + total_sub[simplify_shifts(eq.lhs)] = eq.rhs + push!(diffeq_idxs, ieq) + push!(diff_vars, diff_to_var[iv]) +end + +function add_algebraic_equation!(s::SystemStructure, neweqs, ieq, alge_eqs, algeeq_idxs, total_sub) + eq = neweqs[ieq] + rhs = eq.rhs + if !(eq.lhs isa Number && eq.lhs == 0) + rhs = eq.rhs - eq.lhs + end + push!(alge_eqs, 0 ~ simplify_shifts(Symbolics.fixpoint_sub(rhs, total_sub))) + push!(algeeq_idxs, ieq) +end + +function add_solved_equation!(s::SystemStructure, iv, neweqs, ieq, solved_vars, solved_eqs, solvedeq_idxs, total_sub) + eq = neweqs[ieq] + var = fullvars[iv] + residual = eq.lhs - eq.rhs + a, b, islinear = linear_expansion(residual, var) + @assert islinear + # 0 ~ a * var + b + # var ~ -b/a + if ModelingToolkit._iszero(a) + @warn "Tearing: solving $eq for $var is singular!" + else + rhs = -b / a + neweq = var ~ simplify_shifts(Symbolics.fixpoint_sub( + simplify ? + Symbolics.simplify(rhs) : rhs, + total_sub; operator = ModelingToolkit.Shift)) + push!(solved_eqs, neweq) + push!(solvedeq_idxs, ieq) + push!(solved_vars, iv) + end +end + +""" +Reorder the equations and unknowns to be: + [diffeqs; ...] + [diffvars; ...] +such that the mass matrix is: + [I 0 + 0 0]. + +Update the state to account for the new ordering and equations. +""" +# TODO: BLT sorting +function reorder_vars!(s::SystemStructure, var_eq_matching, eq_ordering, var_ordering, nelim_eq, nelim_var) + @unpack solvable_graph, var_to_diff, eq_to_diff, graph = structure + + eqsperm = zeros(Int, nsrcs(graph)) + for (i, v) in enumerate(eq_ordering) + eqsperm[v] = i + end varsperm = zeros(Int, ndsts(graph)) - for (i, v) in enumerate(invvarsperm) + for (i, v) in enumerate(var_ordering) varsperm[v] = i end - deps = Vector{Int}[i == 1 ? Int[] : collect(1:(i - 1)) - for i in 1:length(solved_equations)] - # Contract the vertices in the structure graph to make the structure match # the new reality of the system we've just created. - graph = contract_variables(graph, var_eq_matching, varsperm, eqsperm, - length(solved_variables), length(solved_variables_set)) + new_graph = contract_variables(graph, var_eq_matching, varsperm, eqsperm, + nelim_eq, nelim_var) - new_var_to_diff = complete(DiffGraph(length(invvarsperm))) + new_var_to_diff = complete(DiffGraph(length(var_ordering))) for (v, d) in enumerate(var_to_diff) v′ = varsperm[v] (v′ > 0 && d !== nothing) || continue d′ = varsperm[d] new_var_to_diff[v′] = d′ > 0 ? d′ : nothing end - new_eq_to_diff = complete(DiffGraph(length(inveqsperm))) + new_eq_to_diff = complete(DiffGraph(length(eq_ordering))) for (v, d) in enumerate(eq_to_diff) v′ = eqsperm[v] (v′ > 0 && d !== nothing) || continue @@ -663,7 +625,53 @@ function solve_and_generate_equations!(state::TearingState, neweqs, var_eq_match end new_fullvars = fullvars[invvarsperm] - new_fullvars, new_var_to_diff, new_eq_to_diff, neweqs, subeqs, graph + # Update system structure + @set! state.structure.graph = complete(new_graph) + @set! state.structure.var_to_diff = new_var_to_diff + @set! state.structure.eq_to_diff = new_eq_to_diff + @set! state.fullvars = new_fullvars +end + +""" +Set the system equations, unknowns, observables post-tearing. +""" +function update_simplified_system!(state::TearingState, neweqs, solved_eqs, dummy_sub, var_eq_matching, extra_unknowns) + @unpack solvable_graph, var_to_diff, eq_to_diff, graph = state.structure + diff_to_var = invview(var_to_diff) + + ispresent = let var_to_diff = var_to_diff, graph = graph + i -> (!isempty(𝑑neighbors(graph, i)) || + (var_to_diff[i] !== nothing && !isempty(𝑑neighbors(graph, var_to_diff[i])))) + end + + sys = state.sys + obs_sub = dummy_sub + for eq in neweqs + isdiffeq(eq) || continue + obs_sub[eq.lhs] = eq.rhs + end + # TODO: compute the dependency correctly so that we don't have to do this + obs = [fast_substitute(observed(sys), obs_sub); solved_eqs] + + unknowns = Any[v + for (i, v) in enumerate(state.fullvars) + if diff_to_var[i] === nothing && ispresent(i)] + unknowns = [unknowns; extra_unknowns] + @set! sys.unknowns = unknowns + + obs, subeqs, deps = cse_and_array_hacks( + sys, obs, solved_eqs, unknowns, neweqs; cse = cse_hack, array = array_hack) + + @set! sys.eqs = neweqs + @set! sys.observed = obs + @set! sys.substitutions = Substitutions(subeqs, deps) + + # Only makes sense for time-dependent + # TODO: generalize to SDE + if sys isa ODESystem + @set! sys.schedule = Schedule(var_eq_matching, dummy_sub) + end + sys = schedule(sys) end # Terminology and Definition: @@ -675,10 +683,8 @@ end # appear in the system. Algebraic variables are variables that are not # differential variables. -import ModelingToolkit: Shift - # Give the order of the variable indexed by dv -function var_order(diff_to_var, dv) +function var_order(diff_to_var, dv) order = 0 while (dv′ = diff_to_var[dv]) !== nothing order += 1 @@ -689,8 +695,7 @@ end function tearing_reassemble(state::TearingState, var_eq_matching, full_var_eq_matching = nothing; simplify = false, mm = nothing, cse_hack = true, array_hack = true) - @unpack fullvars, sys, structure = state - @unpack solvable_graph, var_to_diff, eq_to_diff, graph, lowest_shift = structure + extra_vars = Int[] if full_var_eq_matching !== nothing for v in 𝑑vertices(state.structure.graph) @@ -699,69 +704,21 @@ function tearing_reassemble(state::TearingState, var_eq_matching, push!(extra_vars, v) end end + extra_unknowns = fullvars[extra_vars] neweqs = collect(equations(state)) - diff_to_var = invview(var_to_diff) - is_discrete = is_only_discrete(state.structure) - - shift_sub = Dict() # Structural simplification dummy_sub = Dict() - substitute_dummy_derivatives!(state, neweqs, dummy_sub, var_eq_matching) - - generate_derivative_variables!(state, neweqs, var_eq_matching; - is_discrete, mm, shift_sub) - - new_fullvars, new_var_to_diff, new_eq_to_diff, neweqs, subeqs, graph = - solve_and_generate_equations!(state, neweqs, var_eq_matching; simplify, shift_sub) - - # Update system - var_to_diff = new_var_to_diff - eq_to_diff = new_eq_to_diff - diff_to_var = invview(var_to_diff) - - old_fullvars = fullvars - @set! state.structure.graph = complete(graph) - @set! state.structure.var_to_diff = var_to_diff - @set! state.structure.eq_to_diff = eq_to_diff - @set! state.fullvars = fullvars = new_fullvars - ispresent = let var_to_diff = var_to_diff, graph = graph - i -> (!isempty(𝑑neighbors(graph, i)) || - (var_to_diff[i] !== nothing && !isempty(𝑑neighbors(graph, var_to_diff[i])))) - end - - sys = state.sys - obs_sub = dummy_sub - for eq in neweqs - isdiffeq(eq) || continue - obs_sub[eq.lhs] = eq.rhs - end - # TODO: compute the dependency correctly so that we don't have to do this - obs = [fast_substitute(observed(sys), obs_sub); subeqs] + substitute_derivatives_algevars!(state, neweqs, dummy_sub, var_eq_matching) - unknowns = Any[v - for (i, v) in enumerate(fullvars) - if diff_to_var[i] === nothing && ispresent(i)] - if !isempty(extra_vars) - for v in extra_vars - push!(unknowns, old_fullvars[v]) - end - end - @set! sys.unknowns = unknowns + generate_derivative_variables!(state, neweqs, var_eq_matching; mm) - obs, subeqs, deps = cse_and_array_hacks( - sys, obs, subeqs, unknowns, neweqs; cse = cse_hack, array = array_hack) + neweqs, solved_eqs, eq_ordering, var_ordering, nelim_eq, nelim_var = + generate_system_equations!(state, neweqs, var_eq_matching; simplify) - @set! sys.eqs = neweqs - @set! sys.observed = obs - @set! sys.substitutions = Substitutions(subeqs, deps) + reorder_vars!(state.structure, var_eq_matching, eq_ordering, var_ordering, nelim_eq, nelim_var) - # Only makes sense for time-dependent - # TODO: generalize to SDE - if sys isa ODESystem - @set! sys.schedule = Schedule(var_eq_matching, dummy_sub) - end - sys = schedule(sys) + sys = update_simplified_system!(state, neweqs, solved_eqs, dummy_sub, var_eq_matching, extra_unknowns) @set! state.sys = sys @set! sys.tearing_state = state return invalidate_cache!(sys) diff --git a/src/structural_transformation/utils.jl b/src/structural_transformation/utils.jl index b947fa4269..77f0b62a1c 100644 --- a/src/structural_transformation/utils.jl +++ b/src/structural_transformation/utils.jl @@ -452,9 +452,13 @@ end ### Misc ### -function lower_varname_withshift(var, iv, backshift; unshifted = nothing, allow_zero = true) - backshift <= 0 && return Shift(iv, -backshift)(unshifted, allow_zero) - ds = backshift > 0 ? "$iv-$backshift" : "$iv+$(-backshift)" +function lower_varname_withshift(var, iv) + op = operation(var) + op isa Shift || return var + backshift = op.steps + backshift > 0 && return var + + ds = "$iv-$(-backshift)" d_separator = 'ˍ' if ModelingToolkit.isoperator(var, ModelingToolkit.Shift) @@ -477,15 +481,9 @@ function isdoubleshift(var) ModelingToolkit.isoperator(arguments(var)[1], ModelingToolkit.Shift) end -### Rules -# 1. x(t) -> x(t) -# 2. Shift(t, 0)(x(t)) -> x(t) -# 3. Shift(t, 3)(Shift(t, 2)(x(t)) -> Shift(t, 5)(x(t)) - function simplify_shifts(var) ModelingToolkit.hasshift(var) || return var var isa Equation && return simplify_shifts(var.lhs) ~ simplify_shifts(var.rhs) - ((op = operation(var)) isa Shift) && op.steps == 0 && return simplify_shifts(arguments(var)[1]) if isdoubleshift(var) op1 = operation(var) vv1 = arguments(var)[1] @@ -501,58 +499,3 @@ function simplify_shifts(var) unwrap(var).metadata) end end - -""" -Power expand the shifts. Used for substitution. - -Shift(t, -3)(x(t)) -> Shift(t, -1)(Shift(t, -1)(Shift(t, -1)(x))) -""" -function expand_shifts(var) - ModelingToolkit.hasshift(var) || return var - var = ModelingToolkit.value(var) - - var isa Equation && return expand_shifts(var.lhs) ~ expand_shifts(var.rhs) - op = operation(var) - s = sign(op.steps) - arg = only(arguments(var)) - - if ModelingToolkit.isvariable(arg) && (ModelingToolkit.getvariabletype(arg) === VARIABLE) && isequal(op.t, only(arguments(arg))) - out = arg - for i in 1:op.steps - out = Shift(op.t, s)(out) - end - return out - elseif iscall(arg) - return maketerm(typeof(var), operation(var), expand_shifts.(arguments(var)), - unwrap(var).metadata) - else - return arg - end -end - -""" -Shift(t, 1)(x + z) -> Shift(t, 1)(x) + Shift(t, 1)(z) -""" -function distribute_shift(var) - ModelingToolkit.hasshift(var) || return var - var isa Equation && return distribute_shift(var.lhs) ~ distribute_shift(var.rhs) - shift = operation(var) - expr = only(arguments(var)) - _distribute_shift(expr, shift) -end - -function _distribute_shift(expr, shift) - op = operation(expr) - args = arguments(expr) - - if length(args) == 1 - if ModelingToolkit.isvariable(only(args)) && isequal(op.t, only(args)) - return shift(only(args)) - else - return only(args) - end - else iscall(op) - return maketerm(typeof(expr), operation(expr), _distribute_shift.(args, shift), - unwrap(var).metadata) - end -end diff --git a/src/systems/systemstructure.jl b/src/systems/systemstructure.jl index 89fcebf549..e5a227d9fb 100644 --- a/src/systems/systemstructure.jl +++ b/src/systems/systemstructure.jl @@ -154,7 +154,6 @@ Base.@kwdef mutable struct SystemStructure var_types::Union{Vector{VariableType}, Nothing} """Whether the system is discrete.""" only_discrete::Bool - lowest_shift::Union{Dict, Nothing} end function Base.copy(structure::SystemStructure) @@ -436,7 +435,7 @@ function TearingState(sys; quick_cancel = false, check = true) ts = TearingState(sys, fullvars, SystemStructure(complete(var_to_diff), complete(eq_to_diff), - complete(graph), nothing, var_types, sys isa DiscreteSystem, lowest_shift), + complete(graph), nothing, var_types, sys isa DiscreteSystem), Any[]) if sys isa DiscreteSystem ts = shift_discrete_system(ts, lowest_shift) @@ -466,9 +465,8 @@ end Shift variable x by the largest shift s such that x(k-s) appears in the system of equations. The lowest-shift term will have. """ -function shift_discrete_system(ts::TearingState, lowest_shift) +function shift_discrete_system(ts::TearingState) @unpack fullvars, sys = ts - return ts discvars = OrderedSet() eqs = equations(sys) From 240ab215b2ec457186b889ca53160e9d23db02e8 Mon Sep 17 00:00:00 2001 From: vyudu Date: Mon, 10 Feb 2025 18:38:22 -0500 Subject: [PATCH 0801/1337] fix: properly rename variables inside generate_system_equations --- .../symbolics_tearing.jl | 174 ++++++++++-------- src/structural_transformation/utils.jl | 14 +- src/systems/systemstructure.jl | 16 +- 3 files changed, 119 insertions(+), 85 deletions(-) diff --git a/src/structural_transformation/symbolics_tearing.jl b/src/structural_transformation/symbolics_tearing.jl index b99e6ec21a..74205fcf86 100644 --- a/src/structural_transformation/symbolics_tearing.jl +++ b/src/structural_transformation/symbolics_tearing.jl @@ -248,7 +248,7 @@ called dummy derivatives. State selection is done. All non-differentiated variables are algebraic variables, and all variables that appear differentiated are differential variables. """ -function substitute_derivatives_algevars!(ts::TearingState, neweqs, dummy_sub, var_eq_matching) +function substitute_derivatives_algevars!(ts::TearingState, neweqs, var_eq_matching, dummy_sub) @unpack fullvars, sys, structure = ts @unpack solvable_graph, var_to_diff, eq_to_diff, graph = structure diff_to_var = invview(var_to_diff) @@ -353,30 +353,32 @@ Effects on the system structure: - solvable_graph: - var_eq_matching: match D(x) to the added identity equation """ -function generate_derivative_variables!(ts::TearingState, neweqs, var_eq_matching; - is_discrete = false, mm = nothing) +function generate_derivative_variables!(ts::TearingState, neweqs, var_eq_matching; mm = nothing) @unpack fullvars, sys, structure = ts @unpack solvable_graph, var_to_diff, eq_to_diff, graph = structure eq_var_matching = invview(var_eq_matching) diff_to_var = invview(var_to_diff) iv = ModelingToolkit.has_iv(sys) ? ModelingToolkit.get_iv(sys) : nothing - lower_name = is_discrete ? lower_varname_withshift : lower_varname_with_unit - + is_discrete = is_only_discrete(structure) + lower_varname = is_discrete ? lower_shift_varname : lower_varname_with_unit linear_eqs = mm === nothing ? Dict{Int, Int}() : Dict(reverse(en) for en in enumerate(mm.nzrows)) - # Generate new derivative variables for all unsolved variables that have a derivative in the system + # For variable x, make dummy derivative x_t if the + # derivative is in the system for v in 1:length(var_to_diff) - # Check if a derivative 1) exists and 2) is unsolved for dv = var_to_diff[v] + # For discrete systems, directly substitute lowest-order variable + if is_discrete && diff_to_var[v] == nothing + fullvars[v] = lower_varname(fullvars[v], iv) + end dv isa Int || continue solved = var_eq_matching[dv] isa Int solved && continue # If there's `D(x) = x_t` already, update mappings and continue without # adding new equations/variables - dd = find_duplicate_dd(dv, lineareqs, mm) - + dd = find_duplicate_dd(dv, solvable_graph, linear_eqs, mm) if !isnothing(dd) dummy_eq, v_t = dd var_to_diff[v_t] = var_to_diff[dv] @@ -386,26 +388,25 @@ function generate_derivative_variables!(ts::TearingState, neweqs, var_eq_matchin end dx = fullvars[dv] - order, lv = var_order(diff_to_var, dv) - x_t = is_discrete ? lower_name(fullvars[lv], iv) - : lower_name(fullvars[lv], iv, order) - + order, lv = var_order(dv, diff_to_var) + x_t = is_discrete ? lower_varname(fullvars[dv], iv) : lower_varname(fullvars[lv], iv, order) + # Add `x_t` to the graph - add_dd_variable!(structure, x_t, dv) + v_t = add_dd_variable!(structure, fullvars, x_t, dv) # Add `D(x) - x_t ~ 0` to the graph - add_dd_equation!(structure, neweqs, 0 ~ dx - x_t, dv) + dummy_eq = add_dd_equation!(structure, neweqs, 0 ~ dx - x_t, dv, v_t) # Update matching push!(var_eq_matching, unassigned) var_eq_matching[dv] = unassigned - eq_var_matching[dummy_eq] = dv + eq_var_matching[dummy_eq] = dv end end """ -Check if there's `D(x) = x_t` already. +Check if there's `D(x) = x_t` already. """ -function find_duplicate_dd(dv, lineareqs, mm) +function find_duplicate_dd(dv, solvable_graph, linear_eqs, mm) for eq in 𝑑neighbors(solvable_graph, dv) mi = get(linear_eqs, eq, 0) iszero(mi) && continue @@ -424,28 +425,28 @@ function find_duplicate_dd(dv, lineareqs, mm) return nothing end -function add_dd_variable!(s::SystemStructure, x_t, dv) - push!(s.fullvars, simplify_shifts(x_t)) +function add_dd_variable!(s::SystemStructure, fullvars, x_t, dv) + push!(fullvars, simplify_shifts(x_t)) + v_t = length(fullvars) v_t_idx = add_vertex!(s.var_to_diff) - @assert v_t_idx == ndsts(graph) == ndsts(solvable_graph) == length(fullvars) == - length(var_eq_matching) add_vertex!(s.graph, DST) # TODO: do we care about solvable_graph? We don't use them after # `dummy_derivative_graph`. add_vertex!(s.solvable_graph, DST) - var_to_diff[v_t] = var_to_diff[dv] + s.var_to_diff[v_t] = s.var_to_diff[dv] + v_t end # dv = index of D(x), v_t = index of x_t -function add_dd_equation!(s::SystemStructure, neweqs, eq, dv) +function add_dd_equation!(s::SystemStructure, neweqs, eq, dv, v_t) push!(neweqs, eq) add_vertex!(s.graph, SRC) - v_t = length(s.fullvars) dummy_eq = length(neweqs) add_edge!(s.graph, dummy_eq, dv) add_edge!(s.graph, dummy_eq, v_t) add_vertex!(s.solvable_graph, SRC) add_edge!(s.solvable_graph, dummy_eq, dv) + dummy_eq end """ @@ -463,6 +464,10 @@ function generate_system_equations!(state::TearingState, neweqs, var_eq_matching iv = get_iv(sys) if is_only_discrete(structure) D = Shift(iv, 1) + for v in fullvars + op = operation(v) + op isa Shift && (op.steps < 0) && (total_sub[v] = lower_shift_varname(v, iv)) + end else D = Differential(iv) end @@ -493,24 +498,40 @@ function generate_system_equations!(state::TearingState, neweqs, var_eq_matching eqs = Iterators.reverse(toporder) idep = iv - # Generate differential equations. - # fullvars[iv] is a differential variable of the form D^n(x), and neweqs[ieq] - # is solved to give the RHS. + # Generate equations. + # Solvable equations of differential variables D(x) become differential equations + # Solvable equations of non-differential variables become observable equations + # Non-solvable equations become algebraic equations. for ieq in eqs iv = eq_var_matching[ieq] - if is_solvable(ieq, iv) - if isdervar(iv) - isnothing(D) && - error("Differential found in a non-differential system. Likely this is a bug in the construction of an initialization system. Please report this issue with a reproducible example. Offending equation: $(equations(sys)[ieq])") - add_differential_equation!(structure, iv, neweqs, ieq, - diff_vars, diff_eqs, diffeq_idxs, total_sub) - else - add_solved_equation!(structure, iv, neweqs, ieq, - solved_vars, solved_eqs, solvedeq_idxs, total_sub) + var = fullvars[iv] + eq = neweqs[ieq] + + if is_solvable(ieq, iv) && isdervar(iv) + isnothing(D) && throw(UnexpectedDifferentialError(equations(sys)[ieq])) + order, lv = var_order(iv, diff_to_var) + dx = D(simplify_shifts(fullvars[lv])) + + neweq = make_differential_equation(var, dx, eq, total_sub) + for e in 𝑑neighbors(graph, iv) + e == ieq && continue + rem_edge!(graph, e, iv) + end + + push!(diff_eqs, neweq) + push!(diffeq_idxs, ieq) + push!(diff_vars, diff_to_var[iv]) + elseif is_solvable(ieq, iv) + neweq = make_solved_equation(var, eq, total_sub; simplify) + !isnothing(neweq) && begin + push!(solved_eqs, neweq) + push!(solvedeq_idxs, ieq) + push!(solved_vars, iv) end else - add_algebraic_equation!(structure, neweqs, ieq, - alge_eqs, algeeq_idxs, total_sub) + neweq = make_algebraic_equation(var, eq, total_sub) + push!(alge_eqs, neweq) + push!(algeeq_idxs, ieq) end end @@ -529,39 +550,29 @@ function generate_system_equations!(state::TearingState, neweqs, var_eq_matching return neweqs, solved_eqs, eq_ordering, var_ordering, length(solved_vars), length(solved_vars_set) end -function add_differential_equation!(s::SystemStructure, iv, neweqs, ieq, diff_vars, diff_eqs, diffeqs_idxs, total_sub) - diff_to_var = invview(s.var_to_diff) +struct UnexpectedDifferentialError + eq::Equation +end - order, lv = var_order(diff_to_var, iv) - dx = D(simplify_shifts(fullvars[lv])) - eq = dx ~ simplify_shifts(Symbolics.fixpoint_sub( - Symbolics.symbolic_linear_solve(neweqs[ieq], - fullvars[iv]), - total_sub; operator = ModelingToolkit.Shift)) - for e in 𝑑neighbors(s.graph, iv) - e == ieq && continue - rem_edge!(s.graph, e, iv) - end +function Base.showerror(io::IO, err::UnexpectedDifferentialError) + error("Differential found in a non-differential system. Likely this is a bug in the construction of an initialization system. Please report this issue with a reproducible example. Offending equation: $(err.eq)") +end - push!(diff_eqs, eq) - total_sub[simplify_shifts(eq.lhs)] = eq.rhs - push!(diffeq_idxs, ieq) - push!(diff_vars, diff_to_var[iv]) +function make_differential_equation(var, dx, eq, total_sub) + dx ~ simplify_shifts(Symbolics.fixpoint_sub( + Symbolics.symbolic_linear_solve(eq, var), + total_sub; operator = ModelingToolkit.Shift)) end -function add_algebraic_equation!(s::SystemStructure, neweqs, ieq, alge_eqs, algeeq_idxs, total_sub) - eq = neweqs[ieq] +function make_algebraic_equation(var, eq, total_sub) rhs = eq.rhs if !(eq.lhs isa Number && eq.lhs == 0) rhs = eq.rhs - eq.lhs end - push!(alge_eqs, 0 ~ simplify_shifts(Symbolics.fixpoint_sub(rhs, total_sub))) - push!(algeeq_idxs, ieq) + 0 ~ simplify_shifts(Symbolics.fixpoint_sub(rhs, total_sub)) end -function add_solved_equation!(s::SystemStructure, iv, neweqs, ieq, solved_vars, solved_eqs, solvedeq_idxs, total_sub) - eq = neweqs[ieq] - var = fullvars[iv] +function make_solved_equation(var, eq, total_sub; simplify = false) residual = eq.lhs - eq.rhs a, b, islinear = linear_expansion(residual, var) @assert islinear @@ -569,15 +580,13 @@ function add_solved_equation!(s::SystemStructure, iv, neweqs, ieq, solved_vars, # var ~ -b/a if ModelingToolkit._iszero(a) @warn "Tearing: solving $eq for $var is singular!" + return nothing else rhs = -b / a - neweq = var ~ simplify_shifts(Symbolics.fixpoint_sub( + return var ~ simplify_shifts(Symbolics.fixpoint_sub( simplify ? Symbolics.simplify(rhs) : rhs, total_sub; operator = ModelingToolkit.Shift)) - push!(solved_eqs, neweq) - push!(solvedeq_idxs, ieq) - push!(solved_vars, iv) end end @@ -592,8 +601,8 @@ such that the mass matrix is: Update the state to account for the new ordering and equations. """ # TODO: BLT sorting -function reorder_vars!(s::SystemStructure, var_eq_matching, eq_ordering, var_ordering, nelim_eq, nelim_var) - @unpack solvable_graph, var_to_diff, eq_to_diff, graph = structure +function reorder_vars!(state::TearingState, var_eq_matching, eq_ordering, var_ordering, nelim_eq, nelim_var) + @unpack solvable_graph, var_to_diff, eq_to_diff, graph = state.structure eqsperm = zeros(Int, nsrcs(graph)) for (i, v) in enumerate(eq_ordering) @@ -623,20 +632,26 @@ function reorder_vars!(s::SystemStructure, var_eq_matching, eq_ordering, var_ord d′ = eqsperm[d] new_eq_to_diff[v′] = d′ > 0 ? d′ : nothing end - new_fullvars = fullvars[invvarsperm] + new_fullvars = state.fullvars[var_ordering] + @show new_graph + @show new_var_to_diff # Update system structure @set! state.structure.graph = complete(new_graph) @set! state.structure.var_to_diff = new_var_to_diff @set! state.structure.eq_to_diff = new_eq_to_diff @set! state.fullvars = new_fullvars + state end """ Set the system equations, unknowns, observables post-tearing. """ -function update_simplified_system!(state::TearingState, neweqs, solved_eqs, dummy_sub, var_eq_matching, extra_unknowns) +function update_simplified_system!(state::TearingState, neweqs, solved_eqs, dummy_sub, var_eq_matching, extra_unknowns; + cse_hack = true, array_hack = true) @unpack solvable_graph, var_to_diff, eq_to_diff, graph = state.structure + @show graph + @show var_to_diff diff_to_var = invview(var_to_diff) ispresent = let var_to_diff = var_to_diff, graph = graph @@ -656,7 +671,12 @@ function update_simplified_system!(state::TearingState, neweqs, solved_eqs, dumm unknowns = Any[v for (i, v) in enumerate(state.fullvars) if diff_to_var[i] === nothing && ispresent(i)] + @show unknowns + @show state.fullvars + @show 𝑑neighbors(graph, 5) + @show neweqs unknowns = [unknowns; extra_unknowns] + @show unknowns @set! sys.unknowns = unknowns obs, subeqs, deps = cse_and_array_hacks( @@ -684,7 +704,7 @@ end # differential variables. # Give the order of the variable indexed by dv -function var_order(diff_to_var, dv) +function var_order(dv, diff_to_var) order = 0 while (dv′ = diff_to_var[dv]) !== nothing order += 1 @@ -695,7 +715,6 @@ end function tearing_reassemble(state::TearingState, var_eq_matching, full_var_eq_matching = nothing; simplify = false, mm = nothing, cse_hack = true, array_hack = true) - extra_vars = Int[] if full_var_eq_matching !== nothing for v in 𝑑vertices(state.structure.graph) @@ -704,21 +723,22 @@ function tearing_reassemble(state::TearingState, var_eq_matching, push!(extra_vars, v) end end - extra_unknowns = fullvars[extra_vars] + extra_unknowns = state.fullvars[extra_vars] neweqs = collect(equations(state)) + dummy_sub = Dict() # Structural simplification - dummy_sub = Dict() - substitute_derivatives_algevars!(state, neweqs, dummy_sub, var_eq_matching) + substitute_derivatives_algevars!(state, neweqs, var_eq_matching, dummy_sub) generate_derivative_variables!(state, neweqs, var_eq_matching; mm) neweqs, solved_eqs, eq_ordering, var_ordering, nelim_eq, nelim_var = generate_system_equations!(state, neweqs, var_eq_matching; simplify) - reorder_vars!(state.structure, var_eq_matching, eq_ordering, var_ordering, nelim_eq, nelim_var) + state = reorder_vars!(state, var_eq_matching, eq_ordering, var_ordering, nelim_eq, nelim_var) + + sys = update_simplified_system!(state, neweqs, solved_eqs, dummy_sub, var_eq_matching, extra_unknowns; cse_hack, array_hack) - sys = update_simplified_system!(state, neweqs, solved_eqs, dummy_sub, var_eq_matching, extra_unknowns) @set! state.sys = sys @set! sys.tearing_state = state return invalidate_cache!(sys) diff --git a/src/structural_transformation/utils.jl b/src/structural_transformation/utils.jl index 77f0b62a1c..fec57db080 100644 --- a/src/structural_transformation/utils.jl +++ b/src/structural_transformation/utils.jl @@ -452,9 +452,10 @@ end ### Misc ### -function lower_varname_withshift(var, iv) +# For discrete variables. Turn Shift(t, k)(x(t)) into xₜ₋ₖ(t) +function lower_shift_varname(var, iv) op = operation(var) - op isa Shift || return var + op isa Shift || return Shift(iv, 0)(var, true) # hack to prevent simplification of x(t) - x(t) backshift = op.steps backshift > 0 && return var @@ -476,6 +477,14 @@ function lower_varname_withshift(var, iv) return ModelingToolkit._with_unit(identity, newvar, iv) end +function lower_varname(var, iv, order; is_discrete = false) + if is_discrete + lower_shift_varname(var, iv) + else + lower_varname_with_unit(var, iv, order) + end +end + function isdoubleshift(var) return ModelingToolkit.isoperator(var, ModelingToolkit.Shift) && ModelingToolkit.isoperator(arguments(var)[1], ModelingToolkit.Shift) @@ -484,6 +493,7 @@ end function simplify_shifts(var) ModelingToolkit.hasshift(var) || return var var isa Equation && return simplify_shifts(var.lhs) ~ simplify_shifts(var.rhs) + (op = operation(var)) isa Shift && op.steps == 0 && return first(arguments(var)) if isdoubleshift(var) op1 = operation(var) vv1 = arguments(var)[1] diff --git a/src/systems/systemstructure.jl b/src/systems/systemstructure.jl index e5a227d9fb..ae4d21f225 100644 --- a/src/systems/systemstructure.jl +++ b/src/systems/systemstructure.jl @@ -142,15 +142,16 @@ has_equations(::TransformationState) = true Base.@kwdef mutable struct SystemStructure """Maps the (index of) a variable to the (index of) the variable describing its derivative.""" var_to_diff::DiffGraph - """Maps the (index of) a """ + """Maps the (index of) an equation.""" eq_to_diff::DiffGraph # Can be access as # `graph` to automatically look at the bipartite graph # or as `torn` to assert that tearing has run. - """Incidence graph of the system of equations. An edge from equation x to variable y exists if variable y appears in equation x.""" + """Graph that maps equations to variables that appear in them.""" graph::BipartiteGraph{Int, Nothing} - """.""" + """Graph that connects equations to the variable they will be solved for during simplification.""" solvable_graph::Union{BipartiteGraph{Int, Nothing}, Nothing} + """Variable types (brownian, variable, parameter) in the system.""" var_types::Union{Vector{VariableType}, Nothing} """Whether the system is discrete.""" only_discrete::Bool @@ -200,7 +201,9 @@ function complete!(s::SystemStructure) end mutable struct TearingState{T <: AbstractSystem} <: AbstractTearingState{T} + """The system of equations.""" sys::T + """The set of variables of the system.""" fullvars::Vector structure::SystemStructure extra_eqs::Vector @@ -438,7 +441,7 @@ function TearingState(sys; quick_cancel = false, check = true) complete(graph), nothing, var_types, sys isa DiscreteSystem), Any[]) if sys isa DiscreteSystem - ts = shift_discrete_system(ts, lowest_shift) + ts = shift_discrete_system(ts) end return ts end @@ -475,8 +478,9 @@ function shift_discrete_system(ts::TearingState) end iv = get_iv(sys) - discmap = Dict(k => StructuralTransformations.simplify_shifts(Shift(iv, 1)(k)) for k in discvars - if any(isequal(k), fullvars) && !isa(operation(k), Union{Sample, Hold})) + discmap = Dict(k => StructuralTransformations.simplify_shifts(Shift(iv, 1)(k)) + for k in discvars + if any(isequal(k), fullvars) && !isa(operation(k), Union{Sample, Hold})) for i in eachindex(fullvars) fullvars[i] = StructuralTransformations.simplify_shifts(fast_substitute( From 22a39bd41b2123c48a7f88016f25470041271b69 Mon Sep 17 00:00:00 2001 From: vyudu Date: Mon, 10 Feb 2025 18:42:01 -0500 Subject: [PATCH 0802/1337] delete comments --- src/structural_transformation/utils.jl | 3 --- src/systems/systems.jl | 8 ++++---- test/structural_transformation/utils.jl | 16 ---------------- 3 files changed, 4 insertions(+), 23 deletions(-) diff --git a/src/structural_transformation/utils.jl b/src/structural_transformation/utils.jl index fec57db080..757eb9a8db 100644 --- a/src/structural_transformation/utils.jl +++ b/src/structural_transformation/utils.jl @@ -271,8 +271,6 @@ end function find_solvables!(state::TearingState; kwargs...) @assert state.structure.solvable_graph === nothing - println("in find_solvables") - @show eqs eqs = equations(state) graph = state.structure.graph state.structure.solvable_graph = BipartiteGraph(nsrcs(graph), ndsts(graph)) @@ -280,7 +278,6 @@ function find_solvables!(state::TearingState; kwargs...) for ieq in 1:length(eqs) find_eq_solvables!(state, ieq, to_rm; kwargs...) end - @show eqs return nothing end diff --git a/src/systems/systems.jl b/src/systems/systems.jl index 0a7e4264e4..9c8c272c5c 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. This is \ - # not yet supported. - # """) + error(""" + Encountered algebraic equations when simplifying discrete system. This is \ + not yet supported. + """) end for pass in additional_passes newsys = pass(newsys) diff --git a/test/structural_transformation/utils.jl b/test/structural_transformation/utils.jl index 98d986ebd4..67f6016bc4 100644 --- a/test/structural_transformation/utils.jl +++ b/test/structural_transformation/utils.jl @@ -162,19 +162,3 @@ end structural_simplify(sys; additional_passes = [pass]) @test value[] == 1 end - -@testset "Shift simplification" begin - @variables x(t) y(t) z(t) - @parameters a b c - - # Expand shifts - @test isequal(ST.expand_shifts(Shift(t, -3)(x)), Shift(t, -1)(Shift(t, -1)(Shift(t, -1)(x)))) - expr = a * Shift(t, -2)(x) + Shift(t, 2)(y) + b - @test isequal(ST.expand_shifts(expr), - a * Shift(t, -1)(Shift(t, -1)(x)) + Shift(t, 1)(Shift(t, 1)(y)) + b) - @test isequal(ST.expand_shifts(Shift(t, 2)(Shift(t, 1)(a))), a) - - - # Distribute shifts - -end From 9aaf04a512853d5c9336674455ade5d92487c2f5 Mon Sep 17 00:00:00 2001 From: vyudu Date: Mon, 10 Feb 2025 18:50:23 -0500 Subject: [PATCH 0803/1337] more cleanup --- docs/src/systems/DiscreteSystem.md | 28 ------------------- .../symbolics_tearing.jl | 9 ------ src/systems/systemstructure.jl | 11 ++------ 3 files changed, 3 insertions(+), 45 deletions(-) delete mode 100644 docs/src/systems/DiscreteSystem.md diff --git a/docs/src/systems/DiscreteSystem.md b/docs/src/systems/DiscreteSystem.md deleted file mode 100644 index b6a8061e50..0000000000 --- a/docs/src/systems/DiscreteSystem.md +++ /dev/null @@ -1,28 +0,0 @@ -# DiscreteSystem - -## System Constructors - -```@docs -DiscreteSystem -``` - -## Composition and Accessor Functions - - - `get_eqs(sys)` or `equations(sys)`: The equations that define the discrete system. - - `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 - -```@docs; canonical=false -structural_simplify -``` - -## Problem Constructors - -```@docs; canonical=false -DiscreteProblem(sys::DiscreteSystem, u0map, tspan) -DiscreteFunction(sys::DiscreteSystem, u0map, tspan) -``` diff --git a/src/structural_transformation/symbolics_tearing.jl b/src/structural_transformation/symbolics_tearing.jl index 74205fcf86..eed7bc3e29 100644 --- a/src/structural_transformation/symbolics_tearing.jl +++ b/src/structural_transformation/symbolics_tearing.jl @@ -634,8 +634,6 @@ function reorder_vars!(state::TearingState, var_eq_matching, eq_ordering, var_or end new_fullvars = state.fullvars[var_ordering] - @show new_graph - @show new_var_to_diff # Update system structure @set! state.structure.graph = complete(new_graph) @set! state.structure.var_to_diff = new_var_to_diff @@ -650,8 +648,6 @@ Set the system equations, unknowns, observables post-tearing. function update_simplified_system!(state::TearingState, neweqs, solved_eqs, dummy_sub, var_eq_matching, extra_unknowns; cse_hack = true, array_hack = true) @unpack solvable_graph, var_to_diff, eq_to_diff, graph = state.structure - @show graph - @show var_to_diff diff_to_var = invview(var_to_diff) ispresent = let var_to_diff = var_to_diff, graph = graph @@ -671,12 +667,7 @@ function update_simplified_system!(state::TearingState, neweqs, solved_eqs, dumm unknowns = Any[v for (i, v) in enumerate(state.fullvars) if diff_to_var[i] === nothing && ispresent(i)] - @show unknowns - @show state.fullvars - @show 𝑑neighbors(graph, 5) - @show neweqs unknowns = [unknowns; extra_unknowns] - @show unknowns @set! sys.unknowns = unknowns obs, subeqs, deps = cse_and_array_hacks( diff --git a/src/systems/systemstructure.jl b/src/systems/systemstructure.jl index ae4d21f225..b98bb8c616 100644 --- a/src/systems/systemstructure.jl +++ b/src/systems/systemstructure.jl @@ -140,14 +140,14 @@ get_fullvars(ts::TransformationState) = ts.fullvars has_equations(::TransformationState) = true Base.@kwdef mutable struct SystemStructure - """Maps the (index of) a variable to the (index of) the variable describing its derivative.""" + """Maps the index of variable x to the index of variable D(x).""" var_to_diff::DiffGraph - """Maps the (index of) an equation.""" + """Maps the index of an equation.""" eq_to_diff::DiffGraph # Can be access as # `graph` to automatically look at the bipartite graph # or as `torn` to assert that tearing has run. - """Graph that maps equations to variables that appear in them.""" + """Graph that connects equations to variables that appear in them.""" graph::BipartiteGraph{Int, Nothing} """Graph that connects equations to the variable they will be solved for during simplification.""" solvable_graph::Union{BipartiteGraph{Int, Nothing}, Nothing} @@ -464,15 +464,10 @@ function lower_order_var(dervar, t) diffvar end -""" - Shift variable x by the largest shift s such that x(k-s) appears in the system of equations. - The lowest-shift term will have. -""" function shift_discrete_system(ts::TearingState) @unpack fullvars, sys = ts discvars = OrderedSet() eqs = equations(sys) - for eq in eqs vars!(discvars, eq; op = Union{Sample, Hold}) end From 66266e410c88a61a9c4e6af1c8c829433d5b4f76 Mon Sep 17 00:00:00 2001 From: vyudu Date: Mon, 10 Feb 2025 19:10:34 -0500 Subject: [PATCH 0804/1337] better comments --- src/structural_transformation/symbolics_tearing.jl | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/structural_transformation/symbolics_tearing.jl b/src/structural_transformation/symbolics_tearing.jl index eed7bc3e29..5707ac8386 100644 --- a/src/structural_transformation/symbolics_tearing.jl +++ b/src/structural_transformation/symbolics_tearing.jl @@ -337,11 +337,19 @@ variables and equations, don't add them when they already exist. Documenting the differences to structural simplification for discrete systems: -In discrete systems the lowest-order term is Shift(t, k)(x(t)), instead of x(t). -We want to substitute the k-1 lowest order terms instead of the k-1 highest order terms. +In discrete systems everything gets shifted forward a timestep by `shift_discrete_system` +in order to properly generate the difference equations. + +In the system x(k) ~ x(k-1) + x(k-2), becomes Shift(t, 1)(x(t)) ~ x(t) + Shift(t, -1)(x(t)) + +The lowest-order term is Shift(t, k)(x(t)), instead of x(t). +As such we actually want dummy variables for the k-1 lowest order terms instead of the k-1 highest order terms. -In the system x(k) ~ x(k-1) + x(k-2), we want to lower Shift(t, -1)(x(t)) -> x\_{t-1}(t) + +Since Shift(t, -1)(x) is not a derivative, it is directly substituted in `fullvars`. No equation or variable is added for it. + +For ODESystems D(D(D(x))) in equations is recursively substituted as D(x) ~ x_t, D(x_t) ~ x_tt, etc. The analogue for discrete systems, Shift(t, 1)(Shift(t,1)(Shift(t,1)(Shift(t, -3)(x(t))))) does not actually appear. So `total_sub` in generate_system_equations` is directly initialized with all of the lowered variables `Shift(t, -3)(x) -> x_t-3(t)`, etc. =# """ Generate new derivative variables for the system. From ac737c0e07a15d37753c4698bb46062b3a3f19a5 Mon Sep 17 00:00:00 2001 From: vyudu Date: Mon, 10 Feb 2025 19:21:56 -0500 Subject: [PATCH 0805/1337] init --- docs/src/systems/DiscreteSystem.md | 28 +++++++++++++++++++ .../discrete_system/discrete_system.jl | 13 +++++++++ 2 files changed, 41 insertions(+) create mode 100644 docs/src/systems/DiscreteSystem.md diff --git a/docs/src/systems/DiscreteSystem.md b/docs/src/systems/DiscreteSystem.md new file mode 100644 index 0000000000..dcb98ca1cf --- /dev/null +++ b/docs/src/systems/DiscreteSystem.md @@ -0,0 +1,28 @@ +# DiscreteSystem + +## System Constructors + +```@docs +DiscreteSystem +``` + +## Composition and Accessor Functions + +- `get_eqs(sys)` or `equations(sys)`: The equations that define the discrete system. +- `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 + +```@docs; canonical=false +structural_simplify +``` + +## Problem Constructors + +```@docs; canonical=false +DiscreteProblem(sys::DiscreteSystem, u0map, tspan) +DiscreteFunction(sys::DiscreteSystem, args...) +``` diff --git a/src/systems/discrete_system/discrete_system.jl b/src/systems/discrete_system/discrete_system.jl index bd81255706..cd6ff45b6c 100644 --- a/src/systems/discrete_system/discrete_system.jl +++ b/src/systems/discrete_system/discrete_system.jl @@ -323,6 +323,19 @@ end function SciMLBase.DiscreteFunction{false}(sys::DiscreteSystem, args...; kwargs...) DiscreteFunction{false, SciMLBase.FullSpecialize}(sys, args...; kwargs...) end + +""" +```julia +SciMLBase.DiscreteFunction{iip}(sys::DiscreteSystem, + dvs = unknowns(sys), + ps = parameters(sys); + kwargs...) where {iip} +``` + +Create an `DiscreteFunction` from the [`DiscreteSystem`](@ref). The arguments `dvs` and `ps` +are used to set the order of the dependent variable and parameter vectors, +respectively. +""" function SciMLBase.DiscreteFunction{iip, specialize}( sys::DiscreteSystem, dvs = unknowns(sys), From 992ec21e4f077f4e81837a6fdd3430aa16fd6add Mon Sep 17 00:00:00 2001 From: vyudu Date: Mon, 10 Feb 2025 19:24:42 -0500 Subject: [PATCH 0806/1337] up --- docs/pages.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/pages.jl b/docs/pages.jl index 82cc23d4a8..6e18f403dd 100644 --- a/docs/pages.jl +++ b/docs/pages.jl @@ -41,7 +41,8 @@ pages = [ "systems/JumpSystem.md", "systems/NonlinearSystem.md", "systems/OptimizationSystem.md", - "systems/PDESystem.md"], + "systems/PDESystem.md", + "systems/DiscreteSystem.md"], "comparison.md", "internals.md" ] From 1ab62463adb298386145f57933f4ccd4667d90d1 Mon Sep 17 00:00:00 2001 From: vyudu Date: Mon, 10 Feb 2025 19:30:56 -0500 Subject: [PATCH 0807/1337] fix unassigned indexing --- src/structural_transformation/symbolics_tearing.jl | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/structural_transformation/symbolics_tearing.jl b/src/structural_transformation/symbolics_tearing.jl index 5707ac8386..48f9141dbd 100644 --- a/src/structural_transformation/symbolics_tearing.jl +++ b/src/structural_transformation/symbolics_tearing.jl @@ -512,10 +512,10 @@ function generate_system_equations!(state::TearingState, neweqs, var_eq_matching # Non-solvable equations become algebraic equations. for ieq in eqs iv = eq_var_matching[ieq] - var = fullvars[iv] eq = neweqs[ieq] if is_solvable(ieq, iv) && isdervar(iv) + var = fullvars[iv] isnothing(D) && throw(UnexpectedDifferentialError(equations(sys)[ieq])) order, lv = var_order(iv, diff_to_var) dx = D(simplify_shifts(fullvars[lv])) @@ -530,6 +530,7 @@ function generate_system_equations!(state::TearingState, neweqs, var_eq_matching push!(diffeq_idxs, ieq) push!(diff_vars, diff_to_var[iv]) elseif is_solvable(ieq, iv) + var = fullvars[iv] neweq = make_solved_equation(var, eq, total_sub; simplify) !isnothing(neweq) && begin push!(solved_eqs, neweq) @@ -537,7 +538,7 @@ function generate_system_equations!(state::TearingState, neweqs, var_eq_matching push!(solved_vars, iv) end else - neweq = make_algebraic_equation(var, eq, total_sub) + neweq = make_algebraic_equation(eq, total_sub) push!(alge_eqs, neweq) push!(algeeq_idxs, ieq) end @@ -572,7 +573,7 @@ function make_differential_equation(var, dx, eq, total_sub) total_sub; operator = ModelingToolkit.Shift)) end -function make_algebraic_equation(var, eq, total_sub) +function make_algebraic_equation(eq, total_sub) rhs = eq.rhs if !(eq.lhs isa Number && eq.lhs == 0) rhs = eq.rhs - eq.lhs From 19f58341d16910a7367f6e2907d50b089a705dc4 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 4 Feb 2025 19:25:03 +0530 Subject: [PATCH 0808/1337] refactor: default `create_bindings` to false in `build_function_wrapper` --- src/systems/codegen_utils.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/systems/codegen_utils.jl b/src/systems/codegen_utils.jl index fd54ef01ca..bee7564a4d 100644 --- a/src/systems/codegen_utils.jl +++ b/src/systems/codegen_utils.jl @@ -121,7 +121,7 @@ function build_function_wrapper(sys::AbstractSystem, expr, args...; p_start = 2, p_end = is_time_dependent(sys) ? length(args) - 1 : length(args), wrap_delays = is_dde(sys), wrap_code = identity, add_observed = true, filter_observed = Returns(true), - create_bindings = true, output_type = nothing, mkarray = nothing, + create_bindings = false, output_type = nothing, mkarray = nothing, wrap_mtkparameters = true, extra_assignments = Assignment[], kwargs...) isscalar = !(expr isa AbstractArray || symbolic_type(expr) == ArraySymbolic()) # filter observed equations From 698ccc466bf88576545554902c99d4fd18e4b854 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 11 Feb 2025 12:12:17 +0530 Subject: [PATCH 0809/1337] build: bump Symbolics, SymbolicUtils compats --- Project.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Project.toml b/Project.toml index c069b49b5a..ac865063f2 100644 --- a/Project.toml +++ b/Project.toml @@ -146,8 +146,8 @@ StaticArrays = "0.10, 0.11, 0.12, 1.0" StochasticDiffEq = "6.72.1" StochasticDelayDiffEq = "1.8.1" SymbolicIndexingInterface = "0.3.37" -SymbolicUtils = "3.10.1" -Symbolics = "6.27" +SymbolicUtils = "3.14" +Symbolics = "6.29" URIs = "1" UnPack = "0.1, 1.0" Unitful = "1.1" From c352c09378d4608b927dd307fb04fdbacbdb4316 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 11 Feb 2025 12:13:00 +0530 Subject: [PATCH 0810/1337] refactor: format --- docs/src/systems/DiscreteSystem.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/src/systems/DiscreteSystem.md b/docs/src/systems/DiscreteSystem.md index dcb98ca1cf..5ede50c62a 100644 --- a/docs/src/systems/DiscreteSystem.md +++ b/docs/src/systems/DiscreteSystem.md @@ -6,20 +6,20 @@ DiscreteSystem ``` -## Composition and Accessor Functions - -- `get_eqs(sys)` or `equations(sys)`: The equations that define the discrete system. -- `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. - +## Composition and Accessor Functions + + - `get_eqs(sys)` or `equations(sys)`: The equations that define the discrete system. + - `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 ```@docs; canonical=false structural_simplify ``` - + ## Problem Constructors ```@docs; canonical=false From 079901bd481b8ea4f505761bce6aa476602dd023 Mon Sep 17 00:00:00 2001 From: vyudu Date: Tue, 11 Feb 2025 15:13:17 -0500 Subject: [PATCH 0811/1337] move D, iv into tearing_reassemble --- .../symbolics_tearing.jl | 43 +++++++++---------- 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/src/structural_transformation/symbolics_tearing.jl b/src/structural_transformation/symbolics_tearing.jl index 48f9141dbd..45430595b6 100644 --- a/src/structural_transformation/symbolics_tearing.jl +++ b/src/structural_transformation/symbolics_tearing.jl @@ -248,11 +248,10 @@ called dummy derivatives. State selection is done. All non-differentiated variables are algebraic variables, and all variables that appear differentiated are differential variables. """ -function substitute_derivatives_algevars!(ts::TearingState, neweqs, var_eq_matching, dummy_sub) +function substitute_derivatives_algevars!(ts::TearingState, neweqs, var_eq_matching, dummy_sub; iv = nothing, D = nothing) @unpack fullvars, sys, structure = ts @unpack solvable_graph, var_to_diff, eq_to_diff, graph = structure diff_to_var = invview(var_to_diff) - iv = ModelingToolkit.has_iv(sys) ? ModelingToolkit.get_iv(sys) : nothing for var in 1:length(fullvars) dv = var_to_diff[var] @@ -361,12 +360,11 @@ Effects on the system structure: - solvable_graph: - var_eq_matching: match D(x) to the added identity equation """ -function generate_derivative_variables!(ts::TearingState, neweqs, var_eq_matching; mm = nothing) +function generate_derivative_variables!(ts::TearingState, neweqs, var_eq_matching; mm = nothing, iv = nothing, D = nothing) @unpack fullvars, sys, structure = ts @unpack solvable_graph, var_to_diff, eq_to_diff, graph = structure eq_var_matching = invview(var_eq_matching) diff_to_var = invview(var_to_diff) - iv = ModelingToolkit.has_iv(sys) ? ModelingToolkit.get_iv(sys) : nothing is_discrete = is_only_discrete(structure) lower_varname = is_discrete ? lower_shift_varname : lower_varname_with_unit linear_eqs = mm === nothing ? Dict{Int, Int}() : @@ -461,27 +459,18 @@ end Solve the solvable equations of the system and generate differential (or discrete) equations in terms of the selected states. """ -function generate_system_equations!(state::TearingState, neweqs, var_eq_matching; simplify = false) +function generate_system_equations!(state::TearingState, neweqs, var_eq_matching; simplify = false, iv = nothing, D = nothing) @unpack fullvars, sys, structure = state @unpack solvable_graph, var_to_diff, eq_to_diff, graph = structure eq_var_matching = invview(var_eq_matching) diff_to_var = invview(var_to_diff) total_sub = Dict() - - if ModelingToolkit.has_iv(sys) - iv = get_iv(sys) - if is_only_discrete(structure) - D = Shift(iv, 1) - for v in fullvars - op = operation(v) - op isa Shift && (op.steps < 0) && (total_sub[v] = lower_shift_varname(v, iv)) - end - else - D = Differential(iv) + if is_only_discrete(structure) + for v in fullvars + op = operation(v) + op isa Shift && (op.steps < 0) && (total_sub[v] = lower_shift_varname(v, iv)) end - else - iv = D = nothing - end + end # if var is like D(x) or Shift(t, 1)(x) isdervar = let diff_to_var = diff_to_var @@ -727,13 +716,23 @@ function tearing_reassemble(state::TearingState, var_eq_matching, neweqs = collect(equations(state)) dummy_sub = Dict() + if ModelingToolkit.has_iv(state.sys) + iv = get_iv(state.sys) + if !is_only_discrete(state.structure) + D = Differential(iv) + else + D = Shift(iv, 1) + end + iv = D = nothing + end + # Structural simplification - substitute_derivatives_algevars!(state, neweqs, var_eq_matching, dummy_sub) + substitute_derivatives_algevars!(state, neweqs, var_eq_matching, dummy_sub; iv, D) - generate_derivative_variables!(state, neweqs, var_eq_matching; mm) + generate_derivative_variables!(state, neweqs, var_eq_matching; mm, iv, D) neweqs, solved_eqs, eq_ordering, var_ordering, nelim_eq, nelim_var = - generate_system_equations!(state, neweqs, var_eq_matching; simplify) + generate_system_equations!(state, neweqs, var_eq_matching; simplify, iv, D) state = reorder_vars!(state, var_eq_matching, eq_ordering, var_ordering, nelim_eq, nelim_var) From 9217c62941226cd8dc24a80fe3723810f2463ab5 Mon Sep 17 00:00:00 2001 From: vyudu Date: Tue, 11 Feb 2025 15:29:46 -0500 Subject: [PATCH 0812/1337] refactor iv --- src/structural_transformation/symbolics_tearing.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/src/structural_transformation/symbolics_tearing.jl b/src/structural_transformation/symbolics_tearing.jl index 45430595b6..883166cfa8 100644 --- a/src/structural_transformation/symbolics_tearing.jl +++ b/src/structural_transformation/symbolics_tearing.jl @@ -723,6 +723,7 @@ function tearing_reassemble(state::TearingState, var_eq_matching, else D = Shift(iv, 1) end + else iv = D = nothing end From 54910988509cfdf2f65ed8b8d24f565b3dab8584 Mon Sep 17 00:00:00 2001 From: vyudu Date: Tue, 11 Feb 2025 16:13:47 -0500 Subject: [PATCH 0813/1337] fix comment for eq_to_diff --- src/systems/systemstructure.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/systems/systemstructure.jl b/src/systems/systemstructure.jl index b98bb8c616..6fee78cfd6 100644 --- a/src/systems/systemstructure.jl +++ b/src/systems/systemstructure.jl @@ -142,7 +142,7 @@ has_equations(::TransformationState) = true Base.@kwdef mutable struct SystemStructure """Maps the index of variable x to the index of variable D(x).""" var_to_diff::DiffGraph - """Maps the index of an equation.""" + """Maps the index of an algebraic equation to the index of the equation it is differentiated into.""" eq_to_diff::DiffGraph # Can be access as # `graph` to automatically look at the bipartite graph From 5878ad67ef6a78ad55034d8138a6fee741a9e7d6 Mon Sep 17 00:00:00 2001 From: vyudu Date: Tue, 11 Feb 2025 16:51:06 -0500 Subject: [PATCH 0814/1337] fix total_sub --- src/structural_transformation/symbolics_tearing.jl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/structural_transformation/symbolics_tearing.jl b/src/structural_transformation/symbolics_tearing.jl index 883166cfa8..7cb9798f01 100644 --- a/src/structural_transformation/symbolics_tearing.jl +++ b/src/structural_transformation/symbolics_tearing.jl @@ -464,6 +464,7 @@ function generate_system_equations!(state::TearingState, neweqs, var_eq_matching @unpack solvable_graph, var_to_diff, eq_to_diff, graph = structure eq_var_matching = invview(var_eq_matching) diff_to_var = invview(var_to_diff) + total_sub = Dict() if is_only_discrete(structure) for v in fullvars @@ -515,6 +516,7 @@ function generate_system_equations!(state::TearingState, neweqs, var_eq_matching rem_edge!(graph, e, iv) end + total_sub[simplify_shifts(neweq.lhs)] = neweq.rhs push!(diff_eqs, neweq) push!(diffeq_idxs, ieq) push!(diff_vars, diff_to_var[iv]) From cb64a00cd58209f5c02cab7ae1750dc8e258342c Mon Sep 17 00:00:00 2001 From: vyudu Date: Tue, 11 Feb 2025 22:43:18 -0500 Subject: [PATCH 0815/1337] refactor: sort systems tests into new folder --- test/{ => systems}/abstractsystem.jl | 0 test/{ => systems}/bigsystem.jl | 0 test/{ => systems}/discrete_system.jl | 0 test/{ => systems}/implicit_discrete_system.jl | 0 test/{ => systems}/initializationsystem.jl | 0 test/{ => systems}/jumpsystem.jl | 0 test/{ => systems}/nonlinearsystem.jl | 0 test/{ => systems}/odesystem.jl | 0 test/{ => systems}/optimizationsystem.jl | 0 test/{pde.jl => systems/pdesystem.jl} | 0 test/{ => systems}/sdesystem.jl | 0 test/{ => systems}/steadystatesystems.jl | 0 12 files changed, 0 insertions(+), 0 deletions(-) rename test/{ => systems}/abstractsystem.jl (100%) rename test/{ => systems}/bigsystem.jl (100%) rename test/{ => systems}/discrete_system.jl (100%) rename test/{ => systems}/implicit_discrete_system.jl (100%) rename test/{ => systems}/initializationsystem.jl (100%) rename test/{ => systems}/jumpsystem.jl (100%) rename test/{ => systems}/nonlinearsystem.jl (100%) rename test/{ => systems}/odesystem.jl (100%) rename test/{ => systems}/optimizationsystem.jl (100%) rename test/{pde.jl => systems/pdesystem.jl} (100%) rename test/{ => systems}/sdesystem.jl (100%) rename test/{ => systems}/steadystatesystems.jl (100%) diff --git a/test/abstractsystem.jl b/test/systems/abstractsystem.jl similarity index 100% rename from test/abstractsystem.jl rename to test/systems/abstractsystem.jl diff --git a/test/bigsystem.jl b/test/systems/bigsystem.jl similarity index 100% rename from test/bigsystem.jl rename to test/systems/bigsystem.jl diff --git a/test/discrete_system.jl b/test/systems/discrete_system.jl similarity index 100% rename from test/discrete_system.jl rename to test/systems/discrete_system.jl diff --git a/test/implicit_discrete_system.jl b/test/systems/implicit_discrete_system.jl similarity index 100% rename from test/implicit_discrete_system.jl rename to test/systems/implicit_discrete_system.jl diff --git a/test/initializationsystem.jl b/test/systems/initializationsystem.jl similarity index 100% rename from test/initializationsystem.jl rename to test/systems/initializationsystem.jl diff --git a/test/jumpsystem.jl b/test/systems/jumpsystem.jl similarity index 100% rename from test/jumpsystem.jl rename to test/systems/jumpsystem.jl diff --git a/test/nonlinearsystem.jl b/test/systems/nonlinearsystem.jl similarity index 100% rename from test/nonlinearsystem.jl rename to test/systems/nonlinearsystem.jl diff --git a/test/odesystem.jl b/test/systems/odesystem.jl similarity index 100% rename from test/odesystem.jl rename to test/systems/odesystem.jl diff --git a/test/optimizationsystem.jl b/test/systems/optimizationsystem.jl similarity index 100% rename from test/optimizationsystem.jl rename to test/systems/optimizationsystem.jl diff --git a/test/pde.jl b/test/systems/pdesystem.jl similarity index 100% rename from test/pde.jl rename to test/systems/pdesystem.jl diff --git a/test/sdesystem.jl b/test/systems/sdesystem.jl similarity index 100% rename from test/sdesystem.jl rename to test/systems/sdesystem.jl diff --git a/test/steadystatesystems.jl b/test/systems/steadystatesystems.jl similarity index 100% rename from test/steadystatesystems.jl rename to test/systems/steadystatesystems.jl From a901d097ac630ef6c553cbe947528af617a8ab64 Mon Sep 17 00:00:00 2001 From: vyudu Date: Tue, 11 Feb 2025 22:44:42 -0500 Subject: [PATCH 0816/1337] revert --- test/{systems => }/abstractsystem.jl | 0 test/{systems => }/bigsystem.jl | 0 test/{systems => }/discrete_system.jl | 0 test/{systems => }/implicit_discrete_system.jl | 0 test/{systems => }/initializationsystem.jl | 0 test/{systems => }/jumpsystem.jl | 0 test/{systems => }/nonlinearsystem.jl | 0 test/{systems => }/odesystem.jl | 0 test/{systems => }/optimizationsystem.jl | 0 test/{systems => }/pdesystem.jl | 0 test/{systems => }/sdesystem.jl | 0 test/{systems => }/steadystatesystems.jl | 0 12 files changed, 0 insertions(+), 0 deletions(-) rename test/{systems => }/abstractsystem.jl (100%) rename test/{systems => }/bigsystem.jl (100%) rename test/{systems => }/discrete_system.jl (100%) rename test/{systems => }/implicit_discrete_system.jl (100%) rename test/{systems => }/initializationsystem.jl (100%) rename test/{systems => }/jumpsystem.jl (100%) rename test/{systems => }/nonlinearsystem.jl (100%) rename test/{systems => }/odesystem.jl (100%) rename test/{systems => }/optimizationsystem.jl (100%) rename test/{systems => }/pdesystem.jl (100%) rename test/{systems => }/sdesystem.jl (100%) rename test/{systems => }/steadystatesystems.jl (100%) diff --git a/test/systems/abstractsystem.jl b/test/abstractsystem.jl similarity index 100% rename from test/systems/abstractsystem.jl rename to test/abstractsystem.jl diff --git a/test/systems/bigsystem.jl b/test/bigsystem.jl similarity index 100% rename from test/systems/bigsystem.jl rename to test/bigsystem.jl diff --git a/test/systems/discrete_system.jl b/test/discrete_system.jl similarity index 100% rename from test/systems/discrete_system.jl rename to test/discrete_system.jl diff --git a/test/systems/implicit_discrete_system.jl b/test/implicit_discrete_system.jl similarity index 100% rename from test/systems/implicit_discrete_system.jl rename to test/implicit_discrete_system.jl diff --git a/test/systems/initializationsystem.jl b/test/initializationsystem.jl similarity index 100% rename from test/systems/initializationsystem.jl rename to test/initializationsystem.jl diff --git a/test/systems/jumpsystem.jl b/test/jumpsystem.jl similarity index 100% rename from test/systems/jumpsystem.jl rename to test/jumpsystem.jl diff --git a/test/systems/nonlinearsystem.jl b/test/nonlinearsystem.jl similarity index 100% rename from test/systems/nonlinearsystem.jl rename to test/nonlinearsystem.jl diff --git a/test/systems/odesystem.jl b/test/odesystem.jl similarity index 100% rename from test/systems/odesystem.jl rename to test/odesystem.jl diff --git a/test/systems/optimizationsystem.jl b/test/optimizationsystem.jl similarity index 100% rename from test/systems/optimizationsystem.jl rename to test/optimizationsystem.jl diff --git a/test/systems/pdesystem.jl b/test/pdesystem.jl similarity index 100% rename from test/systems/pdesystem.jl rename to test/pdesystem.jl diff --git a/test/systems/sdesystem.jl b/test/sdesystem.jl similarity index 100% rename from test/systems/sdesystem.jl rename to test/sdesystem.jl diff --git a/test/systems/steadystatesystems.jl b/test/steadystatesystems.jl similarity index 100% rename from test/systems/steadystatesystems.jl rename to test/steadystatesystems.jl From 397b279df53c45a6c923ee89a24125237d4ee44c Mon Sep 17 00:00:00 2001 From: vyudu Date: Tue, 11 Feb 2025 23:08:07 -0500 Subject: [PATCH 0817/1337] add diff_to_var as argument for find_dumplicate_dd, fix substitution of lowest-order variable --- src/structural_transformation/symbolics_tearing.jl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/structural_transformation/symbolics_tearing.jl b/src/structural_transformation/symbolics_tearing.jl index 7cb9798f01..ec1ff45fd7 100644 --- a/src/structural_transformation/symbolics_tearing.jl +++ b/src/structural_transformation/symbolics_tearing.jl @@ -374,9 +374,9 @@ function generate_derivative_variables!(ts::TearingState, neweqs, var_eq_matchin # derivative is in the system for v in 1:length(var_to_diff) dv = var_to_diff[v] - # For discrete systems, directly substitute lowest-order variable + # For discrete systems, directly substitute lowest-order shift if is_discrete && diff_to_var[v] == nothing - fullvars[v] = lower_varname(fullvars[v], iv) + operation(fullvars[v]) isa Shift && (fullvars[v] = lower_varname(fullvars[v], iv)) end dv isa Int || continue solved = var_eq_matching[dv] isa Int @@ -384,7 +384,7 @@ function generate_derivative_variables!(ts::TearingState, neweqs, var_eq_matchin # If there's `D(x) = x_t` already, update mappings and continue without # adding new equations/variables - dd = find_duplicate_dd(dv, solvable_graph, linear_eqs, mm) + dd = find_duplicate_dd(dv, solvable_graph, diff_to_var, linear_eqs, mm) if !isnothing(dd) dummy_eq, v_t = dd var_to_diff[v_t] = var_to_diff[dv] @@ -412,7 +412,7 @@ end """ Check if there's `D(x) = x_t` already. """ -function find_duplicate_dd(dv, solvable_graph, linear_eqs, mm) +function find_duplicate_dd(dv, solvable_graph, diff_to_var, linear_eqs, mm) for eq in 𝑑neighbors(solvable_graph, dv) mi = get(linear_eqs, eq, 0) iszero(mi) && continue From 77c4aed624b6c92e947bffbfcf376812545facb1 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 21 Jan 2025 14:53:52 +0530 Subject: [PATCH 0818/1337] fix: handle case when parameter object is `nothing` in `GetUpdatedMTKParameters` --- src/systems/problem_utils.jl | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/systems/problem_utils.jl b/src/systems/problem_utils.jl index 81be9c3d9e..0bea6b7a08 100644 --- a/src/systems/problem_utils.jl +++ b/src/systems/problem_utils.jl @@ -455,7 +455,9 @@ struct GetUpdatedMTKParameters{G, S} end function (f::GetUpdatedMTKParameters)(prob, initializesol) - mtkp = copy(parameter_values(prob)) + p = parameter_values(prob) + p === nothing && return nothing + mtkp = copy(p) f.setpunknowns(mtkp, f.getpunknowns(initializesol)) mtkp end From 5c873f766b6937f186838422ff2ea10c98978e55 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 21 Jan 2025 14:54:12 +0530 Subject: [PATCH 0819/1337] feat: update `u0map` and `pmap` in `build_operating_point` --- src/systems/problem_utils.jl | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/systems/problem_utils.jl b/src/systems/problem_utils.jl index 0bea6b7a08..29d821a26b 100644 --- a/src/systems/problem_utils.jl +++ b/src/systems/problem_utils.jl @@ -511,6 +511,9 @@ Construct the operating point of the system from the user-provided `u0map` and ` defaults `defs`, constant equations `cmap` (from `get_cmap(sys)`), unknowns `dvs` and parameters `ps`. Return the operating point as a dictionary, the list of unknowns for which no values can be determined, and the list of parameters for which no values can be determined. + +Also updates `u0map` and `pmap` in-place to contain all the initial conditions in `op`, split +by unknowns and parameters respectively. """ function build_operating_point( u0map::AbstractDict, pmap::AbstractDict, defs::AbstractDict, cmap, dvs, ps) @@ -525,6 +528,17 @@ function build_operating_point( for eq in cmap op[eq.lhs] = eq.rhs end + + empty!(u0map) + empty!(pmap) + for (k, v) in op + k = unwrap(k) + if isparameter(k) + pmap[k] = v + elseif !isconstant(k) + u0map[k] = v + end + end return op, missing_unknowns, missing_pars end From ba21ac58e850fa57713edaf3218c8e5e4897d537 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 21 Jan 2025 14:56:54 +0530 Subject: [PATCH 0820/1337] feat: add dummy initialization parameters instead of hard constraints --- src/systems/diffeqs/abstractodesystem.jl | 3 +- src/systems/nonlinear/initializesystem.jl | 66 +++++++++++++++++++---- 2 files changed, 59 insertions(+), 10 deletions(-) diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index ea55dce388..68e98ee4f2 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -1240,7 +1240,8 @@ function InitializationProblem{iip, specialize}(sys::AbstractSystem, meta = get_metadata(isys) if meta isa InitializationSystemMetadata - @set! isys.metadata.oop_reconstruct_u0_p = ReconstructInitializeprob(sys, isys) + @set! isys.metadata.oop_reconstruct_u0_p = ReconstructInitializeprob( + sys, isys; remap = meta.new_params) end ts = get_tearing_state(isys) diff --git a/src/systems/nonlinear/initializesystem.jl b/src/systems/nonlinear/initializesystem.jl index 68c9d62f62..433c4f9ecf 100644 --- a/src/systems/nonlinear/initializesystem.jl +++ b/src/systems/nonlinear/initializesystem.jl @@ -27,6 +27,38 @@ function generate_initializesystem(sys::AbstractSystem; guesses = merge(get_guesses(sys), additional_guesses) idxs_diff = isdiffeq.(eqs) + # PREPROCESSING + # for initial conditions of the form `var => constant`, we instead turn them into + # `var ~ var0` where `var0` is a new parameter, and make `update_initializeprob!` + # update `initializeprob.ps[var0] = prob[var]`. + + # map parameters in `initprob` which need to be updated in `update_initializeprob!` + # to the corresponding expressions that determine their values + new_params = Dict() + u0map = copy(anydict(u0map)) + pmap = copy(anydict(pmap)) + for (k, v) in u0map + k = unwrap(k) + is_variable(sys, k) || continue + (symbolic_type(v) == NotSymbolic() && !is_array_of_symbolics(v)) || continue + newvar = get_initial_value_parameter(k) + new_params[newvar] = k + pmap[newvar] = v + u0map[k] = newvar + defs[newvar] = v + end + for (k, v) in pmap + k = unwrap(k) + is_parameter_solvable(k, pmap, defs, guesses) || continue + (v !== missing && symbolic_type(v) == NotSymbolic() && !is_array_of_symbolics(v)) || + continue + newvar = get_initial_value_parameter(k) + new_params[newvar] = k + pmap[newvar] = v + pmap[k] = newvar + defs[newvar] = v + end + # 1) Use algebraic equations of time-dependent systems as initialization constraints if has_iv(sys) idxs_alge = .!idxs_diff @@ -103,10 +135,6 @@ function generate_initializesystem(sys::AbstractSystem; # 5) process parameters as initialization unknowns paramsubs = Dict() - if pmap isa SciMLBase.NullParameters - pmap = Dict() - end - pmap = todict(pmap) for p in parameters(sys) if is_parameter_solvable(p, pmap, defs, guesses) # If either of them are `missing` the parameter is an unknown @@ -180,6 +208,7 @@ function generate_initializesystem(sys::AbstractSystem; # parameters do not include ones that became initialization unknowns pars = Vector{SymbolicParam}(filter(p -> !haskey(paramsubs, p), parameters(sys))) + pars = [pars; map(unwrap, collect(keys(new_params)))] is_time_dependent(sys) && push!(pars, get_iv(sys)) if is_time_dependent(sys) @@ -214,7 +243,7 @@ function generate_initializesystem(sys::AbstractSystem; end meta = InitializationSystemMetadata( anydict(u0map), anydict(pmap), additional_guesses, - additional_initialization_eqs, extra_metadata, nothing) + additional_initialization_eqs, extra_metadata, nothing, new_params) return NonlinearSystem(eqs_ics, vars, pars; @@ -226,15 +255,33 @@ function generate_initializesystem(sys::AbstractSystem; kwargs...) end +""" + $(TYPEDSIGNATURES) + +Get a new symbolic variable of the same type and size as `sym`, which is a parameter. +""" +function get_initial_value_parameter(sym) + sym = default_toterm(unwrap(sym)) + name = hasname(sym) ? getname(sym) : Symbol(sym) + if iscall(sym) && operation(sym) === getindex + name = Symbol(name, :_, join(arguments(sym)[2:end], "_")) + end + name = Symbol(name, :ₘₜₖ_₀) + newvar = unwrap(similar_variable(sym, name; use_gensym = false)) + return toparam(newvar) +end + struct ReconstructInitializeprob getter::Any setter::Any end -function ReconstructInitializeprob(srcsys::AbstractSystem, dstsys::AbstractSystem) - syms = reduce(vcat, reorder_parameters(dstsys, parameters(dstsys)); init = []) - getter = getu(srcsys, syms) - setter = setp_oop(dstsys, syms) +function ReconstructInitializeprob( + srcsys::AbstractSystem, dstsys::AbstractSystem; remap = Dict()) + syms = [unknowns(dstsys); + reduce(vcat, reorder_parameters(dstsys, parameters(dstsys)); init = [])] + getter = getu(srcsys, map(x -> get(remap, x, x), syms)) + setter = setsym_oop(dstsys, syms) return ReconstructInitializeprob(getter, setter) end @@ -266,6 +313,7 @@ struct InitializationSystemMetadata additional_initialization_eqs::Vector{Equation} extra_metadata::NamedTuple oop_reconstruct_u0_p::Union{Nothing, ReconstructInitializeprob} + new_params::Dict{Any, Any} end function get_possibly_array_fallback_singletons(varmap, p) From 127a0c8d449478a2457ded43fa8d6670b49db34a Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 21 Jan 2025 14:58:13 +0530 Subject: [PATCH 0821/1337] feat: always build initialization problem, handle dummy parameters --- src/systems/problem_utils.jl | 147 ++++++++++++++++++++--------------- 1 file changed, 86 insertions(+), 61 deletions(-) diff --git a/src/systems/problem_utils.jl b/src/systems/problem_utils.jl index 29d821a26b..24e0559d98 100644 --- a/src/systems/problem_utils.jl +++ b/src/systems/problem_utils.jl @@ -556,75 +556,98 @@ function maybe_build_initialization_problem( sys::AbstractSystem, op::AbstractDict, u0map, pmap, t, defs, guesses, missing_unknowns; implicit_dae = false, kwargs...) guesses = merge(ModelingToolkit.guesses(sys), todict(guesses)) - has_observed_u0s = any( - k -> has_observed_with_lhs(sys, k) || has_parameter_dependency_with_lhs(sys, k), - keys(op)) - solvablepars = [p - for p in parameters(sys) - if is_parameter_solvable(p, pmap, defs, guesses)] - has_dependent_unknowns = any(unknowns(sys)) do sym - val = get(op, sym, nothing) - val === nothing && return false - return symbolic_type(val) != NotSymbolic() || is_array_of_symbolics(val) - end - if (((implicit_dae || has_observed_u0s || !isempty(missing_unknowns) || - !isempty(solvablepars) || has_dependent_unknowns) && - (!has_tearing_state(sys) || get_tearing_state(sys) !== nothing)) || - !isempty(initialization_equations(sys))) && - (!is_time_dependent(sys) || t !== nothing) - initializeprob = ModelingToolkit.InitializationProblem( - sys, t, u0map, pmap; guesses, kwargs...) - - if is_time_dependent(sys) - all_init_syms = Set(all_symbols(initializeprob)) - solved_unknowns = filter(var -> var in all_init_syms, unknowns(sys)) - initializeprobmap = getu(initializeprob, solved_unknowns) - else - initializeprobmap = nothing - end - punknowns = [p - for p in all_variable_symbols(initializeprob) - if is_parameter(sys, p)] - if isempty(punknowns) - initializeprobpmap = nothing - else - getpunknowns = getu(initializeprob, punknowns) - setpunknowns = setp(sys, punknowns) - initializeprobpmap = GetUpdatedMTKParameters(getpunknowns, setpunknowns) - end + if t === nothing && is_time_dependent(sys) + t = 0.0 + end - reqd_syms = parameter_symbols(initializeprob) - # we still want the `initialization_data` because it helps with `remake` - if initializeprobmap === nothing && initializeprobpmap === nothing - update_initializeprob! = nothing - else - update_initializeprob! = UpdateInitializeprob( - getu(sys, reqd_syms), setu(initializeprob, reqd_syms)) + initializeprob = ModelingToolkit.InitializationProblem( + sys, t, u0map, pmap; guesses, kwargs...) + meta = get_metadata(initializeprob.f.sys) + new_params = meta.new_params + + if is_time_dependent(sys) + all_init_syms = Set(all_symbols(initializeprob)) + solved_unknowns = filter(var -> var in all_init_syms, unknowns(sys)) + initializeprobmap = getu(initializeprob, solved_unknowns) + else + initializeprobmap = nothing + end + + punknowns = [p + for p in all_variable_symbols(initializeprob) + if is_parameter(sys, p)] + if isempty(punknowns) + initializeprobpmap = nothing + else + getpunknowns = getu(initializeprob, punknowns) + setpunknowns = setp(sys, punknowns) + initializeprobpmap = GetUpdatedMTKParameters(getpunknowns, setpunknowns) + end + + reqd_syms = parameter_symbols(initializeprob) + sources = [get(new_params, x, x) for x in reqd_syms] + # we still want the `initialization_data` because it helps with `remake` + if initializeprobmap === nothing && initializeprobpmap === nothing + update_initializeprob! = nothing + else + update_initializeprob! = UpdateInitializeprob( + getu(sys, sources), setu(initializeprob, reqd_syms)) + end + + for p in punknowns + p = unwrap(p) + stype = symtype(p) + op[p] = get_temporary_value(p) + if iscall(p) && operation(p) === getindex + arrp = arguments(p)[1] + op[arrp] = collect(arrp) end + end - for p in punknowns - p = unwrap(p) - stype = symtype(p) - op[p] = get_temporary_value(p) - if iscall(p) && operation(p) === getindex - arrp = arguments(p)[1] - op[arrp] = collect(arrp) - end + if is_time_dependent(sys) + for v in missing_unknowns + op[v] = zero_var(v) end + empty!(missing_unknowns) + end - if is_time_dependent(sys) - for v in missing_unknowns - op[v] = zero_var(v) - end - empty!(missing_unknowns) + for v in missing_unknowns + op[v] = zero_var(v) + end + empty!(missing_unknowns) + return (; + initialization_data = SciMLBase.OverrideInitData( + initializeprob, update_initializeprob!, initializeprobmap, + initializeprobpmap)) +end + +""" + $(TYPEDSIGNATURES) + +Remove all entries in `varmap` whose keys are not variables/parameters in `sys`, +substituting their values into the rest of `varmap`. Modifies `varmap` in place. +""" +function substitute_extra_variables!(sys::AbstractSystem, varmap::AbstractDict) + tmpmap = anydict() + syms = all_symbols(sys) + for (k, v) in varmap + k = unwrap(k) + if any(isequal(k), syms) || + iscall(k) && + (operation(k) == getindex && any(isequal(arguments(k)[1]), syms) || + operation(k) isa Differential) || isconstant(k) + continue end - return (; - initialization_data = SciMLBase.OverrideInitData( - initializeprob, update_initializeprob!, initializeprobmap, - initializeprobpmap)) + tmpmap[k] = v + end + for k in keys(tmpmap) + delete!(varmap, k) + end + for (k, v) in varmap + varmap[k] = unwrap(fixpoint_sub(v, tmpmap)) end - return (;) + return varmap end """ @@ -716,6 +739,8 @@ function process_SciMLProblem( op, missing_unknowns, missing_pars = build_operating_point( u0map, pmap, defs, cmap, dvs, ps) + substitute_extra_variables!(sys, u0map) + substitute_extra_variables!(sys, pmap) if build_initializeprob kws = maybe_build_initialization_problem( From a7b73ee52df579ac3a79dbd156a7a021633e6efd Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 21 Jan 2025 14:58:32 +0530 Subject: [PATCH 0822/1337] refactor: allow disabling `gensym` in `similar_variable` --- src/utils.jl | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/utils.jl b/src/utils.jl index cf49d9f445..38cfcd1228 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -1174,9 +1174,13 @@ end Create an anonymous symbolic variable of the same shape, size and symtype as `var`, with name `gensym(name)`. Does not support unsized array symbolics. + +If `use_gensym == false`, will not `gensym` the name. """ -function similar_variable(var::BasicSymbolic, name = :anon) - name = gensym(name) +function similar_variable(var::BasicSymbolic, name = :anon; use_gensym = true) + if use_gensym + name = gensym(name) + end stype = symtype(var) sym = Symbolics.variable(name; T = stype) if size(var) !== () From 3d88375f0ea2c4fdfd8fb0e460cf5c9eab3f86ac Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 21 Jan 2025 14:22:04 +0530 Subject: [PATCH 0823/1337] feat: count shift equations as differential equations --- src/utils.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils.jl b/src/utils.jl index 38cfcd1228..3f52b0676f 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -358,7 +358,7 @@ isoperator(expr, op) = iscall(expr) && operation(expr) isa op isoperator(op) = expr -> isoperator(expr, op) isdifferential(expr) = isoperator(expr, Differential) -isdiffeq(eq) = isdifferential(eq.lhs) +isdiffeq(eq) = isdifferential(eq.lhs) || isoperator(eq.lhs, Shift) isvariable(x::Num)::Bool = isvariable(value(x)) function isvariable(x)::Bool From d9a1872743545f3edbf71c354d4bd3add18ec2f9 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 21 Jan 2025 14:22:25 +0530 Subject: [PATCH 0824/1337] test: provide missing parameters to `NonlinearProblem` test --- test/nonlinearsystem.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/nonlinearsystem.jl b/test/nonlinearsystem.jl index cbd95a50d3..c6318b1314 100644 --- a/test/nonlinearsystem.jl +++ b/test/nonlinearsystem.jl @@ -103,7 +103,7 @@ eqs1 = [ lorenz = name -> NonlinearSystem(eqs1, [x, y, z, u, F], [σ, ρ, β], name = name) lorenz1 = lorenz(:lorenz1) -@test_throws ArgumentError NonlinearProblem(complete(lorenz1), zeros(5)) +@test_throws ArgumentError NonlinearProblem(complete(lorenz1), zeros(5), zeros(3)) lorenz2 = lorenz(:lorenz2) @named connected = NonlinearSystem( [s ~ a + lorenz1.x From 420a4972386aba5f7eca63615d39b752d8fa0990 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 21 Jan 2025 14:23:11 +0530 Subject: [PATCH 0825/1337] feat: add `force_time_independent` keyword to `InitializationProblem` --- src/systems/diffeqs/abstractodesystem.jl | 29 +++++++++++++++++------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index 68e98ee4f2..2c07974252 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -1220,22 +1220,35 @@ function InitializationProblem{iip, specialize}(sys::AbstractSystem, check_units = true, use_scc = true, allow_incomplete = false, + force_time_independent = false, kwargs...) where {iip, specialize} if !iscomplete(sys) error("A completed system is required. Call `complete` or `structural_simplify` on the system before creating an `ODEProblem`") end if isempty(u0map) && get_initializesystem(sys) !== nothing isys = get_initializesystem(sys; initialization_eqs, check_units) + simplify_system = false elseif isempty(u0map) && get_initializesystem(sys) === nothing - isys = structural_simplify( - generate_initializesystem( - sys; initialization_eqs, check_units, pmap = parammap, - guesses, extra_metadata = (; use_scc)); fully_determined) + isys = generate_initializesystem( + sys; initialization_eqs, check_units, pmap = parammap, + guesses, extra_metadata = (; use_scc)) + simplify_system = true else - isys = structural_simplify( - generate_initializesystem( - sys; u0map, initialization_eqs, check_units, - pmap = parammap, guesses, extra_metadata = (; use_scc)); fully_determined) + isys = generate_initializesystem( + sys; u0map, initialization_eqs, check_units, + pmap = parammap, guesses, extra_metadata = (; use_scc)) + simplify_system = true + end + + # useful for `SteadyStateProblem` since `f` has to be autonomous and the + # initialization should be too + if force_time_independent + idx = findfirst(isequal(get_iv(sys)), get_ps(isys)) + idx === nothing || deleteat!(get_ps(isys), idx) + end + + if simplify_system + isys = structural_simplify(isys; fully_determined) end meta = get_metadata(isys) From 3153677258da5463e5fb6fbfceeb54da93a37024 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 21 Jan 2025 14:23:45 +0530 Subject: [PATCH 0826/1337] feat: expose `force_time_independent` through `process_SciMLProblem` --- src/systems/problem_utils.jl | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/systems/problem_utils.jl b/src/systems/problem_utils.jl index 24e0559d98..add3fd4405 100644 --- a/src/systems/problem_utils.jl +++ b/src/systems/problem_utils.jl @@ -704,6 +704,8 @@ Keyword arguments: other to attempt to arrive at a numeric value. - `use_scc`: Whether to use `SCCNonlinearProblem` for initialization if the system is fully determined. +- `force_initialization_time_independent`: Whether to force the initialization to not use + the independent variable of `sys`. All other keyword arguments are passed as-is to `constructor`. """ @@ -717,7 +719,8 @@ function process_SciMLProblem( symbolic_u0 = false, warn_cyclic_dependency = false, circular_dependency_max_cycle_length = length(all_symbols(sys)), circular_dependency_max_cycles = 10, - substitution_limit = 100, use_scc = true, kwargs...) + substitution_limit = 100, use_scc = true, + force_initialization_time_independent = false, kwargs...) dvs = unknowns(sys) ps = parameters(sys) iv = has_iv(sys) ? get_iv(sys) : nothing @@ -748,7 +751,8 @@ function process_SciMLProblem( implicit_dae, warn_initialize_determined, initialization_eqs, eval_expression, eval_module, fully_determined, warn_cyclic_dependency, check_units = check_initialization_units, - circular_dependency_max_cycle_length, circular_dependency_max_cycles, use_scc) + circular_dependency_max_cycle_length, circular_dependency_max_cycles, use_scc, + force_time_independent = force_initialization_time_independent) kwargs = merge(kwargs, kws) end From 131ac816c0bbd71a8505b21f82587b217382161b Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 21 Jan 2025 14:24:11 +0530 Subject: [PATCH 0827/1337] fix: remove delayed terms in initialization system of DDEs --- src/systems/nonlinear/initializesystem.jl | 49 +++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/src/systems/nonlinear/initializesystem.jl b/src/systems/nonlinear/initializesystem.jl index 433c4f9ecf..d693940e4d 100644 --- a/src/systems/nonlinear/initializesystem.jl +++ b/src/systems/nonlinear/initializesystem.jl @@ -17,6 +17,9 @@ function generate_initializesystem(sys::AbstractSystem; eqs = Equation[x for x in eqs if x isa Equation] end trueobs, eqs = unhack_observed(observed(sys), eqs) + # remove any observed equations that directly or indirectly contain + # delayed unknowns + isempty(trueobs) || filter_delay_equations_variables!(sys, trueobs) vars = unique([unknowns(sys); getfield.(trueobs, :lhs)]) vars_set = Set(vars) # for efficient in-lookup @@ -271,6 +274,52 @@ function get_initial_value_parameter(sym) return toparam(newvar) end +""" + $(TYPEDSIGNATURES) + +Given `sys` and a list of observed equations `trueobs`, remove all the equations that +directly or indirectly contain a delayed unknown of `sys`. +""" +function filter_delay_equations_variables!(sys::AbstractSystem, trueobs::Vector{Equation}) + is_time_dependent(sys) || return trueobs + banned_vars = Set() + idxs_to_remove = Int[] + for (i, eq) in enumerate(trueobs) + _has_delays(sys, eq.rhs, banned_vars) || continue + push!(idxs_to_remove, i) + push!(banned_vars, eq.lhs) + end + return deleteat!(trueobs, idxs_to_remove) +end + +""" + $(TYPEDSIGNATURES) + +Check if the expression `ex` contains a delayed unknown of `sys` or a term in +`banned`. +""" +function _has_delays(sys::AbstractSystem, ex, banned) + ex = unwrap(ex) + ex in banned && return true + if symbolic_type(ex) == NotSymbolic() + if is_array_of_symbolics(ex) + return any(x -> _has_delays(sys, x, banned), ex) + end + return false + end + iscall(ex) || return false + op = operation(ex) + args = arguments(ex) + if iscalledparameter(ex) + return any(x -> _has_delays(sys, x, banned), args) + end + if issym(op) && length(args) == 1 && is_variable(sys, op(get_iv(sys))) && + iscall(args[1]) && get_iv(sys) in vars(args[1]) + return true + end + return any(x -> _has_delays(sys, x, banned), args) +end + struct ReconstructInitializeprob getter::Any setter::Any From c8a282dafbca7ef1260cae7587ffd6551dd16506 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 21 Jan 2025 14:24:36 +0530 Subject: [PATCH 0828/1337] fix: only add dummy parameters for unknowns of time-dependent systems --- src/systems/nonlinear/initializesystem.jl | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/systems/nonlinear/initializesystem.jl b/src/systems/nonlinear/initializesystem.jl index d693940e4d..566c94ed68 100644 --- a/src/systems/nonlinear/initializesystem.jl +++ b/src/systems/nonlinear/initializesystem.jl @@ -40,15 +40,17 @@ function generate_initializesystem(sys::AbstractSystem; new_params = Dict() u0map = copy(anydict(u0map)) pmap = copy(anydict(pmap)) - for (k, v) in u0map - k = unwrap(k) - is_variable(sys, k) || continue - (symbolic_type(v) == NotSymbolic() && !is_array_of_symbolics(v)) || continue - newvar = get_initial_value_parameter(k) - new_params[newvar] = k - pmap[newvar] = v - u0map[k] = newvar - defs[newvar] = v + if is_time_dependent(sys) + for (k, v) in u0map + k = unwrap(k) + is_variable(sys, k) || continue + (symbolic_type(v) == NotSymbolic() && !is_array_of_symbolics(v)) || continue + newvar = get_initial_value_parameter(k) + new_params[newvar] = k + pmap[newvar] = v + u0map[k] = newvar + defs[newvar] = v + end end for (k, v) in pmap k = unwrap(k) From b4289e6e7086e8d086432d85090adf8ac82e5a0d Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 21 Jan 2025 14:25:16 +0530 Subject: [PATCH 0829/1337] fix: change differential operator when building initialization for `DiscreteSystem` --- src/systems/nonlinear/initializesystem.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/systems/nonlinear/initializesystem.jl b/src/systems/nonlinear/initializesystem.jl index 566c94ed68..23cc504ca4 100644 --- a/src/systems/nonlinear/initializesystem.jl +++ b/src/systems/nonlinear/initializesystem.jl @@ -70,7 +70,7 @@ function generate_initializesystem(sys::AbstractSystem; append!(eqs_ics, eqs[idxs_alge]) # start equation list with algebraic equations eqs_diff = eqs[idxs_diff] - D = Differential(get_iv(sys)) + D = sys isa DiscreteSystem ? Shift(get_iv(sys), 1) : Differential(get_iv(sys)) diffmap = merge( Dict(eq.lhs => eq.rhs for eq in eqs_diff), Dict(D(eq.lhs) => D(eq.rhs) for eq in trueobs) From 1dbe2956741a18589ecdcb9992997dafe828a7f0 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 21 Jan 2025 14:25:51 +0530 Subject: [PATCH 0830/1337] fix: handle recursive default edge case in initializesystem --- src/systems/nonlinear/initializesystem.jl | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/systems/nonlinear/initializesystem.jl b/src/systems/nonlinear/initializesystem.jl index 23cc504ca4..7116f36e9f 100644 --- a/src/systems/nonlinear/initializesystem.jl +++ b/src/systems/nonlinear/initializesystem.jl @@ -90,6 +90,10 @@ function generate_initializesystem(sys::AbstractSystem; function process_u0map_with_dummysubs(y, x) y = get(schedule.dummy_sub, y, y) y = fixpoint_sub(y, diffmap) + # If we have `D(x) ~ x` and provide [D(x) => x, x => 1.0] to `u0map`, then + # without this condition `defs` would get `x => x` instead of retaining + # `x => 1.0`. + isequal(y, x) && return if y ∈ vars_set # variables specified in u0 overrides defaults push!(defs, y => x) From 3b1dbc1fae8ef3d5d42eeac0df9bf715d7456649 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 21 Jan 2025 14:26:23 +0530 Subject: [PATCH 0831/1337] fix: remove occurrences of dummy parameters when remaking initializeprob --- src/systems/nonlinear/initializesystem.jl | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/systems/nonlinear/initializesystem.jl b/src/systems/nonlinear/initializesystem.jl index 7116f36e9f..9736581135 100644 --- a/src/systems/nonlinear/initializesystem.jl +++ b/src/systems/nonlinear/initializesystem.jl @@ -460,6 +460,16 @@ function SciMLBase.remake_initialization_data( merge!(guesses, meta.additional_guesses) use_scc = get(meta.extra_metadata, :use_scc, true) initialization_eqs = meta.additional_initialization_eqs + + # remove occurrences of `keys(new_params)` from `u0map` and `pmap` + for (newvar, oldvar) in meta.new_params + if isequal(get(u0map, oldvar, nothing), newvar) + u0map[oldvar] = pmap[newvar] + elseif isequal(get(pmap, oldvar, nothing), newvar) + pmap[oldvar] = pmap[newvar] + end + delete!(pmap, newvar) + end end else # there is no initializeprob, so the original problem construction From 46f2b821c75afe19afeec5f24cb4b9cc26648cdb Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 21 Jan 2025 14:26:45 +0530 Subject: [PATCH 0832/1337] fix: only set solvable parameter values to zero in limited cases --- src/systems/problem_utils.jl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/systems/problem_utils.jl b/src/systems/problem_utils.jl index add3fd4405..ed887e9386 100644 --- a/src/systems/problem_utils.jl +++ b/src/systems/problem_utils.jl @@ -596,6 +596,8 @@ function maybe_build_initialization_problem( end for p in punknowns + is_parameter_solvable(p, pmap, defs, guesses) || continue + get(op, p, missing) === missing || continue p = unwrap(p) stype = symtype(p) op[p] = get_temporary_value(p) From aa3e6db05f9196deb35b93753625b0862dad677b Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 21 Jan 2025 14:29:38 +0530 Subject: [PATCH 0833/1337] fix: ensure initialization systems for `SteadyStateProblem` do not involve independent variable --- src/systems/diffeqs/abstractodesystem.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index 2c07974252..95a75dddad 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -1045,7 +1045,7 @@ function DiffEqBase.SteadyStateProblem{iip}(sys::AbstractODESystem, u0map, end f, u0, p = process_SciMLProblem(ODEFunction{iip}, sys, u0map, parammap; steady_state = true, - check_length, kwargs...) + check_length, force_initialization_time_independent = true, kwargs...) kwargs = filter_kwargs(kwargs) SteadyStateProblem{iip}(f, u0, p; kwargs...) end From 7e647aee73e7bd6adfa475bddd6b65114463e9ae Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 21 Jan 2025 14:30:16 +0530 Subject: [PATCH 0834/1337] test: disambiguate `observed` in initializesystem tests --- test/initializationsystem.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/initializationsystem.jl b/test/initializationsystem.jl index 026fd2c43d..2ed9c99aaf 100644 --- a/test/initializationsystem.jl +++ b/test/initializationsystem.jl @@ -3,7 +3,7 @@ using StochasticDiffEq, DelayDiffEq, StochasticDelayDiffEq, JumpProcesses using ForwardDiff using SymbolicIndexingInterface, SciMLStructures using SciMLStructures: Tunable -using ModelingToolkit: t_nounits as t, D_nounits as D +using ModelingToolkit: t_nounits as t, D_nounits as D, observed using DynamicQuantities @parameters g From d4192475e54cf9714271775008636e46171557d3 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 21 Jan 2025 14:30:26 +0530 Subject: [PATCH 0835/1337] test: remove unnecessary test --- test/initializationsystem.jl | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/initializationsystem.jl b/test/initializationsystem.jl index 2ed9c99aaf..175eda6465 100644 --- a/test/initializationsystem.jl +++ b/test/initializationsystem.jl @@ -600,8 +600,7 @@ end (ModelingToolkit.System, DDEProblem, MethodOfSteps(Tsit5()), [_x(t - 0.1), 0.0]), (ModelingToolkit.System, SDDEProblem, ImplicitEM(), [_x(t - 0.1) + a, b]) ] - function test_parameter(prob, sym, val, initialval = zero(val)) - @test prob.ps[sym] ≈ initialval + function test_parameter(prob, sym, val) if prob.u0 !== nothing @test init(prob, alg).ps[sym] ≈ val end From 44c05cd595314f463c34f119cf59a60c3c3e1c67 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 21 Jan 2025 14:31:13 +0530 Subject: [PATCH 0836/1337] test: refactor `initialization_data === nothing` tests --- test/initializationsystem.jl | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/test/initializationsystem.jl b/test/initializationsystem.jl index 175eda6465..8c99f730e9 100644 --- a/test/initializationsystem.jl +++ b/test/initializationsystem.jl @@ -687,7 +687,8 @@ end _pmap = merge(pmap, Dict(p => 1.0)) prob = Problem(sys, u0map, (0.0, 1.0), _pmap) @test prob.ps[p] ≈ 1.0 - @test prob.f.initialization_data === nothing + initsys = prob.f.initialization_data.initializeprob.f.sys + @test is_parameter(initsys, p) # Non-floating point @parameters r::Int s::Int @@ -696,7 +697,9 @@ end prob = Problem(sys, u0map, (0.0, 1.0), [r => 1]) @test prob.ps[r] == 1 @test prob.ps[s] == 2 - @test prob.f.initialization_data === nothing + initsys = prob.f.initialization_data.initializeprob.f.sys + @test is_parameter(initsys, r) + @test is_parameter(initsys, s) @mtkbuild sys = System( [D(x) ~ x + rhss[1], p ~ x + y + rhss[2]], t; guesses = [p => 0.0]) @@ -1180,21 +1183,28 @@ end @test integ2.ps[q] ≈ 2cbrt(3 / 28) end +function test_dummy_initialization_equation(prob, var) + initsys = prob.f.initialization_data.initializeprob.f.sys + @test isempty(equations(initsys)) + idx = findfirst(eq -> isequal(var, eq.lhs), observed(initsys)) + @test idx !== nothing && is_parameter(initsys, observed(initsys)[idx].rhs) +end + @testset "Remake problem with no initializeprob" begin @variables x(t) [guess = 1.0] y(t) [guess = 1.0] @parameters p [guess = 1.0] q [guess = 1.0] @mtkbuild sys = ODESystem( [D(x) ~ p * x + q * y, y ~ 2x], t; parameter_dependencies = [q ~ 2p]) prob = ODEProblem(sys, [x => 1.0], (0.0, 1.0), [p => 1.0]) - @test prob.f.initialization_data === nothing + test_dummy_initialization_equation(prob, x) prob2 = remake(prob; u0 = [x => 2.0]) @test prob2[x] == 2.0 - @test prob2.f.initialization_data === nothing + test_dummy_initialization_equation(prob2, x) prob3 = remake(prob; u0 = [y => 2.0]) @test prob3.f.initialization_data !== nothing @test init(prob3)[x] ≈ 1.0 prob4 = remake(prob; p = [p => 1.0]) - @test prob4.f.initialization_data === nothing + test_dummy_initialization_equation(prob4, x) prob5 = remake(prob; p = [p => missing, q => 2.0]) @test prob5.f.initialization_data !== nothing @test init(prob5).ps[p] ≈ 1.0 @@ -1206,12 +1216,12 @@ end @mtkbuild sys = ODESystem( [D(x) ~ p * x + q * y, y ~ 2x], t; parameter_dependencies = [q ~ 2p]) prob = ODEProblem(sys, [:x => 1.0], (0.0, 1.0), [p => 1.0]) - @test prob.f.initialization_data === nothing + test_dummy_initialization_equation(prob, x) prob2 = remake(prob; u0 = [:x => 2.0]) - @test prob2.f.initialization_data === nothing - prob3 = remake(prob; u0 = [:y => 1.0]) - @test prob3.f.initialization_data !== nothing + test_dummy_initialization_equation(prob2, x) + prob3 = remake(prob; u0 = [:y => 1.0, :x => nothing]) @test init(prob3)[x] ≈ 0.5 + @test SciMLBase.successful_retcode(solve(prob3)) end @testset "Issue#3246: type promotion with parameter dependent initialization_eqs" begin From fbcacfb90e6471b10ef3380a32e30de732c5aba7 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 21 Jan 2025 14:31:27 +0530 Subject: [PATCH 0837/1337] test: update initializeprob type promotion test --- test/initializationsystem.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/initializationsystem.jl b/test/initializationsystem.jl index 8c99f730e9..971a7aa9ca 100644 --- a/test/initializationsystem.jl +++ b/test/initializationsystem.jl @@ -950,7 +950,7 @@ end prob2 = remake(prob; u0 = ForwardDiff.Dual.(prob.u0)) @test eltype(state_values(prob2.f.initialization_data.initializeprob)) <: ForwardDiff.Dual - @test eltype(prob2.f.initialization_data.initializeprob.p.tunable) <: Float64 + @test eltype(prob2.f.initialization_data.initializeprob.p.tunable) <: ForwardDiff.Dual @test state_values(prob2.f.initialization_data.initializeprob) ≈ state_values(prob.f.initialization_data.initializeprob) From 0d5adc5ce54c1d0c477573cfa044f0019455d805 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 23 Jan 2025 12:08:05 +0530 Subject: [PATCH 0838/1337] fix: don't build initialization for `JumpProblem` --- src/systems/jumps/jumpsystem.jl | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/systems/jumps/jumpsystem.jl b/src/systems/jumps/jumpsystem.jl index beaa78e1e3..4e72d0853a 100644 --- a/src/systems/jumps/jumpsystem.jl +++ b/src/systems/jumps/jumpsystem.jl @@ -416,7 +416,7 @@ function DiffEqBase.DiscreteProblem(sys::JumpSystem, u0map, tspan::Union{Tuple, end _f, u0, p = process_SciMLProblem(EmptySciMLFunction, sys, u0map, parammap; - t = tspan === nothing ? nothing : tspan[1], use_union, tofloat = false, check_length = false) + t = tspan === nothing ? nothing : tspan[1], use_union, tofloat = false, check_length = false, build_initializeprob = false) f = DiffEqBase.DISCRETE_INPLACE_DEFAULT observedfun = ObservedFunctionCache( @@ -513,7 +513,8 @@ function DiffEqBase.ODEProblem(sys::JumpSystem, u0map, tspan::Union{Tuple, Nothi parameter_dependencies = parameter_dependencies(sys), metadata = get_metadata(sys), gui_metadata = get_gui_metadata(sys)) osys = complete(osys) - return ODEProblem(osys, u0map, tspan, parammap; check_length = false, kwargs...) + return ODEProblem(osys, u0map, tspan, parammap; check_length = false, + build_initializeprob = false, kwargs...) else _, u0, p = process_SciMLProblem(EmptySciMLFunction, sys, u0map, parammap; t = tspan === nothing ? nothing : tspan[1], use_union, tofloat = false, From 2618c64df77a333260c70d0e556ad02a27943032 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 23 Jan 2025 12:08:26 +0530 Subject: [PATCH 0839/1337] fix: don't use observed equations for initialization of `DiscreteSystem` --- src/systems/nonlinear/initializesystem.jl | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/systems/nonlinear/initializesystem.jl b/src/systems/nonlinear/initializesystem.jl index 9736581135..ba8d8c5352 100644 --- a/src/systems/nonlinear/initializesystem.jl +++ b/src/systems/nonlinear/initializesystem.jl @@ -220,7 +220,10 @@ function generate_initializesystem(sys::AbstractSystem; pars = [pars; map(unwrap, collect(keys(new_params)))] is_time_dependent(sys) && push!(pars, get_iv(sys)) - if is_time_dependent(sys) + # FIXME: observed equations for discrete systems are broken. They don't express + # relations at the current time and instead express them in terms of past values. + # This precludes them from being useful in initialization. + if is_time_dependent(sys) && !(sys isa DiscreteSystem) # 8) use observed equations for guesses of observed variables if not provided for eq in trueobs haskey(defs, eq.lhs) && continue From ea8b8d618eae499c4801c6d9fea5b726fd1ec457 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 23 Jan 2025 15:18:44 +0530 Subject: [PATCH 0840/1337] fix: populate observed into `u0map` for time-independent systems --- src/systems/problem_utils.jl | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/systems/problem_utils.jl b/src/systems/problem_utils.jl index ed887e9386..21e0cec890 100644 --- a/src/systems/problem_utils.jl +++ b/src/systems/problem_utils.jl @@ -742,6 +742,8 @@ function process_SciMLProblem( cmap, cs = get_cmap(sys) kwargs = NamedTuple(kwargs) + is_time_dependent(sys) || add_observed!(sys, u0map) + op, missing_unknowns, missing_pars = build_operating_point( u0map, pmap, defs, cmap, dvs, ps) substitute_extra_variables!(sys, u0map) @@ -763,7 +765,7 @@ function process_SciMLProblem( op[iv] = t end - add_observed!(sys, op) + is_time_dependent(sys) && add_observed!(sys, op) add_parameter_dependencies!(sys, op) if warn_cyclic_dependency From c4c9ba2f77d08a11f61ad60c45f1251eccaccf70 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 24 Jan 2025 12:52:58 +0530 Subject: [PATCH 0841/1337] fix: remove incorrect fix to dummy derivatives in `u0map` added in #3337 --- src/systems/diffeqs/abstractodesystem.jl | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index 95a75dddad..1d641cd3cb 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -1313,20 +1313,6 @@ function InitializationProblem{iip, specialize}(sys::AbstractSystem, u0map = merge(ModelingToolkit.guesses(sys), todict(guesses), todict(u0map)) - # Replace dummy derivatives in u0map: D(x) -> x_t etc. - if has_schedule(sys) - schedule = get_schedule(sys) - if !isnothing(schedule) - for (var, val) in u0map - dvar = get(schedule.dummy_sub, var, var) # with dummy derivatives - if dvar !== var # then replace it - delete!(u0map, var) - push!(u0map, dvar => val) - end - end - end - end - fullmap = merge(u0map, parammap) u0T = Union{} for sym in unknowns(isys) From 3d60e045e4a2d73dec88f2514f15ccd36e17e5e9 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 24 Jan 2025 12:53:51 +0530 Subject: [PATCH 0842/1337] fix: do not build initialization for `DiscreteProblem` --- src/systems/discrete_system/discrete_system.jl | 2 +- src/systems/nonlinear/initializesystem.jl | 7 ++----- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/systems/discrete_system/discrete_system.jl b/src/systems/discrete_system/discrete_system.jl index cd6ff45b6c..cb33f5d339 100644 --- a/src/systems/discrete_system/discrete_system.jl +++ b/src/systems/discrete_system/discrete_system.jl @@ -307,7 +307,7 @@ function SciMLBase.DiscreteProblem( u0map = to_varmap(u0map, dvs) u0map = shift_u0map_forward(sys, u0map, defaults(sys)) f, u0, p = process_SciMLProblem( - DiscreteFunction, sys, u0map, parammap; eval_expression, eval_module) + DiscreteFunction, sys, u0map, parammap; eval_expression, eval_module, build_initializeprob = false) u0 = f(u0, p, tspan[1]) DiscreteProblem(f, u0, tspan, p; kwargs...) end diff --git a/src/systems/nonlinear/initializesystem.jl b/src/systems/nonlinear/initializesystem.jl index ba8d8c5352..a4edf23f6f 100644 --- a/src/systems/nonlinear/initializesystem.jl +++ b/src/systems/nonlinear/initializesystem.jl @@ -70,7 +70,7 @@ function generate_initializesystem(sys::AbstractSystem; append!(eqs_ics, eqs[idxs_alge]) # start equation list with algebraic equations eqs_diff = eqs[idxs_diff] - D = sys isa DiscreteSystem ? Shift(get_iv(sys), 1) : Differential(get_iv(sys)) + D = Differential(get_iv(sys)) diffmap = merge( Dict(eq.lhs => eq.rhs for eq in eqs_diff), Dict(D(eq.lhs) => D(eq.rhs) for eq in trueobs) @@ -220,10 +220,7 @@ function generate_initializesystem(sys::AbstractSystem; pars = [pars; map(unwrap, collect(keys(new_params)))] is_time_dependent(sys) && push!(pars, get_iv(sys)) - # FIXME: observed equations for discrete systems are broken. They don't express - # relations at the current time and instead express them in terms of past values. - # This precludes them from being useful in initialization. - if is_time_dependent(sys) && !(sys isa DiscreteSystem) + if is_time_dependent(sys) # 8) use observed equations for guesses of observed variables if not provided for eq in trueobs haskey(defs, eq.lhs) && continue From 2e6d172a5be5a2a3a77f3dac82f784b6138822db Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 24 Jan 2025 12:54:49 +0530 Subject: [PATCH 0843/1337] test: make `NonlinearSystem` initialization tests less temperamental --- test/initializationsystem.jl | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/test/initializationsystem.jl b/test/initializationsystem.jl index 971a7aa9ca..fb4f4199fe 100644 --- a/test/initializationsystem.jl +++ b/test/initializationsystem.jl @@ -786,20 +786,26 @@ end @testset "Parameter initialization" begin function test_parameter(prob, alg, param, val) integ = init(prob, alg) - @test integ.ps[param]≈val rtol=1e-6 + @test integ.ps[param]≈val rtol=1e-5 + # some algorithms are a little temperamental sol = solve(prob, alg) - @test sol.ps[param]≈val rtol=1e-6 + @test sol.ps[param]≈val rtol=1e-5 @test SciMLBase.successful_retcode(sol) end - @variables x=1.0 y=3.0 - @parameters p q - @named sys = NonlinearSystem( - [x + y - p ~ 0, x - q ~ 0]; defaults = [q => missing], - guesses = [q => 1.0], initialization_eqs = [p^2 + q^2 + 2p * q ~ 0]) - sys = complete(sys) + @parameters p=2.0 q=missing [guess = 1.0] c=1.0 + @variables x=1.0 y=2.0 z=3.0 + + eqs = [0 ~ p * (y - x), + 0 ~ x * (q - z) - y, + 0 ~ x * y - c * z] + @mtkbuild sys = NonlinearSystem(eqs; initialization_eqs = [p^2 + q^2 + 2p * q ~ 0]) + # @mtkbuild sys = NonlinearSystem( + # [p * x^2 + q * y^3 ~ 0, x - q ~ 0]; defaults = [q => missing], + # guesses = [q => 1.0], initialization_eqs = [p^2 + q^2 + 2p * q ~ 0]) + for (probT, algs) in prob_alg_combinations - prob = probT(sys, [], [p => 2.0]) + prob = probT(sys, []) @test prob.f.initialization_data !== nothing @test prob.f.initialization_data.initializeprobmap === nothing for alg in algs @@ -950,7 +956,8 @@ end prob2 = remake(prob; u0 = ForwardDiff.Dual.(prob.u0)) @test eltype(state_values(prob2.f.initialization_data.initializeprob)) <: ForwardDiff.Dual - @test eltype(prob2.f.initialization_data.initializeprob.p.tunable) <: ForwardDiff.Dual + @test eltype(prob2.f.initialization_data.initializeprob.p.tunable) <: + ForwardDiff.Dual @test state_values(prob2.f.initialization_data.initializeprob) ≈ state_values(prob.f.initialization_data.initializeprob) From 44a28afc694c009ad4377e11389428637f303225 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 24 Jan 2025 12:55:00 +0530 Subject: [PATCH 0844/1337] test: mark initialization of `JumpSystem` as broken --- test/initializationsystem.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/initializationsystem.jl b/test/initializationsystem.jl index fb4f4199fe..fe5f03b9c7 100644 --- a/test/initializationsystem.jl +++ b/test/initializationsystem.jl @@ -1287,7 +1287,7 @@ end u0s = [I => 1, R => 0] ps = [S0 => 999, β => 0.01, γ => 0.001] dprob = DiscreteProblem(js, u0s, (0.0, 10.0), ps) - @test dprob.f.initialization_data !== nothing + @test_broken dprob.f.initialization_data !== nothing sol = solve(dprob, FunctionMap()) @test sol[S, 1] ≈ 999 @test SciMLBase.successful_retcode(sol) From cab4506c9d5da1a297915081a92e99b11af2cce7 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 24 Jan 2025 12:55:17 +0530 Subject: [PATCH 0845/1337] fix: fix `ReconstructInitializeprob` overriding guesses --- src/systems/nonlinear/initializesystem.jl | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/systems/nonlinear/initializesystem.jl b/src/systems/nonlinear/initializesystem.jl index a4edf23f6f..d323e29f1c 100644 --- a/src/systems/nonlinear/initializesystem.jl +++ b/src/systems/nonlinear/initializesystem.jl @@ -333,10 +333,9 @@ end function ReconstructInitializeprob( srcsys::AbstractSystem, dstsys::AbstractSystem; remap = Dict()) - syms = [unknowns(dstsys); - reduce(vcat, reorder_parameters(dstsys, parameters(dstsys)); init = [])] + syms = reduce(vcat, reorder_parameters(dstsys, parameters(dstsys)); init = []) getter = getu(srcsys, map(x -> get(remap, x, x), syms)) - setter = setsym_oop(dstsys, syms) + setter = setp_oop(dstsys, syms) return ReconstructInitializeprob(getter, setter) end From 8d11252db0d1192c37e4095159cc40b037f41bd5 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 20 Jan 2025 17:34:23 +0530 Subject: [PATCH 0846/1337] feat: propagate `algebraic_only` kwarg in `InitializationProblem` --- src/systems/diffeqs/abstractodesystem.jl | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index 1d641cd3cb..b4c51b52e9 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -1221,6 +1221,7 @@ function InitializationProblem{iip, specialize}(sys::AbstractSystem, use_scc = true, allow_incomplete = false, force_time_independent = false, + algebraic_only = false, kwargs...) where {iip, specialize} if !iscomplete(sys) error("A completed system is required. Call `complete` or `structural_simplify` on the system before creating an `ODEProblem`") @@ -1231,12 +1232,12 @@ function InitializationProblem{iip, specialize}(sys::AbstractSystem, elseif isempty(u0map) && get_initializesystem(sys) === nothing isys = generate_initializesystem( sys; initialization_eqs, check_units, pmap = parammap, - guesses, extra_metadata = (; use_scc)) + guesses, extra_metadata = (; use_scc), algebraic_only) simplify_system = true else isys = generate_initializesystem( sys; u0map, initialization_eqs, check_units, - pmap = parammap, guesses, extra_metadata = (; use_scc)) + pmap = parammap, guesses, extra_metadata = (; use_scc), algebraic_only) simplify_system = true end From 33fe6cd5f9575e29cc576861ae53745fc3eb3864 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 20 Jan 2025 17:34:54 +0530 Subject: [PATCH 0847/1337] fix: filter `nothing` values in `op`/`u0map`/`pmap` in `process_SciMLProblem` --- src/systems/problem_utils.jl | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/systems/problem_utils.jl b/src/systems/problem_utils.jl index 21e0cec890..a0c001f7ab 100644 --- a/src/systems/problem_utils.jl +++ b/src/systems/problem_utils.jl @@ -746,6 +746,9 @@ function process_SciMLProblem( op, missing_unknowns, missing_pars = build_operating_point( u0map, pmap, defs, cmap, dvs, ps) + filter_missing_values!(op) + filter_missing_values!(u0map) + filter_missing_values!(pmap) substitute_extra_variables!(sys, u0map) substitute_extra_variables!(sys, pmap) From 3e37d24628cf9b4481df2c5049691b7a4a1bd896 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 20 Jan 2025 17:35:23 +0530 Subject: [PATCH 0848/1337] feat: propagate `algebraic_only`, `allow_incomplete` kwargs to initialization in `process_SciMLProblem` --- src/systems/problem_utils.jl | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/systems/problem_utils.jl b/src/systems/problem_utils.jl index a0c001f7ab..9160366195 100644 --- a/src/systems/problem_utils.jl +++ b/src/systems/problem_utils.jl @@ -708,6 +708,8 @@ Keyword arguments: determined. - `force_initialization_time_independent`: Whether to force the initialization to not use the independent variable of `sys`. +- `algebraic_only`: Whether to build the initialization problem using only algebraic equations. +- `allow_incomplete`: Whether to allow incomplete initialization problems. All other keyword arguments are passed as-is to `constructor`. """ @@ -722,7 +724,8 @@ function process_SciMLProblem( circular_dependency_max_cycle_length = length(all_symbols(sys)), circular_dependency_max_cycles = 10, substitution_limit = 100, use_scc = true, - force_initialization_time_independent = false, kwargs...) + force_initialization_time_independent = false, algebraic_only = false, + allow_incomplete = false, kwargs...) dvs = unknowns(sys) ps = parameters(sys) iv = has_iv(sys) ? get_iv(sys) : nothing @@ -759,7 +762,7 @@ function process_SciMLProblem( eval_expression, eval_module, fully_determined, warn_cyclic_dependency, check_units = check_initialization_units, circular_dependency_max_cycle_length, circular_dependency_max_cycles, use_scc, - force_time_independent = force_initialization_time_independent) + force_time_independent = force_initialization_time_independent, algebraic_only, allow_incomplete) kwargs = merge(kwargs, kws) end From bca43ba370786387205d84b45d365b088dd8ed52 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 20 Jan 2025 17:37:20 +0530 Subject: [PATCH 0849/1337] fix: use `SciMLBase.get_initial_values` in `linearization_function` --- src/ModelingToolkit.jl | 2 +- src/systems/abstractsystem.jl | 148 +++++++++++++++------------------- 2 files changed, 68 insertions(+), 82 deletions(-) diff --git a/src/ModelingToolkit.jl b/src/ModelingToolkit.jl index 09b59c4ed6..303c88da73 100644 --- a/src/ModelingToolkit.jl +++ b/src/ModelingToolkit.jl @@ -45,7 +45,7 @@ using Compat using AbstractTrees using DiffEqBase, SciMLBase, ForwardDiff using SciMLBase: StandardODEProblem, StandardNonlinearProblem, handle_varmap, TimeDomain, - PeriodicClock, Clock, SolverStepClock, Continuous + PeriodicClock, Clock, SolverStepClock, Continuous, OverrideInit, NoInit using Distributed import JuliaFormatter using MLStyle diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index 8213b8f241..1fab568cb4 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -2394,6 +2394,9 @@ See also [`linearize`](@ref) which provides a higher-level interface. function linearization_function(sys::AbstractSystem, inputs, outputs; simplify = false, initialize = true, + initializealg = nothing, + initialization_abstol = 1e-5, + initialization_reltol = 1e-3, op = Dict(), p = DiffEqBase.NullParameters(), zero_dummy_der = false, @@ -2420,88 +2423,33 @@ function linearization_function(sys::AbstractSystem, inputs, op = merge(defs, op) end sys = ssys - u0map = Dict(k => v for (k, v) in op if is_variable(ssys, k)) - initsys = structural_simplify( - generate_initializesystem( - sys, u0map = u0map, guesses = guesses(sys), algebraic_only = true), - fully_determined = false) - - # HACK: some unknowns may not be involved in any initialization equations, and are - # thus removed from the system during `structural_simplify`. - # This causes `getu(initsys, unknowns(sys))` to fail, so we add them back as parameters - # for now. - missing_unknowns = setdiff(unknowns(sys), all_symbols(initsys)) - if !isempty(missing_unknowns) - if warn_initialize_determined - @warn "Initialization system is underdetermined. No equations for $(missing_unknowns). Initialization will default to using least squares. To suppress this warning pass warn_initialize_determined = false." - end - new_parameters = [parameters(initsys); missing_unknowns] - @set! initsys.ps = new_parameters - initsys = complete(initsys) - end - - if p isa SciMLBase.NullParameters - p = Dict() - else - p = todict(p) - end - x0 = merge(defaults_and_guesses(sys), op) - if has_index_cache(sys) && get_index_cache(sys) !== nothing - sys_ps = MTKParameters(sys, p, x0) - else - sys_ps = varmap_to_vars(p, parameters(sys); defaults = x0) - end - p[get_iv(sys)] = NaN - if has_index_cache(initsys) && get_index_cache(initsys) !== nothing - oldps = MTKParameters(initsys, p, merge(guesses(sys), defaults(sys), op)) - initsys_ps = parameters(initsys) - p_getter = build_explicit_observed_function( - sys, initsys_ps; eval_expression, eval_module) - - u_getter = isempty(unknowns(initsys)) ? (_...) -> nothing : - build_explicit_observed_function( - sys, unknowns(initsys); eval_expression, eval_module) - get_initprob_u_p = let p_getter = p_getter, - p_setter! = setp(initsys, initsys_ps), - u_getter = u_getter - - function (u, p, t) - p_setter!(oldps, p_getter(u, p, t)) - newu = u_getter(u, p, t) - return newu, oldps - end - end - else - get_initprob_u_p = let p_getter = getu(sys, parameters(initsys)), - u_getter = build_explicit_observed_function( - sys, unknowns(initsys); eval_expression, eval_module) - - function (u, p, t) - state = ProblemState(; u, p, t) - return u_getter( - state_values(state), parameter_values(state), current_time(state)), - p_getter(state) - end - end + + if initializealg === nothing + initializealg = initialize ? OverrideInit() : NoInit() end - initfn = NonlinearFunction(initsys; eval_expression, eval_module) - initprobmap = build_explicit_observed_function( - initsys, unknowns(sys); eval_expression, eval_module) + + fun, u0, p = process_SciMLProblem( + ODEFunction{true, SciMLBase.FullSpecialize}, sys, merge( + defaults_and_guesses(sys), op), p; + t = 0.0, build_initializeprob = initializealg isa OverrideInit, + allow_incomplete = true, algebraic_only = true) + prob = ODEProblem(fun, u0, (nothing, nothing), p) + ps = parameters(sys) h = build_explicit_observed_function(sys, outputs; eval_expression, eval_module) lin_fun = let diff_idxs = diff_idxs, alge_idxs = alge_idxs, input_idxs = input_idxs, sts = unknowns(sys), - get_initprob_u_p = get_initprob_u_p, - fun = ODEFunction{true, SciMLBase.FullSpecialize}( - sys, unknowns(sys), ps; eval_expression, eval_module), - initfn = initfn, - initprobmap = initprobmap, + fun = fun, + prob = prob, + sys_ps = p, h = h, + integ_cache = (similar(u0)), chunk = ForwardDiff.Chunk(input_idxs), - sys_ps = sys_ps, - initialize = initialize, + initializealg = initializealg, + initialization_abstol = initialization_abstol, + initialization_reltol = initialization_reltol, initialization_solver_alg = initialization_solver_alg, sys = sys @@ -2521,14 +2469,14 @@ function linearization_function(sys::AbstractSystem, inputs, if u !== nothing # Handle systems without unknowns length(sts) == length(u) || error("Number of unknown variables ($(length(sts))) does not match the number of input unknowns ($(length(u)))") - if initialize && !isempty(alge_idxs) # This is expensive and can be omitted if the user knows that the system is already initialized - residual = fun(u, p, t) - if norm(residual[alge_idxs]) > √(eps(eltype(residual))) - initu0, initp = get_initprob_u_p(u, p, t) - initprob = NonlinearLeastSquaresProblem(initfn, initu0, initp) - nlsol = solve(initprob, initialization_solver_alg) - u = initprobmap(state_values(nlsol), parameter_values(nlsol)) - end + + integ = MockIntegrator{true}(u, p, t, integ_cache) + u, p, success = SciMLBase.get_initial_values( + prob, integ, fun, initializealg, Val(true); + abstol = initialization_abstol, reltol = initialization_reltol, + nlsolve_alg = initialization_solver_alg) + if !success + error("Initialization algorithm $(initializealg) failed with `u = $u` and `p = $p`.") end uf = SciMLBase.UJacobianWrapper(fun, t, p) fg_xz = ForwardDiff.jacobian(uf, u) @@ -2563,6 +2511,44 @@ function linearization_function(sys::AbstractSystem, inputs, return lin_fun, sys end +""" + $(TYPEDEF) + +Mock `DEIntegrator` to allow using `CheckInit` without having to create a new integrator +(and consequently depend on `OrdinaryDiffEq`). + +# Fields + +$(TYPEDFIELDS) +""" +struct MockIntegrator{iip, U, P, T, C} <: SciMLBase.DEIntegrator{Nothing, iip, U, T} + """ + The state vector. + """ + u::U + """ + The parameter object. + """ + p::P + """ + The current time. + """ + t::T + """ + The integrator cache. + """ + cache::C +end + +function MockIntegrator{iip}(u::U, p::P, t::T, cache::C) where {iip, U, P, T, C} + return MockIntegrator{iip, U, P, T, C}(u, p, t, cache) +end + +SymbolicIndexingInterface.state_values(integ::MockIntegrator) = integ.u +SymbolicIndexingInterface.parameter_values(integ::MockIntegrator) = integ.p +SymbolicIndexingInterface.current_time(integ::MockIntegrator) = integ.t +SciMLBase.get_tmp_cache(integ::MockIntegrator) = integ.cache + """ (; A, B, C, D), simplified_sys = linearize_symbolic(sys::AbstractSystem, inputs, outputs; simplify = false, allow_input_derivatives = false, kwargs...) From f524be8f738c66049f301e572773ed21f89f42a8 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 20 Jan 2025 17:45:32 +0530 Subject: [PATCH 0850/1337] fix: do not forward keywords to `linearize` in `AnalysisPoint` linearization functions --- src/systems/analysis_points.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/systems/analysis_points.jl b/src/systems/analysis_points.jl index 9ce5fe2bc1..82b8d9507f 100644 --- a/src/systems/analysis_points.jl +++ b/src/systems/analysis_points.jl @@ -863,7 +863,7 @@ for f in [:get_sensitivity, :get_comp_sensitivity, :get_looptransfer] sys, ap, args...; loop_openings = [], system_modifier = identity, kwargs...) lin_fun, ssys = $(utility_fun)( sys, ap, args...; loop_openings, system_modifier, kwargs...) - ModelingToolkit.linearize(ssys, lin_fun; kwargs...), ssys + ModelingToolkit.linearize(ssys, lin_fun), ssys end end From 77939a3f31e4b37ac8dbdbc50c08c2d2062f0246 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 4 Feb 2025 11:55:38 +0530 Subject: [PATCH 0851/1337] fix: handle `=> nothing` entries in `u0map` in `generate_initializesystem` --- src/systems/nonlinear/initializesystem.jl | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/systems/nonlinear/initializesystem.jl b/src/systems/nonlinear/initializesystem.jl index d323e29f1c..f7fe7ea077 100644 --- a/src/systems/nonlinear/initializesystem.jl +++ b/src/systems/nonlinear/initializesystem.jl @@ -31,6 +31,13 @@ function generate_initializesystem(sys::AbstractSystem; idxs_diff = isdiffeq.(eqs) # PREPROCESSING + # If `=> nothing` in `u0map`, remove the key from `defs` + u0map = copy(anydict(u0map)) + for (k, v) in u0map + v === nothing || continue + delete!(defs, k) + end + filter_missing_values!(u0map) # for initial conditions of the form `var => constant`, we instead turn them into # `var ~ var0` where `var0` is a new parameter, and make `update_initializeprob!` # update `initializeprob.ps[var0] = prob[var]`. @@ -38,7 +45,6 @@ function generate_initializesystem(sys::AbstractSystem; # map parameters in `initprob` which need to be updated in `update_initializeprob!` # to the corresponding expressions that determine their values new_params = Dict() - u0map = copy(anydict(u0map)) pmap = copy(anydict(pmap)) if is_time_dependent(sys) for (k, v) in u0map From 7bdc1f60fd3533c67776705b54790ed21562def9 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 4 Feb 2025 11:57:07 +0530 Subject: [PATCH 0852/1337] fix: output variable in `PerturbOutput` should not have a default --- src/systems/analysis_points.jl | 1 - 1 file changed, 1 deletion(-) diff --git a/src/systems/analysis_points.jl b/src/systems/analysis_points.jl index 82b8d9507f..022a0909ed 100644 --- a/src/systems/analysis_points.jl +++ b/src/systems/analysis_points.jl @@ -591,7 +591,6 @@ function apply_transformation(tf::PerturbOutput, sys::AbstractSystem) # add output variable, equation, default out_var, out_def = get_analysis_variable( ap_ivar, nameof(ap), get_iv(sys); perturb = false) - defs[out_var] = out_def push!(ap_sys_eqs, out_var ~ ap_ivar + wrap(new_var)) push!(unks, out_var) From a3351ff28020e7d6d15192d41662b7075536a565 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 4 Feb 2025 11:58:12 +0530 Subject: [PATCH 0853/1337] fix: better handle `=> nothing` entries in `u0map` --- src/systems/problem_utils.jl | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/src/systems/problem_utils.jl b/src/systems/problem_utils.jl index 9160366195..99326aa285 100644 --- a/src/systems/problem_utils.jl +++ b/src/systems/problem_utils.jl @@ -442,9 +442,20 @@ end $(TYPEDSIGNATURES) Remove keys in `varmap` whose values are `nothing`. + +If `missing_values` is not `nothing`, it is assumed to be a collection and all removed +keys will be added to it. """ -function filter_missing_values!(varmap::AbstractDict) - filter!(kvp -> kvp[2] !== nothing, varmap) +function filter_missing_values!(varmap::AbstractDict; missing_values = nothing) + filter!(varmap) do kvp + if kvp[2] !== nothing + return true + end + if missing_values !== nothing + push!(missing_values, kvp[1]) + end + return false + end end struct GetUpdatedMTKParameters{G, S} @@ -523,14 +534,17 @@ function build_operating_point( haskey(op, k) && continue op[k] = v end + filter_missing_values!(op; missing_values = missing_unknowns) + merge!(op, pmap) missing_pars = add_fallbacks!(op, ps, defs) + filter_missing_values!(op; missing_values = missing_pars) for eq in cmap op[eq.lhs] = eq.rhs end - empty!(u0map) - empty!(pmap) + filter!(kvp -> kvp[2] === nothing, u0map) + filter!(kvp -> kvp[2] === nothing, pmap) for (k, v) in op k = unwrap(k) if isparameter(k) @@ -539,6 +553,7 @@ function build_operating_point( u0map[k] = v end end + return op, missing_unknowns, missing_pars end @@ -749,9 +764,6 @@ function process_SciMLProblem( op, missing_unknowns, missing_pars = build_operating_point( u0map, pmap, defs, cmap, dvs, ps) - filter_missing_values!(op) - filter_missing_values!(u0map) - filter_missing_values!(pmap) substitute_extra_variables!(sys, u0map) substitute_extra_variables!(sys, pmap) From 54bc7146f84d1bca4e0926a6cda78e85c25b9e49 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 4 Feb 2025 11:59:59 +0530 Subject: [PATCH 0854/1337] test: fix some issues with initialization of InverseControlledTank --- test/downstream/inversemodel.jl | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/test/downstream/inversemodel.jl b/test/downstream/inversemodel.jl index 8c2c6bebb9..523f8e6c0e 100644 --- a/test/downstream/inversemodel.jl +++ b/test/downstream/inversemodel.jl @@ -32,7 +32,7 @@ rc = 0.25 # Reference concentration gamma(t), [description = "Reaction speed"] xc(t) = c0, [description = "Concentration"] xT(t) = T0, [description = "Temperature"] - xT_c(t) = T0, [description = "Cooling temperature"] + xT_c(t), [description = "Cooling temperature"] end @components begin @@ -77,7 +77,11 @@ begin end # Create an MTK-compatible constructor - RefFilter(; y0, name) = ODESystem(Fss; name, x0 = init_filter(y0)) + function RefFilter(; name) + sys = ODESystem(Fss; name) + delete!(defaults(sys), @nonamespace(sys.x)[1]) + return sys + end end @mtkmodel InverseControlledTank begin begin @@ -97,10 +101,10 @@ end ff_gain = Gain(k = 1) # To allow turning ff off controller = PI(gainPI.k = 10, T = 500) tank = MixingTank(xc = c_start, xT = T_start, c0 = c0, T0 = T0) - inverse_tank = MixingTank(xc = c_start, xT = T_start, c0 = c0, T0 = T0) + inverse_tank = MixingTank(xc = nothing, xT = T_start, c0 = c0, T0 = T0) feedback = Feedback() add = Add() - filter = RefFilter(y0 = c_start) # Initialize filter states to the initial concentration + filter = RefFilter() noise_filter = FirstOrder(k = 1, T = 1, x = T_start) # limiter = Gain(k=1) limiter = Limiter(y_max = 370, y_min = 250) # Saturate the control input From d626783dd912c54dfc7191c0a8f699c56093abcc Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 4 Feb 2025 12:00:28 +0530 Subject: [PATCH 0855/1337] test: add #3319 as a test --- test/downstream/inversemodel.jl | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/test/downstream/inversemodel.jl b/test/downstream/inversemodel.jl index 523f8e6c0e..7102995b66 100644 --- a/test/downstream/inversemodel.jl +++ b/test/downstream/inversemodel.jl @@ -173,3 +173,32 @@ matrices2, _ = Blocks.get_comp_sensitivity(model, :y; op) # Test that we get the @test_broken matrices1.f_x ≈ matrices2.A[1:7, 1:7] nsys = get_named_comp_sensitivity(model, :y; op) # Test that we get the same result when calling an even higher-level API @test matrices2.A ≈ nsys.A + +@testset "Issue #3319" begin + op1 = Dict( + D(cm.inverse_tank.xT) => 1, + cm.tank.xc => 0.65 + ) + + op2 = Dict( + D(cm.inverse_tank.xT) => 1, + cm.tank.xc => 0.45 + ) + + output = :y + lin_fun, ssys = Blocks.get_sensitivity_function(model, output) + matrices1 = linearize(ssys, lin_fun, op = op1) + matrices2 = linearize(ssys, lin_fun, op = op2) + S1f = ss(matrices1...) + S2f = ss(matrices2...) + @test S1f != S2f + + matrices1, ssys = Blocks.get_sensitivity(model, output; op = op1) + matrices2, ssys = Blocks.get_sensitivity(model, output; op = op2) + S1 = ss(matrices1...) + S2 = ss(matrices2...) + @test S1 != S2 + + @test S1 == S1f + @test S2 == S2f +end From e2dc475e33a6d8b40f0a6e18aacc4ac000551b74 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 4 Feb 2025 13:16:41 +0530 Subject: [PATCH 0856/1337] test: fix initialization of model in tests --- test/input_output_handling.jl | 3 ++- test/split_parameters.jl | 6 ++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/test/input_output_handling.jl b/test/input_output_handling.jl index de6fc92b5c..382c002108 100644 --- a/test/input_output_handling.jl +++ b/test/input_output_handling.jl @@ -131,7 +131,8 @@ model = ODESystem(eqs, t; systems = [torque, inertia1, inertia2, spring, damper] name = :name) model_outputs = [inertia1.w, inertia2.w, inertia1.phi, inertia2.phi] model_inputs = [torque.tau.u] -matrices, ssys = linearize(model, model_inputs, model_outputs); +matrices, ssys = linearize( + model, model_inputs, model_outputs); @test length(ModelingToolkit.outputs(ssys)) == 4 if VERSION >= v"1.8" # :opaque_closure not supported before diff --git a/test/split_parameters.jl b/test/split_parameters.jl index 2f8667faa8..b4b7314838 100644 --- a/test/split_parameters.jl +++ b/test/split_parameters.jl @@ -164,14 +164,16 @@ function SystemModel(u = nothing; name = :model) t; systems = [torque, inertia1, inertia2, spring, damper, u]) end - ODESystem(eqs, t; systems = [torque, inertia1, inertia2, spring, damper], name) + ODESystem(eqs, t; systems = [torque, inertia1, inertia2, spring, damper], + name) end model = SystemModel() # Model with load disturbance @named d = Step(start_time = 1.0, duration = 10.0, offset = 0.0, height = 1.0) # Disturbance model_outputs = [model.inertia1.w, model.inertia2.w, model.inertia1.phi, model.inertia2.phi] # This is the state realization we want to control inputs = [model.torque.tau.u] -matrices, ssys = ModelingToolkit.linearize(wr(model), inputs, model_outputs) +matrices, ssys = ModelingToolkit.linearize( + wr(model), inputs, model_outputs) # Design state-feedback gain using LQR # Define cost matrices From 5a75d6590948b904ca450d58bd6e9226ad260cd3 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 5 Feb 2025 17:09:03 +0530 Subject: [PATCH 0857/1337] refactor: remove `wrap_array_vars`, `wrap_mtkparameters`, `wrap_parameter_dependencies` --- src/systems/abstractsystem.jl | 209 ---------------------------------- 1 file changed, 209 deletions(-) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index 1fab568cb4..9929e5d010 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -206,217 +206,8 @@ function wrap_assignments(isscalar, assignments; let_block = false) end end -function wrap_parameter_dependencies(sys::AbstractSystem, isscalar) - wrap_assignments(isscalar, [eq.lhs ← eq.rhs for eq in parameter_dependencies(sys)]) -end - -""" - $(TYPEDSIGNATURES) - -Add the necessary assignment statements to allow use of unscalarized array variables -in the generated code. `expr` is the expression returned by the function. `dvs` and -`ps` are the unknowns and parameters of the system `sys` to use in the generated code. -`inputs` can be specified as an array of symbolics if the generated function has inputs. -If `history == true`, the generated function accepts a history function. `cachesyms` are -extra variables (arrays of variables) stored in the cache array(s) of the parameter -object. `extra_args` are extra arguments appended to the end of the argument list. - -The function is assumed to have the signature `f(du, u, h, x, p, cache_syms..., t, extra_args...)` -Where: -- `du` is the optional buffer to write to for in-place functions. -- `u` is the list of unknowns. This argument is not present if `dvs === nothing`. -- `h` is the optional history function, present if `history == true`. -- `x` is the array of inputs, present only if `inputs !== nothing`. Values are assumed - to be in the order of variables passed to `inputs`. -- `p` is the parameter object. -- `cache_syms` are the cache variables. These are part of the splatted parameter object. -- `t` is time, present only if the system is time dependent. -- `extra_args` are the extra arguments passed to the function, present only if - `extra_args` is non-empty. -""" -function wrap_array_vars( - sys::AbstractSystem, exprs; dvs = unknowns(sys), ps = parameters(sys), - inputs = nothing, history = false, cachesyms::Tuple = (), extra_args::Tuple = ()) - isscalar = !(exprs isa AbstractArray) - var_to_arridxs = Dict() - - if dvs === nothing - uind = 0 - else - uind = 1 - for (i, x) in enumerate(dvs) - iscall(x) && operation(x) == getindex || continue - arg = arguments(x)[1] - inds = get!(() -> [], var_to_arridxs, arg) - push!(inds, (uind, i)) - end - end - p_start = uind + 1 + history - rps = (reorder_parameters(sys, ps)..., cachesyms...) - if inputs !== nothing - rps = (inputs, rps...) - end - if has_iv(sys) - rps = (rps..., get_iv(sys)) - end - rps = (rps..., extra_args...) - for sym in reduce(vcat, rps; init = []) - iscall(sym) && operation(sym) == getindex || continue - arg = arguments(sym)[1] - - bufferidx = findfirst(buf -> any(isequal(sym), buf), rps) - idxinbuffer = findfirst(isequal(sym), rps[bufferidx]) - inds = get!(() -> [], var_to_arridxs, arg) - push!(inds, (p_start + bufferidx - 1, idxinbuffer)) - end - - viewsyms = Dict() - splitsyms = Dict() - for (arrsym, idxs) in var_to_arridxs - length(idxs) == length(arrsym) || continue - # allequal(first, idxs) is a 1.11 feature - if allequal(Iterators.map(first, idxs)) - viewsyms[arrsym] = (first(first(idxs)), reshape(last.(idxs), size(arrsym))) - else - splitsyms[arrsym] = reshape(idxs, size(arrsym)) - end - end - if isscalar - function (expr) - Func( - expr.args, - [], - Let( - vcat( - [sym ← :(view($(expr.args[i].name), $idxs)) - for (sym, (i, idxs)) in viewsyms], - [sym ← - MakeArray([expr.args[bufi].elems[vali] for (bufi, vali) in idxs], - expr.args[idxs[1][1]]) for (sym, idxs) in splitsyms] - ), - expr.body, - false - ) - ) - end - else - function (expr) - Func( - expr.args, - [], - Let( - vcat( - [sym ← :(view($(expr.args[i].name), $idxs)) - for (sym, (i, idxs)) in viewsyms], - [sym ← - MakeArray([expr.args[bufi].elems[vali] for (bufi, vali) in idxs], - expr.args[idxs[1][1]]) for (sym, idxs) in splitsyms] - ), - expr.body, - false - ) - ) - end, - function (expr) - Func( - expr.args, - [], - Let( - vcat( - [sym ← :(view($(expr.args[i + 1].name), $idxs)) - for (sym, (i, idxs)) in viewsyms], - [sym ← MakeArray( - [expr.args[bufi + 1].elems[vali] for (bufi, vali) in idxs], - expr.args[idxs[1][1] + 1]) for (sym, idxs) in splitsyms] - ), - expr.body, - false - ) - ) - end - end -end - const MTKPARAMETERS_ARG = Sym{Vector{Vector}}(:___mtkparameters___) -""" - wrap_mtkparameters(sys::AbstractSystem, isscalar::Bool, p_start = 2, offset = Int(is_time_dependent(sys))) - -Return function(s) to be passed to the `wrap_code` keyword of `build_function` which -allow the compiled function to be called as `f(u, p, t)` where `p isa MTKParameters` -instead of `f(u, p..., t)`. `isscalar` denotes whether the function expression being -wrapped is for a scalar value. `p_start` is the index of the argument containing -the first parameter vector in the out-of-place version of the function. For example, -if a history function (DDEs) was passed before `p`, then the function before wrapping -would have the signature `f(u, h, p..., t)` and hence `p_start` would need to be `3`. - -`offset` is the number of arguments at the end of the argument list to ignore. Defaults -to 1 if the system is time-dependent (to ignore `t`) and 0 otherwise. - -The returned function is `identity` if the system does not have an `IndexCache`. -""" -function wrap_mtkparameters(sys::AbstractSystem, isscalar::Bool, p_start = 2, - offset = Int(is_time_dependent(sys))) - if has_index_cache(sys) && get_index_cache(sys) !== nothing - if isscalar - function (expr) - param_args = expr.args[p_start:(end - offset)] - param_buffer_idxs = findall(x -> x isa DestructuredArgs, param_args) - param_buffer_args = param_args[param_buffer_idxs] - destructured_mtkparams = DestructuredArgs( - [x.name for x in param_buffer_args], - MTKPARAMETERS_ARG; inds = param_buffer_idxs) - Func( - [ - expr.args[begin:(p_start - 1)]..., - destructured_mtkparams, - expr.args[(end - offset + 1):end]... - ], - [], - Let(param_buffer_args, expr.body, false) - ) - end - else - function (expr) - param_args = expr.args[p_start:(end - offset)] - param_buffer_idxs = findall(x -> x isa DestructuredArgs, param_args) - param_buffer_args = param_args[param_buffer_idxs] - destructured_mtkparams = DestructuredArgs( - [x.name for x in param_buffer_args], - MTKPARAMETERS_ARG; inds = param_buffer_idxs) - Func( - [ - expr.args[begin:(p_start - 1)]..., - destructured_mtkparams, - expr.args[(end - offset + 1):end]... - ], - [], - Let(param_buffer_args, expr.body, false) - ) - end, - function (expr) - param_args = expr.args[(p_start + 1):(end - offset)] - param_buffer_idxs = findall(x -> x isa DestructuredArgs, param_args) - param_buffer_args = param_args[param_buffer_idxs] - destructured_mtkparams = DestructuredArgs( - [x.name for x in param_buffer_args], - MTKPARAMETERS_ARG; inds = param_buffer_idxs) - Func( - [ - expr.args[begin:p_start]..., - destructured_mtkparams, - expr.args[(end - offset + 1):end]... - ], - [], - Let(param_buffer_args, expr.body, false) - ) - end - end - else - identity - end -end - mutable struct Substitutions subs::Vector{Equation} deps::Vector{Vector{Int}} From 2956f0b36fa3f7dbc5e51a12d378bc35ee4a0493 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 5 Feb 2025 17:10:23 +0530 Subject: [PATCH 0858/1337] feat: add `Initial` operator --- src/systems/abstractsystem.jl | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index 9929e5d010..ea988e4c37 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -650,6 +650,40 @@ function isscheduled(sys::AbstractSystem) end end +struct Initial <: Symbolics.Operator end +Initial(x) = Initial()(x) +SymbolicUtils.promote_symtype(::Type{Initial}, T) = T +SymbolicUtils.isbinop(::Initial) = false +Base.nameof(::Initial) = :Initial +Base.show(io::IO, x::Initial) = print(io, "Initial") +input_timedomain(::Initial, _ = nothing) = Continuous() +output_timedomain(::Initial, _ = nothing) = Continuous() + +function (f::Initial)(x) + iw = Symbolics.iswrapped(x) + x = unwrap(x) + if symbolic_type(x) == NotSymbolic() + return x + end + if iscall(x) && operation(x) isa Differential + x = default_toterm(x) + end + iscall(x) && operation(x) isa Initial && return x + result = if symbolic_type(x) == ArraySymbolic() + Symbolics.array_term(f, toparam(x)) + elseif iscall(x) && operation(x) == getindex + arr = arguments(x)[1] + term(getindex, f(toparam(arr)), arguments(x)[2:end]...) + else + term(f, toparam(x)) + end + result = toparam(result) + if iw + result = wrap(result) + end + return result +end + """ $(TYPEDSIGNATURES) From 3096d09be4bb0228c816ca916f4803a1055aaa78 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 5 Feb 2025 17:10:44 +0530 Subject: [PATCH 0859/1337] feat: add initialization parameters in `complete` --- src/systems/abstractsystem.jl | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index ea988e4c37..1bb73657ba 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -684,6 +684,31 @@ function (f::Initial)(x) return result end +function add_initialization_parameters(sys::AbstractSystem) + is_time_dependent(sys) || return sys + @assert !has_systems(sys) || isempty(get_systems(sys)) + eqs = equations(sys) + if !(eqs isa Vector{Equation}) + eqs = Equation[x for x in eqs if x isa Equation] + end + obs, eqs = unhack_observed(observed(sys), eqs) + all_uvars = Set{BasicSymbolic}() + for x in Iterators.flatten((unknowns(sys), Iterators.map(eq -> eq.lhs, obs))) + x = unwrap(x) + if iscall(x) && operation(x) == getindex + push!(all_uvars, arguments(x)[1]) + else + push!(all_uvars, x) + end + end + all_uvars = collect(all_uvars) + initials = map(Initial(), all_uvars) + # existing_initials = filter(x -> iscall(x) && (operation(x) isa Initial), parameters(sys)) + @set! sys.ps = unique!([get_ps(sys); initials]) + @set! sys.defaults = merge(get_defaults(sys), Dict(initials .=> zero_var.(initials))) + return sys +end + """ $(TYPEDSIGNATURES) @@ -716,6 +741,7 @@ function complete(sys::AbstractSystem; split = true, flatten = true) @set! newsys.parent = complete(sys; split = false, flatten = false) end sys = newsys + sys = add_initialization_parameters(sys) end if split && has_index_cache(sys) @set! sys.index_cache = IndexCache(sys) From c916452b8fc388182ba49db4b4781475be724feb Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 5 Feb 2025 17:11:16 +0530 Subject: [PATCH 0860/1337] feat: optionally filter `Initial` parameters in `parameters` --- src/systems/abstractsystem.jl | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index 1bb73657ba..2bb7db41cf 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -1277,7 +1277,7 @@ Get the parameters of the system `sys` and its subsystems. See also [`@parameters`](@ref) and [`ModelingToolkit.get_ps`](@ref). """ -function parameters(sys::AbstractSystem) +function parameters(sys::AbstractSystem; initial_parameters = !is_time_dependent(sys)) ps = get_ps(sys) if ps == SciMLBase.NullParameters() return [] @@ -1286,7 +1286,12 @@ function parameters(sys::AbstractSystem) ps = first.(ps) end systems = get_systems(sys) - unique(isempty(systems) ? ps : [ps; reduce(vcat, namespace_parameters.(systems))]) + result = unique(isempty(systems) ? ps : + [ps; reduce(vcat, namespace_parameters.(systems))]) + if !initial_parameters + filter!(x -> !iscall(x) || !isa(operation(x), Initial), result) + end + return result end function dependent_parameters(sys::AbstractSystem) @@ -1314,7 +1319,7 @@ function parameter_dependencies(sys::AbstractSystem) end function full_parameters(sys::AbstractSystem) - vcat(parameters(sys), dependent_parameters(sys)) + vcat(parameters(sys; initial_parameters = true), dependent_parameters(sys)) end """ From 665d1d33c4237a83ef8c2b27f64b7cd3ce8a1183 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 5 Feb 2025 17:12:09 +0530 Subject: [PATCH 0861/1337] feat: put `Initial` parameters at the end of tunables --- src/systems/index_cache.jl | 39 ++++++++++++++++++++++---------------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/src/systems/index_cache.jl b/src/systems/index_cache.jl index e4ca0b6b48..d807691cf1 100644 --- a/src/systems/index_cache.jl +++ b/src/systems/index_cache.jl @@ -96,6 +96,7 @@ function IndexCache(sys::AbstractSystem) end tunable_buffers = Dict{Any, Set{BasicSymbolic}}() + initial_param_buffers = Dict{Any, Set{BasicSymbolic}}() constant_buffers = Dict{Any, Set{BasicSymbolic}}() nonnumeric_buffers = Dict{Any, Set{SymbolicParam}}() @@ -191,7 +192,7 @@ function IndexCache(sys::AbstractSystem) [BufferTemplate(symtype, length(buf)) for buf in disc_syms_by_partition]) end - for p in parameters(sys) + for p in parameters(sys; initial_parameters = true) p = unwrap(p) ctype = symtype(p) if ctype <: FnType @@ -206,7 +207,11 @@ function IndexCache(sys::AbstractSystem) (ctype == Real || ctype <: AbstractFloat || ctype <: AbstractArray{Real} || ctype <: AbstractArray{<:AbstractFloat}) - tunable_buffers + if iscall(p) && operation(p) isa Initial + initial_param_buffers + else + tunable_buffers + end else constant_buffers end @@ -246,20 +251,22 @@ function IndexCache(sys::AbstractSystem) tunable_idxs = TunableIndexMap() tunable_buffer_size = 0 - for (i, (_, buf)) in enumerate(tunable_buffers) - for (j, p) in enumerate(buf) - idx = if size(p) == () - tunable_buffer_size + 1 - else - reshape( - (tunable_buffer_size + 1):(tunable_buffer_size + length(p)), size(p)) - end - tunable_buffer_size += length(p) - tunable_idxs[p] = idx - tunable_idxs[default_toterm(p)] = idx - if hasname(p) && (!iscall(p) || operation(p) !== getindex) - symbol_to_variable[getname(p)] = p - symbol_to_variable[getname(default_toterm(p))] = p + for buffers in (tunable_buffers, initial_param_buffers) + for (i, (_, buf)) in enumerate(buffers) + for (j, p) in enumerate(buf) + idx = if size(p) == () + tunable_buffer_size + 1 + else + reshape( + (tunable_buffer_size + 1):(tunable_buffer_size + length(p)), size(p)) + end + tunable_buffer_size += length(p) + tunable_idxs[p] = idx + tunable_idxs[default_toterm(p)] = idx + if hasname(p) && (!iscall(p) || operation(p) !== getindex) + symbol_to_variable[getname(p)] = p + symbol_to_variable[getname(default_toterm(p))] = p + end end end end From aeffa9595d4c87fa6fda6874e3237476231a3513 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 5 Feb 2025 17:12:21 +0530 Subject: [PATCH 0862/1337] feat: add single-argument `reorder_parameters` --- src/systems/index_cache.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/systems/index_cache.jl b/src/systems/index_cache.jl index d807691cf1..a956e17313 100644 --- a/src/systems/index_cache.jl +++ b/src/systems/index_cache.jl @@ -445,7 +445,8 @@ function check_index_map(idxmap, sym) end end -function reorder_parameters(sys::AbstractSystem, ps; kwargs...) +function reorder_parameters( + sys::AbstractSystem, ps = parameters(sys; initial_parameters = true); kwargs...) if has_index_cache(sys) && get_index_cache(sys) !== nothing reorder_parameters(get_index_cache(sys), ps; kwargs...) elseif ps isa Tuple From a726293b573f6bef4c329dad29539d82aff549f7 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 5 Feb 2025 17:12:50 +0530 Subject: [PATCH 0863/1337] fix: fix `zero_var` for array symbolics --- src/systems/problem_utils.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/systems/problem_utils.jl b/src/systems/problem_utils.jl index 99326aa285..86a7c12779 100644 --- a/src/systems/problem_utils.jl +++ b/src/systems/problem_utils.jl @@ -251,7 +251,7 @@ symbolics return an empty array of the appropriate `eltype`. function zero_var(x::Symbolic{T}) where {V <: Number, T <: Union{V, AbstractArray{V}}} if Symbolics.isarraysymbolic(x) if is_sized_array_symbolic(x) - return zeros(T, size(x)) + return zeros(eltype(T), size(x)) else return T[] end From 2a7432b6fe9432854949421a4507dee6695a3396 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 5 Feb 2025 17:15:11 +0530 Subject: [PATCH 0864/1337] fix: handle `Initial` parameters in `MTKParameters` constructor --- src/systems/parameter_buffer.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/systems/parameter_buffer.jl b/src/systems/parameter_buffer.jl index bc4e62a773..3c4144c8be 100644 --- a/src/systems/parameter_buffer.jl +++ b/src/systems/parameter_buffer.jl @@ -33,10 +33,10 @@ function MTKParameters( else error("Cannot create MTKParameters if system does not have index_cache") end - all_ps = Set(unwrap.(parameters(sys))) - union!(all_ps, default_toterm.(unwrap.(parameters(sys)))) + all_ps = Set(unwrap.(parameters(sys; initial_parameters = true))) + union!(all_ps, default_toterm.(unwrap.(parameters(sys; initial_parameters = true)))) if p isa Vector && !(eltype(p) <: Pair) && !isempty(p) - ps = parameters(sys) + ps = parameters(sys; initial_parameters = true) length(p) == length(ps) || error("Invalid parameters") p = ps .=> p end From d19134fce41b8829b888a46643b61ac0945a99fe Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 5 Feb 2025 17:15:38 +0530 Subject: [PATCH 0865/1337] fix: handle `Initial` parameters in `tunable_parameters` --- src/variables.jl | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/variables.jl b/src/variables.jl index 536119f107..0068055832 100644 --- a/src/variables.jl +++ b/src/variables.jl @@ -414,7 +414,7 @@ end ## System interface """ - tunable_parameters(sys, p = parameters(sys); default=true) + tunable_parameters(sys, p = parameters(sys; initial_parameters = true); default=true) Get all parameters of `sys` that are marked as `tunable`. @@ -433,7 +433,8 @@ call `Symbolics.scalarize(tunable_parameters(sys))` and concatenate the resultin See also [`getbounds`](@ref), [`istunable`](@ref), [`MTKParameters`](@ref), [`complete`](@ref) """ -function tunable_parameters(sys, p = parameters(sys); default = true) +function tunable_parameters( + sys, p = parameters(sys; initial_parameters = true); default = true) filter(x -> istunable(x, default), p) end From 6531c87da96eebcfbcd54c10898eb73b36145a62 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 5 Feb 2025 17:15:57 +0530 Subject: [PATCH 0866/1337] fix: handle `Initial` parameters in code generation --- src/inputoutput.jl | 11 +++++------ src/systems/abstractsystem.jl | 6 +++--- src/systems/callbacks.jl | 14 ++++++++------ src/systems/diffeqs/abstractodesystem.jl | 15 ++++++++------- src/systems/diffeqs/odesystem.jl | 2 +- src/systems/diffeqs/sdesystem.jl | 2 +- src/systems/jumps/jumpsystem.jl | 4 ++-- src/systems/nonlinear/initializesystem.jl | 4 +++- src/systems/nonlinear/nonlinearsystem.jl | 13 ++++++++----- src/systems/optimization/constraints_system.jl | 8 +++++--- src/systems/optimization/modelingtoolkitize.jl | 2 +- src/systems/optimization/optimizationsystem.jl | 7 ++++--- 12 files changed, 49 insertions(+), 39 deletions(-) diff --git a/src/inputoutput.jl b/src/inputoutput.jl index 7d2fa875b1..2407ca6942 100644 --- a/src/inputoutput.jl +++ b/src/inputoutput.jl @@ -211,7 +211,7 @@ function generate_control_function(sys::AbstractODESystem, inputs = unbound_inpu sys, _ = io_preprocessing(sys, inputs, []; simplify, kwargs...) dvs = unknowns(sys) - ps = parameters(sys) + ps = parameters(sys; initial_parameters = true) ps = setdiff(ps, inputs) if disturbance_inputs !== nothing # remove from inputs since we do not want them as actual inputs to the dynamics @@ -234,16 +234,14 @@ function generate_control_function(sys::AbstractODESystem, inputs = unbound_inpu [eq.rhs for eq in eqs] # TODO: add an optional check on the ordering of observed equations - u = map(x -> time_varying_as_func(value(x), sys), dvs) - p = map(x -> time_varying_as_func(value(x), sys), ps) - p = reorder_parameters(sys, p) + p = reorder_parameters(sys, ps) t = get_iv(sys) # pre = has_difference ? (ex -> ex) : get_postprocess_fbody(sys) if disturbance_argument - args = (u, inputs, p..., t, disturbance_inputs) + args = (dvs, inputs, p..., t, disturbance_inputs) else - args = (u, inputs, p..., t) + args = (dvs, inputs, p..., t) end if implicit_dae ddvs = map(Differential(get_iv(sys)), dvs) @@ -252,6 +250,7 @@ function generate_control_function(sys::AbstractODESystem, inputs = unbound_inpu f = build_function_wrapper(sys, rhss, args...; p_start = 3 + implicit_dae, p_end = length(p) + 2 + implicit_dae) f = eval_or_rgf.(f; eval_expression, eval_module) + ps = setdiff(parameters(sys), inputs, disturbance_inputs) (; f, dvs, ps, io_sys = sys) end diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index 2bb7db41cf..ee0017150d 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -161,7 +161,7 @@ time-independent systems. If `split=true` (the default) was passed to [`complete object. """ function generate_custom_function(sys::AbstractSystem, exprs, dvs = unknowns(sys), - ps = parameters(sys); + ps = parameters(sys; initial_parameters = true); expression = Val{true}, eval_expression = false, eval_module = @__MODULE__, cachesyms::Tuple = (), kwargs...) if !iscomplete(sys) @@ -533,7 +533,7 @@ function SymbolicIndexingInterface.get_all_timeseries_indexes(sys::AbstractSyste end function SymbolicIndexingInterface.parameter_symbols(sys::AbstractSystem) - return parameters(sys) + return parameters(sys; initial_parameters = true) end function SymbolicIndexingInterface.is_independent_variable(sys::AbstractSystem, sym) @@ -2432,7 +2432,7 @@ function linearize_symbolic(sys::AbstractSystem, inputs, kwargs...) sts = unknowns(sys) t = get_iv(sys) - ps = parameters(sys) + ps = parameters(sys; initial_parameters = true) p = reorder_parameters(sys, ps) fun_expr = generate_function(sys, sts, ps; expression = Val{true})[1] diff --git a/src/systems/callbacks.jl b/src/systems/callbacks.jl index a58ff3f8ec..5cefe65651 100644 --- a/src/systems/callbacks.jl +++ b/src/systems/callbacks.jl @@ -682,7 +682,7 @@ function compile_affect(eqs::Vector{Equation}, cb, sys, dvs, ps; outputidxs = no end function generate_rootfinding_callback(sys::AbstractTimeDependentSystem, - dvs = unknowns(sys), ps = parameters(sys); kwargs...) + 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...) @@ -693,7 +693,7 @@ generate_rootfinding_callback and thus we can produce a ContinuousCallback inste """ function generate_single_rootfinding_callback( eq, cb, sys::AbstractTimeDependentSystem, dvs = unknowns(sys), - ps = parameters(sys); kwargs...) + ps = parameters(sys; initial_parameters = true); kwargs...) if !isequal(eq.lhs, 0) eq = 0 ~ eq.lhs - eq.rhs end @@ -736,7 +736,7 @@ end function generate_vector_rootfinding_callback( cbs, sys::AbstractTimeDependentSystem, dvs = unknowns(sys), - ps = parameters(sys); rootfind = SciMLBase.RightRootFind, + 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) @@ -861,7 +861,7 @@ function compile_affect_fn(cb, sys::AbstractTimeDependentSystem, dvs, ps, kwargs end function generate_rootfinding_callback(cbs, sys::AbstractTimeDependentSystem, - dvs = unknowns(sys), ps = parameters(sys); kwargs...) + 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) @@ -949,7 +949,9 @@ function invalid_variables(sys, expr) end function unassignable_variables(sys, expr) assignable_syms = reduce( - vcat, Symbolics.scalarize.(vcat(unknowns(sys), parameters(sys))); init = []) + 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) @@ -1075,7 +1077,7 @@ function generate_discrete_callback(cb, sys, dvs, ps; postprocess_affect_expr! = end function generate_discrete_callbacks(sys::AbstractSystem, dvs = unknowns(sys), - ps = parameters(sys); kwargs...) + ps = parameters(sys; initial_parameters = true); kwargs...) has_discrete_events(sys) || return nothing symcbs = discrete_events(sys) isempty(symcbs) && return nothing diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index b4c51b52e9..5d3fa93cde 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -105,7 +105,8 @@ function calculate_control_jacobian(sys::AbstractODESystem; end function generate_tgrad( - sys::AbstractODESystem, dvs = unknowns(sys), ps = parameters(sys); + sys::AbstractODESystem, dvs = unknowns(sys), ps = parameters( + sys; initial_parameters = true); simplify = false, kwargs...) tgrad = calculate_tgrad(sys, simplify = simplify) p = reorder_parameters(sys, ps) @@ -117,7 +118,7 @@ function generate_tgrad( end function generate_jacobian(sys::AbstractODESystem, dvs = unknowns(sys), - ps = parameters(sys); + ps = parameters(sys; initial_parameters = true); simplify = false, sparse = false, kwargs...) jac = calculate_jacobian(sys; simplify = simplify, sparse = sparse) p = reorder_parameters(sys, ps) @@ -129,7 +130,7 @@ function generate_jacobian(sys::AbstractODESystem, dvs = unknowns(sys), end function generate_control_jacobian(sys::AbstractODESystem, dvs = unknowns(sys), - ps = parameters(sys); + ps = parameters(sys; initial_parameters = true); simplify = false, sparse = false, kwargs...) jac = calculate_control_jacobian(sys; simplify = simplify, sparse = sparse) p = reorder_parameters(sys, ps) @@ -137,7 +138,7 @@ function generate_control_jacobian(sys::AbstractODESystem, dvs = unknowns(sys), end function generate_dae_jacobian(sys::AbstractODESystem, dvs = unknowns(sys), - ps = parameters(sys); simplify = false, sparse = false, + ps = parameters(sys; initial_parameters = true); simplify = false, sparse = false, kwargs...) jac_u = calculate_jacobian(sys; simplify = simplify, sparse = sparse) derivatives = Differential(get_iv(sys)).(unknowns(sys)) @@ -153,7 +154,7 @@ function generate_dae_jacobian(sys::AbstractODESystem, dvs = unknowns(sys), end function generate_function(sys::AbstractODESystem, dvs = unknowns(sys), - ps = parameters(sys); + ps = parameters(sys; initial_parameters = true); implicit_dae = false, ddvs = implicit_dae ? map(Differential(get_iv(sys)), dvs) : nothing, @@ -699,7 +700,7 @@ function SymbolicTstops( term(:, t0, unwrap(val), t1; type = AbstractArray{Real}) end end - rps = reorder_parameters(sys, parameters(sys)) + rps = reorder_parameters(sys) tstops, _ = build_function_wrapper(sys, tstops, rps..., t0, @@ -825,7 +826,7 @@ function DiffEqBase.DAEProblem{iip}(sys::AbstractODESystem, du0map, u0map, tspan end function generate_history(sys::AbstractODESystem, u0; expression = Val{false}, kwargs...) - p = reorder_parameters(sys, parameters(sys)) + p = reorder_parameters(sys) build_function_wrapper( sys, u0, p..., get_iv(sys); expression, p_start = 1, p_end = length(p), similarto = typeof(u0), wrap_delays = false, kwargs...) diff --git a/src/systems/diffeqs/odesystem.jl b/src/systems/diffeqs/odesystem.jl index 96fd8534ae..0b2adaedb5 100644 --- a/src/systems/diffeqs/odesystem.jl +++ b/src/systems/diffeqs/odesystem.jl @@ -432,7 +432,7 @@ function build_explicit_observed_function(sys, ts; eval_module = @__MODULE__, output_type = Array, checkbounds = true, - ps = parameters(sys), + ps = parameters(sys; initial_parameters = true), return_inplace = false, param_only = false, op = Operator, diff --git a/src/systems/diffeqs/sdesystem.jl b/src/systems/diffeqs/sdesystem.jl index 92a8ddd710..00265cfc67 100644 --- a/src/systems/diffeqs/sdesystem.jl +++ b/src/systems/diffeqs/sdesystem.jl @@ -418,7 +418,7 @@ function __get_num_diag_noise(mat) end function generate_diffusion_function(sys::SDESystem, dvs = unknowns(sys), - ps = parameters(sys); isdde = false, kwargs...) + ps = parameters(sys; initial_parameters = true); isdde = false, kwargs...) eqs = get_noiseeqs(sys) p = reorder_parameters(sys, ps) return build_function_wrapper(sys, eqs, dvs, p..., get_iv(sys); kwargs...) diff --git a/src/systems/jumps/jumpsystem.jl b/src/systems/jumps/jumpsystem.jl index 4e72d0853a..164c38e161 100644 --- a/src/systems/jumps/jumpsystem.jl +++ b/src/systems/jumps/jumpsystem.jl @@ -282,7 +282,7 @@ function generate_rate_function(js::JumpSystem, rate) csubs = Dict(c => getdefault(c) for c in consts) rate = substitute(rate, csubs) end - p = reorder_parameters(js, parameters(js)) + p = reorder_parameters(js) rf = build_function_wrapper(js, rate, unknowns(js), p..., get_iv(js), expression = Val{true}) @@ -634,7 +634,7 @@ end function JumpSysMajParamMapper(js::JumpSystem, p; jseqs = nothing, rateconsttype = Float64) eqs = (jseqs === nothing) ? equations(js) : jseqs paramexprs = [maj.scaled_rates for maj in eqs.x[1]] - psyms = reduce(vcat, reorder_parameters(js, parameters(js)); init = []) + psyms = reduce(vcat, reorder_parameters(js); init = []) paramdict = Dict(value(k) => value(v) for (k, v) in zip(psyms, vcat(p...))) JumpSysMajParamMapper{typeof(paramexprs), typeof(psyms), rateconsttype}(paramexprs, psyms, diff --git a/src/systems/nonlinear/initializesystem.jl b/src/systems/nonlinear/initializesystem.jl index f7fe7ea077..1988534c8c 100644 --- a/src/systems/nonlinear/initializesystem.jl +++ b/src/systems/nonlinear/initializesystem.jl @@ -339,7 +339,9 @@ end function ReconstructInitializeprob( srcsys::AbstractSystem, dstsys::AbstractSystem; remap = Dict()) - syms = reduce(vcat, reorder_parameters(dstsys, parameters(dstsys)); init = []) + syms = reduce( + vcat, reorder_parameters(dstsys, parameters(dstsys; initial_parameters = true)); + init = []) getter = getu(srcsys, map(x -> get(remap, x, x), syms)) setter = setp_oop(dstsys, syms) return ReconstructInitializeprob(getter, setter) diff --git a/src/systems/nonlinear/nonlinearsystem.jl b/src/systems/nonlinear/nonlinearsystem.jl index b5256ca4ca..6f5de67fa7 100644 --- a/src/systems/nonlinear/nonlinearsystem.jl +++ b/src/systems/nonlinear/nonlinearsystem.jl @@ -248,7 +248,8 @@ function calculate_jacobian(sys::NonlinearSystem; sparse = false, simplify = fal end function generate_jacobian( - sys::NonlinearSystem, vs = unknowns(sys), ps = parameters(sys); + sys::NonlinearSystem, vs = unknowns(sys), ps = parameters( + sys; initial_parameters = true); sparse = false, simplify = false, kwargs...) jac = calculate_jacobian(sys, sparse = sparse, simplify = simplify) p = reorder_parameters(sys, ps) @@ -268,7 +269,8 @@ function calculate_hessian(sys::NonlinearSystem; sparse = false, simplify = fals end function generate_hessian( - sys::NonlinearSystem, vs = unknowns(sys), ps = parameters(sys); + sys::NonlinearSystem, vs = unknowns(sys), ps = parameters( + sys; initial_parameters = true); sparse = false, simplify = false, kwargs...) hess = calculate_hessian(sys, sparse = sparse, simplify = simplify) p = reorder_parameters(sys, ps) @@ -276,7 +278,8 @@ function generate_hessian( end function generate_function( - sys::NonlinearSystem, dvs = unknowns(sys), ps = parameters(sys); + sys::NonlinearSystem, dvs = unknowns(sys), ps = parameters( + sys; initial_parameters = true); scalar = false, kwargs...) rhss = [deq.rhs for deq in equations(sys)] dvs′ = value.(dvs) @@ -569,7 +572,7 @@ end function CacheWriter(sys::AbstractSystem, buffer_types::Vector{TypeT}, exprs::Dict{TypeT, Vector{Any}}, solsyms, obseqs::Vector{Equation}; eval_expression = false, eval_module = @__MODULE__) - ps = parameters(sys) + ps = parameters(sys; initial_parameters = true) rps = reorder_parameters(sys, ps) obs_assigns = [eq.lhs ← eq.rhs for eq in obseqs] body = map(eachindex(buffer_types), buffer_types) do i, T @@ -589,7 +592,7 @@ struct SCCNonlinearFunction{iip} end function SCCNonlinearFunction{iip}( sys::NonlinearSystem, _eqs, _dvs, _obs, cachesyms; eval_expression = false, eval_module = @__MODULE__, kwargs...) where {iip} - ps = parameters(sys) + ps = parameters(sys; initial_parameters = true) rps = reorder_parameters(sys, ps) obs_assignments = [eq.lhs ← eq.rhs for eq in _obs] diff --git a/src/systems/optimization/constraints_system.jl b/src/systems/optimization/constraints_system.jl index 275079297a..06795916f8 100644 --- a/src/systems/optimization/constraints_system.jl +++ b/src/systems/optimization/constraints_system.jl @@ -171,7 +171,8 @@ function calculate_jacobian(sys::ConstraintsSystem; sparse = false, simplify = f end function generate_jacobian( - sys::ConstraintsSystem, vs = unknowns(sys), ps = parameters(sys); + sys::ConstraintsSystem, vs = unknowns(sys), ps = parameters( + sys; initial_parameters = true); sparse = false, simplify = false, kwargs...) jac = calculate_jacobian(sys, sparse = sparse, simplify = simplify) p = reorder_parameters(sys, ps) @@ -190,7 +191,8 @@ function calculate_hessian(sys::ConstraintsSystem; sparse = false, simplify = fa end function generate_hessian( - sys::ConstraintsSystem, vs = unknowns(sys), ps = parameters(sys); + sys::ConstraintsSystem, vs = unknowns(sys), ps = parameters( + sys; initial_parameters = true); sparse = false, simplify = false, kwargs...) hess = calculate_hessian(sys, sparse = sparse, simplify = simplify) p = reorder_parameters(sys, ps) @@ -198,7 +200,7 @@ function generate_hessian( end function generate_function(sys::ConstraintsSystem, dvs = unknowns(sys), - ps = parameters(sys); + ps = parameters(sys; initial_parameters = true); kwargs...) lhss = generate_canonical_form_lhss(sys) p = reorder_parameters(sys, value.(ps)) diff --git a/src/systems/optimization/modelingtoolkitize.jl b/src/systems/optimization/modelingtoolkitize.jl index 0228a944cc..27dccc251a 100644 --- a/src/systems/optimization/modelingtoolkitize.jl +++ b/src/systems/optimization/modelingtoolkitize.jl @@ -53,7 +53,7 @@ function modelingtoolkitize(prob::DiffEqBase.OptimizationProblem; idx = parameter_index(prob, sym) old_to_new[unwrap(sym)] = unwrap(p_names[idx]) end - order = reorder_parameters(prob.f.sys, parameters(prob.f.sys)) + order = reorder_parameters(prob.f.sys) for arr in order for i in eachindex(arr) arr[i] = old_to_new[arr[i]] diff --git a/src/systems/optimization/optimizationsystem.jl b/src/systems/optimization/optimizationsystem.jl index 3ae98be084..c1c2237852 100644 --- a/src/systems/optimization/optimizationsystem.jl +++ b/src/systems/optimization/optimizationsystem.jl @@ -194,7 +194,7 @@ function calculate_gradient(sys::OptimizationSystem) end function generate_gradient(sys::OptimizationSystem, vs = unknowns(sys), - ps = parameters(sys); kwargs...) + ps = parameters(sys; initial_parameters = true); kwargs...) grad = calculate_gradient(sys) p = reorder_parameters(sys, ps) return build_function_wrapper(sys, grad, vs, p...; kwargs...) @@ -205,7 +205,8 @@ function calculate_hessian(sys::OptimizationSystem) end function generate_hessian( - sys::OptimizationSystem, vs = unknowns(sys), ps = parameters(sys); + sys::OptimizationSystem, vs = unknowns(sys), ps = parameters( + sys; initial_parameters = true); sparse = false, kwargs...) if sparse hess = sparsehessian(objective(sys), unknowns(sys)) @@ -217,7 +218,7 @@ function generate_hessian( end function generate_function(sys::OptimizationSystem, vs = unknowns(sys), - ps = parameters(sys); + ps = parameters(sys; initial_parameters = true); kwargs...) eqs = objective(sys) p = reorder_parameters(sys, ps) From f5cce1dd9a89768efc379bd6e2d5f3e8bda15f17 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 5 Feb 2025 17:16:20 +0530 Subject: [PATCH 0867/1337] test: fix usage of incorrect `MTKParameters` object in `generate_custom_function` test --- test/generate_custom_function.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/test/generate_custom_function.jl b/test/generate_custom_function.jl index 9b64b20c12..ea1e429e30 100644 --- a/test/generate_custom_function.jl +++ b/test/generate_custom_function.jl @@ -30,6 +30,7 @@ fn5 = generate_custom_function(sys, ifelse(!p4, p1, p2[2]); expression = Val(fal @variables x y[1:3] sys = complete(NonlinearSystem(Equation[], [x; y], [p1, p2, p3, p4]; name = :sys)) +p = MTKParameters(sys, []) fn1 = generate_custom_function(sys, x + y[1] + p1 + p2[1] + p3; expression = Val(false)) @test fn1(u0, p) == 6.0 From c8f488d3b6a325ca29dfeca7af297cffe3907e0d Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 5 Feb 2025 17:17:20 +0530 Subject: [PATCH 0868/1337] test: refactor parameter dependency tests --- test/parameter_dependencies.jl | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/test/parameter_dependencies.jl b/test/parameter_dependencies.jl index 1f1aab9953..7f71120564 100644 --- a/test/parameter_dependencies.jl +++ b/test/parameter_dependencies.jl @@ -26,8 +26,8 @@ using NonlinearSolve continuous_events = [cb1, cb2], discrete_events = [cb3] ) - @test isequal(only(parameters(sys)), p1) - @test Set(full_parameters(sys)) == Set([p1, p2]) + @test !(p2 in Set(parameters(sys))) + @test p2 in Set(full_parameters(sys)) prob = ODEProblem(sys, [x => 1.0], (0.0, 1.5), jac = true) @test prob.ps[p1] == 1.0 @test prob.ps[p2] == 2.0 @@ -82,8 +82,8 @@ end parameter_dependencies = [p2 => 2p1] ) sys = extend(sys2, sys1) - @test isequal(only(parameters(sys)), p1) - @test Set(full_parameters(sys)) == Set([p1, p2]) + @test !(p2 in Set(parameters(sys))) + @test p2 in Set(full_parameters(sys)) prob = ODEProblem(complete(sys)) get_dep = getu(prob, 2p2) @test get_dep(prob) == 4 @@ -259,8 +259,8 @@ end @named sys = ODESystem(eqs, t) @named sdesys = SDESystem(sys, noiseeqs; parameter_dependencies = [ρ => 2σ]) sdesys = complete(sdesys) - @test Set(parameters(sdesys)) == Set([σ, β]) - @test Set(full_parameters(sdesys)) == Set([σ, β, ρ]) + @test !(ρ in Set(parameters(sdesys))) + @test ρ in Set(full_parameters(sdesys)) prob = SDEProblem( sdesys, [x => 1.0, y => 0.0, z => 0.0], (0.0, 100.0), [σ => 10.0, β => 2.33]) @@ -358,17 +358,18 @@ end ps = prob.p buffer, repack, _ = canonicalize(Tunable(), ps) - @test only(buffer) == 3.0 - buffer[1] = 4.0 + idx = parameter_index(sys, p1) + @test buffer[idx.idx] == 3.0 + buffer[idx.idx] = 4.0 ps = repack(buffer) @test getp(sys, p1)(ps) == 4.0 @test getp(sys, p2)(ps) == 8.0 - replace!(Tunable(), ps, [1.0]) + replace!(Tunable(), ps, ones(length(ps.tunable))) @test getp(sys, p1)(ps) == 1.0 @test getp(sys, p2)(ps) == 2.0 - ps2 = replace(Tunable(), ps, [2.0]) + ps2 = replace(Tunable(), ps, 2 .* ps.tunable) @test getp(sys, p1)(ps2) == 2.0 @test getp(sys, p2)(ps2) == 4.0 end From 1fc8c5f70e51399f85bb95ea61a6f90630a45821 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 5 Feb 2025 17:26:54 +0530 Subject: [PATCH 0869/1337] refactor: rename `build_operating_point` to `build_operating_point!` --- src/systems/nonlinear/initializesystem.jl | 2 +- src/systems/problem_utils.jl | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/systems/nonlinear/initializesystem.jl b/src/systems/nonlinear/initializesystem.jl index 1988534c8c..1cb9dee3bf 100644 --- a/src/systems/nonlinear/initializesystem.jl +++ b/src/systems/nonlinear/initializesystem.jl @@ -520,7 +520,7 @@ function SciMLBase.remake_initialization_data( filter_missing_values!(u0map) filter_missing_values!(pmap) - op, missing_unknowns, missing_pars = build_operating_point( + op, missing_unknowns, missing_pars = build_operating_point!( u0map, pmap, defs, cmap, dvs, ps) kws = maybe_build_initialization_problem( sys, op, u0map, pmap, t0, defs, guesses, missing_unknowns; diff --git a/src/systems/problem_utils.jl b/src/systems/problem_utils.jl index 86a7c12779..c25ae5ba48 100644 --- a/src/systems/problem_utils.jl +++ b/src/systems/problem_utils.jl @@ -526,7 +526,7 @@ no values can be determined, and the list of parameters for which no values can Also updates `u0map` and `pmap` in-place to contain all the initial conditions in `op`, split by unknowns and parameters respectively. """ -function build_operating_point( +function build_operating_point!( u0map::AbstractDict, pmap::AbstractDict, defs::AbstractDict, cmap, dvs, ps) op = add_toterms(u0map) missing_unknowns = add_fallbacks!(op, dvs, defs) @@ -762,7 +762,7 @@ function process_SciMLProblem( is_time_dependent(sys) || add_observed!(sys, u0map) - op, missing_unknowns, missing_pars = build_operating_point( + op, missing_unknowns, missing_pars = build_operating_point!( u0map, pmap, defs, cmap, dvs, ps) substitute_extra_variables!(sys, u0map) substitute_extra_variables!(sys, pmap) From 2ec306b8af347c9050b97d8150d75f86291656b1 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 7 Feb 2025 14:08:09 +0530 Subject: [PATCH 0870/1337] HACK: fix: add `maketerm` for `Initial` operator --- src/systems/abstractsystem.jl | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index ee0017150d..74e10352e9 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -684,6 +684,10 @@ function (f::Initial)(x) return result end +function SymbolicUtils.maketerm(::Type{<:BasicSymbolic}, ::Initial, args, meta) + return metadata(Initial()(args...), meta) +end + function add_initialization_parameters(sys::AbstractSystem) is_time_dependent(sys) || return sys @assert !has_systems(sys) || isempty(get_systems(sys)) From ecaa585a2e5e0267f4f27582a4832ec54182b326 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 7 Feb 2025 14:08:36 +0530 Subject: [PATCH 0871/1337] fix: filter existing `Initial`s in `add_initialization_parameters` --- src/systems/abstractsystem.jl | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index 74e10352e9..0deef1bc4c 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -707,9 +707,15 @@ function add_initialization_parameters(sys::AbstractSystem) end all_uvars = collect(all_uvars) initials = map(Initial(), all_uvars) - # existing_initials = filter(x -> iscall(x) && (operation(x) isa Initial), parameters(sys)) - @set! sys.ps = unique!([get_ps(sys); initials]) - @set! sys.defaults = merge(get_defaults(sys), Dict(initials .=> zero_var.(initials))) + existing_initials = filter( + x -> iscall(x) && (operation(x) isa Initial), parameters(sys)) + @set! sys.ps = unique!([setdiff(get_ps(sys), existing_initials); initials]) + defs = copy(get_defaults(sys)) + for x in existing_initials + delete!(defs, x) + end + merge!(defs, Dict(initials .=> zero_var.(initials))) + @set! sys.defaults = defs return sys end From 5150659d3c83f33410707bbe05e519028d3c2628 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 7 Feb 2025 14:09:37 +0530 Subject: [PATCH 0872/1337] fix: handle `Initial` parameters in `flatten` --- src/systems/diffeqs/odesystem.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/systems/diffeqs/odesystem.jl b/src/systems/diffeqs/odesystem.jl index 0b2adaedb5..8febfc253b 100644 --- a/src/systems/diffeqs/odesystem.jl +++ b/src/systems/diffeqs/odesystem.jl @@ -361,7 +361,7 @@ function flatten(sys::ODESystem, noeqs = false) return ODESystem(noeqs ? Equation[] : equations(sys), get_iv(sys), unknowns(sys), - parameters(sys), + parameters(sys; initial_parameters = true), parameter_dependencies = parameter_dependencies(sys), guesses = guesses(sys), observed = observed(sys), From 50bfcdd14336cd42219b2355e6c5ec442a73952a Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 7 Feb 2025 14:10:30 +0530 Subject: [PATCH 0873/1337] refactor: use problem building utils in `MTKParameters` constructor --- src/systems/parameter_buffer.jl | 99 ++++++++------------------------- 1 file changed, 24 insertions(+), 75 deletions(-) diff --git a/src/systems/parameter_buffer.jl b/src/systems/parameter_buffer.jl index 3c4144c8be..07b47a5ec4 100644 --- a/src/systems/parameter_buffer.jl +++ b/src/systems/parameter_buffer.jl @@ -27,7 +27,7 @@ the default behavior). """ function MTKParameters( sys::AbstractSystem, p, u0 = Dict(); tofloat = false, use_union = false, - t0 = nothing) + t0 = nothing, substitution_limit = 1000) ic = if has_index_cache(sys) && get_index_cache(sys) !== nothing get_index_cache(sys) else @@ -35,83 +35,32 @@ function MTKParameters( end all_ps = Set(unwrap.(parameters(sys; initial_parameters = true))) union!(all_ps, default_toterm.(unwrap.(parameters(sys; initial_parameters = true)))) - if p isa Vector && !(eltype(p) <: Pair) && !isempty(p) - ps = parameters(sys; initial_parameters = true) - length(p) == length(ps) || error("Invalid parameters") - p = ps .=> p - end - if p isa SciMLBase.NullParameters || isempty(p) - p = Dict() - end - p = todict(p) - defs = Dict(default_toterm(unwrap(k)) => v for (k, v) in defaults(sys)) - if eltype(u0) <: Pair - u0 = todict(u0) - elseif u0 isa AbstractArray && !isempty(u0) - u0 = Dict(unknowns(sys) .=> vec(u0)) - elseif u0 === nothing || isempty(u0) - u0 = Dict() - end - defs = merge(defs, u0) - defs = merge(Dict(eq.lhs => eq.rhs for eq in observed(sys)), defs) - bigdefs = merge(defs, p) + + dvs = unknowns(sys) + ps = parameters(sys; initial_parameters = true) + u0 = to_varmap(u0, dvs) + symbols_to_symbolics!(sys, u0) + p = to_varmap(p, ps) + symbols_to_symbolics!(sys, p) + defs = add_toterms(recursive_unwrap(defaults(sys))) + cmap, cs = get_cmap(sys) + + is_time_dependent(sys) && add_observed!(sys, u0) + add_parameter_dependencies!(sys, p) + + op, missing_unknowns, missing_pars = build_operating_point!(sys, + u0, p, defs, cmap, dvs, ps) + if t0 !== nothing - bigdefs[get_iv(sys)] = t0 - end - p = Dict() - missing_params = Set() - pdeps = has_parameter_dependencies(sys) ? parameter_dependencies(sys) : [] - - for sym in all_ps - ttsym = default_toterm(sym) - isarr = iscall(sym) && operation(sym) === getindex - arrparent = isarr ? arguments(sym)[1] : nothing - ttarrparent = isarr ? default_toterm(arrparent) : nothing - pname = hasname(sym) ? getname(sym) : nothing - ttpname = hasname(ttsym) ? getname(ttsym) : nothing - p[sym] = p[ttsym] = if haskey(bigdefs, sym) - bigdefs[sym] - elseif haskey(bigdefs, ttsym) - bigdefs[ttsym] - elseif haskey(bigdefs, pname) - isarr ? bigdefs[pname][arguments(sym)[2:end]...] : bigdefs[pname] - elseif haskey(bigdefs, ttpname) - isarr ? bigdefs[ttpname][arguments(sym)[2:end]...] : bigdefs[pname] - elseif isarr && haskey(bigdefs, arrparent) - bigdefs[arrparent][arguments(sym)[2:end]...] - elseif isarr && haskey(bigdefs, ttarrparent) - bigdefs[ttarrparent][arguments(sym)[2:end]...] - end - if get(p, sym, nothing) === nothing - push!(missing_params, sym) - continue - end - # We may encounter the `ttsym` version first, add it to `missing_params` - # then encounter the "normal" version of a parameter or vice versa - # Remove the old one in `missing_params` just in case - delete!(missing_params, sym) - delete!(missing_params, ttsym) - end - - if !isempty(pdeps) - for eq in pdeps - sym = eq.lhs - expr = eq.rhs - sym = unwrap(sym) - ttsym = default_toterm(sym) - delete!(missing_params, sym) - delete!(missing_params, ttsym) - p[sym] = p[ttsym] = expr - end + op[get_iv(sys)] = t0 end - isempty(missing_params) || throw(MissingParametersError(collect(missing_params))) - p = Dict(unwrap(k) => (bigdefs[unwrap(k)] = fixpoint_sub(v, bigdefs)) for (k, v) in p) - for (sym, _) in p - if iscall(sym) && operation(sym) === getindex && - first(arguments(sym)) in all_ps - error("Scalarized parameter values ($sym) are not supported. Instead of `[p[1] => 1.0, p[2] => 2.0]` use `[p => [1.0, 2.0]]`") - end + isempty(missing_pars) || throw(MissingParametersError(collect(missing_pars))) + evaluate_varmap!(op, ps; limit = substitution_limit) + + p = op + filter!(p) do kvp + kvp[1] in all_ps end tunable_buffer = Vector{ic.tunable_buffer_size.type}( From 5432f51e7bd26db808941d18026dcf2fd13c0574 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 7 Feb 2025 14:10:51 +0530 Subject: [PATCH 0874/1337] fix: fix scalarization of array defaults in `structural_simplify` --- src/systems/systems.jl | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/systems/systems.jl b/src/systems/systems.jl index f8630f2d20..ef0f966eee 100644 --- a/src/systems/systems.jl +++ b/src/systems/systems.jl @@ -53,17 +53,6 @@ function structural_simplify( @set! newsys.parent = complete(sys; split, flatten = false) end newsys = complete(newsys; split) - if has_defaults(newsys) && (defs = get_defaults(newsys)) !== nothing - ks = collect(keys(defs)) # take copy to avoid mutating defs while iterating. - for k in ks - if Symbolics.isarraysymbolic(k) && Symbolics.shape(k) !== Symbolics.Unknown() - defs[k] === missing && continue - for i in eachindex(k) - defs[k[i]] = defs[k][i] - end - end - end - end if newsys′ isa Tuple idxs = [parameter_index(newsys, i) for i in io[1]] return newsys, idxs From 49a8ccf57df145c8fb7b6edbc479572cedfc062f Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 7 Feb 2025 14:13:50 +0530 Subject: [PATCH 0875/1337] test: fix tests taking into account new initialization parameters --- test/input_output_handling.jl | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/input_output_handling.jl b/test/input_output_handling.jl index 382c002108..b1d68bc323 100644 --- a/test/input_output_handling.jl +++ b/test/input_output_handling.jl @@ -169,7 +169,7 @@ end @test isequal(dvs[], x) @test isempty(ps) - p = nothing + p = [rand()] x = [rand()] u = [rand()] @test f[1](x, u, p, 1) == -x + u @@ -187,7 +187,7 @@ end @test isequal(dvs[], x) @test isempty(ps) - p = nothing + p = [rand()] x = [rand()] u = [rand()] @test f[1](x, u, p, 1) == -x + u @@ -205,7 +205,7 @@ end @test isequal(dvs[], x) @test isempty(ps) - p = nothing + p = [rand()] x = [rand()] u = [rand()] d = [rand()] @@ -431,7 +431,7 @@ matrices, ssys = linearize(augmented_sys, (; io_sys,) = ModelingToolkit.generate_control_function(sys, simplify = true) obsfn = ModelingToolkit.build_explicit_observed_function( io_sys, [x + u * t]; inputs = [u]) - @test obsfn([1.0], [2.0], nothing, 3.0) == [7.0] + @test obsfn([1.0], [2.0], MTKParameters(io_sys, []), 3.0) == [7.0] end # https://github.com/SciML/ModelingToolkit.jl/issues/2896 @@ -441,8 +441,8 @@ end eqs = [D(x) ~ c * x] @named sys = ODESystem(eqs, t, [x], []) - f, dvs, ps = ModelingToolkit.generate_control_function(sys, simplify = true) - @test f[1]([0.5], nothing, nothing, 0.0) == [1.0] + f, dvs, ps, io_sys = ModelingToolkit.generate_control_function(sys, simplify = true) + @test f[1]([0.5], nothing, MTKParameters(io_sys, []), 0.0) == [1.0] end @testset "With callable symbolic" begin From be89d29c73a411d2ccab996c33a67cd0e463e3e8 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 7 Feb 2025 14:14:13 +0530 Subject: [PATCH 0876/1337] test: account for additional initialization parameter defaults --- test/modelingtoolkitize.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/modelingtoolkitize.jl b/test/modelingtoolkitize.jl index 3bcfdecc8b..28ce7b5e01 100644 --- a/test/modelingtoolkitize.jl +++ b/test/modelingtoolkitize.jl @@ -460,7 +460,7 @@ sys = modelingtoolkitize(prob) p = [10.0, 28.0, 2.66] sprob = SDEProblem(sdef!, sdeg!, u0, tspan, p) sys = complete(modelingtoolkitize(sprob)) - @test length(ModelingToolkit.defaults(sys)) == 6 + @test length(ModelingToolkit.defaults(sys)) == 2length(u0) + length(p) sprob2 = SDEProblem(sys, [], tspan) truevals = similar(u0) From 8ca484f67d29fc7e1387bbdfe1168773bea7e5f1 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 7 Feb 2025 14:14:29 +0530 Subject: [PATCH 0877/1337] test: account for additional initialization parameters --- test/odesystem.jl | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/test/odesystem.jl b/test/odesystem.jl index 8281fafe93..a9677aad26 100644 --- a/test/odesystem.jl +++ b/test/odesystem.jl @@ -273,7 +273,10 @@ prob12 = ODEProblem(sys, u0, tspan, [k₁ => 0.04, k₂ => 3e7, k₃ => 1e4]) prob13 = ODEProblem(sys, u0, tspan, (k₁ => 0.04, k₂ => 3e7, k₃ => 1e4)) prob14 = ODEProblem(sys, u0, tspan, p2) for p in [prob1, prob14] - @test p.p == MTKParameters(sys, [k₁ => 0.04, k₂ => 3e7, k₃ => 1e4]) + @test p.p isa MTKParameters + p.ps[k₁] ≈ 0.04 + p.ps[k₂] ≈ 3e7 + p.ps[k₃] ≈ 1e-4 @test Set(Num.(unknowns(sys)) .=> p.u0) == Set([y₁ => 1, y₂ => 0, y₃ => 0]) end # test remake with symbols @@ -674,7 +677,8 @@ let prob = DAEProblem(sys, du0, u0, (0, 50)) @test prob.u0 ≈ u0 @test prob.du0 ≈ du0 - @test vcat(prob.p...) ≈ [1] + @test prob.p isa MTKParameters + @test prob.ps[k] ≈ 1 sol = solve(prob, IDA()) @test sol[y] ≈ 0.9 * sol[x[1]] + sol[x[2]] @test isapprox(sol[x[1]][end], 1, atol = 1e-3) @@ -683,7 +687,8 @@ let (0, 50)) @test prob.u0 ≈ [0.5, 0] @test prob.du0 ≈ [0, 0] - @test vcat(prob.p...) ≈ [1] + @test prob.p isa MTKParameters + @test prob.ps[k] ≈ 1 sol = solve(prob, IDA()) @test isapprox(sol[x[1]][end], 1, atol = 1e-3) @@ -691,7 +696,8 @@ let (0, 50), [k => 2]) @test prob.u0 ≈ [0.5, 0] @test prob.du0 ≈ [0, 0] - @test vcat(prob.p...) ≈ [2] + @test prob.p isa MTKParameters + @test prob.ps[k] ≈ 2 sol = solve(prob, IDA()) @test isapprox(sol[x[1]][end], 2, atol = 1e-3) @@ -715,7 +721,9 @@ let pmap = (k1 => 1.0, k2 => 1) tspan = (0.0, 1.0) prob = ODEProblem(sys, u0map, tspan, pmap; tofloat = false) - @test (prob.p...,) == ([1], [1.0]) || (prob.p...,) == ([1.0], [1]) + @test prob.p isa MTKParameters + @test prob.ps[k1] ≈ 1.0 + @test prob.ps[k2] == 1 && prob.ps[k2] isa Int prob = ODEProblem(sys, u0map, tspan, pmap) @test vcat(prob.p...) isa Vector{Float64} @@ -1288,9 +1296,13 @@ end t, [u..., x..., o...], [p...]) sys1, = structural_simplify(sys, ([x...], [])) fn1, = ModelingToolkit.generate_function(sys1; expression = Val{false}) - @test_nowarn fn1(ones(4), (2ones(2), 3ones(2, 2)), 4.0) + ps = MTKParameters(sys1, [x => 2ones(2), p => 3ones(2, 2)]) + @test_nowarn fn1(ones(4), ps, 4.0) sys2, = structural_simplify(sys, ([x...], []); split = false) fn2, = ModelingToolkit.generate_function(sys2; expression = Val{false}) + ps = zeros(8) + setp(sys2, x)(ps, 2ones(2)) + setp(sys2, p)(ps, 2ones(2, 2)) @test_nowarn fn2(ones(4), 2ones(6), 4.0) end From ec3d3a87ea6f281450ea2f30370a5c826c52592f Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 7 Feb 2025 14:17:27 +0530 Subject: [PATCH 0878/1337] refactor: don't create new parameters in `generate_initializesystem` --- src/systems/diffeqs/abstractodesystem.jl | 2 +- src/systems/nonlinear/initializesystem.jl | 58 ++++------------------- src/systems/problem_utils.jl | 4 +- 3 files changed, 12 insertions(+), 52 deletions(-) diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index 5d3fa93cde..d8996000cf 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -1256,7 +1256,7 @@ function InitializationProblem{iip, specialize}(sys::AbstractSystem, meta = get_metadata(isys) if meta isa InitializationSystemMetadata @set! isys.metadata.oop_reconstruct_u0_p = ReconstructInitializeprob( - sys, isys; remap = meta.new_params) + sys, isys) end ts = get_tearing_state(isys) diff --git a/src/systems/nonlinear/initializesystem.jl b/src/systems/nonlinear/initializesystem.jl index 1cb9dee3bf..6696f6b7c2 100644 --- a/src/systems/nonlinear/initializesystem.jl +++ b/src/systems/nonlinear/initializesystem.jl @@ -38,37 +38,7 @@ function generate_initializesystem(sys::AbstractSystem; delete!(defs, k) end filter_missing_values!(u0map) - # for initial conditions of the form `var => constant`, we instead turn them into - # `var ~ var0` where `var0` is a new parameter, and make `update_initializeprob!` - # update `initializeprob.ps[var0] = prob[var]`. - - # map parameters in `initprob` which need to be updated in `update_initializeprob!` - # to the corresponding expressions that determine their values - new_params = Dict() - pmap = copy(anydict(pmap)) - if is_time_dependent(sys) - for (k, v) in u0map - k = unwrap(k) - is_variable(sys, k) || continue - (symbolic_type(v) == NotSymbolic() && !is_array_of_symbolics(v)) || continue - newvar = get_initial_value_parameter(k) - new_params[newvar] = k - pmap[newvar] = v - u0map[k] = newvar - defs[newvar] = v - end - end - for (k, v) in pmap - k = unwrap(k) - is_parameter_solvable(k, pmap, defs, guesses) || continue - (v !== missing && symbolic_type(v) == NotSymbolic() && !is_array_of_symbolics(v)) || - continue - newvar = get_initial_value_parameter(k) - new_params[newvar] = k - pmap[newvar] = v - pmap[k] = newvar - defs[newvar] = v - end + pmap = anydict(pmap) # 1) Use algebraic equations of time-dependent systems as initialization constraints if has_iv(sys) @@ -222,8 +192,8 @@ function generate_initializesystem(sys::AbstractSystem; end # parameters do not include ones that became initialization unknowns - pars = Vector{SymbolicParam}(filter(p -> !haskey(paramsubs, p), parameters(sys))) - pars = [pars; map(unwrap, collect(keys(new_params)))] + pars = Vector{SymbolicParam}(filter( + p -> !haskey(paramsubs, p), parameters(sys; initial_parameters = true))) is_time_dependent(sys) && push!(pars, get_iv(sys)) if is_time_dependent(sys) @@ -258,7 +228,7 @@ function generate_initializesystem(sys::AbstractSystem; end meta = InitializationSystemMetadata( anydict(u0map), anydict(pmap), additional_guesses, - additional_initialization_eqs, extra_metadata, nothing, new_params) + additional_initialization_eqs, extra_metadata, nothing) return NonlinearSystem(eqs_ics, vars, pars; @@ -338,11 +308,14 @@ struct ReconstructInitializeprob end function ReconstructInitializeprob( - srcsys::AbstractSystem, dstsys::AbstractSystem; remap = Dict()) + srcsys::AbstractSystem, dstsys::AbstractSystem) syms = reduce( vcat, reorder_parameters(dstsys, parameters(dstsys; initial_parameters = true)); init = []) - getter = getu(srcsys, map(x -> get(remap, x, x), syms)) + srcsyms = map(syms) do sym + iscall(sym) && operation(sym) isa Initial ? arguments(sym)[1] : sym + end + getter = getu(srcsys, srcsyms) setter = setp_oop(dstsys, syms) return ReconstructInitializeprob(getter, setter) end @@ -375,7 +348,6 @@ struct InitializationSystemMetadata additional_initialization_eqs::Vector{Equation} extra_metadata::NamedTuple oop_reconstruct_u0_p::Union{Nothing, ReconstructInitializeprob} - new_params::Dict{Any, Any} end function get_possibly_array_fallback_singletons(varmap, p) @@ -467,16 +439,6 @@ function SciMLBase.remake_initialization_data( merge!(guesses, meta.additional_guesses) use_scc = get(meta.extra_metadata, :use_scc, true) initialization_eqs = meta.additional_initialization_eqs - - # remove occurrences of `keys(new_params)` from `u0map` and `pmap` - for (newvar, oldvar) in meta.new_params - if isequal(get(u0map, oldvar, nothing), newvar) - u0map[oldvar] = pmap[newvar] - elseif isequal(get(pmap, oldvar, nothing), newvar) - pmap[oldvar] = pmap[newvar] - end - delete!(pmap, newvar) - end end else # there is no initializeprob, so the original problem construction @@ -520,7 +482,7 @@ function SciMLBase.remake_initialization_data( filter_missing_values!(u0map) filter_missing_values!(pmap) - op, missing_unknowns, missing_pars = build_operating_point!( + op, missing_unknowns, missing_pars = build_operating_point!(sys, u0map, pmap, defs, cmap, dvs, ps) kws = maybe_build_initialization_problem( sys, op, u0map, pmap, t0, defs, guesses, missing_unknowns; diff --git a/src/systems/problem_utils.jl b/src/systems/problem_utils.jl index c25ae5ba48..487b32e0d4 100644 --- a/src/systems/problem_utils.jl +++ b/src/systems/problem_utils.jl @@ -579,7 +579,6 @@ function maybe_build_initialization_problem( initializeprob = ModelingToolkit.InitializationProblem( sys, t, u0map, pmap; guesses, kwargs...) meta = get_metadata(initializeprob.f.sys) - new_params = meta.new_params if is_time_dependent(sys) all_init_syms = Set(all_symbols(initializeprob)) @@ -601,13 +600,12 @@ function maybe_build_initialization_problem( end reqd_syms = parameter_symbols(initializeprob) - sources = [get(new_params, x, x) for x in reqd_syms] # we still want the `initialization_data` because it helps with `remake` if initializeprobmap === nothing && initializeprobpmap === nothing update_initializeprob! = nothing else update_initializeprob! = UpdateInitializeprob( - getu(sys, sources), setu(initializeprob, reqd_syms)) + getu(sys, reqd_syms), setu(initializeprob, reqd_syms)) end for p in punknowns From 4cef87863f575f8b543d3fb92a10b5eeca15a53f Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 7 Feb 2025 14:18:51 +0530 Subject: [PATCH 0879/1337] fix: better handling of extra symbolics and splitting in `build_operating_point` --- src/systems/problem_utils.jl | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/src/systems/problem_utils.jl b/src/systems/problem_utils.jl index 487b32e0d4..6b01bb0fdb 100644 --- a/src/systems/problem_utils.jl +++ b/src/systems/problem_utils.jl @@ -526,7 +526,7 @@ no values can be determined, and the list of parameters for which no values can Also updates `u0map` and `pmap` in-place to contain all the initial conditions in `op`, split by unknowns and parameters respectively. """ -function build_operating_point!( +function build_operating_point!(sys::AbstractSystem, u0map::AbstractDict, pmap::AbstractDict, defs::AbstractDict, cmap, dvs, ps) op = add_toterms(u0map) missing_unknowns = add_fallbacks!(op, dvs, defs) @@ -545,15 +545,33 @@ function build_operating_point!( filter!(kvp -> kvp[2] === nothing, u0map) filter!(kvp -> kvp[2] === nothing, pmap) + neithermap = anydict() + for (k, v) in op k = unwrap(k) - if isparameter(k) + if is_parameter(sys, k) pmap[k] = v - elseif !isconstant(k) + elseif is_variable(sys, k) || has_observed_with_lhs(sys, k) + if symbolic_type(v) == NotSymbolic() && !is_array_of_symbolics(v) && + v !== nothing + op[Initial(k)] = v + pmap[Initial(k)] = v + op[k] = Initial(k) + v = Initial(k) + end u0map[k] = v + else + neithermap[k] = v end end + for k in keys(u0map) + u0map[k] = fixpoint_sub(u0map[k], neithermap) + end + for k in keys(pmap) + pmap[k] = fixpoint_sub(pmap[k], neithermap) + end + return op, missing_unknowns, missing_pars end @@ -760,10 +778,8 @@ function process_SciMLProblem( is_time_dependent(sys) || add_observed!(sys, u0map) - op, missing_unknowns, missing_pars = build_operating_point!( + op, missing_unknowns, missing_pars = build_operating_point!(sys, u0map, pmap, defs, cmap, dvs, ps) - substitute_extra_variables!(sys, u0map) - substitute_extra_variables!(sys, pmap) if build_initializeprob kws = maybe_build_initialization_problem( From 91f4bae2f0e59cb670048ce84935c44ebec64b5c Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 7 Feb 2025 14:19:54 +0530 Subject: [PATCH 0880/1337] refactor: remove `substitute_extra_variables!` --- src/systems/problem_utils.jl | 28 ---------------------------- 1 file changed, 28 deletions(-) diff --git a/src/systems/problem_utils.jl b/src/systems/problem_utils.jl index 6b01bb0fdb..ffe9bd655b 100644 --- a/src/systems/problem_utils.jl +++ b/src/systems/problem_utils.jl @@ -655,34 +655,6 @@ function maybe_build_initialization_problem( initializeprobpmap)) end -""" - $(TYPEDSIGNATURES) - -Remove all entries in `varmap` whose keys are not variables/parameters in `sys`, -substituting their values into the rest of `varmap`. Modifies `varmap` in place. -""" -function substitute_extra_variables!(sys::AbstractSystem, varmap::AbstractDict) - tmpmap = anydict() - syms = all_symbols(sys) - for (k, v) in varmap - k = unwrap(k) - if any(isequal(k), syms) || - iscall(k) && - (operation(k) == getindex && any(isequal(arguments(k)[1]), syms) || - operation(k) isa Differential) || isconstant(k) - continue - end - tmpmap[k] = v - end - for k in keys(tmpmap) - delete!(varmap, k) - end - for (k, v) in varmap - varmap[k] = unwrap(fixpoint_sub(v, tmpmap)) - end - return varmap -end - """ $(TYPEDSIGNATURES) From 7f0342d89968cc708f9df3e8dd9ded93fd4757c1 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 7 Feb 2025 14:20:11 +0530 Subject: [PATCH 0881/1337] fix: handle `Initial` parameters in `process_SciMLProblem` --- src/systems/problem_utils.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/systems/problem_utils.jl b/src/systems/problem_utils.jl index ffe9bd655b..cb80512dc3 100644 --- a/src/systems/problem_utils.jl +++ b/src/systems/problem_utils.jl @@ -730,7 +730,7 @@ function process_SciMLProblem( force_initialization_time_independent = false, algebraic_only = false, allow_incomplete = false, kwargs...) dvs = unknowns(sys) - ps = parameters(sys) + ps = parameters(sys; initial_parameters = true) iv = has_iv(sys) ? get_iv(sys) : nothing eqs = equations(sys) @@ -742,7 +742,7 @@ function process_SciMLProblem( u0map = to_varmap(u0map, dvs) symbols_to_symbolics!(sys, u0map) _pmap = pmap - pmap = to_varmap(pmap, ps) + pmap = to_varmap(pmap, parameters(sys)) symbols_to_symbolics!(sys, pmap) defs = add_toterms(recursive_unwrap(defaults(sys))) cmap, cs = get_cmap(sys) From 8b9728a39715e89251030b375e01fe586685e7ee Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 7 Feb 2025 14:20:21 +0530 Subject: [PATCH 0882/1337] fix: handle `Initial` parameters in `get_u0_p` --- src/systems/problem_utils.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/systems/problem_utils.jl b/src/systems/problem_utils.jl index cb80512dc3..11f6551bbe 100644 --- a/src/systems/problem_utils.jl +++ b/src/systems/problem_utils.jl @@ -852,7 +852,7 @@ function get_u0_p(sys, tofloat = true, symbolic_u0 = false) dvs = unknowns(sys) - ps = parameters(sys) + ps = parameters(sys; initial_parameters = true) defs = defaults(sys) if t0 !== nothing From 6273543f73f7ac03c857c3ae58d885fbd1c23c2b Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 7 Feb 2025 19:14:08 +0530 Subject: [PATCH 0883/1337] fix: fix infinite recursion in `full_equations` --- src/structural_transformation/symbolics_tearing.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/structural_transformation/symbolics_tearing.jl b/src/structural_transformation/symbolics_tearing.jl index d95951c41e..92e121c028 100644 --- a/src/structural_transformation/symbolics_tearing.jl +++ b/src/structural_transformation/symbolics_tearing.jl @@ -84,7 +84,7 @@ function eq_derivative!(ts::TearingState{ODESystem}, ieq::Int; kwargs...) end function tearing_sub(expr, dict, s) - expr = Symbolics.fixpoint_sub(expr, dict) + expr = Symbolics.fixpoint_sub(expr, dict; operator = ModelingToolkit.Initial) s ? simplify(expr) : expr end From 7c457e51adec53c8ab99cf6b4dc51d1551d1b845 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 7 Feb 2025 19:14:27 +0530 Subject: [PATCH 0884/1337] fix: fix modelingtoolkitize --- src/systems/diffeqs/modelingtoolkitize.jl | 14 ++++++++------ src/systems/parameter_buffer.jl | 9 +++++++++ 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/src/systems/diffeqs/modelingtoolkitize.jl b/src/systems/diffeqs/modelingtoolkitize.jl index 7ab68e3bee..e7321c4d2b 100644 --- a/src/systems/diffeqs/modelingtoolkitize.jl +++ b/src/systems/diffeqs/modelingtoolkitize.jl @@ -93,6 +93,8 @@ function modelingtoolkitize( else Dict() end + filter!(x -> !iscall(x) || !(operation(x) isa Initial), params) + filter!(x -> !iscall(x[1]) || !(operation(x[1]) isa Initial), default_p) de = ODESystem(eqs, t, sts, params, defaults = merge(default_u0, default_p); name = gensym(:MTKizedODE), @@ -207,17 +209,17 @@ function define_params(p::MTKParameters, names = nothing) for _ in buf push!( ps, - if names === nothing - toparam(variable(:α, i)) - else - toparam(variable(names[i])) - end + toparam(variable(:α, i)) ) end end return identity.(ps) else - return collect(values(names)) + new_p = as_any_buffer(p) + for (k, v) in names + new_p[k] = v + end + return reduce(vcat, new_p; init = []) end end diff --git a/src/systems/parameter_buffer.jl b/src/systems/parameter_buffer.jl index 07b47a5ec4..ab33cc5b63 100644 --- a/src/systems/parameter_buffer.jl +++ b/src/systems/parameter_buffer.jl @@ -536,6 +536,15 @@ function _remake_buffer(indp, oldbuf::MTKParameters, idxs, vals; validate = true return newbuf end +function as_any_buffer(p::MTKParameters) + @set! p.tunable = similar(p.tunable, Any) + @set! p.discrete = Tuple(similar(buf, Any) for buf in p.discrete) + @set! p.constant = Tuple(similar(buf, Any) for buf in p.constant) + @set! p.nonnumeric = Tuple(similar(buf, Any) for buf in p.nonnumeric) + @set! p.caches = Tuple(similar(buf, Any) for buf in p.caches) + return p +end + struct NestedGetIndex{T} x::T end From 9d88d5f3a4b21ad639c860a67312507ca24ac97f Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 7 Feb 2025 19:15:32 +0530 Subject: [PATCH 0885/1337] fix: fix handling of array unknowns in `add_initialization_parameters` --- src/systems/abstractsystem.jl | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index 0deef1bc4c..b022d8f24f 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -714,7 +714,16 @@ function add_initialization_parameters(sys::AbstractSystem) for x in existing_initials delete!(defs, x) end - merge!(defs, Dict(initials .=> zero_var.(initials))) + for ivar in initials + if symbolic_type(ivar) == ScalarSymbolic() + defs[ivar] = zero_var(ivar) + else + defs[ivar] = collect(ivar) + for scal_ivar in defs[ivar] + defs[scal_ivar] = zero_var(scal_ivar) + end + end + end @set! sys.defaults = defs return sys end From 14b28d485d8e4fe910e520160e2da046671da2a5 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 7 Feb 2025 19:15:45 +0530 Subject: [PATCH 0886/1337] fix: respect `validate` kward in `_remake_buffer` --- src/systems/parameter_buffer.jl | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/systems/parameter_buffer.jl b/src/systems/parameter_buffer.jl index ab33cc5b63..f2d0867a32 100644 --- a/src/systems/parameter_buffer.jl +++ b/src/systems/parameter_buffer.jl @@ -459,10 +459,12 @@ function _remake_buffer(indp, oldbuf::MTKParameters, idxs, vals; validate = true @set! newbuf.nonnumeric = Tuple(similar(buf, Any) for buf in newbuf.nonnumeric) function handle_parameter(ic, sym, idx, val) - if sym === nothing - validate_parameter_type(ic, idx, val) - else - validate_parameter_type(ic, sym, idx, val) + if validate + if sym === nothing + validate_parameter_type(ic, idx, val) + else + validate_parameter_type(ic, sym, idx, val) + end end # `ParameterIndex(idx)` turns off size validation since it relies on there # being an existing value From 7b5f29d1067b91aeb128657d064216726cc52689 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 7 Feb 2025 19:18:08 +0530 Subject: [PATCH 0887/1337] fix: handle constant initial conditions of derivatives of unknowns --- src/systems/problem_utils.jl | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/systems/problem_utils.jl b/src/systems/problem_utils.jl index 11f6551bbe..dcb9416da3 100644 --- a/src/systems/problem_utils.jl +++ b/src/systems/problem_utils.jl @@ -551,7 +551,9 @@ function build_operating_point!(sys::AbstractSystem, k = unwrap(k) if is_parameter(sys, k) pmap[k] = v - elseif is_variable(sys, k) || has_observed_with_lhs(sys, k) + elseif is_variable(sys, k) || has_observed_with_lhs(sys, k) || + iscall(k) && + operation(k) isa Differential && is_variable(sys, arguments(k)[1]) if symbolic_type(v) == NotSymbolic() && !is_array_of_symbolics(v) && v !== nothing op[Initial(k)] = v @@ -594,7 +596,7 @@ function maybe_build_initialization_problem( t = 0.0 end - initializeprob = ModelingToolkit.InitializationProblem( + initializeprob = ModelingToolkit.InitializationProblem{true, SciMLBase.FullSpecialize}( sys, t, u0map, pmap; guesses, kwargs...) meta = get_metadata(initializeprob.f.sys) From 8142b76a15afcc9fc9bc2d6cc26e8a720c8e13fe Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 10 Feb 2025 18:45:39 +0530 Subject: [PATCH 0888/1337] feat: implement `is_time_dependent` for `PDESystem` --- src/systems/pde/pdesystem.jl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/systems/pde/pdesystem.jl b/src/systems/pde/pdesystem.jl index eac540e401..96e6a6b276 100644 --- a/src/systems/pde/pdesystem.jl +++ b/src/systems/pde/pdesystem.jl @@ -166,3 +166,5 @@ function Base.show(io::IO, ::MIME"text/plain", sys::PDESystem) print(io, "Default Parameter Values", get_defaults(sys)) return nothing end + +SymbolicIndexingInterface.is_time_dependent(::AbstractMultivariateSystem) = true From 2d9194debaac2d12a20c5753a07efdb96730ec08 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 10 Feb 2025 18:46:00 +0530 Subject: [PATCH 0889/1337] fix: retain parameter dependencies in `build_operating_point!` --- src/systems/problem_utils.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/systems/problem_utils.jl b/src/systems/problem_utils.jl index dcb9416da3..38dec78335 100644 --- a/src/systems/problem_utils.jl +++ b/src/systems/problem_utils.jl @@ -549,7 +549,7 @@ function build_operating_point!(sys::AbstractSystem, for (k, v) in op k = unwrap(k) - if is_parameter(sys, k) + if is_parameter(sys, k) || has_parameter_dependency_with_lhs(sys, k) pmap[k] = v elseif is_variable(sys, k) || has_observed_with_lhs(sys, k) || iscall(k) && From cecb15c210b2c5de34ef8f68e6737c76d944cf70 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 10 Feb 2025 18:46:05 +0530 Subject: [PATCH 0890/1337] feat: export `Initial` --- src/ModelingToolkit.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ModelingToolkit.jl b/src/ModelingToolkit.jl index 303c88da73..c63011fbea 100644 --- a/src/ModelingToolkit.jl +++ b/src/ModelingToolkit.jl @@ -278,7 +278,7 @@ export toexpr, get_variables export simplify, substitute export build_function export modelingtoolkitize -export generate_initializesystem +export generate_initializesystem, Initial export alg_equations, diff_equations, has_alg_equations, has_diff_equations export get_alg_eqs, get_diff_eqs, has_alg_eqs, has_diff_eqs From 33e844b312f723defa002f7793500ba79d29a20a Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 10 Feb 2025 18:46:26 +0530 Subject: [PATCH 0891/1337] refactor: make `symbol_to_symbolic` a global utility function --- src/systems/diffeqs/odesystem.jl | 14 ++------------ src/utils.jl | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/src/systems/diffeqs/odesystem.jl b/src/systems/diffeqs/odesystem.jl index 8febfc253b..e53b9223e6 100644 --- a/src/systems/diffeqs/odesystem.jl +++ b/src/systems/diffeqs/odesystem.jl @@ -445,20 +445,10 @@ function build_explicit_observed_function(sys, ts; end allsyms = all_symbols(sys) - function symbol_to_symbolic(sym) - sym isa Symbol || return sym - idx = findfirst(x -> (hasname(x) ? getname(x) : Symbol(x)) == sym, allsyms) - idx === nothing && return sym - sym = allsyms[idx] - if iscall(sym) && operation(sym) == getindex - sym = arguments(sym)[1] - end - return sym - end if symbolic_type(ts) == NotSymbolic() && ts isa AbstractArray - ts = map(symbol_to_symbolic, ts) + ts = map(x -> symbol_to_symbolic(sys, x; allsyms), ts) else - ts = symbol_to_symbolic(ts) + ts = symbol_to_symbolic(sys, ts; allsyms) end vs = ModelingToolkit.vars(ts; op) diff --git a/src/utils.jl b/src/utils.jl index 3f52b0676f..fc6e49d894 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -1249,3 +1249,21 @@ function process_equations(eqs, iv) diffvars, allunknowns, ps, Equation[diffeq; algeeq; compressed_eqs] end + +""" + $(TYPEDSIGNATURES) + +If `sym isa Symbol`, try and convert it to a symbolic by matching against symbolic +variables in `allsyms`. If `sym` is not a `Symbol` or no match was found, return +`sym` as-is. +""" +function symbol_to_symbolic(sys::AbstractSystem, sym; allsyms = all_symbols(sys)) + sym isa Symbol || return sym + idx = findfirst(x -> (hasname(x) ? getname(x) : Symbol(x)) == sym, allsyms) + idx === nothing && return sym + sym = allsyms[idx] + if iscall(sym) && operation(sym) == getindex + sym = arguments(sym)[1] + end + return sym +end From b42075a3b97473980b894fa4851d0ca23fbf9d7f Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 10 Feb 2025 18:47:03 +0530 Subject: [PATCH 0892/1337] feat: implement `SciMLBase.late_binding_update_u0_p` to update `Initial` parameters --- src/systems/nonlinear/initializesystem.jl | 27 +++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/systems/nonlinear/initializesystem.jl b/src/systems/nonlinear/initializesystem.jl index 6696f6b7c2..d32a1befcc 100644 --- a/src/systems/nonlinear/initializesystem.jl +++ b/src/systems/nonlinear/initializesystem.jl @@ -490,6 +490,33 @@ function SciMLBase.remake_initialization_data( return get(kws, :initialization_data, nothing) end +function SciMLBase.late_binding_update_u0_p( + prob, sys::AbstractSystem, u0, p, t0, newu0, newp) + u0 === missing && return newu0, newp + eltype(u0) <: Pair || return newu0, newp + + newu0 = DiffEqBase.promote_u0(newu0, newp, t0) + tunables, repack, alias = SciMLStructures.canonicalize(SciMLStructures.Tunable(), newp) + tunables = DiffEqBase.promote_u0(tunables, newu0, t0) + newp = repack(tunables) + + allsyms = all_symbols(sys) + for (k, v) in u0 + v === nothing && continue + (symbolic_type(v) == NotSymbolic() && !is_array_of_symbolics(v)) || continue + if k isa Symbol + k2 = symbol_to_symbolic(sys, k; allsyms) + # if it is returned as-is, there is no match so skip it + k2 === k && continue + k = k2 + end + is_parameter(sys, Initial(k)) || continue + setp(sys, Initial(k))(newp, v) + end + + return newu0, newp +end + """ Counteracts the CSE/array variable hacks in `symbolics_tearing.jl` so it works with initialization. From 9d7e4131920dd147e56f4fd7173674ae696a744e Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 10 Feb 2025 18:47:29 +0530 Subject: [PATCH 0893/1337] fix: provide history function to `reconstruct_fn` in `remake_initialization_data` --- src/systems/nonlinear/initializesystem.jl | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/systems/nonlinear/initializesystem.jl b/src/systems/nonlinear/initializesystem.jl index d32a1befcc..f03785cf3c 100644 --- a/src/systems/nonlinear/initializesystem.jl +++ b/src/systems/nonlinear/initializesystem.jl @@ -403,8 +403,13 @@ function SciMLBase.remake_initialization_data( else reconstruct_fn = ReconstructInitializeprob(sys, oldinitsys) end + # the history function doesn't matter because `reconstruct_fn` is only going to + # update the values of parameters, which aren't time dependent. The reason it + # is called is because `Initial` parameters are calculated from the corresponding + # state values. + history_fn = is_time_dependent(sys) && !is_markovian(sys) ? Returns(newu0) : nothing new_initu0, new_initp = reconstruct_fn( - ProblemState(; u = newu0, p = newp, t = t0), oldinitprob) + ProblemState(; u = newu0, p = newp, t = t0, h = history_fn), oldinitprob) if oldinitprob.f.resid_prototype === nothing newf = oldinitprob.f else From fe329d0d9e36f10f8f4d56c811c28011a72c74a9 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 10 Feb 2025 18:47:52 +0530 Subject: [PATCH 0894/1337] test: fix unsatisfiable initialization --- test/initializationsystem.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/initializationsystem.jl b/test/initializationsystem.jl index fe5f03b9c7..3a208bb940 100644 --- a/test/initializationsystem.jl +++ b/test/initializationsystem.jl @@ -1207,7 +1207,8 @@ end prob2 = remake(prob; u0 = [x => 2.0]) @test prob2[x] == 2.0 test_dummy_initialization_equation(prob2, x) - prob3 = remake(prob; u0 = [y => 2.0]) + # otherwise we have `x ~ 2, y ~ 2` which is unsatisfiable + prob3 = remake(prob; u0 = [x => nothing, y => 2.0]) @test prob3.f.initialization_data !== nothing @test init(prob3)[x] ≈ 1.0 prob4 = remake(prob; p = [p => 1.0]) From f5cdf2146414873f5a47fd1f0e9a13723d862901 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 10 Feb 2025 18:48:19 +0530 Subject: [PATCH 0895/1337] test: account for additional initialization parameters --- test/discrete_system.jl | 4 +++- test/mtkparameters.jl | 2 +- test/odesystem.jl | 10 ++++++++-- test/symbolic_indexing_interface.jl | 3 ++- 4 files changed, 14 insertions(+), 5 deletions(-) diff --git a/test/discrete_system.jl b/test/discrete_system.jl index b63f66d4e4..78afafd51d 100644 --- a/test/discrete_system.jl +++ b/test/discrete_system.jl @@ -83,7 +83,9 @@ prob_map2 = DiscreteProblem(sys) sol_map2 = solve(prob_map2, FunctionMap()); @test sol_map.u ≈ sol_map2.u -@test sol_map.prob.p == sol_map2.prob.p +for p in parameters(sys) + @test sol_map.prob.ps[p] ≈ sol_map2.prob.ps[p] +end @test_throws Any sol_map2[R2] @test sol_map2[R2(k + 1)][begin:(end - 1)] == sol_map2[R][(begin + 1):end] # Direct Implementation diff --git a/test/mtkparameters.jl b/test/mtkparameters.jl index ce524fdb76..a4e7ef9052 100644 --- a/test/mtkparameters.jl +++ b/test/mtkparameters.jl @@ -109,7 +109,7 @@ eq = D(X) ~ p[1] - p[2] * X u0 = [X => 1.0] ps = [p => [2.0, 0.1]] p = MTKParameters(osys, ps, u0) -@test p.tunable == [2.0, 0.1] +@test p.tunable == [2.0, 0.1, 1.0] # Ensure partial update promotes the buffer @parameters p q r diff --git a/test/odesystem.jl b/test/odesystem.jl index a9677aad26..65ceaabc16 100644 --- a/test/odesystem.jl +++ b/test/odesystem.jl @@ -287,7 +287,10 @@ u01 = [y₁ => 1, y₂ => 1, y₃ => 1] prob_pmap = remake(prob14; p = p3, u0 = u01) prob_dpmap = remake(prob14; p = Dict(p3), u0 = Dict(u01)) for p in [prob_pmap, prob_dpmap] - @test p.p == MTKParameters(sys, [k₁ => 0.05, k₂ => 2e7, k₃ => 1.1e4]) + @test p.p isa MTKParameters + p.ps[k₁] ≈ 0.05 + p.ps[k₂] ≈ 2e7 + p.ps[k₃] ≈ 1.1e-4 @test Set(Num.(unknowns(sys)) .=> p.u0) == Set([y₁ => 1, y₂ => 1, y₃ => 1]) end sol_pmap = solve(prob_pmap, Rodas5()) @@ -315,7 +318,10 @@ sol_dpmap = solve(prob_dpmap, Rodas5()) prob = ODEProblem(sys, Pair[]) prob_new = SciMLBase.remake(prob, p = Dict(sys1.a => 3.0, b => 4.0), u0 = Dict(sys1.x => 1.0)) - @test prob_new.p == MTKParameters(sys, [b => 4.0, sys1.a => 3.0, sys.sys2.a => 1.0]) + @test prob_new.p isa MTKParameters + @test prob_new.ps[b] ≈ 4.0 + @test prob_new.ps[sys1.a] ≈ 3.0 + @test prob_new.ps[sys.sys2.a] ≈ 1.0 @test prob_new.u0 == [1.0, 0.0] end diff --git a/test/symbolic_indexing_interface.jl b/test/symbolic_indexing_interface.jl index 4a7cd926b4..5979cd71ac 100644 --- a/test/symbolic_indexing_interface.jl +++ b/test/symbolic_indexing_interface.jl @@ -23,7 +23,8 @@ using SciMLStructures: Tunable @test parameter_index.( (odesys,), [x, y, t, ParameterIndex(Tunable(), 1), :x, :y]) == [nothing, nothing, nothing, ParameterIndex(Tunable(), 1), nothing, nothing] - @test isequal(parameter_symbols(odesys), [a, b]) + @test isequal( + Set(parameter_symbols(odesys)), Set([a, b, Initial(x), Initial(y), Initial(xy)])) @test all(is_independent_variable.((odesys,), [t, :t])) @test all(.!is_independent_variable.((odesys,), [x, y, a, :x, :y, :a])) @test isequal(independent_variable_symbols(odesys), [t]) From 525324a7d48d6906ae7f297d96877fca8952ceea Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 10 Feb 2025 23:35:56 +0530 Subject: [PATCH 0896/1337] fix: don't copy variables to `Initial` parameters in `ReconstructInitializeprob` --- src/systems/nonlinear/initializesystem.jl | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/systems/nonlinear/initializesystem.jl b/src/systems/nonlinear/initializesystem.jl index f03785cf3c..ed338720cf 100644 --- a/src/systems/nonlinear/initializesystem.jl +++ b/src/systems/nonlinear/initializesystem.jl @@ -310,12 +310,9 @@ end function ReconstructInitializeprob( srcsys::AbstractSystem, dstsys::AbstractSystem) syms = reduce( - vcat, reorder_parameters(dstsys, parameters(dstsys; initial_parameters = true)); + vcat, reorder_parameters(dstsys, parameters(dstsys)); init = []) - srcsyms = map(syms) do sym - iscall(sym) && operation(sym) isa Initial ? arguments(sym)[1] : sym - end - getter = getu(srcsys, srcsyms) + getter = getu(srcsys, syms) setter = setp_oop(dstsys, syms) return ReconstructInitializeprob(getter, setter) end @@ -338,6 +335,10 @@ function (rip::ReconstructInitializeprob)(srcvalp, dstvalp) else u0 = T.(state_values(dstvalp)) end + buf, repack, alias = SciMLStructures.canonicalize(SciMLStructures.Tunable(), newp) + if eltype(buf) != T + newp = repack(T.(buf)) + end return u0, newp end From b0749b647be9cb779b1480e255274abf3bb88df9 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 11 Feb 2025 14:53:23 +0530 Subject: [PATCH 0897/1337] feat: add `Initial` parameters for real-valued parameter dependencies --- src/systems/abstractsystem.jl | 57 +++++++++++++++-------- src/systems/nonlinear/initializesystem.jl | 12 ++--- src/systems/problem_utils.jl | 8 +++- 3 files changed, 51 insertions(+), 26 deletions(-) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index b022d8f24f..9278afc366 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -689,26 +689,33 @@ function SymbolicUtils.maketerm(::Type{<:BasicSymbolic}, ::Initial, args, meta) end function add_initialization_parameters(sys::AbstractSystem) - is_time_dependent(sys) || return sys @assert !has_systems(sys) || isempty(get_systems(sys)) - eqs = equations(sys) - if !(eqs isa Vector{Equation}) - eqs = Equation[x for x in eqs if x isa Equation] - end - obs, eqs = unhack_observed(observed(sys), eqs) - all_uvars = Set{BasicSymbolic}() - for x in Iterators.flatten((unknowns(sys), Iterators.map(eq -> eq.lhs, obs))) - x = unwrap(x) - if iscall(x) && operation(x) == getindex - push!(all_uvars, arguments(x)[1]) - else - push!(all_uvars, x) + all_initialvars = Set{BasicSymbolic}() + # time-independent systems don't initialize unknowns + if is_time_dependent(sys) + eqs = equations(sys) + if !(eqs isa Vector{Equation}) + eqs = Equation[x for x in eqs if x isa Equation] + end + obs, eqs = unhack_observed(observed(sys), eqs) + for x in Iterators.flatten((unknowns(sys), Iterators.map(eq -> eq.lhs, obs))) + x = unwrap(x) + if iscall(x) && operation(x) == getindex + push!(all_initialvars, arguments(x)[1]) + else + push!(all_initialvars, x) + end end end - all_uvars = collect(all_uvars) - initials = map(Initial(), all_uvars) - existing_initials = filter( - x -> iscall(x) && (operation(x) isa Initial), parameters(sys)) + for eq in parameter_dependencies(sys) + is_variable_floatingpoint(eq.lhs) || continue + push!(all_initialvars, eq.lhs) + end + all_initialvars = collect(all_initialvars) + initials = map(Initial(), all_initialvars) + # existing_initials = filter( + # x -> iscall(x) && (operation(x) isa Initial), parameters(sys)) + existing_initials = [] @set! sys.ps = unique!([setdiff(get_ps(sys), existing_initials); initials]) defs = copy(get_defaults(sys)) for x in existing_initials @@ -1296,7 +1303,7 @@ Get the parameters of the system `sys` and its subsystems. See also [`@parameters`](@ref) and [`ModelingToolkit.get_ps`](@ref). """ -function parameters(sys::AbstractSystem; initial_parameters = !is_time_dependent(sys)) +function parameters(sys::AbstractSystem; initial_parameters = false) ps = get_ps(sys) if ps == SciMLBase.NullParameters() return [] @@ -1308,7 +1315,19 @@ function parameters(sys::AbstractSystem; initial_parameters = !is_time_dependent result = unique(isempty(systems) ? ps : [ps; reduce(vcat, namespace_parameters.(systems))]) if !initial_parameters - filter!(x -> !iscall(x) || !isa(operation(x), Initial), result) + if is_time_dependent(sys) + # time-dependent systems have `Initial` parameters for all their + # unknowns/pdeps, all of which should be hidden. + filter!(x -> !iscall(x) || !isa(operation(x), Initial), result) + else + # time-independent systems only have `Initial` parameters for + # pdeps. Any other `Initial` parameters should be kept (e.g. initialization + # systems) + filter!( + x -> !iscall(x) || !isa(operation(x), Initial) || + !has_parameter_dependency_with_lhs(sys, only(arguments(x))), + result) + end end return result end diff --git a/src/systems/nonlinear/initializesystem.jl b/src/systems/nonlinear/initializesystem.jl index ed338720cf..da66ce66d7 100644 --- a/src/systems/nonlinear/initializesystem.jl +++ b/src/systems/nonlinear/initializesystem.jl @@ -207,6 +207,12 @@ function generate_initializesystem(sys::AbstractSystem; append!(eqs_ics, trueobs) end + if is_time_dependent(sys) + vars = [vars; collect(values(paramsubs))] + else + vars = collect(values(paramsubs)) + end + # even if `p => tovar(p)` is in `paramsubs`, `isparameter(p[1]) === true` after substitution # so add scalarized versions as well for k in collect(keys(paramsubs)) @@ -217,12 +223,6 @@ function generate_initializesystem(sys::AbstractSystem; end eqs_ics = Symbolics.substitute.(eqs_ics, (paramsubs,)) - if is_time_dependent(sys) - vars = [vars; collect(values(paramsubs))] - else - vars = collect(values(paramsubs)) - end - for k in keys(defs) defs[k] = substitute(defs[k], paramsubs) end diff --git a/src/systems/problem_utils.jl b/src/systems/problem_utils.jl index 38dec78335..87aafb8f73 100644 --- a/src/systems/problem_utils.jl +++ b/src/systems/problem_utils.jl @@ -549,8 +549,14 @@ function build_operating_point!(sys::AbstractSystem, for (k, v) in op k = unwrap(k) - if is_parameter(sys, k) || has_parameter_dependency_with_lhs(sys, k) + if is_parameter(sys, k) pmap[k] = v + elseif has_parameter_dependency_with_lhs(sys, k) && is_variable_floatingpoint(k) && + v !== nothing && !isequal(v, Initial(k)) + op[Initial(k)] = v + pmap[Initial(k)] = v + op[k] = Initial(k) + pmap[k] = Initial(k) elseif is_variable(sys, k) || has_observed_with_lhs(sys, k) || iscall(k) && operation(k) isa Differential && is_variable(sys, arguments(k)[1]) From 35ef0ab8b9f1585ae40284cae141be9045d2bdac Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 11 Feb 2025 14:53:37 +0530 Subject: [PATCH 0898/1337] test: update tests to consider `Initial` parameters of pdeps --- test/mtkparameters.jl | 2 +- test/parameter_dependencies.jl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/mtkparameters.jl b/test/mtkparameters.jl index a4e7ef9052..3ac8f7569d 100644 --- a/test/mtkparameters.jl +++ b/test/mtkparameters.jl @@ -40,7 +40,7 @@ setp(sys, a)(ps, 1.0) @test getp(sys, a)(ps) == getp(sys, b)(ps) / 2 == getp(sys, c)(ps) / 3 == 1.0 -for (portion, values) in [(Tunable(), [1.0, 5.0, 6.0, 7.0]) +for (portion, values) in [(Tunable(), [1.0, 2.0, 5.0, 6.0, 7.0]) (Discrete(), [3.0]) (Constants(), vcat([0.1, 0.2, 0.3], ones(9), [4.0]))] buffer, repack, alias = canonicalize(portion, ps) diff --git a/test/parameter_dependencies.jl b/test/parameter_dependencies.jl index 7f71120564..f88f3f03b7 100644 --- a/test/parameter_dependencies.jl +++ b/test/parameter_dependencies.jl @@ -326,7 +326,7 @@ end eqs = [0 ~ p1 * x * exp(x) + p2] @mtkbuild sys = NonlinearSystem(eqs; parameter_dependencies = [p2 => 2p1]) @test isequal(only(parameters(sys)), p1) - @test Set(full_parameters(sys)) == Set([p1, p2]) + @test Set(full_parameters(sys)) == Set([p1, p2, Initial(p2)]) prob = NonlinearProblem(sys, [x => 1.0]) @test prob.ps[p1] == 1.0 @test prob.ps[p2] == 2.0 From bfae1d224cf960af46f4ffc64c5b4e60e71aff71 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 11 Feb 2025 16:18:40 +0530 Subject: [PATCH 0899/1337] refactor: move linearization-related code to new `linearization.jl` file --- src/ModelingToolkit.jl | 1 + src/linearization.jl | 537 +++++++++++++++++++++++++++++++++ src/systems/abstractsystem.jl | 538 ---------------------------------- 3 files changed, 538 insertions(+), 538 deletions(-) create mode 100644 src/linearization.jl diff --git a/src/ModelingToolkit.jl b/src/ModelingToolkit.jl index c63011fbea..7d08a9aac2 100644 --- a/src/ModelingToolkit.jl +++ b/src/ModelingToolkit.jl @@ -152,6 +152,7 @@ include("systems/imperative_affect.jl") include("systems/callbacks.jl") include("systems/codegen_utils.jl") include("systems/problem_utils.jl") +include("linearization.jl") include("systems/nonlinear/nonlinearsystem.jl") include("systems/nonlinear/homotopy_continuation.jl") diff --git a/src/linearization.jl b/src/linearization.jl new file mode 100644 index 0000000000..bbf0d8e2c1 --- /dev/null +++ b/src/linearization.jl @@ -0,0 +1,537 @@ +""" + lin_fun, simplified_sys = linearization_function(sys::AbstractSystem, inputs, outputs; simplify = false, initialize = true, initialization_solver_alg = TrustRegion(), kwargs...) + +Return a function that linearizes the system `sys`. The function [`linearize`](@ref) provides a higher-level and easier to use interface. + +`lin_fun` is a function `(variables, p, t) -> (; f_x, f_z, g_x, g_z, f_u, g_u, h_x, h_z, h_u)`, i.e., it returns a NamedTuple with the Jacobians of `f,g,h` for the nonlinear `sys` (technically for `simplified_sys`) on the form + +```math +\\begin{aligned} +ẋ &= f(x, z, u) \\\\ +0 &= g(x, z, u) \\\\ +y &= h(x, z, u) +\\end{aligned} +``` + +where `x` are differential unknown variables, `z` algebraic variables, `u` inputs and `y` outputs. To obtain a linear statespace representation, see [`linearize`](@ref). The input argument `variables` is a vector defining the operating point, corresponding to `unknowns(simplified_sys)` and `p` is a vector corresponding to the parameters of `simplified_sys`. Note: all variables in `inputs` have been converted to parameters in `simplified_sys`. + +The `simplified_sys` has undergone [`structural_simplify`](@ref) and had any occurring input or output variables replaced with the variables provided in arguments `inputs` and `outputs`. The unknowns of this system also indicate the order of the unknowns that holds for the linearized matrices. + +# Arguments: + + - `sys`: An [`ODESystem`](@ref). This function will automatically apply simplification passes on `sys` and return the resulting `simplified_sys`. + - `inputs`: A vector of variables that indicate the inputs of the linearized input-output model. + - `outputs`: A vector of variables that indicate the outputs of the linearized input-output model. + - `simplify`: Apply simplification in tearing. + - `initialize`: If true, a check is performed to ensure that the operating point is consistent (satisfies algebraic equations). If the op is not consistent, initialization is performed. + - `initialization_solver_alg`: A NonlinearSolve algorithm to use for solving for a feasible set of state and algebraic variables that satisfies the specified operating point. + - `kwargs`: Are passed on to `find_solvables!` + +See also [`linearize`](@ref) which provides a higher-level interface. +""" +function linearization_function(sys::AbstractSystem, inputs, + outputs; simplify = false, + initialize = true, + initializealg = nothing, + initialization_abstol = 1e-5, + initialization_reltol = 1e-3, + op = Dict(), + p = DiffEqBase.NullParameters(), + zero_dummy_der = false, + initialization_solver_alg = TrustRegion(), + eval_expression = false, eval_module = @__MODULE__, + warn_initialize_determined = true, + kwargs...) + op = Dict(op) + inputs isa AbstractVector || (inputs = [inputs]) + outputs isa AbstractVector || (outputs = [outputs]) + inputs = mapreduce(vcat, inputs; init = []) do var + symbolic_type(var) == ArraySymbolic() ? collect(var) : [var] + end + outputs = mapreduce(vcat, outputs; init = []) do var + symbolic_type(var) == ArraySymbolic() ? collect(var) : [var] + end + ssys, diff_idxs, alge_idxs, input_idxs = io_preprocessing(sys, inputs, outputs; + simplify, + kwargs...) + if zero_dummy_der + dummyder = setdiff(unknowns(ssys), unknowns(sys)) + defs = Dict(x => 0.0 for x in dummyder) + @set! ssys.defaults = merge(defs, defaults(ssys)) + op = merge(defs, op) + end + sys = ssys + + if initializealg === nothing + initializealg = initialize ? OverrideInit() : NoInit() + end + + fun, u0, p = process_SciMLProblem( + ODEFunction{true, SciMLBase.FullSpecialize}, sys, merge( + defaults_and_guesses(sys), op), p; + t = 0.0, build_initializeprob = initializealg isa OverrideInit, + allow_incomplete = true, algebraic_only = true) + prob = ODEProblem(fun, u0, (nothing, nothing), p) + + ps = parameters(sys) + h = build_explicit_observed_function(sys, outputs; eval_expression, eval_module) + lin_fun = let diff_idxs = diff_idxs, + alge_idxs = alge_idxs, + input_idxs = input_idxs, + sts = unknowns(sys), + fun = fun, + prob = prob, + sys_ps = p, + h = h, + integ_cache = (similar(u0)), + chunk = ForwardDiff.Chunk(input_idxs), + initializealg = initializealg, + initialization_abstol = initialization_abstol, + initialization_reltol = initialization_reltol, + initialization_solver_alg = initialization_solver_alg, + sys = sys + + function (u, p, t) + if !isa(p, MTKParameters) + p = todict(p) + newps = deepcopy(sys_ps) + for (k, v) in p + if is_parameter(sys, k) + v = fixpoint_sub(v, p) + setp(sys, k)(newps, v) + end + end + p = newps + end + + if u !== nothing # Handle systems without unknowns + length(sts) == length(u) || + error("Number of unknown variables ($(length(sts))) does not match the number of input unknowns ($(length(u)))") + + integ = MockIntegrator{true}(u, p, t, integ_cache) + u, p, success = SciMLBase.get_initial_values( + prob, integ, fun, initializealg, Val(true); + abstol = initialization_abstol, reltol = initialization_reltol, + nlsolve_alg = initialization_solver_alg) + if !success + error("Initialization algorithm $(initializealg) failed with `u = $u` and `p = $p`.") + end + uf = SciMLBase.UJacobianWrapper(fun, t, p) + fg_xz = ForwardDiff.jacobian(uf, u) + h_xz = ForwardDiff.jacobian( + let p = p, t = t + xz -> h(xz, p, t) + end, u) + pf = SciMLBase.ParamJacobianWrapper(fun, t, u) + fg_u = jacobian_wrt_vars(pf, p, input_idxs, chunk) + else + length(sts) == 0 || + error("Number of unknown variables (0) does not match the number of input unknowns ($(length(u)))") + fg_xz = zeros(0, 0) + h_xz = fg_u = zeros(0, length(inputs)) + end + hp = let u = u, t = t + _hp(p) = h(u, p, t) + _hp + end + h_u = jacobian_wrt_vars(hp, p, input_idxs, chunk) + (f_x = fg_xz[diff_idxs, diff_idxs], + f_z = fg_xz[diff_idxs, alge_idxs], + g_x = fg_xz[alge_idxs, diff_idxs], + g_z = fg_xz[alge_idxs, alge_idxs], + f_u = fg_u[diff_idxs, :], + g_u = fg_u[alge_idxs, :], + h_x = h_xz[:, diff_idxs], + h_z = h_xz[:, alge_idxs], + h_u = h_u) + end + end + return lin_fun, sys +end + +""" + $(TYPEDEF) + +Mock `DEIntegrator` to allow using `CheckInit` without having to create a new integrator +(and consequently depend on `OrdinaryDiffEq`). + +# Fields + +$(TYPEDFIELDS) +""" +struct MockIntegrator{iip, U, P, T, C} <: SciMLBase.DEIntegrator{Nothing, iip, U, T} + """ + The state vector. + """ + u::U + """ + The parameter object. + """ + p::P + """ + The current time. + """ + t::T + """ + The integrator cache. + """ + cache::C +end + +function MockIntegrator{iip}(u::U, p::P, t::T, cache::C) where {iip, U, P, T, C} + return MockIntegrator{iip, U, P, T, C}(u, p, t, cache) +end + +SymbolicIndexingInterface.state_values(integ::MockIntegrator) = integ.u +SymbolicIndexingInterface.parameter_values(integ::MockIntegrator) = integ.p +SymbolicIndexingInterface.current_time(integ::MockIntegrator) = integ.t +SciMLBase.get_tmp_cache(integ::MockIntegrator) = integ.cache + +""" + (; A, B, C, D), simplified_sys = linearize_symbolic(sys::AbstractSystem, inputs, outputs; simplify = false, allow_input_derivatives = false, kwargs...) + +Similar to [`linearize`](@ref), but returns symbolic matrices `A,B,C,D` rather than numeric. While `linearize` uses ForwardDiff to perform the linearization, this function uses `Symbolics.jacobian`. + +See [`linearize`](@ref) for a description of the arguments. + +# Extended help +The named tuple returned as the first argument additionally contains the jacobians `f_x, f_z, g_x, g_z, f_u, g_u, h_x, h_z, h_u` of +```math +\\begin{aligned} +ẋ &= f(x, z, u) \\\\ +0 &= g(x, z, u) \\\\ +y &= h(x, z, u) +\\end{aligned} +``` +where `x` are differential unknown variables, `z` algebraic variables, `u` inputs and `y` outputs. +""" +function linearize_symbolic(sys::AbstractSystem, inputs, + outputs; simplify = false, allow_input_derivatives = false, + eval_expression = false, eval_module = @__MODULE__, + kwargs...) + sys, diff_idxs, alge_idxs, input_idxs = io_preprocessing( + sys, inputs, outputs; simplify, + kwargs...) + sts = unknowns(sys) + t = get_iv(sys) + ps = parameters(sys; initial_parameters = true) + p = reorder_parameters(sys, ps) + + fun_expr = generate_function(sys, sts, ps; expression = Val{true})[1] + fun = eval_or_rgf(fun_expr; eval_expression, eval_module) + dx = fun(sts, p, t) + + h = build_explicit_observed_function(sys, outputs; eval_expression, eval_module) + y = h(sts, p, t) + + fg_xz = Symbolics.jacobian(dx, sts) + fg_u = Symbolics.jacobian(dx, inputs) + h_xz = Symbolics.jacobian(y, sts) + h_u = Symbolics.jacobian(y, inputs) + f_x = fg_xz[diff_idxs, diff_idxs] + f_z = fg_xz[diff_idxs, alge_idxs] + g_x = fg_xz[alge_idxs, diff_idxs] + g_z = fg_xz[alge_idxs, alge_idxs] + f_u = fg_u[diff_idxs, :] + g_u = fg_u[alge_idxs, :] + h_x = h_xz[:, diff_idxs] + h_z = h_xz[:, alge_idxs] + + nx, nu = size(f_u) + nz = size(f_z, 2) + ny = size(h_x, 1) + + D = h_u + + if isempty(g_z) # ODE + A = f_x + B = f_u + C = h_x + else + gz = lu(g_z; check = false) + issuccess(gz) || + error("g_z not invertible, this indicates that the DAE is of index > 1.") + gzgx = -(gz \ g_x) + A = [f_x f_z + gzgx*f_x gzgx*f_z] + B = [f_u + gzgx * f_u] # The cited paper has zeros in the bottom block, see derivation in https://github.com/SciML/ModelingToolkit.jl/pull/1691 for the correct formula + + C = [h_x h_z] + Bs = -(gz \ g_u) # This equation differ from the cited paper, the paper is likely wrong since their equaiton leads to a dimension mismatch. + if !iszero(Bs) + if !allow_input_derivatives + der_inds = findall(vec(any(!iszero, Bs, dims = 1))) + @show typeof(der_inds) + error("Input derivatives appeared in expressions (-g_z\\g_u != 0), the following inputs appeared differentiated: $(ModelingToolkit.inputs(sys)[der_inds]). Call `linearize_symbolic` with keyword argument `allow_input_derivatives = true` to allow this and have the returned `B` matrix be of double width ($(2nu)), where the last $nu inputs are the derivatives of the first $nu inputs.") + end + B = [B [zeros(nx, nu); Bs]] + D = [D zeros(ny, nu)] + end + end + + (; A, B, C, D, f_x, f_z, g_x, g_z, f_u, g_u, h_x, h_z, h_u), sys +end + +function markio!(state, orig_inputs, inputs, outputs; check = true) + fullvars = get_fullvars(state) + inputset = Dict{Any, Bool}(i => false for i in inputs) + outputset = Dict{Any, Bool}(o => false for o in outputs) + for (i, v) in enumerate(fullvars) + if v in keys(inputset) + if v in keys(outputset) + v = setio(v, true, true) + outputset[v] = true + else + v = setio(v, true, false) + end + inputset[v] = true + fullvars[i] = v + elseif v in keys(outputset) + v = setio(v, false, true) + outputset[v] = true + fullvars[i] = v + else + if isinput(v) + push!(orig_inputs, v) + end + v = setio(v, false, false) + fullvars[i] = v + end + end + if check + ikeys = keys(filter(!last, inputset)) + if !isempty(ikeys) + error( + "Some specified inputs were not found in system. The following variables were not found ", + ikeys) + end + end + check && (all(values(outputset)) || + error( + "Some specified outputs were not found in system. The following Dict indicates the found variables ", + outputset)) + state, orig_inputs +end + +""" + (; A, B, C, D), simplified_sys = linearize(sys, inputs, outputs; t=0.0, op = Dict(), allow_input_derivatives = false, zero_dummy_der=false, kwargs...) + (; A, B, C, D) = linearize(simplified_sys, lin_fun; t=0.0, op = Dict(), allow_input_derivatives = false, zero_dummy_der=false) + +Linearize `sys` between `inputs` and `outputs`, both vectors of variables. Return a NamedTuple with the matrices of a linear statespace representation +on the form + +```math +\\begin{aligned} +ẋ &= Ax + Bu\\\\ +y &= Cx + Du +\\end{aligned} +``` + +The first signature automatically calls [`linearization_function`](@ref) internally, +while the second signature expects the outputs of [`linearization_function`](@ref) as input. + +`op` denotes the operating point around which to linearize. If none is provided, +the default values of `sys` are used. + +If `allow_input_derivatives = false`, an error will be thrown if input derivatives (``u̇``) appear as inputs in the linearized equations. If input derivatives are allowed, the returned `B` matrix will be of double width, corresponding to the input `[u; u̇]`. + +`zero_dummy_der` can be set to automatically set the operating point to zero for all dummy derivatives. + +See also [`linearization_function`](@ref) which provides a lower-level interface, [`linearize_symbolic`](@ref) and [`ModelingToolkit.reorder_unknowns`](@ref). + +See extended help for an example. + +The implementation and notation follows that of +["Linear Analysis Approach for Modelica Models", Allain et al. 2009](https://ep.liu.se/ecp/043/075/ecp09430097.pdf) + +# Extended help + +This example builds the following feedback interconnection and linearizes it from the input of `F` to the output of `P`. + +``` + + r ┌─────┐ ┌─────┐ ┌─────┐ +───►│ ├──────►│ │ u │ │ + │ F │ │ C ├────►│ P │ y + └─────┘ ┌►│ │ │ ├─┬─► + │ └─────┘ └─────┘ │ + │ │ + └─────────────────────┘ +``` + +```julia +using ModelingToolkit +using ModelingToolkit: t_nounits as t, D_nounits as D +function plant(; name) + @variables x(t) = 1 + @variables u(t)=0 y(t)=0 + eqs = [D(x) ~ -x + u + y ~ x] + ODESystem(eqs, t; name = name) +end + +function ref_filt(; name) + @variables x(t)=0 y(t)=0 + @variables u(t)=0 [input = true] + eqs = [D(x) ~ -2 * x + u + y ~ x] + ODESystem(eqs, t, name = name) +end + +function controller(kp; name) + @variables y(t)=0 r(t)=0 u(t)=0 + @parameters kp = kp + eqs = [ + u ~ kp * (r - y), + ] + ODESystem(eqs, t; name = name) +end + +@named f = ref_filt() +@named c = controller(1) +@named p = plant() + +connections = [f.y ~ c.r # filtered reference to controller reference + c.u ~ p.u # controller output to plant input + p.y ~ c.y] + +@named cl = ODESystem(connections, t, systems = [f, c, p]) + +lsys0, ssys = linearize(cl, [f.u], [p.x]) +desired_order = [f.x, p.x] +lsys = ModelingToolkit.reorder_unknowns(lsys0, unknowns(ssys), desired_order) + +@assert lsys.A == [-2 0; 1 -2] +@assert lsys.B == [1; 0;;] +@assert lsys.C == [0 1] +@assert lsys.D[] == 0 + +## Symbolic linearization +lsys_sym, _ = ModelingToolkit.linearize_symbolic(cl, [f.u], [p.x]) + +@assert substitute(lsys_sym.A, ModelingToolkit.defaults(cl)) == lsys.A +``` +""" +function linearize(sys, lin_fun; t = 0.0, op = Dict(), allow_input_derivatives = false, + p = DiffEqBase.NullParameters()) + x0 = merge(defaults(sys), Dict(missing_variable_defaults(sys)), op) + u0, defs = get_u0(sys, x0, p) + if has_index_cache(sys) && get_index_cache(sys) !== nothing + if p isa SciMLBase.NullParameters + p = op + elseif p isa Dict + p = merge(p, op) + elseif p isa Vector && eltype(p) <: Pair + p = merge(Dict(p), op) + elseif p isa Vector + p = merge(Dict(parameters(sys) .=> p), op) + end + end + linres = lin_fun(u0, p, t) + f_x, f_z, g_x, g_z, f_u, g_u, h_x, h_z, h_u = linres + + nx, nu = size(f_u) + nz = size(f_z, 2) + ny = size(h_x, 1) + + D = h_u + + if isempty(g_z) + A = f_x + B = f_u + C = h_x + @assert iszero(g_x) + @assert iszero(g_z) + @assert iszero(g_u) + else + gz = lu(g_z; check = false) + issuccess(gz) || + error("g_z not invertible, this indicates that the DAE is of index > 1.") + gzgx = -(gz \ g_x) + A = [f_x f_z + gzgx*f_x gzgx*f_z] + B = [f_u + gzgx * f_u] # The cited paper has zeros in the bottom block, see derivation in https://github.com/SciML/ModelingToolkit.jl/pull/1691 for the correct formula + + C = [h_x h_z] + Bs = -(gz \ g_u) # This equation differ from the cited paper, the paper is likely wrong since their equaiton leads to a dimension mismatch. + if !iszero(Bs) + if !allow_input_derivatives + der_inds = findall(vec(any(!=(0), Bs, dims = 1))) + error("Input derivatives appeared in expressions (-g_z\\g_u != 0), the following inputs appeared differentiated: $(inputs(sys)[der_inds]). Call `linearize` with keyword argument `allow_input_derivatives = true` to allow this and have the returned `B` matrix be of double width ($(2nu)), where the last $nu inputs are the derivatives of the first $nu inputs.") + end + B = [B [zeros(nx, nu); Bs]] + D = [D zeros(ny, nu)] + end + end + + (; A, B, C, D) +end + +function linearize(sys, inputs, outputs; op = Dict(), t = 0.0, + allow_input_derivatives = false, + zero_dummy_der = false, + kwargs...) + lin_fun, ssys = linearization_function(sys, + inputs, + outputs; + zero_dummy_der, + op, + kwargs...) + linearize(ssys, lin_fun; op, t, allow_input_derivatives), ssys +end + +""" + (; Ã, B̃, C̃, D̃) = similarity_transform(sys, T; unitary=false) + +Perform a similarity transform `T : Tx̃ = x` on linear system represented by matrices in NamedTuple `sys` such that + +``` +Ã = T⁻¹AT +B̃ = T⁻¹ B +C̃ = CT +D̃ = D +``` + +If `unitary=true`, `T` is assumed unitary and the matrix adjoint is used instead of the inverse. +""" +function similarity_transform(sys::NamedTuple, T; unitary = false) + if unitary + A = T'sys.A * T + B = T'sys.B + else + Tf = lu(T) + A = Tf \ sys.A * T + B = Tf \ sys.B + end + C = sys.C * T + D = sys.D + (; A, B, C, D) +end + +""" + reorder_unknowns(sys::NamedTuple, old, new) + +Permute the state representation of `sys` obtained from [`linearize`](@ref) so that the state unknown is changed from `old` to `new` +Example: + +``` +lsys, ssys = linearize(pid, [reference.u, measurement.u], [ctr_output.u]) +desired_order = [int.x, der.x] # Unknowns that are present in unknowns(ssys) +lsys = ModelingToolkit.reorder_unknowns(lsys, unknowns(ssys), desired_order) +``` + +See also [`ModelingToolkit.similarity_transform`](@ref) +""" +function reorder_unknowns(sys::NamedTuple, old, new) + nx = length(old) + length(new) == nx || error("old and new must have the same length") + perm = [findfirst(isequal(n), old) for n in new] + issorted(perm) && return sys # shortcut return, no reordering + P = zeros(Int, nx, nx) + for i in 1:nx # Build permutation matrix + P[i, perm[i]] = 1 + end + similarity_transform(sys, P; unitary = true) +end diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index 9278afc366..8093ca5a13 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -2254,544 +2254,6 @@ function io_preprocessing(sys::AbstractSystem, inputs, sys, diff_idxs, alge_idxs, input_idxs end -""" - lin_fun, simplified_sys = linearization_function(sys::AbstractSystem, inputs, outputs; simplify = false, initialize = true, initialization_solver_alg = TrustRegion(), kwargs...) - -Return a function that linearizes the system `sys`. The function [`linearize`](@ref) provides a higher-level and easier to use interface. - -`lin_fun` is a function `(variables, p, t) -> (; f_x, f_z, g_x, g_z, f_u, g_u, h_x, h_z, h_u)`, i.e., it returns a NamedTuple with the Jacobians of `f,g,h` for the nonlinear `sys` (technically for `simplified_sys`) on the form - -```math -\\begin{aligned} -ẋ &= f(x, z, u) \\\\ -0 &= g(x, z, u) \\\\ -y &= h(x, z, u) -\\end{aligned} -``` - -where `x` are differential unknown variables, `z` algebraic variables, `u` inputs and `y` outputs. To obtain a linear statespace representation, see [`linearize`](@ref). The input argument `variables` is a vector defining the operating point, corresponding to `unknowns(simplified_sys)` and `p` is a vector corresponding to the parameters of `simplified_sys`. Note: all variables in `inputs` have been converted to parameters in `simplified_sys`. - -The `simplified_sys` has undergone [`structural_simplify`](@ref) and had any occurring input or output variables replaced with the variables provided in arguments `inputs` and `outputs`. The unknowns of this system also indicate the order of the unknowns that holds for the linearized matrices. - -# Arguments: - - - `sys`: An [`ODESystem`](@ref). This function will automatically apply simplification passes on `sys` and return the resulting `simplified_sys`. - - `inputs`: A vector of variables that indicate the inputs of the linearized input-output model. - - `outputs`: A vector of variables that indicate the outputs of the linearized input-output model. - - `simplify`: Apply simplification in tearing. - - `initialize`: If true, a check is performed to ensure that the operating point is consistent (satisfies algebraic equations). If the op is not consistent, initialization is performed. - - `initialization_solver_alg`: A NonlinearSolve algorithm to use for solving for a feasible set of state and algebraic variables that satisfies the specified operating point. - - `kwargs`: Are passed on to `find_solvables!` - -See also [`linearize`](@ref) which provides a higher-level interface. -""" -function linearization_function(sys::AbstractSystem, inputs, - outputs; simplify = false, - initialize = true, - initializealg = nothing, - initialization_abstol = 1e-5, - initialization_reltol = 1e-3, - op = Dict(), - p = DiffEqBase.NullParameters(), - zero_dummy_der = false, - initialization_solver_alg = TrustRegion(), - eval_expression = false, eval_module = @__MODULE__, - warn_initialize_determined = true, - kwargs...) - op = Dict(op) - inputs isa AbstractVector || (inputs = [inputs]) - outputs isa AbstractVector || (outputs = [outputs]) - inputs = mapreduce(vcat, inputs; init = []) do var - symbolic_type(var) == ArraySymbolic() ? collect(var) : [var] - end - outputs = mapreduce(vcat, outputs; init = []) do var - symbolic_type(var) == ArraySymbolic() ? collect(var) : [var] - end - ssys, diff_idxs, alge_idxs, input_idxs = io_preprocessing(sys, inputs, outputs; - simplify, - kwargs...) - if zero_dummy_der - dummyder = setdiff(unknowns(ssys), unknowns(sys)) - defs = Dict(x => 0.0 for x in dummyder) - @set! ssys.defaults = merge(defs, defaults(ssys)) - op = merge(defs, op) - end - sys = ssys - - if initializealg === nothing - initializealg = initialize ? OverrideInit() : NoInit() - end - - fun, u0, p = process_SciMLProblem( - ODEFunction{true, SciMLBase.FullSpecialize}, sys, merge( - defaults_and_guesses(sys), op), p; - t = 0.0, build_initializeprob = initializealg isa OverrideInit, - allow_incomplete = true, algebraic_only = true) - prob = ODEProblem(fun, u0, (nothing, nothing), p) - - ps = parameters(sys) - h = build_explicit_observed_function(sys, outputs; eval_expression, eval_module) - lin_fun = let diff_idxs = diff_idxs, - alge_idxs = alge_idxs, - input_idxs = input_idxs, - sts = unknowns(sys), - fun = fun, - prob = prob, - sys_ps = p, - h = h, - integ_cache = (similar(u0)), - chunk = ForwardDiff.Chunk(input_idxs), - initializealg = initializealg, - initialization_abstol = initialization_abstol, - initialization_reltol = initialization_reltol, - initialization_solver_alg = initialization_solver_alg, - sys = sys - - function (u, p, t) - if !isa(p, MTKParameters) - p = todict(p) - newps = deepcopy(sys_ps) - for (k, v) in p - if is_parameter(sys, k) - v = fixpoint_sub(v, p) - setp(sys, k)(newps, v) - end - end - p = newps - end - - if u !== nothing # Handle systems without unknowns - length(sts) == length(u) || - error("Number of unknown variables ($(length(sts))) does not match the number of input unknowns ($(length(u)))") - - integ = MockIntegrator{true}(u, p, t, integ_cache) - u, p, success = SciMLBase.get_initial_values( - prob, integ, fun, initializealg, Val(true); - abstol = initialization_abstol, reltol = initialization_reltol, - nlsolve_alg = initialization_solver_alg) - if !success - error("Initialization algorithm $(initializealg) failed with `u = $u` and `p = $p`.") - end - uf = SciMLBase.UJacobianWrapper(fun, t, p) - fg_xz = ForwardDiff.jacobian(uf, u) - h_xz = ForwardDiff.jacobian( - let p = p, t = t - xz -> h(xz, p, t) - end, u) - pf = SciMLBase.ParamJacobianWrapper(fun, t, u) - fg_u = jacobian_wrt_vars(pf, p, input_idxs, chunk) - else - length(sts) == 0 || - error("Number of unknown variables (0) does not match the number of input unknowns ($(length(u)))") - fg_xz = zeros(0, 0) - h_xz = fg_u = zeros(0, length(inputs)) - end - hp = let u = u, t = t - _hp(p) = h(u, p, t) - _hp - end - h_u = jacobian_wrt_vars(hp, p, input_idxs, chunk) - (f_x = fg_xz[diff_idxs, diff_idxs], - f_z = fg_xz[diff_idxs, alge_idxs], - g_x = fg_xz[alge_idxs, diff_idxs], - g_z = fg_xz[alge_idxs, alge_idxs], - f_u = fg_u[diff_idxs, :], - g_u = fg_u[alge_idxs, :], - h_x = h_xz[:, diff_idxs], - h_z = h_xz[:, alge_idxs], - h_u = h_u) - end - end - return lin_fun, sys -end - -""" - $(TYPEDEF) - -Mock `DEIntegrator` to allow using `CheckInit` without having to create a new integrator -(and consequently depend on `OrdinaryDiffEq`). - -# Fields - -$(TYPEDFIELDS) -""" -struct MockIntegrator{iip, U, P, T, C} <: SciMLBase.DEIntegrator{Nothing, iip, U, T} - """ - The state vector. - """ - u::U - """ - The parameter object. - """ - p::P - """ - The current time. - """ - t::T - """ - The integrator cache. - """ - cache::C -end - -function MockIntegrator{iip}(u::U, p::P, t::T, cache::C) where {iip, U, P, T, C} - return MockIntegrator{iip, U, P, T, C}(u, p, t, cache) -end - -SymbolicIndexingInterface.state_values(integ::MockIntegrator) = integ.u -SymbolicIndexingInterface.parameter_values(integ::MockIntegrator) = integ.p -SymbolicIndexingInterface.current_time(integ::MockIntegrator) = integ.t -SciMLBase.get_tmp_cache(integ::MockIntegrator) = integ.cache - -""" - (; A, B, C, D), simplified_sys = linearize_symbolic(sys::AbstractSystem, inputs, outputs; simplify = false, allow_input_derivatives = false, kwargs...) - -Similar to [`linearize`](@ref), but returns symbolic matrices `A,B,C,D` rather than numeric. While `linearize` uses ForwardDiff to perform the linearization, this function uses `Symbolics.jacobian`. - -See [`linearize`](@ref) for a description of the arguments. - -# Extended help -The named tuple returned as the first argument additionally contains the jacobians `f_x, f_z, g_x, g_z, f_u, g_u, h_x, h_z, h_u` of -```math -\\begin{aligned} -ẋ &= f(x, z, u) \\\\ -0 &= g(x, z, u) \\\\ -y &= h(x, z, u) -\\end{aligned} -``` -where `x` are differential unknown variables, `z` algebraic variables, `u` inputs and `y` outputs. -""" -function linearize_symbolic(sys::AbstractSystem, inputs, - outputs; simplify = false, allow_input_derivatives = false, - eval_expression = false, eval_module = @__MODULE__, - kwargs...) - sys, diff_idxs, alge_idxs, input_idxs = io_preprocessing( - sys, inputs, outputs; simplify, - kwargs...) - sts = unknowns(sys) - t = get_iv(sys) - ps = parameters(sys; initial_parameters = true) - p = reorder_parameters(sys, ps) - - fun_expr = generate_function(sys, sts, ps; expression = Val{true})[1] - fun = eval_or_rgf(fun_expr; eval_expression, eval_module) - dx = fun(sts, p, t) - - h = build_explicit_observed_function(sys, outputs; eval_expression, eval_module) - y = h(sts, p, t) - - fg_xz = Symbolics.jacobian(dx, sts) - fg_u = Symbolics.jacobian(dx, inputs) - h_xz = Symbolics.jacobian(y, sts) - h_u = Symbolics.jacobian(y, inputs) - f_x = fg_xz[diff_idxs, diff_idxs] - f_z = fg_xz[diff_idxs, alge_idxs] - g_x = fg_xz[alge_idxs, diff_idxs] - g_z = fg_xz[alge_idxs, alge_idxs] - f_u = fg_u[diff_idxs, :] - g_u = fg_u[alge_idxs, :] - h_x = h_xz[:, diff_idxs] - h_z = h_xz[:, alge_idxs] - - nx, nu = size(f_u) - nz = size(f_z, 2) - ny = size(h_x, 1) - - D = h_u - - if isempty(g_z) # ODE - A = f_x - B = f_u - C = h_x - else - gz = lu(g_z; check = false) - issuccess(gz) || - error("g_z not invertible, this indicates that the DAE is of index > 1.") - gzgx = -(gz \ g_x) - A = [f_x f_z - gzgx*f_x gzgx*f_z] - B = [f_u - gzgx * f_u] # The cited paper has zeros in the bottom block, see derivation in https://github.com/SciML/ModelingToolkit.jl/pull/1691 for the correct formula - - C = [h_x h_z] - Bs = -(gz \ g_u) # This equation differ from the cited paper, the paper is likely wrong since their equaiton leads to a dimension mismatch. - if !iszero(Bs) - if !allow_input_derivatives - der_inds = findall(vec(any(!iszero, Bs, dims = 1))) - @show typeof(der_inds) - error("Input derivatives appeared in expressions (-g_z\\g_u != 0), the following inputs appeared differentiated: $(ModelingToolkit.inputs(sys)[der_inds]). Call `linearize_symbolic` with keyword argument `allow_input_derivatives = true` to allow this and have the returned `B` matrix be of double width ($(2nu)), where the last $nu inputs are the derivatives of the first $nu inputs.") - end - B = [B [zeros(nx, nu); Bs]] - D = [D zeros(ny, nu)] - end - end - - (; A, B, C, D, f_x, f_z, g_x, g_z, f_u, g_u, h_x, h_z, h_u), sys -end - -function markio!(state, orig_inputs, inputs, outputs; check = true) - fullvars = get_fullvars(state) - inputset = Dict{Any, Bool}(i => false for i in inputs) - outputset = Dict{Any, Bool}(o => false for o in outputs) - for (i, v) in enumerate(fullvars) - if v in keys(inputset) - if v in keys(outputset) - v = setio(v, true, true) - outputset[v] = true - else - v = setio(v, true, false) - end - inputset[v] = true - fullvars[i] = v - elseif v in keys(outputset) - v = setio(v, false, true) - outputset[v] = true - fullvars[i] = v - else - if isinput(v) - push!(orig_inputs, v) - end - v = setio(v, false, false) - fullvars[i] = v - end - end - if check - ikeys = keys(filter(!last, inputset)) - if !isempty(ikeys) - error( - "Some specified inputs were not found in system. The following variables were not found ", - ikeys) - end - end - check && (all(values(outputset)) || - error( - "Some specified outputs were not found in system. The following Dict indicates the found variables ", - outputset)) - state, orig_inputs -end - -""" - (; A, B, C, D), simplified_sys = linearize(sys, inputs, outputs; t=0.0, op = Dict(), allow_input_derivatives = false, zero_dummy_der=false, kwargs...) - (; A, B, C, D) = linearize(simplified_sys, lin_fun; t=0.0, op = Dict(), allow_input_derivatives = false, zero_dummy_der=false) - -Linearize `sys` between `inputs` and `outputs`, both vectors of variables. Return a NamedTuple with the matrices of a linear statespace representation -on the form - -```math -\\begin{aligned} -ẋ &= Ax + Bu\\\\ -y &= Cx + Du -\\end{aligned} -``` - -The first signature automatically calls [`linearization_function`](@ref) internally, -while the second signature expects the outputs of [`linearization_function`](@ref) as input. - -`op` denotes the operating point around which to linearize. If none is provided, -the default values of `sys` are used. - -If `allow_input_derivatives = false`, an error will be thrown if input derivatives (``u̇``) appear as inputs in the linearized equations. If input derivatives are allowed, the returned `B` matrix will be of double width, corresponding to the input `[u; u̇]`. - -`zero_dummy_der` can be set to automatically set the operating point to zero for all dummy derivatives. - -See also [`linearization_function`](@ref) which provides a lower-level interface, [`linearize_symbolic`](@ref) and [`ModelingToolkit.reorder_unknowns`](@ref). - -See extended help for an example. - -The implementation and notation follows that of -["Linear Analysis Approach for Modelica Models", Allain et al. 2009](https://ep.liu.se/ecp/043/075/ecp09430097.pdf) - -# Extended help - -This example builds the following feedback interconnection and linearizes it from the input of `F` to the output of `P`. - -``` - - r ┌─────┐ ┌─────┐ ┌─────┐ -───►│ ├──────►│ │ u │ │ - │ F │ │ C ├────►│ P │ y - └─────┘ ┌►│ │ │ ├─┬─► - │ └─────┘ └─────┘ │ - │ │ - └─────────────────────┘ -``` - -```julia -using ModelingToolkit -using ModelingToolkit: t_nounits as t, D_nounits as D -function plant(; name) - @variables x(t) = 1 - @variables u(t)=0 y(t)=0 - eqs = [D(x) ~ -x + u - y ~ x] - ODESystem(eqs, t; name = name) -end - -function ref_filt(; name) - @variables x(t)=0 y(t)=0 - @variables u(t)=0 [input = true] - eqs = [D(x) ~ -2 * x + u - y ~ x] - ODESystem(eqs, t, name = name) -end - -function controller(kp; name) - @variables y(t)=0 r(t)=0 u(t)=0 - @parameters kp = kp - eqs = [ - u ~ kp * (r - y), - ] - ODESystem(eqs, t; name = name) -end - -@named f = ref_filt() -@named c = controller(1) -@named p = plant() - -connections = [f.y ~ c.r # filtered reference to controller reference - c.u ~ p.u # controller output to plant input - p.y ~ c.y] - -@named cl = ODESystem(connections, t, systems = [f, c, p]) - -lsys0, ssys = linearize(cl, [f.u], [p.x]) -desired_order = [f.x, p.x] -lsys = ModelingToolkit.reorder_unknowns(lsys0, unknowns(ssys), desired_order) - -@assert lsys.A == [-2 0; 1 -2] -@assert lsys.B == [1; 0;;] -@assert lsys.C == [0 1] -@assert lsys.D[] == 0 - -## Symbolic linearization -lsys_sym, _ = ModelingToolkit.linearize_symbolic(cl, [f.u], [p.x]) - -@assert substitute(lsys_sym.A, ModelingToolkit.defaults(cl)) == lsys.A -``` -""" -function linearize(sys, lin_fun; t = 0.0, op = Dict(), allow_input_derivatives = false, - p = DiffEqBase.NullParameters()) - x0 = merge(defaults(sys), Dict(missing_variable_defaults(sys)), op) - u0, defs = get_u0(sys, x0, p) - if has_index_cache(sys) && get_index_cache(sys) !== nothing - if p isa SciMLBase.NullParameters - p = op - elseif p isa Dict - p = merge(p, op) - elseif p isa Vector && eltype(p) <: Pair - p = merge(Dict(p), op) - elseif p isa Vector - p = merge(Dict(parameters(sys) .=> p), op) - end - end - linres = lin_fun(u0, p, t) - f_x, f_z, g_x, g_z, f_u, g_u, h_x, h_z, h_u = linres - - nx, nu = size(f_u) - nz = size(f_z, 2) - ny = size(h_x, 1) - - D = h_u - - if isempty(g_z) - A = f_x - B = f_u - C = h_x - @assert iszero(g_x) - @assert iszero(g_z) - @assert iszero(g_u) - else - gz = lu(g_z; check = false) - issuccess(gz) || - error("g_z not invertible, this indicates that the DAE is of index > 1.") - gzgx = -(gz \ g_x) - A = [f_x f_z - gzgx*f_x gzgx*f_z] - B = [f_u - gzgx * f_u] # The cited paper has zeros in the bottom block, see derivation in https://github.com/SciML/ModelingToolkit.jl/pull/1691 for the correct formula - - C = [h_x h_z] - Bs = -(gz \ g_u) # This equation differ from the cited paper, the paper is likely wrong since their equaiton leads to a dimension mismatch. - if !iszero(Bs) - if !allow_input_derivatives - der_inds = findall(vec(any(!=(0), Bs, dims = 1))) - error("Input derivatives appeared in expressions (-g_z\\g_u != 0), the following inputs appeared differentiated: $(inputs(sys)[der_inds]). Call `linearize` with keyword argument `allow_input_derivatives = true` to allow this and have the returned `B` matrix be of double width ($(2nu)), where the last $nu inputs are the derivatives of the first $nu inputs.") - end - B = [B [zeros(nx, nu); Bs]] - D = [D zeros(ny, nu)] - end - end - - (; A, B, C, D) -end - -function linearize(sys, inputs, outputs; op = Dict(), t = 0.0, - allow_input_derivatives = false, - zero_dummy_der = false, - kwargs...) - lin_fun, ssys = linearization_function(sys, - inputs, - outputs; - zero_dummy_der, - op, - kwargs...) - linearize(ssys, lin_fun; op, t, allow_input_derivatives), ssys -end - -""" - (; Ã, B̃, C̃, D̃) = similarity_transform(sys, T; unitary=false) - -Perform a similarity transform `T : Tx̃ = x` on linear system represented by matrices in NamedTuple `sys` such that - -``` -Ã = T⁻¹AT -B̃ = T⁻¹ B -C̃ = CT -D̃ = D -``` - -If `unitary=true`, `T` is assumed unitary and the matrix adjoint is used instead of the inverse. -""" -function similarity_transform(sys::NamedTuple, T; unitary = false) - if unitary - A = T'sys.A * T - B = T'sys.B - else - Tf = lu(T) - A = Tf \ sys.A * T - B = Tf \ sys.B - end - C = sys.C * T - D = sys.D - (; A, B, C, D) -end - -""" - reorder_unknowns(sys::NamedTuple, old, new) - -Permute the state representation of `sys` obtained from [`linearize`](@ref) so that the state unknown is changed from `old` to `new` -Example: - -``` -lsys, ssys = linearize(pid, [reference.u, measurement.u], [ctr_output.u]) -desired_order = [int.x, der.x] # Unknowns that are present in unknowns(ssys) -lsys = ModelingToolkit.reorder_unknowns(lsys, unknowns(ssys), desired_order) -``` - -See also [`ModelingToolkit.similarity_transform`](@ref) -""" -function reorder_unknowns(sys::NamedTuple, old, new) - nx = length(old) - length(new) == nx || error("old and new must have the same length") - perm = [findfirst(isequal(n), old) for n in new] - issorted(perm) && return sys # shortcut return, no reordering - P = zeros(Int, nx, nx) - for i in 1:nx # Build permutation matrix - P[i, perm[i]] = 1 - end - similarity_transform(sys, P; unitary = true) -end - @latexrecipe function f(sys::AbstractSystem) return latexify(equations(sys)) end From 382ce9b8c6bcb4d3afc4bd7f5db92aff04f2b0e9 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 11 Feb 2025 16:43:55 +0530 Subject: [PATCH 0900/1337] refactor: use new `LinearizationFunction` instead of closure in `linearization_function` --- src/linearization.jl | 183 +++++++++++++++++++++++++++---------------- 1 file changed, 115 insertions(+), 68 deletions(-) diff --git a/src/linearization.jl b/src/linearization.jl index bbf0d8e2c1..e517c02616 100644 --- a/src/linearization.jl +++ b/src/linearization.jl @@ -75,78 +75,125 @@ function linearization_function(sys::AbstractSystem, inputs, ps = parameters(sys) h = build_explicit_observed_function(sys, outputs; eval_expression, eval_module) - lin_fun = let diff_idxs = diff_idxs, - alge_idxs = alge_idxs, - input_idxs = input_idxs, - sts = unknowns(sys), - fun = fun, - prob = prob, - sys_ps = p, - h = h, - integ_cache = (similar(u0)), - chunk = ForwardDiff.Chunk(input_idxs), - initializealg = initializealg, - initialization_abstol = initialization_abstol, - initialization_reltol = initialization_reltol, - initialization_solver_alg = initialization_solver_alg, - sys = sys - - function (u, p, t) - if !isa(p, MTKParameters) - p = todict(p) - newps = deepcopy(sys_ps) - for (k, v) in p - if is_parameter(sys, k) - v = fixpoint_sub(v, p) - setp(sys, k)(newps, v) - end - end - p = newps - end - if u !== nothing # Handle systems without unknowns - length(sts) == length(u) || - error("Number of unknown variables ($(length(sts))) does not match the number of input unknowns ($(length(u)))") - - integ = MockIntegrator{true}(u, p, t, integ_cache) - u, p, success = SciMLBase.get_initial_values( - prob, integ, fun, initializealg, Val(true); - abstol = initialization_abstol, reltol = initialization_reltol, - nlsolve_alg = initialization_solver_alg) - if !success - error("Initialization algorithm $(initializealg) failed with `u = $u` and `p = $p`.") - end - uf = SciMLBase.UJacobianWrapper(fun, t, p) - fg_xz = ForwardDiff.jacobian(uf, u) - h_xz = ForwardDiff.jacobian( - let p = p, t = t - xz -> h(xz, p, t) - end, u) - pf = SciMLBase.ParamJacobianWrapper(fun, t, u) - fg_u = jacobian_wrt_vars(pf, p, input_idxs, chunk) - else - length(sts) == 0 || - error("Number of unknown variables (0) does not match the number of input unknowns ($(length(u)))") - fg_xz = zeros(0, 0) - h_xz = fg_u = zeros(0, length(inputs)) - end - hp = let u = u, t = t - _hp(p) = h(u, p, t) - _hp + initialization_kwargs = (; + abstol = initialization_abstol, reltol = initialization_reltol, + nlsolve_alg = initialization_solver_alg) + lin_fun = LinearizationFunction( + diff_idxs, alge_idxs, input_idxs, length(unknowns(sys)), prob, h, similar(u0), + ForwardDiff.Chunk(input_idxs), initializealg, initialization_kwargs) + return lin_fun, sys +end + +""" + $(TYPEDEF) + +A callable struct which linearizes a system. + +# Fields + +$(TYPEDFIELDS) +""" +struct LinearizationFunction{ + DI <: AbstractVector{Int}, AI <: AbstractVector{Int}, II, P <: ODEProblem, + H, C, Ch, IA <: SciMLBase.DAEInitializationAlgorithm, IK} + """ + The indexes of differential equations in the linearized system. + """ + diff_idxs::DI + """ + The indexes of algebraic equations in the linearized system. + """ + alge_idxs::AI + """ + The indexes of parameters in the linearized system which represent + input variables. + """ + input_idxs::II + """ + The number of unknowns in the linearized system. + """ + num_states::Int + """ + The `ODEProblem` of the linearized system. + """ + prob::P + """ + A function which takes `(u, p, t)` and returns the outputs of the linearized system. + """ + h::H + """ + Any required cache buffers. + """ + caches::C + # TODO: Use DI? + """ + A `ForwardDiff.Chunk` for taking the jacobian with respect to the inputs. + """ + chunk::Ch + """ + The initialization algorithm to use. + """ + initializealg::IA + """ + Keyword arguments to be passed to `SciMLBase.get_initial_values`. + """ + initialize_kwargs::IK +end + +function (linfun::LinearizationFunction)(u, p, t) + if eltype(p) <: Pair + p = todict(p) + newps = copy(parameter_values(linfun.prob)) + for (k, v) in p + if is_parameter(linfun, k) + v = fixpoint_sub(v, p) + setp(linfun, k)(newps, v) end - h_u = jacobian_wrt_vars(hp, p, input_idxs, chunk) - (f_x = fg_xz[diff_idxs, diff_idxs], - f_z = fg_xz[diff_idxs, alge_idxs], - g_x = fg_xz[alge_idxs, diff_idxs], - g_z = fg_xz[alge_idxs, alge_idxs], - f_u = fg_u[diff_idxs, :], - g_u = fg_u[alge_idxs, :], - h_x = h_xz[:, diff_idxs], - h_z = h_xz[:, alge_idxs], - h_u = h_u) end + p = newps end - return lin_fun, sys + + fun = linfun.prob.f + if u !== nothing # Handle systems without unknowns + linfun.num_states == length(u) || + error("Number of unknown variables ($(linfun.num_states)) does not match the number of input unknowns ($(length(u)))") + integ_cache = linfun.caches + integ = MockIntegrator{true}(u, p, t, integ_cache) + u, p, success = SciMLBase.get_initial_values( + linfun.prob, integ, fun, linfun.initializealg, Val(true); + linfun.initialize_kwargs...) + if !success + error("Initialization algorithm $(linfun.initializealg) failed with `u = $u` and `p = $p`.") + end + uf = SciMLBase.UJacobianWrapper(fun, t, p) + fg_xz = ForwardDiff.jacobian(uf, u) + h_xz = ForwardDiff.jacobian( + let p = p, t = t, h = linfun.h + xz -> h(xz, p, t) + end, u) + pf = SciMLBase.ParamJacobianWrapper(fun, t, u) + fg_u = jacobian_wrt_vars(pf, p, linfun.input_idxs, linfun.chunk) + else + linfun.num_states == 0 || + error("Number of unknown variables (0) does not match the number of input unknowns ($(length(u)))") + fg_xz = zeros(0, 0) + h_xz = fg_u = zeros(0, length(linfun.input_idxs)) + end + hp = let u = u, t = t, h = linfun.h + _hp(p) = h(u, p, t) + _hp + end + h_u = jacobian_wrt_vars(hp, p, linfun.input_idxs, linfun.chunk) + (f_x = fg_xz[linfun.diff_idxs, linfun.diff_idxs], + f_z = fg_xz[linfun.diff_idxs, linfun.alge_idxs], + g_x = fg_xz[linfun.alge_idxs, linfun.diff_idxs], + g_z = fg_xz[linfun.alge_idxs, linfun.alge_idxs], + f_u = fg_u[linfun.diff_idxs, :], + g_u = fg_u[linfun.alge_idxs, :], + h_x = h_xz[:, linfun.diff_idxs], + h_z = h_xz[:, linfun.alge_idxs], + h_u = h_u) end """ From f0590aedc64335fbaeb17e07e4c9f22e635aa7bf Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 11 Feb 2025 17:52:42 +0530 Subject: [PATCH 0901/1337] feat: add `LinearizationProblem` --- src/linearization.jl | 126 ++++++++++++++++++++++++++----------------- 1 file changed, 76 insertions(+), 50 deletions(-) diff --git a/src/linearization.jl b/src/linearization.jl index e517c02616..872ad7c6ed 100644 --- a/src/linearization.jl +++ b/src/linearization.jl @@ -141,6 +141,13 @@ struct LinearizationFunction{ initialize_kwargs::IK end +SymbolicIndexingInterface.symbolic_container(f::LinearizationFunction) = f.prob +SymbolicIndexingInterface.state_values(f::LinearizationFunction) = state_values(f.prob) +function SymbolicIndexingInterface.parameter_values(f::LinearizationFunction) + parameter_values(f.prob) +end +SymbolicIndexingInterface.current_time(f::LinearizationFunction) = current_time(f.prob) + function (linfun::LinearizationFunction)(u, p, t) if eltype(p) <: Pair p = todict(p) @@ -234,6 +241,61 @@ SymbolicIndexingInterface.parameter_values(integ::MockIntegrator) = integ.p SymbolicIndexingInterface.current_time(integ::MockIntegrator) = integ.t SciMLBase.get_tmp_cache(integ::MockIntegrator) = integ.cache +mutable struct LinearizationProblem{F <: LinearizationFunction, T} + const f::F + t::T +end + +SymbolicIndexingInterface.symbolic_container(p::LinearizationProblem) = p.f +SymbolicIndexingInterface.state_values(p::LinearizationProblem) = state_values(p.f) +SymbolicIndexingInterface.parameter_values(p::LinearizationProblem) = parameter_values(p.f) +SymbolicIndexingInterface.current_time(p::LinearizationProblem) = p.t + +function CommonSolve.solve(prob::LinearizationProblem; allow_input_derivatives = false) + u0 = state_values(prob) + p = parameter_values(prob) + t = current_time(prob) + linres = prob.f(u0, p, t) + f_x, f_z, g_x, g_z, f_u, g_u, h_x, h_z, h_u = linres + + nx, nu = size(f_u) + nz = size(f_z, 2) + ny = size(h_x, 1) + + D = h_u + + if isempty(g_z) + A = f_x + B = f_u + C = h_x + @assert iszero(g_x) + @assert iszero(g_z) + @assert iszero(g_u) + else + gz = lu(g_z; check = false) + issuccess(gz) || + error("g_z not invertible, this indicates that the DAE is of index > 1.") + gzgx = -(gz \ g_x) + A = [f_x f_z + gzgx*f_x gzgx*f_z] + B = [f_u + gzgx * f_u] # The cited paper has zeros in the bottom block, see derivation in https://github.com/SciML/ModelingToolkit.jl/pull/1691 for the correct formula + + C = [h_x h_z] + Bs = -(gz \ g_u) # This equation differ from the cited paper, the paper is likely wrong since their equaiton leads to a dimension mismatch. + if !iszero(Bs) + if !allow_input_derivatives + der_inds = findall(vec(any(!=(0), Bs, dims = 1))) + error("Input derivatives appeared in expressions (-g_z\\g_u != 0), the following inputs appeared differentiated: $(inputs(sys)[der_inds]). Call `linearize` with keyword argument `allow_input_derivatives = true` to allow this and have the returned `B` matrix be of double width ($(2nu)), where the last $nu inputs are the derivatives of the first $nu inputs.") + end + B = [B [zeros(nx, nu); Bs]] + D = [D zeros(ny, nu)] + end + end + + (; A, B, C, D) +end + """ (; A, B, C, D), simplified_sys = linearize_symbolic(sys::AbstractSystem, inputs, outputs; simplify = false, allow_input_derivatives = false, kwargs...) @@ -460,60 +522,24 @@ lsys_sym, _ = ModelingToolkit.linearize_symbolic(cl, [f.u], [p.x]) @assert substitute(lsys_sym.A, ModelingToolkit.defaults(cl)) == lsys.A ``` """ -function linearize(sys, lin_fun; t = 0.0, op = Dict(), allow_input_derivatives = false, +function linearize(sys, lin_fun::LinearizationFunction; t = 0.0, + op = Dict(), allow_input_derivatives = false, p = DiffEqBase.NullParameters()) - x0 = merge(defaults(sys), Dict(missing_variable_defaults(sys)), op) - u0, defs = get_u0(sys, x0, p) - if has_index_cache(sys) && get_index_cache(sys) !== nothing - if p isa SciMLBase.NullParameters - p = op - elseif p isa Dict - p = merge(p, op) - elseif p isa Vector && eltype(p) <: Pair - p = merge(Dict(p), op) - elseif p isa Vector - p = merge(Dict(parameters(sys) .=> p), op) + prob = LinearizationProblem(lin_fun, t) + op = anydict(op) + evaluate_varmap!(op, unknowns(sys)) + for (k, v) in op + if is_parameter(prob, Initial(k)) + setu(prob, Initial(k))(prob, v) + else + setu(prob, k)(prob, v) end end - linres = lin_fun(u0, p, t) - f_x, f_z, g_x, g_z, f_u, g_u, h_x, h_z, h_u = linres - - nx, nu = size(f_u) - nz = size(f_z, 2) - ny = size(h_x, 1) - - D = h_u - - if isempty(g_z) - A = f_x - B = f_u - C = h_x - @assert iszero(g_x) - @assert iszero(g_z) - @assert iszero(g_u) - else - gz = lu(g_z; check = false) - issuccess(gz) || - error("g_z not invertible, this indicates that the DAE is of index > 1.") - gzgx = -(gz \ g_x) - A = [f_x f_z - gzgx*f_x gzgx*f_z] - B = [f_u - gzgx * f_u] # The cited paper has zeros in the bottom block, see derivation in https://github.com/SciML/ModelingToolkit.jl/pull/1691 for the correct formula - - C = [h_x h_z] - Bs = -(gz \ g_u) # This equation differ from the cited paper, the paper is likely wrong since their equaiton leads to a dimension mismatch. - if !iszero(Bs) - if !allow_input_derivatives - der_inds = findall(vec(any(!=(0), Bs, dims = 1))) - error("Input derivatives appeared in expressions (-g_z\\g_u != 0), the following inputs appeared differentiated: $(inputs(sys)[der_inds]). Call `linearize` with keyword argument `allow_input_derivatives = true` to allow this and have the returned `B` matrix be of double width ($(2nu)), where the last $nu inputs are the derivatives of the first $nu inputs.") - end - B = [B [zeros(nx, nu); Bs]] - D = [D zeros(ny, nu)] - end + p = anydict(p) + for (k, v) in p + setu(prob, k)(prob, v) end - - (; A, B, C, D) + return solve(prob; allow_input_derivatives) end function linearize(sys, inputs, outputs; op = Dict(), t = 0.0, From 0f5c008c17d13979b1829c5ef2e8e3b732159c58 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 11 Feb 2025 18:11:13 +0530 Subject: [PATCH 0902/1337] feat: add `LinearizationProblem` constructor, `getindex` and `.ps` syntax for symbolic indexing --- src/linearization.jl | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/linearization.jl b/src/linearization.jl index 872ad7c6ed..c6c7b84860 100644 --- a/src/linearization.jl +++ b/src/linearization.jl @@ -246,11 +246,31 @@ mutable struct LinearizationProblem{F <: LinearizationFunction, T} t::T end +function LinearizationProblem(sys::AbstractSystem, inputs, outputs; t = 0.0, kwargs...) + linfun, _ = linearization_function(sys, inputs, outputs; kwargs...) + return LinearizationProblem(linfun, t) +end + SymbolicIndexingInterface.symbolic_container(p::LinearizationProblem) = p.f SymbolicIndexingInterface.state_values(p::LinearizationProblem) = state_values(p.f) SymbolicIndexingInterface.parameter_values(p::LinearizationProblem) = parameter_values(p.f) SymbolicIndexingInterface.current_time(p::LinearizationProblem) = p.t +function Base.getindex(prob::LinearizationProblem, idx) + getu(prob, idx)(prob) +end + +function Base.setindex!(prob::LinearizationProblem, val, idx) + setu(prob, idx)(prob, val) +end + +function Base.getproperty(prob::LinearizationProblem, x::Symbol) + if x == :ps + return ParameterIndexingProxy(prob) + end + return getfield(prob, x) +end + function CommonSolve.solve(prob::LinearizationProblem; allow_input_derivatives = false) u0 = state_values(prob) p = parameter_values(prob) From f9c328fb6dddaf6a569182c7aa6ac1d07b56171a Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 11 Feb 2025 18:11:22 +0530 Subject: [PATCH 0903/1337] feat: export `LinearizationProblem` --- src/ModelingToolkit.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ModelingToolkit.jl b/src/ModelingToolkit.jl index 7d08a9aac2..6501a4e026 100644 --- a/src/ModelingToolkit.jl +++ b/src/ModelingToolkit.jl @@ -259,7 +259,8 @@ export Term, Sym export SymScope, LocalScope, ParentScope, DelayParentScope, GlobalScope export independent_variable, equations, controls, observed, full_equations export initialization_equations, guesses, defaults, parameter_dependencies, hierarchy -export structural_simplify, expand_connections, linearize, linearization_function +export structural_simplify, expand_connections, linearize, linearization_function, + LinearizationProblem export calculate_jacobian, generate_jacobian, generate_function, generate_custom_function export calculate_control_jacobian, generate_control_jacobian From 494a7e7bdec619c9024103eb14b276e7ccf81f7c Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 11 Feb 2025 18:27:53 +0530 Subject: [PATCH 0904/1337] docs: add docstrings for linearization --- src/linearization.jl | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/linearization.jl b/src/linearization.jl index c6c7b84860..ad881c7f33 100644 --- a/src/linearization.jl +++ b/src/linearization.jl @@ -148,6 +148,11 @@ function SymbolicIndexingInterface.parameter_values(f::LinearizationFunction) end SymbolicIndexingInterface.current_time(f::LinearizationFunction) = current_time(f.prob) +""" + $(TYPEDSIGNATURES) + +Linearize the wrapped system at the point given by `(u, p, t)`. +""" function (linfun::LinearizationFunction)(u, p, t) if eltype(p) <: Pair p = todict(p) @@ -241,11 +246,33 @@ SymbolicIndexingInterface.parameter_values(integ::MockIntegrator) = integ.p SymbolicIndexingInterface.current_time(integ::MockIntegrator) = integ.t SciMLBase.get_tmp_cache(integ::MockIntegrator) = integ.cache +""" + $(TYPEDEF) + +A struct representing a linearization operation to be performed. Can be symbolically +indexed to efficiently update the operating point for multiple linearizations in a loop. +The value of the independent variable can be set by mutating the `.t` field of this struct. +""" mutable struct LinearizationProblem{F <: LinearizationFunction, T} + """ + The wrapped `LinearizationFunction` + """ const f::F t::T end +""" + $(TYPEDSIGNATURES) + +Construct a `LinearizationProblem` for linearizing the system `sys` with the given +`inputs` and `outputs`. + +# Keyword arguments + +- `t`: The value of the independent variable + +All other keyword arguments are forwarded to `linearization_function`. +""" function LinearizationProblem(sys::AbstractSystem, inputs, outputs; t = 0.0, kwargs...) linfun, _ = linearization_function(sys, inputs, outputs; kwargs...) return LinearizationProblem(linfun, t) From 4eee373d697376ddd3f0cc8c4c7584630afd3fe0 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 11 Feb 2025 20:30:15 +0530 Subject: [PATCH 0905/1337] fix: don't use `defaults_and_guesses` for operating point --- src/linearization.jl | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/linearization.jl b/src/linearization.jl index ad881c7f33..621db21f8c 100644 --- a/src/linearization.jl +++ b/src/linearization.jl @@ -67,8 +67,7 @@ function linearization_function(sys::AbstractSystem, inputs, end fun, u0, p = process_SciMLProblem( - ODEFunction{true, SciMLBase.FullSpecialize}, sys, merge( - defaults_and_guesses(sys), op), p; + ODEFunction{true, SciMLBase.FullSpecialize}, sys, op, p; t = 0.0, build_initializeprob = initializealg isa OverrideInit, allow_incomplete = true, algebraic_only = true) prob = ODEProblem(fun, u0, (nothing, nothing), p) From ce0533fd6c640905d700f94348252b788c5be438 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 11 Feb 2025 20:30:38 +0530 Subject: [PATCH 0906/1337] fix: handle case when `u0 === nothing` in `linearization_function` --- src/linearization.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/linearization.jl b/src/linearization.jl index 621db21f8c..78dd5b1178 100644 --- a/src/linearization.jl +++ b/src/linearization.jl @@ -79,7 +79,8 @@ function linearization_function(sys::AbstractSystem, inputs, abstol = initialization_abstol, reltol = initialization_reltol, nlsolve_alg = initialization_solver_alg) lin_fun = LinearizationFunction( - diff_idxs, alge_idxs, input_idxs, length(unknowns(sys)), prob, h, similar(u0), + diff_idxs, alge_idxs, input_idxs, length(unknowns(sys)), + prob, h, u0 === nothing ? nothing : similar(u0), ForwardDiff.Chunk(input_idxs), initializealg, initialization_kwargs) return lin_fun, sys end From e16cf4d49ee8bccd8196be6363919ccd9e249b6b Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 11 Feb 2025 20:31:38 +0530 Subject: [PATCH 0907/1337] fix: fix initialization of `linearize` testset --- test/downstream/linearize.jl | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/test/downstream/linearize.jl b/test/downstream/linearize.jl index 5119e2f630..cdb33e6a0e 100644 --- a/test/downstream/linearize.jl +++ b/test/downstream/linearize.jl @@ -47,8 +47,8 @@ lsys, ssys = linearize(sys, r, r) # Test allow scalars ``` function plant(; name) - @variables x(t) = 1 - @variables u(t)=0 y(t)=0 + @variables x(t) + @variables u(t) y(t) D = Differential(t) eqs = [D(x) ~ -x + u y ~ x] @@ -56,7 +56,7 @@ function plant(; name) end function filt_(; name) - @variables x(t)=0 y(t)=0 + @variables x(t) y(t) @variables u(t)=0 [input = true] D = Differential(t) eqs = [D(x) ~ -2 * x + u @@ -65,7 +65,7 @@ function filt_(; name) end function controller(kp; name) - @variables y(t)=0 r(t)=0 u(t)=0 + @variables y(t)=0 r(t)=0 u(t) @parameters kp = kp eqs = [ u ~ kp * (r - y) @@ -108,7 +108,8 @@ Nd = 10 @named pid = LimPID(; k, Ti, Td, Nd) @unpack reference, measurement, ctr_output = pid -lsys0, ssys = linearize(pid, [reference.u, measurement.u], [ctr_output.u]) +lsys0, ssys = linearize(pid, [reference.u, measurement.u], [ctr_output.u]; + op = Dict(reference.u => 0.0, measurement.u => 0.0)) @unpack int, der = pid desired_order = [int.x, der.x] lsys = ModelingToolkit.reorder_unknowns(lsys0, unknowns(ssys), desired_order) @@ -314,7 +315,7 @@ matrices = linfun([1.0], Dict(p => 3.0), 1.0) end @testset "Issue #2941" begin - @variables x(t) y(t) + @variables x(t) y(t) [guess = 1.0] @parameters p eqs = [0 ~ x * log(y) - p] @named sys = ODESystem(eqs, t; defaults = [p => 1.0]) From f34a4e0445c8813078dcb7933a782020c4b3c3db Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 11 Feb 2025 20:31:50 +0530 Subject: [PATCH 0908/1337] test: minimally test `LinearizationProblem` --- test/downstream/linearize.jl | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/test/downstream/linearize.jl b/test/downstream/linearize.jl index cdb33e6a0e..3cab641d26 100644 --- a/test/downstream/linearize.jl +++ b/test/downstream/linearize.jl @@ -1,4 +1,5 @@ using ModelingToolkit, Test +using CommonSolve: solve # r is an input, and y is an output. @independent_variables t @@ -14,11 +15,13 @@ eqs = [u ~ kp * (r - y) @named sys = ODESystem(eqs, t) lsys, ssys = linearize(sys, [r], [y]) +lprob = LinearizationProblem(sys, [r], [y]) +lsys2 = solve(lprob) -@test lsys.A[] == -2 -@test lsys.B[] == 1 -@test lsys.C[] == 1 -@test lsys.D[] == 0 +@test lsys.A[] == lsys2.A[] == -2 +@test lsys.B[] == lsys2.B[] == 1 +@test lsys.C[] == lsys2.C[] == 1 +@test lsys.D[] == lsys2.D[] == 0 lsys, ssys = linearize(sys, [r], [r]) From 7793cb3557591e3183934ceb93f8455811991e11 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 11 Feb 2025 22:08:27 +0530 Subject: [PATCH 0909/1337] fix: handle `nothing` values in `InitializationProblem` --- src/systems/diffeqs/abstractodesystem.jl | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index d8996000cf..c7eff90e03 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -1331,7 +1331,9 @@ function InitializationProblem{iip, specialize}(sys::AbstractSystem, end if u0T != Union{} u0T = eltype(u0T) - u0map = Dict(k => if symbolic_type(v) == NotSymbolic() && !is_array_of_symbolics(v) + u0map = Dict(k => if v === nothing + nothing + elseif symbolic_type(v) == NotSymbolic() && !is_array_of_symbolics(v) v isa AbstractArray ? u0T.(v) : u0T(v) else v From 2a2b8b37bd8640190ec0178bf3b6d3640490155b Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 11 Feb 2025 22:08:43 +0530 Subject: [PATCH 0910/1337] fix: handle `nothing` values in `linearize` --- src/linearization.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/src/linearization.jl b/src/linearization.jl index 78dd5b1178..469c2cd766 100644 --- a/src/linearization.jl +++ b/src/linearization.jl @@ -576,6 +576,7 @@ function linearize(sys, lin_fun::LinearizationFunction; t = 0.0, op = anydict(op) evaluate_varmap!(op, unknowns(sys)) for (k, v) in op + v === nothing && continue if is_parameter(prob, Initial(k)) setu(prob, Initial(k))(prob, v) else From db2ae7b10f66f926d1115c5a81f3b2746d498d69 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 11 Feb 2025 22:08:53 +0530 Subject: [PATCH 0911/1337] test: fix initialization of `linearization_dd` testset --- test/downstream/linearization_dd.jl | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/test/downstream/linearization_dd.jl b/test/downstream/linearization_dd.jl index 11dc65f619..fc5d3b6785 100644 --- a/test/downstream/linearization_dd.jl +++ b/test/downstream/linearization_dd.jl @@ -25,14 +25,11 @@ eqs = [connect(link1.TX1, cart.flange) connect(link1.TY1, fixed.flange)] @named model = ODESystem(eqs, t, [], []; systems = [link1, cart, force, fixed]) -def = ModelingToolkit.defaults(model) -def[cart.s] = 10 -def[cart.v] = 0 -def[link1.A] = -pi / 2 -def[link1.dA] = 0 lin_outputs = [cart.s, cart.v, link1.A, link1.dA] lin_inputs = [force.f.u] +# => nothing to remove extra defaults +op = Dict(cart.s => 10, cart.v => 0, link1.A => -pi/2, link1.dA => 0, force.f.u => 0, link1.x1 => nothing, link1.y1 => nothing, link1.x2 => nothing, link1.x_cm => nothing) @test_broken begin @info "named_ss" G = named_ss(model, lin_inputs, lin_outputs, allow_symbolic = true, op = def, @@ -46,7 +43,7 @@ lin_inputs = [force.f.u] @test minimum(abs, ps) < 1e-6 @test minimum(abs, complex(0, 1.3777260367206716) .- ps) < 1e-10 - lsys, syss = linearize(model, lin_inputs, lin_outputs, allow_symbolic = true, op = def, + lsys, syss = linearize(model, lin_inputs, lin_outputs, allow_symbolic = true, op = op, allow_input_derivatives = true, zero_dummy_der = true) lsyss, sysss = ModelingToolkit.linearize_symbolic(model, lin_inputs, lin_outputs; allow_input_derivatives = true) From f0f1da91ad5f88a90b6ac5a599fda41f72d88c8e Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 12 Feb 2025 12:17:22 +0530 Subject: [PATCH 0912/1337] fix: disable discovering defaults from metadata in `flatten(::ODESystem)` --- src/systems/diffeqs/odesystem.jl | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/src/systems/diffeqs/odesystem.jl b/src/systems/diffeqs/odesystem.jl index e53b9223e6..76fcb9b823 100644 --- a/src/systems/diffeqs/odesystem.jl +++ b/src/systems/diffeqs/odesystem.jl @@ -245,7 +245,8 @@ function ODESystem(deqs::AbstractVector{<:Equation}, iv, dvs, ps; metadata = nothing, gui_metadata = nothing, is_dde = nothing, - tstops = []) + tstops = [], + discover_from_metadata = true) name === nothing && throw(ArgumentError("The `name` keyword must be provided. Please consider using the `@named` macro")) @assert all(control -> any(isequal.(control, ps)), controls) "All controls must also be parameters." @@ -264,12 +265,16 @@ function ODESystem(deqs::AbstractVector{<:Equation}, iv, dvs, ps; defaults = Dict{Any, Any}(todict(defaults)) guesses = Dict{Any, Any}(todict(guesses)) var_to_name = Dict() - process_variables!(var_to_name, defaults, guesses, dvs′) - process_variables!(var_to_name, defaults, guesses, ps′) - process_variables!( - var_to_name, defaults, guesses, [eq.lhs for eq in parameter_dependencies]) - process_variables!( - var_to_name, defaults, guesses, [eq.rhs for eq in parameter_dependencies]) + let defaults = discover_from_metadata ? defaults : Dict(), + guesses = discover_from_metadata ? guesses : Dict() + + process_variables!(var_to_name, defaults, guesses, dvs′) + process_variables!(var_to_name, defaults, guesses, ps′) + process_variables!( + var_to_name, defaults, guesses, [eq.lhs for eq in parameter_dependencies]) + process_variables!( + var_to_name, defaults, guesses, [eq.rhs for eq in parameter_dependencies]) + end defaults = Dict{Any, Any}(value(k) => value(v) for (k, v) in pairs(defaults) if v !== nothing) guesses = Dict{Any, Any}(value(k) => value(v) @@ -375,7 +380,11 @@ function flatten(sys::ODESystem, noeqs = false) is_dde = is_dde(sys), tstops = symbolic_tstops(sys), metadata = get_metadata(sys), - checks = false) + checks = false, + # without this, any defaults/guesses obtained from metadata that were + # later removed by the user will be re-added. Right now, we just want to + # retain `defaults(sys)` as-is. + discover_from_metadata = false) end end From 648956d71ad9736c1815eb151d3a0b6db3669afe Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 12 Feb 2025 12:17:35 +0530 Subject: [PATCH 0913/1337] test: fix inversemodel testset --- test/downstream/inversemodel.jl | 36 +++++++++++++++------------------ 1 file changed, 16 insertions(+), 20 deletions(-) diff --git a/test/downstream/inversemodel.jl b/test/downstream/inversemodel.jl index 7102995b66..8573964551 100644 --- a/test/downstream/inversemodel.jl +++ b/test/downstream/inversemodel.jl @@ -68,18 +68,10 @@ begin Ftf = tf(1, [(100), 1])^3 Fss = ss(Ftf) - "Compute initial state that yields y0 as output" - function init_filter(y0) - (; A, B, C, D) = Fss - Fx0 = -A \ B * y0 - @assert C * Fx0≈[y0] "C*Fx0*y0 ≈ y0 failed, got $(C*Fx0*y0) ≈ $(y0)]" - Fx0 - end - # Create an MTK-compatible constructor function RefFilter(; name) sys = ODESystem(Fss; name) - delete!(defaults(sys), @nonamespace(sys.x)[1]) + empty!(ModelingToolkit.get_defaults(sys)) return sys end end @@ -140,7 +132,7 @@ op = Dict(D(cm.inverse_tank.xT) => 1, cm.tank.xc => 0.65) tspan = (0.0, 1000.0) # https://github.com/SciML/ModelingToolkit.jl/issues/2786 -prob = ODEProblem(ssys, op, tspan; build_initializeprob = false) +prob = ODEProblem(ssys, op, tspan) sol = solve(prob, Rodas5P()) @test SciMLBase.successful_retcode(sol) @@ -150,27 +142,29 @@ sol = solve(prob, Rodas5P()) @test sol(tspan[2], idxs = cm.tank.xc)≈getp(prob, cm.ref.k)(prob) atol=1e-2 # Test that the inverse model led to the correct reference -Sf, simplified_sys = Blocks.get_sensitivity_function(model, :y) # This should work without providing an operating opint containing a dummy derivative -x, _ = ModelingToolkit.get_u0_p(simplified_sys, op) -p = ModelingToolkit.MTKParameters(simplified_sys, op) +# we need to provide `op` so the initialization system knows what to hold constant +# the values don't matter +Sf, simplified_sys = Blocks.get_sensitivity_function(model, :y; op); # This should work without providing an operating opint containing a dummy derivative +x = state_values(Sf) +p = parameter_values(Sf) # If this somehow passes, mention it on # https://github.com/SciML/ModelingToolkit.jl/issues/2786 matrices1 = Sf(x, p, 0) -matrices2, _ = Blocks.get_sensitivity(model, :y; op) # Test that we get the same result when calling the higher-level API -@test_broken matrices1.f_x ≈ matrices2.A[1:7, 1:7] +matrices2, _ = Blocks.get_sensitivity(model, :y; op); # Test that we get the same result when calling the higher-level API +@test matrices1.f_x ≈ matrices2.A[1:7, 1:7] nsys = get_named_sensitivity(model, :y; op) # Test that we get the same result when calling an even higher-level API @test matrices2.A ≈ nsys.A # Test the same thing for comp sensitivities -Sf, simplified_sys = Blocks.get_comp_sensitivity_function(model, :y) # This should work without providing an operating opint containing a dummy derivative -x, _ = ModelingToolkit.get_u0_p(simplified_sys, op) -p = ModelingToolkit.MTKParameters(simplified_sys, op) +Sf, simplified_sys = Blocks.get_comp_sensitivity_function(model, :y; op); # This should work without providing an operating opint containing a dummy derivative +x = state_values(Sf) +p = parameter_values(Sf) # If this somehow passes, mention it on # https://github.com/SciML/ModelingToolkit.jl/issues/2786 matrices1 = Sf(x, p, 0) matrices2, _ = Blocks.get_comp_sensitivity(model, :y; op) # Test that we get the same result when calling the higher-level API -@test_broken matrices1.f_x ≈ matrices2.A[1:7, 1:7] +@test matrices1.f_x ≈ matrices2.A[1:7, 1:7] nsys = get_named_comp_sensitivity(model, :y; op) # Test that we get the same result when calling an even higher-level API @test matrices2.A ≈ nsys.A @@ -186,7 +180,9 @@ nsys = get_named_comp_sensitivity(model, :y; op) # Test that we get the same res ) output = :y - lin_fun, ssys = Blocks.get_sensitivity_function(model, output) + # we need to provide `op` so the initialization system knows which + # values to hold constant + lin_fun, ssys = Blocks.get_sensitivity_function(model, output; op = op1) matrices1 = linearize(ssys, lin_fun, op = op1) matrices2 = linearize(ssys, lin_fun, op = op2) S1f = ss(matrices1...) From 370343b3ab5d4d127df7b0427a8e07d20c509fdf Mon Sep 17 00:00:00 2001 From: Fredrik Bagge Carlson Date: Wed, 12 Feb 2025 07:08:03 +0100 Subject: [PATCH 0914/1337] update pendcart test --- test/downstream/linearization_dd.jl | 61 +++++++++++++++-------------- 1 file changed, 31 insertions(+), 30 deletions(-) diff --git a/test/downstream/linearization_dd.jl b/test/downstream/linearization_dd.jl index fc5d3b6785..c4076b6aad 100644 --- a/test/downstream/linearization_dd.jl +++ b/test/downstream/linearization_dd.jl @@ -29,33 +29,34 @@ lin_outputs = [cart.s, cart.v, link1.A, link1.dA] lin_inputs = [force.f.u] # => nothing to remove extra defaults -op = Dict(cart.s => 10, cart.v => 0, link1.A => -pi/2, link1.dA => 0, force.f.u => 0, link1.x1 => nothing, link1.y1 => nothing, link1.x2 => nothing, link1.x_cm => nothing) -@test_broken begin - @info "named_ss" - G = named_ss(model, lin_inputs, lin_outputs, allow_symbolic = true, op = def, - allow_input_derivatives = true, zero_dummy_der = true) - G = sminreal(G) - @info "minreal" - G = minreal(G) - @info "poles" - ps = poles(G) - - @test minimum(abs, ps) < 1e-6 - @test minimum(abs, complex(0, 1.3777260367206716) .- ps) < 1e-10 - - lsys, syss = linearize(model, lin_inputs, lin_outputs, allow_symbolic = true, op = op, - allow_input_derivatives = true, zero_dummy_der = true) - lsyss, sysss = ModelingToolkit.linearize_symbolic(model, lin_inputs, lin_outputs; - allow_input_derivatives = true) - - dummyder = setdiff(unknowns(sysss), unknowns(model)) - def = merge(ModelingToolkit.guesses(model), def, Dict(x => 0.0 for x in dummyder)) - def[link1.fy1] = -def[link1.g] * def[link1.m] - - @test substitute(lsyss.A, def) ≈ lsys.A - # We cannot pivot symbolically, so the part where a linear solve is required - # is not reliable. - @test substitute(lsyss.B, def)[1:6, 1] ≈ lsys.B[1:6, 1] - @test substitute(lsyss.C, def) ≈ lsys.C - @test substitute(lsyss.D, def) ≈ lsys.D -end +op = Dict(cart.s => 10, cart.v => 0, link1.A => -pi / 2, link1.dA => 0, force.f.u => 0, + link1.x1 => nothing, link1.y1 => nothing, link1.x2 => nothing, link1.x_cm => nothing) +@info "named_ss" +G = named_ss(model, lin_inputs, lin_outputs; allow_symbolic = true, op, + allow_input_derivatives = true, zero_dummy_der = true) +G = sminreal(G) +@info "minreal" +G = minreal(G) +@info "poles" +ps = poles(G) + +@test minimum(abs, ps) < 1e-6 +@test minimum(abs, complex(0, 1.3777260367206716) .- ps) < 1e-10 + +lsys, syss = linearize(model, lin_inputs, lin_outputs, allow_symbolic = true, op = op, + allow_input_derivatives = true, zero_dummy_der = true) +lsyss, sysss = ModelingToolkit.linearize_symbolic(model, lin_inputs, lin_outputs; + allow_input_derivatives = true) + +dummyder = setdiff(unknowns(sysss), unknowns(model)) +# op2 = merge(ModelingToolkit.guesses(model), op, Dict(x => 0.0 for x in dummyder)) +op2 = merge(ModelingToolkit.defaults(syss), op) +op2[link1.fy1] = -op2[link1.g] * op2[link1.m] +op2[cart.f] = 0 + +@test substitute(lsyss.A, op2) ≈ lsys.A +# We cannot pivot symbolically, so the part where a linear solve is required +# is not reliable. +@test substitute(lsyss.B, op2)[1:6, 1] ≈ lsys.B[1:6, 1] +@test substitute(lsyss.C, op2) ≈ lsys.C +@test substitute(lsyss.D, op2) ≈ lsys.D From 9ed0fe2c662e90926917e9142468f1d2de560cdd Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 12 Feb 2025 13:55:55 +0530 Subject: [PATCH 0915/1337] test: fix initialization of some tests --- test/input_output_handling.jl | 13 ++++++++----- test/split_parameters.jl | 5 +++-- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/test/input_output_handling.jl b/test/input_output_handling.jl index b1d68bc323..4191571ed8 100644 --- a/test/input_output_handling.jl +++ b/test/input_output_handling.jl @@ -128,16 +128,17 @@ eqs = [connect(torque.flange, inertia1.flange_a) connect(inertia2.flange_a, spring.flange_b, damper.flange_b) y ~ inertia2.w + torque.tau.u] model = ODESystem(eqs, t; systems = [torque, inertia1, inertia2, spring, damper], - name = :name) + name = :name, guesses = [spring.flange_a.phi => 0.0]) model_outputs = [inertia1.w, inertia2.w, inertia1.phi, inertia2.phi] model_inputs = [torque.tau.u] +op = Dict(torque.tau.u => 0.0) matrices, ssys = linearize( - model, model_inputs, model_outputs); + model, model_inputs, model_outputs; op); @test length(ModelingToolkit.outputs(ssys)) == 4 if VERSION >= v"1.8" # :opaque_closure not supported before let # Just to have a local scope for D - matrices, ssys = linearize(model, model_inputs, [y]) + matrices, ssys = linearize(model, model_inputs, [y]; op) A, B, C, D = matrices obsf = ModelingToolkit.build_explicit_observed_function(ssys, [y], @@ -321,7 +322,8 @@ function SystemModel(u = nothing; name = :model) u ]) end - ODESystem(eqs, t; systems = [torque, inertia1, inertia2, spring, damper], name) + ODESystem(eqs, t; systems = [torque, inertia1, inertia2, spring, damper], + name, guesses = [spring.flange_a.phi => 0.0]) end model = SystemModel() # Model with load disturbance @@ -410,7 +412,8 @@ matrices, ssys = linearize(augmented_sys, augmented_sys.u, augmented_sys.input.u[2], augmented_sys.d - ], outs) + ], outs; + op = [augmented_sys.u => 0.0, augmented_sys.input.u[2] => 0.0, augmented_sys.d => 0.0]) @test matrices.A ≈ [A [1; 0]; zeros(1, 2) -0.001] @test matrices.B == I @test matrices.C == [C zeros(2)] diff --git a/test/split_parameters.jl b/test/split_parameters.jl index b4b7314838..a38dd24c38 100644 --- a/test/split_parameters.jl +++ b/test/split_parameters.jl @@ -165,15 +165,16 @@ function SystemModel(u = nothing; name = :model) systems = [torque, inertia1, inertia2, spring, damper, u]) end ODESystem(eqs, t; systems = [torque, inertia1, inertia2, spring, damper], - name) + name, guesses = [spring.flange_a.phi => 0.0]) end model = SystemModel() # Model with load disturbance @named d = Step(start_time = 1.0, duration = 10.0, offset = 0.0, height = 1.0) # Disturbance model_outputs = [model.inertia1.w, model.inertia2.w, model.inertia1.phi, model.inertia2.phi] # This is the state realization we want to control inputs = [model.torque.tau.u] +op = [model.torque.tau.u => 0.0] matrices, ssys = ModelingToolkit.linearize( - wr(model), inputs, model_outputs) + wr(model), inputs, model_outputs; op) # Design state-feedback gain using LQR # Define cost matrices From b664e1cd9381f09e36a13bb173e5d108a94d0f78 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 12 Feb 2025 14:22:27 +0530 Subject: [PATCH 0916/1337] fix: unhack observed in `process_SciMLProblem` --- src/systems/problem_utils.jl | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/systems/problem_utils.jl b/src/systems/problem_utils.jl index 87aafb8f73..be05d8fcff 100644 --- a/src/systems/problem_utils.jl +++ b/src/systems/problem_utils.jl @@ -756,7 +756,12 @@ function process_SciMLProblem( cmap, cs = get_cmap(sys) kwargs = NamedTuple(kwargs) - is_time_dependent(sys) || add_observed!(sys, u0map) + if eltype(eqs) <: Equation + obs, eqs = unhack_observed(observed(sys), eqs) + else + obs, _ = unhack_observed(observed(sys), Equation[x for x in eqs if x isa Equation]) + end + is_time_dependent(sys) || add_observed_equations!(u0map, obs) op, missing_unknowns, missing_pars = build_operating_point!(sys, u0map, pmap, defs, cmap, dvs, ps) @@ -777,7 +782,7 @@ function process_SciMLProblem( op[iv] = t end - is_time_dependent(sys) && add_observed!(sys, op) + is_time_dependent(sys) && add_observed_equations!(op, obs) add_parameter_dependencies!(sys, op) if warn_cyclic_dependency From c73b0a261daf2761ba5ef3f7c83f7bb3f5583fd9 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 12 Feb 2025 14:22:47 +0530 Subject: [PATCH 0917/1337] test: test mutation of `Initial` parameters --- test/initializationsystem.jl | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/test/initializationsystem.jl b/test/initializationsystem.jl index 3a208bb940..4abefb00ee 100644 --- a/test/initializationsystem.jl +++ b/test/initializationsystem.jl @@ -1312,3 +1312,22 @@ end sol = solve(prob, Tsit5()) @test sol.ps[p] ≈ [2.0, 4.0] end + +@testset "Issue#3318: Mutating `Initial` parameters works" begin + @variables x(t) y(t)[1:2] [guess = ones(2)] + @parameters p[1:2, 1:2] + @mtkbuild sys = ODESystem( + [D(x) ~ x, D(y) ~ p * y], t; initialization_eqs = [x^2 + y[1]^2 + y[2]^2 ~ 4]) + prob = ODEProblem(sys, [x => 1.0, y[1] => 1], (0.0, 1.0), [p => 2ones(2, 2)]) + integ = init(prob, Tsit5()) + @test integ[x] ≈ 1.0 + @test integ[y] ≈ [1.0, sqrt(2.0)] + prob.ps[Initial(x)] = 0.5 + integ = init(prob, Tsit5()) + @test integ[x] ≈ 0.5 + @test integ[y] ≈ [1.0, sqrt(2.75)] + prob.ps[Initial(y[1])] = 0.5 + integ = init(prob, Tsit5()) + @test integ[x] ≈ 0.5 + @test integ[y]≈[0.5, sqrt(3.5)] atol=1e-6 +end From 472454094522c5358fce06385a0dbb5f11b58b4a Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 12 Feb 2025 14:37:00 +0530 Subject: [PATCH 0918/1337] test: test Issue#3342 --- test/initializationsystem.jl | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/test/initializationsystem.jl b/test/initializationsystem.jl index 4abefb00ee..392516cf8b 100644 --- a/test/initializationsystem.jl +++ b/test/initializationsystem.jl @@ -1331,3 +1331,26 @@ end @test integ[x] ≈ 0.5 @test integ[y]≈[0.5, sqrt(3.5)] atol=1e-6 end + +@testset "Issue#3342" begin + @variables x(t) y(t) + stop!(integrator, _, _, _) = terminate!(integrator) + @named sys = ODESystem([D(x) ~ 1.0 + D(y) ~ 1.0], t; initialization_eqs = [ + y ~ 0.0 + ], + continuous_events = [ + [y ~ 0.5] => (stop!, [y], [], [], nothing) + ]) + sys = structural_simplify(sys) + prob0 = ODEProblem(sys, [x => NaN], (0.0, 1.0), []) + + # final_x(x0) is equivalent to x0 + 0.5 + function final_x(x0) + prob = remake(prob0; u0 = [x => x0]) + sol = solve(prob) + return sol[x][end] + end + @test final_x(0.3) ≈ 0.8 # should be 0.8 + @test ForwardDiff.derivative(final_x, 0.3) ≈ 1.0 +end From ca1f389e5c6204ef0a10c06c5efcaabd7d2ad44a Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 12 Feb 2025 14:45:09 +0530 Subject: [PATCH 0919/1337] test: test initialization for unsimplified systems --- test/initializationsystem.jl | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/test/initializationsystem.jl b/test/initializationsystem.jl index 392516cf8b..0990101069 100644 --- a/test/initializationsystem.jl +++ b/test/initializationsystem.jl @@ -1354,3 +1354,10 @@ end @test final_x(0.3) ≈ 0.8 # should be 0.8 @test ForwardDiff.derivative(final_x, 0.3) ≈ 1.0 end + +@testset "Issue#3330: Initialization for unsimplified systems" begin + @variables x(t) [guess = 1.0] + @mtkbuild sys = ODESystem(D(x) ~ x, t; initialization_eqs = [x^2 ~ 4]) + prob = ODEProblem(sys, [], (0.0, 1.0)) + @test prob.f.initialization_data !== nothing +end From 6f38a1382de62cc7190f432352b01826406b7f6c Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 12 Feb 2025 14:50:41 +0530 Subject: [PATCH 0920/1337] fix: check `hasname` for parameters in `propertynames` --- src/systems/abstractsystem.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index 8093ca5a13..9f8e292a81 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -956,6 +956,7 @@ function Base.propertynames(sys::AbstractSystem; private = false) push!(names, getname(s)) end has_ps(sys) && for s in get_ps(sys) + hasname(s) || continue push!(names, getname(s)) end has_observed(sys) && for s in get_observed(sys) From 158f3f899c72f5d77af57c0dad06bb5939ef174d Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 12 Feb 2025 15:12:27 +0530 Subject: [PATCH 0921/1337] build: bump SciMLBase compat --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index ac865063f2..ec7939a58c 100644 --- a/Project.toml +++ b/Project.toml @@ -135,7 +135,7 @@ RecursiveArrayTools = "3.26" Reexport = "0.2, 1" RuntimeGeneratedFunctions = "0.5.9" SCCNonlinearSolve = "1.0.0" -SciMLBase = "2.72.1" +SciMLBase = "2.73" SciMLStructures = "1.0" Serialization = "1" Setfield = "0.7, 0.8, 1" From c12374f3dcbaaa01d679a092d8be956cca10e3c0 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 12 Feb 2025 21:43:01 +0530 Subject: [PATCH 0922/1337] docs: add documentation for `Initial` parameters --- docs/src/tutorials/initialization.md | 70 ++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/docs/src/tutorials/initialization.md b/docs/src/tutorials/initialization.md index 26dbb58899..27eb2ae28d 100644 --- a/docs/src/tutorials/initialization.md +++ b/docs/src/tutorials/initialization.md @@ -203,6 +203,73 @@ long enough you will see that `λ = 0` is required for this equation, but since problem constructor. Additionally, any warning about not being fully determined can be suppressed via passing `warn_initialize_determined = false`. +## Constant constraints in initialization + +Consider the pendulum system again: + +```@repl init +equations(pend) +observed(pend) +``` + +Suppose we want to solve the same system with multiple different initial +y-velocities from a given position. + +```@example init +prob = ODEProblem( + pend, [x => 1, D(y) => 0], (0.0, 1.5), [g => 1], guesses = [λ => 0, y => 1, x => 1]) +sol1 = solve(prob, Rodas5P()) +``` + +```@example init +sol1[D(y), 1] +``` + +Repeatedly re-creating the `ODEProblem` with different values of `D(y)` and `x` or +repeatedly calling `remake` is slow. Instead, for any `variable => constant` constraint +in the `ODEProblem` initialization (whether provided to the `ODEProblem` constructor or +a default value) we can update the `constant` value. ModelingToolkit refers to these +values using the `Initial` operator. For example: + +```@example init +prob.ps[[Initial(x), Initial(D(y))]] +``` + +To solve with a different starting y-velocity, we can simply do + +```@example init +prob.ps[Initial(D(y))] = -0.1 +sol2 = solve(prob, Rodas5P()) +``` + +```@example init +sol2[D(y), 1] +``` + +Note that this _only_ applies for constant constraints for the current ODEProblem. +For example, `D(x)` does not have a constant constraint - it is solved for by +initialization. Thus, mutating `Initial(D(x))` does not have any effect: + +```@repl init +sol2[D(x), 1] +prob.ps[Initial(D(x))] = 1.0 +sol3 = solve(prob, Rodas5P()) +sol3[D(x), 1] +``` + +To enforce this constraint, we would have to `remake` the problem (or construct a new one). + +```@repl init +prob2 = remake(prob; u0 = [y => 0.0, D(x) => 0.0, x => nothing, D(y) => nothing]); +sol4 = solve(prob2, Rodas5P()) +sol4[D(x), 1] +``` + +Note the need to provide `x => nothing, D(y) => nothing` to override the previously +provided initial conditions. Since `remake` is a partial update, the constraints provided +to it are merged with the ones already present in the problem. Existing constraints can be +removed by providing a value of `nothing`. + ## Initialization of parameters Parameters may also be treated as unknowns in the initialization system. Doing so works @@ -231,6 +298,9 @@ constraints provided to it. The new values will be combined with the original variable-value mapping provided to `ODEProblem` and used to construct the initialization problem. +The variable on the left hand side of all parameter dependencies also has an `Initial` +variant, which is used if a constant constraint is provided for the variable. + ### Parameter initialization by example Consider the following system, where the sum of two unknowns is a constant parameter From 5d190f6239871024e58ab8a3ee633095f1bf7803 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 12 Feb 2025 21:54:28 +0530 Subject: [PATCH 0923/1337] fix: better handle overrides for differentiated variables in `remake` --- src/systems/nonlinear/initializesystem.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/src/systems/nonlinear/initializesystem.jl b/src/systems/nonlinear/initializesystem.jl index da66ce66d7..77138c9887 100644 --- a/src/systems/nonlinear/initializesystem.jl +++ b/src/systems/nonlinear/initializesystem.jl @@ -428,6 +428,7 @@ function SciMLBase.remake_initialization_data( ps = parameters(sys) u0map = to_varmap(u0, dvs) symbols_to_symbolics!(sys, u0map) + add_toterms!(u0map) pmap = to_varmap(p, ps) symbols_to_symbolics!(sys, pmap) guesses = Dict() From 7baf7881852c7b888b3e3eaceced5fb4be31d904 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 13 Feb 2025 12:54:24 +0530 Subject: [PATCH 0924/1337] docs: import `DynamicQuantities` in doc example requiring it --- docs/src/basics/Variable_metadata.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/src/basics/Variable_metadata.md b/docs/src/basics/Variable_metadata.md index b2cc472f2f..43d0a7f2ea 100644 --- a/docs/src/basics/Variable_metadata.md +++ b/docs/src/basics/Variable_metadata.md @@ -201,6 +201,7 @@ state_priority(important_dof) Units for variables can be designated using symbolic metadata. For more information, please see the [model validation and units](@ref units) section of the docs. Note that `getunit` is not equivalent to `get_unit` - the former is a metadata getter for individual variables (and is provided so the same interface function for `unit` exists like other metadata), while the latter is used to handle more general symbolic expressions. ```@example metadata +using DynamicQuantities @variables speed [unit = u"m/s"] hasunit(speed) ``` From dfa4db8263225e24ec7de06b3516b18038c1fb28 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 13 Feb 2025 12:54:50 +0530 Subject: [PATCH 0925/1337] docs: update remake doc page to account for extra tunables --- docs/src/examples/remake.md | 55 +++++++++++++------------------------ 1 file changed, 19 insertions(+), 36 deletions(-) diff --git a/docs/src/examples/remake.md b/docs/src/examples/remake.md index f4396ef7a7..47ac02ee0a 100644 --- a/docs/src/examples/remake.md +++ b/docs/src/examples/remake.md @@ -45,12 +45,22 @@ parameters to optimize. ```@example Remake using SymbolicIndexingInterface: parameter_values, state_values -using SciMLStructures: Tunable, replace, replace! +using SciMLStructures: Tunable, canonicalize, replace, replace! +using PreallocationTools function loss(x, p) odeprob = p[1] # ODEProblem stored as parameters to avoid using global variables ps = parameter_values(odeprob) # obtain the parameter object from the problem - ps = replace(Tunable(), ps, x) # create a copy with the values passed to the loss function + diffcache = p[5] + # get an appropriately typed preallocated buffer to store the `x` values in + buffer = get_tmp(diffcache, x) + # copy the current values to this buffer + copyto!(buffer, canonicalize(Tunable(), ps)[1]) + # create a copy of the parameter object with the buffer + ps = replace(Tunable(), ps, buffer) + # set the updated values in the parameter object + setter = p[4] + setter(ps, x) # remake the problem, passing in our new parameter object newprob = remake(odeprob; p = ps) timesteps = p[2] @@ -81,49 +91,22 @@ We can perform the optimization as below: ```@example Remake using Optimization using OptimizationOptimJL +using SymbolicIndexingInterface # manually create an OptimizationFunction to ensure usage of `ForwardDiff`, which will # require changing the types of parameters from `Float64` to `ForwardDiff.Dual` optfn = OptimizationFunction(loss, Optimization.AutoForwardDiff()) +# function to set the parameters we are optimizing +setter = setp(odeprob, [α, β, γ, δ]) +# `DiffCache` to avoid allocations +diffcache = DiffCache(canonicalize(Tunable(), parameter_values(odeprob))[1]) # parameter object is a tuple, to store differently typed objects together optprob = OptimizationProblem( - optfn, rand(4), (odeprob, timesteps, data), lb = 0.1zeros(4), ub = 3ones(4)) + optfn, rand(4), (odeprob, timesteps, data, setter, diffcache), + lb = 0.1zeros(4), ub = 3ones(4)) sol = solve(optprob, BFGS()) ``` -To identify which values correspond to which parameters, we can `replace!` them into the -`ODEProblem`: - -```@example Remake -replace!(Tunable(), parameter_values(odeprob), sol.u) -odeprob.ps[[α, β, γ, δ]] -``` - -`replace!` operates in-place, so the values being replaced must be of the same type as those -stored in the parameter object, or convertible to that type. For demonstration purposes, we -can construct a loss function that uses `replace!`, and calculate gradients using -`AutoFiniteDiff` rather than `AutoForwardDiff`. - -```@example Remake -function loss2(x, p) - odeprob = p[1] # ODEProblem stored as parameters to avoid using global variables - newprob = remake(odeprob) # copy the problem with `remake` - # update the parameter values in-place - replace!(Tunable(), parameter_values(newprob), x) - timesteps = p[2] - sol = solve(newprob, AutoTsit5(Rosenbrock23()); saveat = timesteps) - truth = p[3] - data = Array(sol) - return sum((truth .- data) .^ 2) / length(truth) -end - -# use finite-differencing to calculate derivatives -optfn2 = OptimizationFunction(loss2, Optimization.AutoFiniteDiff()) -optprob2 = OptimizationProblem( - optfn2, rand(4), (odeprob, timesteps, data), lb = 0.1zeros(4), ub = 3ones(4)) -sol = solve(optprob2, BFGS()) -``` - # Re-creating the problem There are multiple ways to re-create a problem with new state/parameter values. We will go From c5b9fe0aba10cbba84a21b2c7476964d921db2b0 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 13 Feb 2025 12:55:26 +0530 Subject: [PATCH 0926/1337] fix: handle `state_priority` of wrapped variables --- src/variables.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/src/variables.jl b/src/variables.jl index 0068055832..83f0681a09 100644 --- a/src/variables.jl +++ b/src/variables.jl @@ -127,6 +127,7 @@ isoutput(x) = isvarkind(VariableOutput, x) # irreducibility is independent from IO. isirreducible(x) = isvarkind(VariableIrreducible, x) setirreducible(x, v::Bool) = setmetadata(x, VariableIrreducible, v) +state_priority(x::Union{Num, Symbolics.Arr}) = state_priority(unwrap(x)) state_priority(x) = convert(Float64, getmetadata(x, VariableStatePriority, 0.0))::Float64 function default_toterm(x) From 1e9d28fbf91663320d9358bbeb69df09a5e40e60 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 13 Feb 2025 12:55:36 +0530 Subject: [PATCH 0927/1337] test: add guess for FMI test --- test/fmi/fmi.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/fmi/fmi.jl b/test/fmi/fmi.jl index 39f624f616..a311bf44d2 100644 --- a/test/fmi/fmi.jl +++ b/test/fmi/fmi.jl @@ -235,7 +235,8 @@ end t; systems = [adder1, adder2]) prob = ODEProblem( - sys, [adder1.c => 1.0, adder2.c => 1.0, adder1.a => 2.0], (0.0, 1.0)) + sys, [adder1.c => 1.0, adder2.c => 1.0, adder1.a => 2.0], + (0.0, 1.0); guesses = [adder2.a => 0.0]) return sys, prob end @named adder1 = SimpleAdder() From 6bc28a8a8bb645b3a3819207e48e92f070f2f987 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 13 Feb 2025 12:56:07 +0530 Subject: [PATCH 0928/1337] fix: handle non-standard array indexes in `parameter_index` --- src/ModelingToolkit.jl | 1 + src/systems/index_cache.jl | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/ModelingToolkit.jl b/src/ModelingToolkit.jl index 6501a4e026..eaa7af85d2 100644 --- a/src/ModelingToolkit.jl +++ b/src/ModelingToolkit.jl @@ -56,6 +56,7 @@ using RecursiveArrayTools import Graphs: SimpleDiGraph, add_edge!, incidence_matrix import BlockArrays: BlockArray, BlockedArray, Block, blocksize, blocksizes, blockpush!, undef_blocks, blocks +using OffsetArrays: Origin import CommonSolve import EnumX diff --git a/src/systems/index_cache.jl b/src/systems/index_cache.jl index a956e17313..d71352da9f 100644 --- a/src/systems/index_cache.jl +++ b/src/systems/index_cache.jl @@ -397,7 +397,8 @@ function SymbolicIndexingInterface.parameter_index(ic::IndexCache, sym) pidx = parameter_index(ic, args[1]) pidx === nothing && return nothing if pidx.portion == SciMLStructures.Tunable() - ParameterIndex(pidx.portion, reshape(pidx.idx, size(args[1]))[args[2:end]...], + ParameterIndex(pidx.portion, + Origin(first.(axes((args[1]))))(reshape(pidx.idx, size(args[1])))[args[2:end]...], pidx.validate_size) else ParameterIndex(pidx.portion, (pidx.idx..., args[2:end]...), pidx.validate_size) From 3e5c436509f2963e1746cdaba564aaacbf92c92e Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 13 Feb 2025 12:56:46 +0530 Subject: [PATCH 0929/1337] fix: better handle scalarized array parameters with unscalarized defaults in `is_parameter_solvable` --- src/systems/nonlinear/initializesystem.jl | 25 ++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/src/systems/nonlinear/initializesystem.jl b/src/systems/nonlinear/initializesystem.jl index 77138c9887..07f745de18 100644 --- a/src/systems/nonlinear/initializesystem.jl +++ b/src/systems/nonlinear/initializesystem.jl @@ -355,16 +355,27 @@ function get_possibly_array_fallback_singletons(varmap, p) if haskey(varmap, p) return varmap[p] end - symbolic_type(p) == ArraySymbolic() || return nothing - scal = collect(p) - if all(x -> haskey(varmap, x), scal) - res = [varmap[x] for x in scal] - if any(x -> x === nothing, res) + if symbolic_type(p) == ArraySymbolic() + scal = collect(p) + if all(x -> haskey(varmap, x), scal) + res = [varmap[x] for x in scal] + if any(x -> x === nothing, res) + return nothing + elseif any(x -> x === missing, res) + return missing + end + return res + end + elseif iscall(p) && operation(p) == getindex + arrp = arguments(p)[1] + val = get_possibly_array_fallback_singletons(varmap, arrp) + if val === nothing return nothing - elseif any(x -> x === missing, res) + elseif val === missing return missing + else + return val end - return res end return nothing end From 85f98802048ded584e10dad034958776c3f50238 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 13 Feb 2025 13:04:27 +0530 Subject: [PATCH 0930/1337] test: move `generate_custom_function` tests to `InterfaceII` --- test/runtests.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/runtests.jl b/test/runtests.jl index 9537b1b44e..5b3c0f5a8b 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -60,7 +60,6 @@ end @safetestset "FuncAffect Test" include("funcaffect.jl") @safetestset "Constants Test" include("constants.jl") @safetestset "Parameter Dependency Test" include("parameter_dependencies.jl") - @safetestset "Generate Custom Function Test" include("generate_custom_function.jl") @safetestset "Equation Type Accessors Test" include("equation_type_accessors.jl") @safetestset "Equations with complex values" include("complex.jl") end @@ -75,6 +74,7 @@ end if GROUP == "All" || GROUP == "InterfaceII" @testset "InterfaceII" begin + @safetestset "Generate Custom Function Test" include("generate_custom_function.jl") @safetestset "IndexCache Test" include("index_cache.jl") @safetestset "Variable Utils Test" include("variable_utils.jl") @safetestset "Variable Metadata Test" include("test_variable_metadata.jl") From 471e77c1dae4ac7d98d02244f733fe3ef3075a39 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 13 Feb 2025 13:05:28 +0530 Subject: [PATCH 0931/1337] test: rename `generate_custom_function` testset to `code_generation` --- test/code_generation.jl | 56 ++++++++++++++++++++++++++++++++ test/generate_custom_function.jl | 53 ------------------------------ test/runtests.jl | 2 +- 3 files changed, 57 insertions(+), 54 deletions(-) create mode 100644 test/code_generation.jl delete mode 100644 test/generate_custom_function.jl diff --git a/test/code_generation.jl b/test/code_generation.jl new file mode 100644 index 0000000000..cc7a82a561 --- /dev/null +++ b/test/code_generation.jl @@ -0,0 +1,56 @@ +using ModelingToolkit +using ModelingToolkit: t_nounits as t, D_nounits as D + +@testset "`generate_custom_function`" begin + @variables x(t) y(t)[1:3] + @parameters p1=1.0 p2[1:3]=[1.0, 2.0, 3.0] p3::Int=1 p4::Bool=false + + sys = complete(ODESystem(Equation[], t, [x; y], [p1, p2, p3, p4]; name = :sys)) + u0 = [1.0, 2.0, 3.0, 4.0] + p = ModelingToolkit.MTKParameters(sys, []) + + fn1 = generate_custom_function( + sys, x + y[1] + p1 + p2[1] + p3 * t; expression = Val(false)) + @test fn1(u0, p, 0.0) == 5.0 + + fn2 = generate_custom_function( + sys, x + y[1] + p1 + p2[1] + p3 * t, [x], [p1, p2, p3]; expression = Val(false)) + @test fn1(u0, p, 0.0) == 5.0 + + fn3_oop, fn3_iip = generate_custom_function( + sys, [x + y[2], y[3] + p2[2], p1 + p3, 3t]; expression = Val(false)) + + buffer = zeros(4) + fn3_iip(buffer, u0, p, 1.0) + @test buffer == [4.0, 6.0, 2.0, 3.0] + @test fn3_oop(u0, p, 1.0) == [4.0, 6.0, 2.0, 3.0] + + fn4 = generate_custom_function(sys, ifelse(p4, p1, p2[2]); expression = Val(false)) + @test fn4(u0, p, 1.0) == 2.0 + fn5 = generate_custom_function(sys, ifelse(!p4, p1, p2[2]); expression = Val(false)) + @test fn5(u0, p, 1.0) == 1.0 + + @variables x y[1:3] + sys = complete(NonlinearSystem(Equation[], [x; y], [p1, p2, p3, p4]; name = :sys)) + p = MTKParameters(sys, []) + + fn1 = generate_custom_function(sys, x + y[1] + p1 + p2[1] + p3; expression = Val(false)) + @test fn1(u0, p) == 6.0 + + fn2 = generate_custom_function( + sys, x + y[1] + p1 + p2[1] + p3, [x], [p1, p2, p3]; expression = Val(false)) + @test fn1(u0, p) == 6.0 + + fn3_oop, fn3_iip = generate_custom_function( + sys, [x + y[2], y[3] + p2[2], p1 + p3]; expression = Val(false)) + + buffer = zeros(3) + fn3_iip(buffer, u0, p) + @test buffer == [4.0, 6.0, 2.0] + @test fn3_oop(u0, p, 1.0) == [4.0, 6.0, 2.0] + + fn4 = generate_custom_function(sys, ifelse(p4, p1, p2[2]); expression = Val(false)) + @test fn4(u0, p, 1.0) == 2.0 + fn5 = generate_custom_function(sys, ifelse(!p4, p1, p2[2]); expression = Val(false)) + @test fn5(u0, p, 1.0) == 1.0 +end diff --git a/test/generate_custom_function.jl b/test/generate_custom_function.jl deleted file mode 100644 index ea1e429e30..0000000000 --- a/test/generate_custom_function.jl +++ /dev/null @@ -1,53 +0,0 @@ -using ModelingToolkit -using ModelingToolkit: t_nounits as t, D_nounits as D - -@variables x(t) y(t)[1:3] -@parameters p1=1.0 p2[1:3]=[1.0, 2.0, 3.0] p3::Int=1 p4::Bool=false - -sys = complete(ODESystem(Equation[], t, [x; y], [p1, p2, p3, p4]; name = :sys)) -u0 = [1.0, 2.0, 3.0, 4.0] -p = ModelingToolkit.MTKParameters(sys, []) - -fn1 = generate_custom_function(sys, x + y[1] + p1 + p2[1] + p3 * t; expression = Val(false)) -@test fn1(u0, p, 0.0) == 5.0 - -fn2 = generate_custom_function( - sys, x + y[1] + p1 + p2[1] + p3 * t, [x], [p1, p2, p3]; expression = Val(false)) -@test fn1(u0, p, 0.0) == 5.0 - -fn3_oop, fn3_iip = generate_custom_function( - sys, [x + y[2], y[3] + p2[2], p1 + p3, 3t]; expression = Val(false)) - -buffer = zeros(4) -fn3_iip(buffer, u0, p, 1.0) -@test buffer == [4.0, 6.0, 2.0, 3.0] -@test fn3_oop(u0, p, 1.0) == [4.0, 6.0, 2.0, 3.0] - -fn4 = generate_custom_function(sys, ifelse(p4, p1, p2[2]); expression = Val(false)) -@test fn4(u0, p, 1.0) == 2.0 -fn5 = generate_custom_function(sys, ifelse(!p4, p1, p2[2]); expression = Val(false)) -@test fn5(u0, p, 1.0) == 1.0 - -@variables x y[1:3] -sys = complete(NonlinearSystem(Equation[], [x; y], [p1, p2, p3, p4]; name = :sys)) -p = MTKParameters(sys, []) - -fn1 = generate_custom_function(sys, x + y[1] + p1 + p2[1] + p3; expression = Val(false)) -@test fn1(u0, p) == 6.0 - -fn2 = generate_custom_function( - sys, x + y[1] + p1 + p2[1] + p3, [x], [p1, p2, p3]; expression = Val(false)) -@test fn1(u0, p) == 6.0 - -fn3_oop, fn3_iip = generate_custom_function( - sys, [x + y[2], y[3] + p2[2], p1 + p3]; expression = Val(false)) - -buffer = zeros(3) -fn3_iip(buffer, u0, p) -@test buffer == [4.0, 6.0, 2.0] -@test fn3_oop(u0, p, 1.0) == [4.0, 6.0, 2.0] - -fn4 = generate_custom_function(sys, ifelse(p4, p1, p2[2]); expression = Val(false)) -@test fn4(u0, p, 1.0) == 2.0 -fn5 = generate_custom_function(sys, ifelse(!p4, p1, p2[2]); expression = Val(false)) -@test fn5(u0, p, 1.0) == 1.0 diff --git a/test/runtests.jl b/test/runtests.jl index 5b3c0f5a8b..966b02cacb 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -74,7 +74,7 @@ end if GROUP == "All" || GROUP == "InterfaceII" @testset "InterfaceII" begin - @safetestset "Generate Custom Function Test" include("generate_custom_function.jl") + @safetestset "Code Generation Test" include("code_generation.jl") @safetestset "IndexCache Test" include("index_cache.jl") @safetestset "Variable Utils Test" include("variable_utils.jl") @safetestset "Variable Metadata Test" include("test_variable_metadata.jl") From a50081d29d0fe59ab0982bab024065db19a63ccb Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 13 Feb 2025 13:15:56 +0530 Subject: [PATCH 0932/1337] fix: handle non-standard array indexes in codegen --- src/systems/codegen_utils.jl | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/systems/codegen_utils.jl b/src/systems/codegen_utils.jl index bee7564a4d..1c9e6ce53c 100644 --- a/src/systems/codegen_utils.jl +++ b/src/systems/codegen_utils.jl @@ -34,7 +34,7 @@ function array_variable_assignments(args...) # get and/or construct the buffer storing indexes idxbuffer = get!( () -> map(Returns((0, 0)), eachindex(arrvar)), var_to_arridxs, arrvar) - idxbuffer[arguments(var)[2:end]...] = (i, j) + Origin(first.(axes(arrvar))...)(idxbuffer)[arguments(var)[2:end]...] = (i, j) end end @@ -59,18 +59,22 @@ function array_variable_assignments(args...) idxs = SArray{Tuple{size(idxs)...}}(idxs) end # view and reshape - push!(assignments, - arrvar ← - term(reshape, term(view, generated_argument_name(buffer_idx), idxs), - size(arrvar))) + + expr = term(reshape, term(view, generated_argument_name(buffer_idx), idxs), + size(arrvar)) else elems = map(idxs) do idx i, j = idx term(getindex, generated_argument_name(i), j) end - # use `MakeArray` and generate a stack-allocated array - push!(assignments, arrvar ← MakeArray(elems, SArray)) + # use `MakeArray` syntax and generate a stack-allocated array + expr = term(SymbolicUtils.Code.create_array, SArray, nothing, + Val(ndims(arrvar)), Val(length(arrvar)), elems...) + end + if any(x -> !isone(first(x)), axes(arrvar)) + expr = term(Origin(first.(axes(arrvar))...), expr) end + push!(assignments, arrvar ← expr) end return assignments From 4d9552c33ef7477066fb800e628427927b8c1c55 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 13 Feb 2025 13:16:21 +0530 Subject: [PATCH 0933/1337] test: test code generation for non-standard array indexes --- test/code_generation.jl | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/test/code_generation.jl b/test/code_generation.jl index cc7a82a561..cf3d660b81 100644 --- a/test/code_generation.jl +++ b/test/code_generation.jl @@ -1,4 +1,4 @@ -using ModelingToolkit +using ModelingToolkit, OrdinaryDiffEq, SymbolicIndexingInterface using ModelingToolkit: t_nounits as t, D_nounits as D @testset "`generate_custom_function`" begin @@ -54,3 +54,27 @@ using ModelingToolkit: t_nounits as t, D_nounits as D fn5 = generate_custom_function(sys, ifelse(!p4, p1, p2[2]); expression = Val(false)) @test fn5(u0, p, 1.0) == 1.0 end + +@testset "Non-standard array variables" begin + @variables x(t) + @parameters p[0:2] (f::Function)(..) + @mtkbuild sys = ODESystem(D(x) ~ p[0] * x + p[1] * t + p[2] + f(p), t) + prob = ODEProblem(sys, [x => 1.0], (0.0, 1.0), [p => [1.0, 2.0, 3.0], f => sum]) + @test prob.ps[p] == [1.0, 2.0, 3.0] + @test prob.ps[p[0]] == 1.0 + sol = solve(prob, Tsit5()) + @test SciMLBase.successful_retcode(sol) + + @testset "Array split across buffers" begin + @variables x(t)[0:2] + @parameters p[1:2] (f::Function)(..) + @named sys = ODESystem( + [D(x[0]) ~ p[1] * x[0] + x[2], D(x[1]) ~ p[2] * f(x) + x[2]], t) + sys, = structural_simplify(sys, ([x[2]], [])) + @test is_parameter(sys, x[2]) + prob = ODEProblem(sys, [x[0] => 1.0, x[1] => 1.0], (0.0, 1.0), + [p => ones(2), f => sum, x[2] => 2.0]) + sol = solve(prob, Tsit5()) + @test SciMLBase.successful_retcode(sol) + end +end From c78e9f0089acc4db816b70058f42330693c60658 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 13 Feb 2025 13:22:26 +0530 Subject: [PATCH 0934/1337] refactor: remove dead code --- src/systems/abstractsystem.jl | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index 9f8e292a81..8f46570b09 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -713,14 +713,8 @@ function add_initialization_parameters(sys::AbstractSystem) end all_initialvars = collect(all_initialvars) initials = map(Initial(), all_initialvars) - # existing_initials = filter( - # x -> iscall(x) && (operation(x) isa Initial), parameters(sys)) - existing_initials = [] - @set! sys.ps = unique!([setdiff(get_ps(sys), existing_initials); initials]) + @set! sys.ps = unique!([get_ps(sys); initials]) defs = copy(get_defaults(sys)) - for x in existing_initials - delete!(defs, x) - end for ivar in initials if symbolic_type(ivar) == ScalarSymbolic() defs[ivar] = zero_var(ivar) From 2748f7f2e42e136e97c45a24b91aa3180a88a326 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 13 Feb 2025 13:22:48 +0530 Subject: [PATCH 0935/1337] docs: add documentation for `Initial` operator --- src/systems/abstractsystem.jl | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index 8f46570b09..41c487dadc 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -650,6 +650,12 @@ function isscheduled(sys::AbstractSystem) end end +""" + Initial(x) + +The `Initial` operator. Used by initializaton to store constant constraints on variables +of a system. See the documentation section on initialization for more information. +""" struct Initial <: Symbolics.Operator end Initial(x) = Initial()(x) SymbolicUtils.promote_symtype(::Type{Initial}, T) = T @@ -660,23 +666,31 @@ input_timedomain(::Initial, _ = nothing) = Continuous() output_timedomain(::Initial, _ = nothing) = Continuous() function (f::Initial)(x) + # wrap output if wrapped input 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 Initial && return x result = if symbolic_type(x) == ArraySymbolic() + # create an array for `Initial(array)` Symbolics.array_term(f, toparam(x)) elseif iscall(x) && operation(x) == getindex + # instead of `Initial(x[1])` create `Initial(x)[1]` + # which allows parameter indexing to handle this case automatically. arr = arguments(x)[1] term(getindex, f(toparam(arr)), arguments(x)[2:end]...) else term(f, toparam(x)) end + # the result should be a parameter result = toparam(result) if iw result = wrap(result) @@ -684,6 +698,7 @@ function (f::Initial)(x) return result end +# This is required so `fast_substitute` works function SymbolicUtils.maketerm(::Type{<:BasicSymbolic}, ::Initial, args, meta) return metadata(Initial()(args...), meta) end From 10250a2cb26a46f08d9ce59309233a6c6bb4cead Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 13 Feb 2025 15:45:42 +0530 Subject: [PATCH 0936/1337] docs: add PreallocationTools.jl to docs environment --- docs/Project.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/Project.toml b/docs/Project.toml index 40fcb16581..0b1393cf9e 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -15,6 +15,7 @@ Optimization = "7f7a1694-90dd-40f0-9382-eb1efda571ba" OptimizationOptimJL = "36348300-93cb-4f02-beb5-3c3902f8871e" OrdinaryDiffEq = "1dea7af3-3e70-54e6-95c3-0bf5283fa5ed" Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80" +PreallocationTools = "d236fae5-4411-538c-8e31-a6e3d9e00b46" SciMLStructures = "53ae85a6-f571-4167-b2af-e1d143709226" Setfield = "efcf1570-3423-57d1-acb7-fd33fddbac46" StochasticDiffEq = "789caeaf-c7a9-5a7d-9973-96adeb23e2a0" From bb42719158e876d21a9ddf12b934141632fd2823 Mon Sep 17 00:00:00 2001 From: vyudu Date: Thu, 13 Feb 2025 15:46:43 -0500 Subject: [PATCH 0937/1337] fix: fix initialization of DiscreteSystem with renamed variables --- .../StructuralTransformations.jl | 3 +- .../symbolics_tearing.jl | 20 +++++++----- src/structural_transformation/utils.jl | 32 ++++++++++--------- .../discrete_system/discrete_system.jl | 6 ++-- src/utils.jl | 2 ++ src/variables.jl | 7 +++- test/runtests.jl | 3 ++ 7 files changed, 45 insertions(+), 28 deletions(-) diff --git a/src/structural_transformation/StructuralTransformations.jl b/src/structural_transformation/StructuralTransformations.jl index 1220d517cc..510352ad59 100644 --- a/src/structural_transformation/StructuralTransformations.jl +++ b/src/structural_transformation/StructuralTransformations.jl @@ -22,7 +22,7 @@ using ModelingToolkit: ODESystem, AbstractSystem, var_from_nested_derivative, Di get_postprocess_fbody, vars!, IncrementalCycleTracker, add_edge_checked!, topological_sort, invalidate_cache!, Substitutions, get_or_construct_tearing_state, - filter_kwargs, lower_varname_with_unit, setio, SparseMatrixCLIL, + filter_kwargs, lower_varname_with_unit, lower_shift_varname_with_unit, setio, SparseMatrixCLIL, get_fullvars, has_equations, observed, Schedule, schedule @@ -63,6 +63,7 @@ export torn_system_jacobian_sparsity export full_equations export but_ordered_incidence, lowest_order_variable_mask, highest_order_variable_mask export computed_highest_diff_variables +export shift2term, lower_shift_varname include("utils.jl") include("pantelides.jl") diff --git a/src/structural_transformation/symbolics_tearing.jl b/src/structural_transformation/symbolics_tearing.jl index ec1ff45fd7..ffbd6e3452 100644 --- a/src/structural_transformation/symbolics_tearing.jl +++ b/src/structural_transformation/symbolics_tearing.jl @@ -366,7 +366,6 @@ function generate_derivative_variables!(ts::TearingState, neweqs, var_eq_matchin eq_var_matching = invview(var_eq_matching) diff_to_var = invview(var_to_diff) is_discrete = is_only_discrete(structure) - lower_varname = is_discrete ? lower_shift_varname : lower_varname_with_unit linear_eqs = mm === nothing ? Dict{Int, Int}() : Dict(reverse(en) for en in enumerate(mm.nzrows)) @@ -375,9 +374,9 @@ function generate_derivative_variables!(ts::TearingState, neweqs, var_eq_matchin for v in 1:length(var_to_diff) dv = var_to_diff[v] # For discrete systems, directly substitute lowest-order shift - if is_discrete && diff_to_var[v] == nothing - operation(fullvars[v]) isa Shift && (fullvars[v] = lower_varname(fullvars[v], iv)) - end + #if is_discrete && diff_to_var[v] == nothing + # operation(fullvars[v]) isa Shift && (fullvars[v] = lower_shift_varname_with_unit(fullvars[v], iv)) + #end dv isa Int || continue solved = var_eq_matching[dv] isa Int solved && continue @@ -395,7 +394,8 @@ function generate_derivative_variables!(ts::TearingState, neweqs, var_eq_matchin dx = fullvars[dv] order, lv = var_order(dv, diff_to_var) - x_t = is_discrete ? lower_varname(fullvars[dv], iv) : lower_varname(fullvars[lv], iv, order) + x_t = is_discrete ? lower_shift_varname_with_unit(fullvars[dv], iv) : + Symbolics.diff2term(fullvars[dv]) # Add `x_t` to the graph v_t = add_dd_variable!(structure, fullvars, x_t, dv) @@ -467,11 +467,15 @@ function generate_system_equations!(state::TearingState, neweqs, var_eq_matching total_sub = Dict() if is_only_discrete(structure) - for v in fullvars + for (i, v) in enumerate(fullvars) op = operation(v) - op isa Shift && (op.steps < 0) && (total_sub[v] = lower_shift_varname(v, iv)) + op isa Shift && (op.steps < 0) && begin + lowered = lower_shift_varname_with_unit(v, iv) + total_sub[v] = lowered + fullvars[i] = lowered + end end - end + end # if var is like D(x) or Shift(t, 1)(x) isdervar = let diff_to_var = diff_to_var diff --git a/src/structural_transformation/utils.jl b/src/structural_transformation/utils.jl index 757eb9a8db..f64a8da132 100644 --- a/src/structural_transformation/utils.jl +++ b/src/structural_transformation/utils.jl @@ -453,16 +453,25 @@ end function lower_shift_varname(var, iv) op = operation(var) op isa Shift || return Shift(iv, 0)(var, true) # hack to prevent simplification of x(t) - x(t) - backshift = op.steps - backshift > 0 && return var + if op.steps < 0 + return shift2term(var) + else + return var + end +end - ds = "$iv-$(-backshift)" - d_separator = 'ˍ' +function shift2term(var) + backshift = operation(var).steps + iv = operation(var).t + num = join(Char(0x2080 + d) for d in reverse!(digits(-backshift))) + ds = join([Char(0x209c), Char(0x208b), num]) + #ds = "$iv-$(-backshift)" + #d_separator = 'ˍ' if ModelingToolkit.isoperator(var, ModelingToolkit.Shift) O = only(arguments(var)) oldop = operation(O) - newname = Symbol(string(nameof(oldop)), d_separator, ds) + newname = Symbol(string(nameof(oldop)), ds) else O = var oldop = operation(var) @@ -470,16 +479,9 @@ function lower_shift_varname(var, iv) newname = Symbol(varname, d_separator, ds) end newvar = maketerm(typeof(O), Symbolics.rename(oldop, newname), Symbolics.children(O), Symbolics.metadata(O)) - setmetadata(newvar, Symbolics.VariableSource, (:variables, newname)) - return ModelingToolkit._with_unit(identity, newvar, iv) -end - -function lower_varname(var, iv, order; is_discrete = false) - if is_discrete - lower_shift_varname(var, iv) - else - lower_varname_with_unit(var, iv, order) - end + newvar = setmetadata(newvar, Symbolics.VariableSource, (:variables, newname)) + newvar = setmetadata(newvar, ModelingToolkit.VariableUnshifted, O) + return newvar end function isdoubleshift(var) diff --git a/src/systems/discrete_system/discrete_system.jl b/src/systems/discrete_system/discrete_system.jl index cd6ff45b6c..9a9ac47853 100644 --- a/src/systems/discrete_system/discrete_system.jl +++ b/src/systems/discrete_system/discrete_system.jl @@ -275,10 +275,10 @@ function shift_u0map_forward(sys::DiscreteSystem, u0map, defs) end for var in unknowns(sys) op = operation(var) - op isa Shift || continue haskey(updated, var) && continue - root = first(arguments(var)) - haskey(defs, root) || error("Initial condition for $var not provided.") + root = getunshifted(var) + isnothing(root) && continue + haskey(defs, root) || error("Initial condition for $root not provided.") updated[var] = defs[root] end return updated diff --git a/src/utils.jl b/src/utils.jl index cf49d9f445..c3a0f637a1 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -1028,6 +1028,8 @@ end diff2term_with_unit(x, t) = _with_unit(diff2term, x, t) lower_varname_with_unit(var, iv, order) = _with_unit(lower_varname, var, iv, iv, order) +shift2term_with_unit(x, t) = _with_unit(shift2term, x, t) +lower_shift_varname_with_unit(var, iv) = _with_unit(lower_shift_varname, var, iv, iv) """ $(TYPEDSIGNATURES) diff --git a/src/variables.jl b/src/variables.jl index 536119f107..a29a119607 100644 --- a/src/variables.jl +++ b/src/variables.jl @@ -6,6 +6,7 @@ struct VariableOutput end struct VariableIrreducible end struct VariableStatePriority end struct VariableMisc end +struct VariableUnshifted end Symbolics.option_to_metadata_type(::Val{:unit}) = VariableUnit Symbolics.option_to_metadata_type(::Val{:connect}) = VariableConnectType Symbolics.option_to_metadata_type(::Val{:input}) = VariableInput @@ -13,6 +14,7 @@ Symbolics.option_to_metadata_type(::Val{:output}) = VariableOutput Symbolics.option_to_metadata_type(::Val{:irreducible}) = VariableIrreducible Symbolics.option_to_metadata_type(::Val{:state_priority}) = VariableStatePriority Symbolics.option_to_metadata_type(::Val{:misc}) = VariableMisc +Symbolics.option_to_metadata_type(::Val{:unshifted}) = VariableUnshifted """ dump_variable_metadata(var) @@ -133,7 +135,7 @@ function default_toterm(x) if iscall(x) && (op = operation(x)) isa Operator if !(op isa Differential) if op isa Shift && op.steps < 0 - return x + return shift2term(x) end x = normalize_to_differential(op)(arguments(x)...) end @@ -600,3 +602,6 @@ getunit(x::Symbolic) = Symbolics.getmetadata(x, VariableUnit, nothing) Check if the variable `x` has a unit. """ hasunit(x) = getunit(x) !== nothing + +getunshifted(x) = getunshifted(unwrap(x)) +getunshifted(x::Symbolic) = Symbolics.getmetadata(x, VariableUnshifted, nothing) diff --git a/test/runtests.jl b/test/runtests.jl index 9537b1b44e..e600305232 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -22,6 +22,8 @@ function activate_downstream_env() Pkg.instantiate() end +@testset begin include("discrete_system.jl") end +#= @time begin if GROUP == "All" || GROUP == "InterfaceI" @testset "InterfaceI" begin @@ -136,3 +138,4 @@ end @safetestset "InfiniteOpt Extension Test" include("extensions/test_infiniteopt.jl") end end +=# From 09f31b23ca7338e3a646b26a3bc582bd58633a2a Mon Sep 17 00:00:00 2001 From: vyudu Date: Thu, 13 Feb 2025 15:49:35 -0500 Subject: [PATCH 0938/1337] revert runtest --- test/runtests.jl | 3 --- 1 file changed, 3 deletions(-) diff --git a/test/runtests.jl b/test/runtests.jl index e600305232..9537b1b44e 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -22,8 +22,6 @@ function activate_downstream_env() Pkg.instantiate() end -@testset begin include("discrete_system.jl") end -#= @time begin if GROUP == "All" || GROUP == "InterfaceI" @testset "InterfaceI" begin @@ -138,4 +136,3 @@ end @safetestset "InfiniteOpt Extension Test" include("extensions/test_infiniteopt.jl") end end -=# From f4a9d1369555a35e3c59542ce259fbcf3e6ba5f2 Mon Sep 17 00:00:00 2001 From: vyudu Date: Thu, 13 Feb 2025 16:15:48 -0500 Subject: [PATCH 0939/1337] up --- .../implicit_discrete_system.jl | 23 +++---------------- 1 file changed, 3 insertions(+), 20 deletions(-) diff --git a/src/systems/discrete_system/implicit_discrete_system.jl b/src/systems/discrete_system/implicit_discrete_system.jl index 3a137eb8c1..82ef66d17e 100644 --- a/src/systems/discrete_system/implicit_discrete_system.jl +++ b/src/systems/discrete_system/implicit_discrete_system.jl @@ -238,8 +238,6 @@ function ImplicitDiscreteSystem(eqs, iv; kwargs...) return ImplicitDiscreteSystem(eqs, iv, collect(allunknowns), collect(new_ps); kwargs...) end -# basically at every timestep it should build a nonlinear solve -# Previous timesteps should be treated as parameters? is this right? function flatten(sys::ImplicitDiscreteSystem, noeqs = false) systems = get_systems(sys) @@ -263,25 +261,10 @@ end function generate_function( sys::ImplicitDiscreteSystem, dvs = unknowns(sys), ps = parameters(sys); wrap_code = identity, kwargs...) - if !iscomplete(sys) - error("A completed system is required. Call `complete` or `structural_simplify` on the system.") - end - p = (reorder_parameters(sys, unwrap.(ps))..., cachesyms...) - isscalar = !(exprs isa AbstractArray) - pre, sol_states = get_substitutions_and_solved_unknowns(sys, isscalar ? [exprs] : exprs) - if postprocess_fbody === nothing - postprocess_fbody = pre - end - if states === nothing - states = sol_states - end exprs = [eq.lhs - eq.rhs for eq in equations(sys)] - u = map(Shift(iv, -1), dvs) - u_next = dvs - - wrap_code = wrap_code .∘ wrap_array_vars(sys, exprs) .∘ wrap_parameter_dependencies(sys, false) - - build_function(exprs, u_next, u, p..., get_iv(sys)) + u = dvs + u_next = map(Shift(iv, 1), u) + generate_custom_function(sys, exprs, u_next, u, ps..., get_iv(sys); kwargs...) end function shift_u0map_forward(sys::ImplicitDiscreteSystem, u0map, defs) From a25aab4ca128caef01a18560aaf32e1c5fa8e04d Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 14 Feb 2025 14:56:36 +0530 Subject: [PATCH 0940/1337] test: increase tolerances of FMI CS test --- test/fmi/fmi.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/fmi/fmi.jl b/test/fmi/fmi.jl index a311bf44d2..f3ab3549cf 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-3, + Val(2); fmu, type = :CS, communication_step_size = 1e-4, reinitializealg = BrownFullBasicInit()) @test MTK.isinput(adder.a) @test MTK.isinput(adder.b) @@ -169,9 +169,9 @@ end sol = solve(prob, Rodas5P(autodiff = false), abstol = 1e-8, reltol = 1e-8) @test SciMLBase.successful_retcode(sol) - @test truesol(sol.t; idxs = [truesys.a, truesys.b, truesys.c]).u≈sol.u rtol=1e-2 + @test truesol(sol.t; idxs = [truesys.a, truesys.b, truesys.c]).u≈sol.u rtol=4e-2 # sys.adder.c is a discrete variable - @test truesol(sol.t; idxs = truesys.adder.c).u≈sol(sol.t; idxs = sys.adder.c).u rtol=1e-3 + @test truesol(sol.t; idxs = truesys.adder.c).u≈sol(sol.t; idxs = sys.adder.c).u rtol=4e-2 end function build_sspace_model(sspace) From 7767fb509c40f7838f3f6988dc9e279d499da625 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 14 Feb 2025 15:05:18 +0530 Subject: [PATCH 0941/1337] fix: handle unsized array symbolics in `get_possibly_array_fallback_singletons` --- src/systems/nonlinear/initializesystem.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/src/systems/nonlinear/initializesystem.jl b/src/systems/nonlinear/initializesystem.jl index 07f745de18..38cc65daf6 100644 --- a/src/systems/nonlinear/initializesystem.jl +++ b/src/systems/nonlinear/initializesystem.jl @@ -356,6 +356,7 @@ function get_possibly_array_fallback_singletons(varmap, p) return varmap[p] end if symbolic_type(p) == ArraySymbolic() + is_sized_array_symbolic(p) || return nothing scal = collect(p) if all(x -> haskey(varmap, x), scal) res = [varmap[x] for x in scal] From 0b7273786b89f810a5e89eda32b71bd39a9d9b72 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 14 Feb 2025 15:13:27 +0530 Subject: [PATCH 0942/1337] fix: search for required parameter dependencies in observed equations --- src/systems/codegen_utils.jl | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/systems/codegen_utils.jl b/src/systems/codegen_utils.jl index 1c9e6ce53c..5a645c2047 100644 --- a/src/systems/codegen_utils.jl +++ b/src/systems/codegen_utils.jl @@ -160,6 +160,9 @@ function build_function_wrapper(sys::AbstractSystem, expr, args...; p_start = 2, end # similarly for parameter dependency equations pdepidxs = observed_equations_used_by(sys, expr; obs = pdeps) + for i in obsidxs + union!(pdepidxs, observed_equations_used_by(sys, obs[i].rhs; obs = pdeps)) + end # assignments for reconstructing scalarized array symbolics assignments = array_variable_assignments(args...) From daa789856714d3926c265307a94bc6f3f7eecbbd Mon Sep 17 00:00:00 2001 From: vyudu Date: Fri, 14 Feb 2025 06:25:44 -0500 Subject: [PATCH 0943/1337] frefactor use toterm instead of lower_varname --- src/structural_transformation/symbolics_tearing.jl | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/structural_transformation/symbolics_tearing.jl b/src/structural_transformation/symbolics_tearing.jl index ffbd6e3452..47c7da371f 100644 --- a/src/structural_transformation/symbolics_tearing.jl +++ b/src/structural_transformation/symbolics_tearing.jl @@ -366,6 +366,7 @@ function generate_derivative_variables!(ts::TearingState, neweqs, var_eq_matchin eq_var_matching = invview(var_eq_matching) diff_to_var = invview(var_to_diff) is_discrete = is_only_discrete(structure) + toterm = is_discrete ? shift2term_with_unit : diff2term_with_unit linear_eqs = mm === nothing ? Dict{Int, Int}() : Dict(reverse(en) for en in enumerate(mm.nzrows)) @@ -373,10 +374,6 @@ function generate_derivative_variables!(ts::TearingState, neweqs, var_eq_matchin # derivative is in the system for v in 1:length(var_to_diff) dv = var_to_diff[v] - # For discrete systems, directly substitute lowest-order shift - #if is_discrete && diff_to_var[v] == nothing - # operation(fullvars[v]) isa Shift && (fullvars[v] = lower_shift_varname_with_unit(fullvars[v], iv)) - #end dv isa Int || continue solved = var_eq_matching[dv] isa Int solved && continue @@ -394,8 +391,7 @@ function generate_derivative_variables!(ts::TearingState, neweqs, var_eq_matchin dx = fullvars[dv] order, lv = var_order(dv, diff_to_var) - x_t = is_discrete ? lower_shift_varname_with_unit(fullvars[dv], iv) : - Symbolics.diff2term(fullvars[dv]) + x_t = toterm(fullvars[dv]) # Add `x_t` to the graph v_t = add_dd_variable!(structure, fullvars, x_t, dv) @@ -470,7 +466,7 @@ function generate_system_equations!(state::TearingState, neweqs, var_eq_matching for (i, v) in enumerate(fullvars) op = operation(v) op isa Shift && (op.steps < 0) && begin - lowered = lower_shift_varname_with_unit(v, iv) + lowered = shift2term_with_unit(v) total_sub[v] = lowered fullvars[i] = lowered end From ae19eeb84b3b2d3cf6ca6db1a658581b8f9ba714 Mon Sep 17 00:00:00 2001 From: vyudu Date: Fri, 14 Feb 2025 06:50:46 -0500 Subject: [PATCH 0944/1337] fix unit --- src/structural_transformation/StructuralTransformations.jl | 2 +- src/structural_transformation/symbolics_tearing.jl | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/structural_transformation/StructuralTransformations.jl b/src/structural_transformation/StructuralTransformations.jl index 510352ad59..7d2b9afa26 100644 --- a/src/structural_transformation/StructuralTransformations.jl +++ b/src/structural_transformation/StructuralTransformations.jl @@ -11,7 +11,7 @@ using SymbolicUtils: maketerm, iscall using ModelingToolkit using ModelingToolkit: ODESystem, AbstractSystem, var_from_nested_derivative, Differential, - unknowns, equations, vars, Symbolic, diff2term_with_unit, value, + unknowns, equations, vars, Symbolic, diff2term_with_unit, shift2term_with_unit, value, operation, arguments, Sym, Term, simplify, symbolic_linear_solve, isdiffeq, isdifferential, isirreducible, empty_substitutions, get_substitutions, diff --git a/src/structural_transformation/symbolics_tearing.jl b/src/structural_transformation/symbolics_tearing.jl index 47c7da371f..779f5931f5 100644 --- a/src/structural_transformation/symbolics_tearing.jl +++ b/src/structural_transformation/symbolics_tearing.jl @@ -366,7 +366,6 @@ function generate_derivative_variables!(ts::TearingState, neweqs, var_eq_matchin eq_var_matching = invview(var_eq_matching) diff_to_var = invview(var_to_diff) is_discrete = is_only_discrete(structure) - toterm = is_discrete ? shift2term_with_unit : diff2term_with_unit linear_eqs = mm === nothing ? Dict{Int, Int}() : Dict(reverse(en) for en in enumerate(mm.nzrows)) @@ -391,7 +390,8 @@ function generate_derivative_variables!(ts::TearingState, neweqs, var_eq_matchin dx = fullvars[dv] order, lv = var_order(dv, diff_to_var) - x_t = toterm(fullvars[dv]) + x_t = is_discrete ? lower_shift_varname_with_unit(fullvars[dv], iv) + : lower_varname_with_unit(fullvars[lv], iv, order) # Add `x_t` to the graph v_t = add_dd_variable!(structure, fullvars, x_t, dv) @@ -466,7 +466,7 @@ function generate_system_equations!(state::TearingState, neweqs, var_eq_matching for (i, v) in enumerate(fullvars) op = operation(v) op isa Shift && (op.steps < 0) && begin - lowered = shift2term_with_unit(v) + lowered = lower_shift_varname_with_unit(v, iv) total_sub[v] = lowered fullvars[i] = lowered end From 406d0a861d4904d10cf99ad11dbc118f38a0c718 Mon Sep 17 00:00:00 2001 From: vyudu Date: Fri, 14 Feb 2025 06:55:14 -0500 Subject: [PATCH 0945/1337] fix parse error --- src/structural_transformation/symbolics_tearing.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/structural_transformation/symbolics_tearing.jl b/src/structural_transformation/symbolics_tearing.jl index 779f5931f5..a166bb064b 100644 --- a/src/structural_transformation/symbolics_tearing.jl +++ b/src/structural_transformation/symbolics_tearing.jl @@ -390,8 +390,8 @@ function generate_derivative_variables!(ts::TearingState, neweqs, var_eq_matchin dx = fullvars[dv] order, lv = var_order(dv, diff_to_var) - x_t = is_discrete ? lower_shift_varname_with_unit(fullvars[dv], iv) - : lower_varname_with_unit(fullvars[lv], iv, order) + x_t = is_discrete ? lower_shift_varname_with_unit(fullvars[dv], iv) : + lower_varname_with_unit(fullvars[lv], iv, order) # Add `x_t` to the graph v_t = add_dd_variable!(structure, fullvars, x_t, dv) From 5f231c751a5def2e2a7d9911ae961793c8e2e0e2 Mon Sep 17 00:00:00 2001 From: vyudu Date: Fri, 14 Feb 2025 07:54:04 -0500 Subject: [PATCH 0946/1337] fix: fix initialization cases --- src/ModelingToolkit.jl | 3 ++ .../StructuralTransformations.jl | 2 +- src/structural_transformation/utils.jl | 5 ++-- .../discrete_system/discrete_system.jl | 10 +++++-- .../implicit_discrete_system.jl | 25 +++++++++------- src/systems/systems.jl | 4 +-- src/systems/systemstructure.jl | 2 +- test/implicit_discrete_system.jl | 30 +++++++++++++++++++ test/runtests.jl | 1 + 9 files changed, 62 insertions(+), 20 deletions(-) create mode 100644 test/implicit_discrete_system.jl diff --git a/src/ModelingToolkit.jl b/src/ModelingToolkit.jl index 09b59c4ed6..ce3db8bde7 100644 --- a/src/ModelingToolkit.jl +++ b/src/ModelingToolkit.jl @@ -123,6 +123,7 @@ abstract type AbstractTimeIndependentSystem <: AbstractSystem end abstract type AbstractODESystem <: AbstractTimeDependentSystem end abstract type AbstractMultivariateSystem <: AbstractSystem end abstract type AbstractOptimizationSystem <: AbstractTimeIndependentSystem end +abstract type AbstractDiscreteSystem <: AbstractTimeDependentSystem end function independent_variable end @@ -165,6 +166,7 @@ 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") @@ -230,6 +232,7 @@ export DAEFunctionExpr, DAEProblemExpr export SDESystem, SDEFunction, SDEFunctionExpr, SDEProblemExpr export SystemStructure export DiscreteSystem, DiscreteProblem, DiscreteFunction, DiscreteFunctionExpr +export ImplicitDiscreteSystem, ImplicitDiscreteProblem, ImplicitDiscreteFunction, ImplicitDiscreteFunctionExpr export JumpSystem export ODEProblem, SDEProblem export NonlinearFunction, NonlinearFunctionExpr diff --git a/src/structural_transformation/StructuralTransformations.jl b/src/structural_transformation/StructuralTransformations.jl index 510352ad59..c7b3957c62 100644 --- a/src/structural_transformation/StructuralTransformations.jl +++ b/src/structural_transformation/StructuralTransformations.jl @@ -63,7 +63,7 @@ export torn_system_jacobian_sparsity export full_equations export but_ordered_incidence, lowest_order_variable_mask, highest_order_variable_mask export computed_highest_diff_variables -export shift2term, lower_shift_varname +export shift2term, lower_shift_varname, simplify_shifts include("utils.jl") include("pantelides.jl") diff --git a/src/structural_transformation/utils.jl b/src/structural_transformation/utils.jl index f64a8da132..db79c8a124 100644 --- a/src/structural_transformation/utils.jl +++ b/src/structural_transformation/utils.jl @@ -452,11 +452,10 @@ end # For discrete variables. Turn Shift(t, k)(x(t)) into xₜ₋ₖ(t) function lower_shift_varname(var, iv) op = operation(var) - op isa Shift || return Shift(iv, 0)(var, true) # hack to prevent simplification of x(t) - x(t) - if op.steps < 0 + if op isa Shift && op.steps < 0 return shift2term(var) else - return var + return Shift(iv, 0)(var, true) end end diff --git a/src/systems/discrete_system/discrete_system.jl b/src/systems/discrete_system/discrete_system.jl index 9a9ac47853..703efcc462 100644 --- a/src/systems/discrete_system/discrete_system.jl +++ b/src/systems/discrete_system/discrete_system.jl @@ -17,7 +17,7 @@ eqs = [x(k+1) ~ σ*(y-x), @named de = DiscreteSystem(eqs) ``` """ -struct DiscreteSystem <: AbstractTimeDependentSystem +struct DiscreteSystem <: AbstractDiscreteSystem """ A tag for the system. If two systems have the same tag, then they are structurally identical. @@ -237,6 +237,8 @@ function DiscreteSystem(eqs, iv; kwargs...) collect(allunknowns), collect(new_ps); kwargs...) end +DiscreteSystem(eq::Equation, args...; kwargs...) = DiscreteSystem([eq], args...; kwargs...) + function flatten(sys::DiscreteSystem, noeqs = false) systems = get_systems(sys) if isempty(systems) @@ -271,14 +273,16 @@ function shift_u0map_forward(sys::DiscreteSystem, u0map, defs) if !((op = operation(k)) isa Shift) 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)).") end - updated[Shift(iv, op.steps + 1)(arguments(k)[1])] = v + k_next = Shift(iv, op.steps + 1)(arguments(k)[1]) + operation(k_next) isa Shift ? updated[shift2term(k_next)] = v : + updated[k_next] = v end for var in unknowns(sys) op = operation(var) haskey(updated, var) && continue root = getunshifted(var) isnothing(root) && continue - haskey(defs, root) || error("Initial condition for $root not provided.") + haskey(defs, root) || error("Initial condition for $var not provided.") updated[var] = defs[root] end return updated diff --git a/src/systems/discrete_system/implicit_discrete_system.jl b/src/systems/discrete_system/implicit_discrete_system.jl index 82ef66d17e..e0450a1ada 100644 --- a/src/systems/discrete_system/implicit_discrete_system.jl +++ b/src/systems/discrete_system/implicit_discrete_system.jl @@ -239,6 +239,8 @@ function ImplicitDiscreteSystem(eqs, iv; kwargs...) collect(allunknowns), collect(new_ps); kwargs...) end +ImplicitDiscreteSystem(eq::Equation, args...; kwargs...) = ImplicitDiscreteSystem([eq], args...; kwargs...) + function flatten(sys::ImplicitDiscreteSystem, noeqs = false) systems = get_systems(sys) if isempty(systems) @@ -261,10 +263,14 @@ end function generate_function( sys::ImplicitDiscreteSystem, dvs = unknowns(sys), ps = parameters(sys); wrap_code = identity, kwargs...) - exprs = [eq.lhs - eq.rhs for eq in equations(sys)] - u = dvs - u_next = map(Shift(iv, 1), u) - generate_custom_function(sys, exprs, u_next, u, ps..., get_iv(sys); kwargs...) + iv = get_iv(sys) + exprs = map(equations(sys)) do eq + _iszero(eq.lhs) ? eq.rhs : (simplify_shifts(Shift(iv, -1)(eq.rhs)) - simplify_shifts(Shift(iv, -1)(eq.lhs))) + end + + u_next = dvs + u = map(Shift(iv, -1), u_next) + build_function_wrapper(sys, exprs, u_next, u, ps..., iv; p_start = 3, kwargs...) end function shift_u0map_forward(sys::ImplicitDiscreteSystem, u0map, defs) @@ -275,13 +281,13 @@ function shift_u0map_forward(sys::ImplicitDiscreteSystem, u0map, defs) if !((op = operation(k)) isa Shift) 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)).") end - updated[Shift(iv, op.steps + 1)(arguments(k)[1])] = v + updated[shift2term(k)] = v end for var in unknowns(sys) op = operation(var) - op isa Shift || continue haskey(updated, var) && continue - root = first(arguments(var)) + root = getunshifted(var) + isnothing(root) && continue haskey(defs, root) || error("Initial condition for $var not provided.") updated[var] = defs[root] end @@ -301,7 +307,7 @@ function SciMLBase.ImplicitDiscreteProblem( kwargs... ) if !iscomplete(sys) - error("A completed `ImplicitDiscreteSystem` is required. Call `complete` or `structural_simplify` on the system before creating a `ImplicitDiscreteProblem`") + error("A completed `ImplicitDiscreteSystem` is required. Call `complete` or `structural_simplify` on the system before creating a `ImplicitDiscreteProblem`.") end dvs = unknowns(sys) ps = parameters(sys) @@ -312,8 +318,7 @@ function SciMLBase.ImplicitDiscreteProblem( u0map = shift_u0map_forward(sys, u0map, defaults(sys)) f, u0, p = process_SciMLProblem( ImplicitDiscreteFunction, sys, u0map, parammap; eval_expression, eval_module) - u0 = f(u0, p, tspan[1]) - NonlinearProblem(f, u0, tspan, p; kwargs...) + ImplicitDiscreteProblem(f, u0, tspan, p; kwargs...) end function SciMLBase.ImplicitDiscreteFunction(sys::ImplicitDiscreteSystem, args...; kwargs...) diff --git a/src/systems/systems.jl b/src/systems/systems.jl index f8630f2d20..40f3353884 100644 --- a/src/systems/systems.jl +++ b/src/systems/systems.jl @@ -42,8 +42,8 @@ function structural_simplify( if newsys isa DiscreteSystem && any(eq -> symbolic_type(eq.lhs) == NotSymbolic(), equations(newsys)) error(""" - Encountered algebraic equations when simplifying discrete system. This is \ - not yet supported. + Encountered algebraic equations when simplifying discrete system. Please construct \ + an ImplicitDiscreteSystem instead. """) end for pass in additional_passes diff --git a/src/systems/systemstructure.jl b/src/systems/systemstructure.jl index 6fee78cfd6..a6e6b1d218 100644 --- a/src/systems/systemstructure.jl +++ b/src/systems/systemstructure.jl @@ -438,7 +438,7 @@ function TearingState(sys; quick_cancel = false, check = true) ts = TearingState(sys, fullvars, SystemStructure(complete(var_to_diff), complete(eq_to_diff), - complete(graph), nothing, var_types, sys isa DiscreteSystem), + complete(graph), nothing, var_types, sys isa AbstractDiscreteSystem), Any[]) if sys isa DiscreteSystem ts = shift_discrete_system(ts) diff --git a/test/implicit_discrete_system.jl b/test/implicit_discrete_system.jl new file mode 100644 index 0000000000..88e79acc77 --- /dev/null +++ b/test/implicit_discrete_system.jl @@ -0,0 +1,30 @@ +using ModelingToolkit, Test +using ModelingToolkit: t_nounits as t + +k = ShiftIndex(t) +@variables x(t) = 1 +@mtkbuild sys = ImplicitDiscreteSystem([x(k) ~ x(k)*x(k-1) - 3], t) +tspan = (0, 10) + +# Shift(t, -1)(x(t)) - x_{t-1}(t) +# -3 - x(t) + x(t)*x_{t-1} +f = ImplicitDiscreteFunction(sys) +u_next = [3., 1.5] +@test f(u_next, [2.,3.], [], t) ≈ [0., 0.] +u_next = [0., 0.] +@test f(u_next, [2.,3.], [], t) ≈ [3., -3.] + +resid = rand(2) +f(resid, u_next, [2.,3.], [], t) +@test resid ≈ [3., -3.] + +# Initialization cases. +prob = ImplicitDiscreteProblem(sys, [x(k-1) => 3.], tspan) +@test prob.u0 == [3., 1.] +prob = ImplicitDiscreteProblem(sys, [], tspan) +@test prob.u0 == [1., 1.] +@variables x(t) +@mtkbuild sys = ImplicitDiscreteSystem([x(k) ~ x(k)*x(k-1) - 3], t) +@test_throws ErrorException prob = ImplicitDiscreteProblem(sys, [], tspan) + +# Test solvers diff --git a/test/runtests.jl b/test/runtests.jl index 9537b1b44e..e774bbe4cc 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -80,6 +80,7 @@ end @safetestset "Variable Metadata Test" include("test_variable_metadata.jl") @safetestset "OptimizationSystem Test" include("optimizationsystem.jl") @safetestset "Discrete System" include("discrete_system.jl") + @safetestset "Implicit Discrete System" include("implicit_discrete_system.jl") @safetestset "SteadyStateSystem Test" include("steadystatesystems.jl") @safetestset "SDESystem Test" include("sdesystem.jl") @safetestset "DDESystem Test" include("dde.jl") From a826a6241725db58ca87beb3f252f15d7c658273 Mon Sep 17 00:00:00 2001 From: vyudu Date: Fri, 14 Feb 2025 08:00:39 -0500 Subject: [PATCH 0947/1337] fix unit --- src/structural_transformation/symbolics_tearing.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/structural_transformation/symbolics_tearing.jl b/src/structural_transformation/symbolics_tearing.jl index ffbd6e3452..a3a90abdfb 100644 --- a/src/structural_transformation/symbolics_tearing.jl +++ b/src/structural_transformation/symbolics_tearing.jl @@ -395,7 +395,7 @@ function generate_derivative_variables!(ts::TearingState, neweqs, var_eq_matchin dx = fullvars[dv] order, lv = var_order(dv, diff_to_var) x_t = is_discrete ? lower_shift_varname_with_unit(fullvars[dv], iv) : - Symbolics.diff2term(fullvars[dv]) + lower_varname_with_unit(fullvars[lv], iv, order) # Add `x_t` to the graph v_t = add_dd_variable!(structure, fullvars, x_t, dv) From b58993313f0b1178b715ba1e9195a871cfba3a60 Mon Sep 17 00:00:00 2001 From: vyudu Date: Fri, 14 Feb 2025 08:02:01 -0500 Subject: [PATCH 0948/1337] add doc file --- docs/src/systems/ImplicitDiscreteSystem.md | 28 ++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 docs/src/systems/ImplicitDiscreteSystem.md diff --git a/docs/src/systems/ImplicitDiscreteSystem.md b/docs/src/systems/ImplicitDiscreteSystem.md new file mode 100644 index 0000000000..d02ccc5e42 --- /dev/null +++ b/docs/src/systems/ImplicitDiscreteSystem.md @@ -0,0 +1,28 @@ +# ImplicitDiscreteSystem + +## System Constructors + +```@docs +ImplicitDiscreteSystem +``` + +## Composition and Accessor Functions + + - `get_eqs(sys)` or `equations(sys)`: The equations that define the implicit discrete system. + - `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 + +```@docs; canonical=false +structural_simplify +``` + +## Problem Constructors + +```@docs; canonical=false +ImplicitDiscreteProblem(sys::ImplicitDiscreteSystem, u0map, tspan) +ImplicitDiscreteFunction(sys::ImplicitDiscreteSystem, args...) +``` From f5770844623e89d82d40e64b6489d92188d4b290 Mon Sep 17 00:00:00 2001 From: vyudu Date: Fri, 14 Feb 2025 08:11:58 -0500 Subject: [PATCH 0949/1337] add to docs/pages.jl --- docs/pages.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/pages.jl b/docs/pages.jl index 6e18f403dd..0b2c867507 100644 --- a/docs/pages.jl +++ b/docs/pages.jl @@ -42,7 +42,8 @@ pages = [ "systems/NonlinearSystem.md", "systems/OptimizationSystem.md", "systems/PDESystem.md", - "systems/DiscreteSystem.md"], + "systems/DiscreteSystem.md", + "systems/ImplicitDiscreteSystem.md"], "comparison.md", "internals.md" ] From f88b9af23e1dd3460a1afb6945ecb1765a537e53 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 14 Feb 2025 22:56:31 +0530 Subject: [PATCH 0950/1337] build: bump minor version --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index ec7939a58c..ce3ece8e9a 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "ModelingToolkit" uuid = "961ee093-0014-501f-94e3-6117800e7a78" authors = ["Yingbo Ma ", "Chris Rackauckas and contributors"] -version = "9.62.0" +version = "9.63.0" [deps] AbstractTrees = "1520ce14-60c1-5f80-bbc7-55ef81b5835c" From 05c49ca836af1612bac7317634d3e234717e3a62 Mon Sep 17 00:00:00 2001 From: CompatHelper Julia Date: Sat, 15 Feb 2025 00:23:47 +0000 Subject: [PATCH 0951/1337] CompatHelper: add new compat entry for PreallocationTools at version 0.4 for package docs, (keep existing compat) --- docs/Project.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/Project.toml b/docs/Project.toml index 0b1393cf9e..1f43934352 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -39,6 +39,7 @@ Optimization = "3.9, 4" OptimizationOptimJL = "0.1, 0.4" OrdinaryDiffEq = "6.31" Plots = "1.36" +PreallocationTools = "0.4" SciMLStructures = "1.1" Setfield = "1" StochasticDiffEq = "6" From c9822671b594b937dc67a7847f40451073eb0582 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Sun, 16 Feb 2025 18:19:14 +0530 Subject: [PATCH 0952/1337] feat: add `GeneratedFunctionWrapper` --- src/systems/codegen_utils.jl | 56 ++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/src/systems/codegen_utils.jl b/src/systems/codegen_utils.jl index 5a645c2047..910d0d1c1e 100644 --- a/src/systems/codegen_utils.jl +++ b/src/systems/codegen_utils.jl @@ -230,3 +230,59 @@ function build_function_wrapper(sys::AbstractSystem, expr, args...; p_start = 2, end return build_function(expr, args...; wrap_code, similarto, kwargs...) end + +""" + $(TYPEDEF) + +A wrapper around a generated in-place and out-of-place function. The type-parameter `P` +must be a 3-tuple where the first element is the index of the parameter object in the +arguments, the second is the expected number of arguments in the out-of-place variant +of the function, and the third is a boolean indicating whether the generated functions +are for a split system. For scalar functions, the inplace variant can be `nothing`. +""" +struct GeneratedFunctionWrapper{P, O, I} <: Function + f_oop::O + f_iip::I +end + +function GeneratedFunctionWrapper{P}(foop::O, fiip::I) where {P, O, I} + GeneratedFunctionWrapper{P, O, I}(foop, fiip) +end + +function (gfw::GeneratedFunctionWrapper)(args...) + _generated_call(gfw, args...) +end + +@generated function _generated_call(gfw::GeneratedFunctionWrapper{P}, args...) where {P} + paramidx, nargs, issplit = P + iip = false + # IIP case has one more argument + if length(args) == nargs + 1 + nargs += 1 + paramidx += 1 + iip = true + end + if length(args) != nargs + throw(ArgumentError("Expected $nargs arguments, got $(length(args)).")) + end + + # the function to use + f = iip ? :(gfw.f_iip) : :(gfw.f_oop) + # non-split systems just call it as-is + if !issplit + return :($f(args...)) + end + if args[paramidx] <: Union{Tuple, MTKParameters} && + !(args[paramidx] <: Tuple{Vararg{Number}}) + # for split systems, call it as-is if the parameter object is a tuple or MTKParameters + # but not if it is a tuple of numbers + return :($f(args...)) + else + # The user provided a single buffer/tuple for the parameter object, so wrap that + # one in a tuple + fargs = ntuple(Val(length(args))) do i + i == paramidx ? :((args[$i],)) : :(args[$i]) + end + return :($f($(fargs...))) + end +end From 4087cbfa9d2adecc932691e4d56516a8ce84edaf Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Sun, 16 Feb 2025 18:19:38 +0530 Subject: [PATCH 0953/1337] fix: use `GeneratedFunctionWrapper` in codegen --- src/systems/abstractsystem.jl | 35 +------------ src/systems/diffeqs/abstractodesystem.jl | 52 ++++++------------- src/systems/diffeqs/odesystem.jl | 9 +++- src/systems/diffeqs/sdesystem.jl | 19 +++---- .../discrete_system/discrete_system.jl | 3 +- src/systems/jumps/jumpsystem.jl | 6 +-- src/systems/nonlinear/nonlinearsystem.jl | 27 +++++----- 7 files changed, 47 insertions(+), 104 deletions(-) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index 41c487dadc..8baad025d1 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -422,28 +422,7 @@ function SymbolicIndexingInterface.timeseries_parameter_index(sys::AbstractSyste end function SymbolicIndexingInterface.parameter_observed(sys::AbstractSystem, sym) - if has_index_cache(sys) && (ic = get_index_cache(sys)) !== nothing - rawobs = build_explicit_observed_function( - sys, sym; param_only = true, return_inplace = true) - if rawobs isa Tuple - if is_time_dependent(sys) - obsfn = let oop = rawobs[1], iip = rawobs[2] - f1a(p, t) = oop(p, t) - f1a(out, p, t) = iip(out, p, t) - end - else - obsfn = let oop = rawobs[1], iip = rawobs[2] - f1b(p) = oop(p) - f1b(out, p) = iip(out, p) - end - end - else - obsfn = rawobs - end - else - obsfn = build_explicit_observed_function(sys, sym; param_only = true) - end - return obsfn + return build_explicit_observed_function(sys, sym; param_only = true) end function has_observed_with_lhs(sys, sym) @@ -579,18 +558,8 @@ function SymbolicIndexingInterface.observed( end end end - _fn = build_explicit_observed_function( + return build_explicit_observed_function( sys, sym; eval_expression, eval_module, checkbounds) - - if is_time_dependent(sys) - return _fn - else - return let _fn = _fn - fn2(u, p) = _fn(u, p) - fn2(::Nothing, p) = _fn([], p) - fn2 - end - end end function SymbolicIndexingInterface.default_values(sys::AbstractSystem) diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index c7eff90e03..04f7b468d2 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -320,9 +320,7 @@ function DiffEqBase.ODEFunction{iip, specialize}(sys::AbstractODESystem, expression_module = eval_module, checkbounds = checkbounds, kwargs...) f_oop, f_iip = eval_or_rgf.(f_gen; eval_expression, eval_module) - - f(u, p, t) = f_oop(u, p, t) - f(du, u, p, t) = f_iip(du, u, p, t) + f = GeneratedFunctionWrapper{(2, 3, is_split(sys))}(f_oop, f_iip) if specialize === SciMLBase.FunctionWrapperSpecialize && iip if u0 === nothing || p === nothing || t === nothing @@ -338,10 +336,7 @@ function DiffEqBase.ODEFunction{iip, specialize}(sys::AbstractODESystem, expression_module = eval_module, checkbounds = checkbounds, kwargs...) tgrad_oop, tgrad_iip = eval_or_rgf.(tgrad_gen; eval_expression, eval_module) - - ___tgrad(u, p, t) = tgrad_oop(u, p, t) - ___tgrad(J, u, p, t) = tgrad_iip(J, u, p, t) - _tgrad = ___tgrad + _tgrad = GeneratedFunctionWrapper{(2, 3, is_split(sys))}(tgrad_oop, tgrad_iip) else _tgrad = nothing end @@ -354,8 +349,7 @@ function DiffEqBase.ODEFunction{iip, specialize}(sys::AbstractODESystem, checkbounds = checkbounds, kwargs...) jac_oop, jac_iip = eval_or_rgf.(jac_gen; eval_expression, eval_module) - _jac(u, p, t) = jac_oop(u, p, t) - _jac(J, u, p, t) = jac_iip(J, u, p, t) + _jac = GeneratedFunctionWrapper{(2, 3, is_split(sys))}(jac_oop, jac_iip) else _jac = nothing end @@ -435,8 +429,7 @@ function DiffEqBase.DAEFunction{iip}(sys::AbstractODESystem, dvs = unknowns(sys) expression_module = eval_module, checkbounds = checkbounds, kwargs...) f_oop, f_iip = eval_or_rgf.(f_gen; eval_expression, eval_module) - f(du, u, p, t) = f_oop(du, u, p, t) - f(out, du, u, p, t) = f_iip(out, du, u, p, t) + f = GeneratedFunctionWrapper{(3, 4, is_split(sys))}(f_oop, f_iip) if jac jac_gen = generate_dae_jacobian(sys, dvs, ps; @@ -446,8 +439,7 @@ function DiffEqBase.DAEFunction{iip}(sys::AbstractODESystem, dvs = unknowns(sys) checkbounds = checkbounds, kwargs...) jac_oop, jac_iip = eval_or_rgf.(jac_gen; eval_expression, eval_module) - _jac(du, u, p, ˍ₋gamma, t) = jac_oop(du, u, p, ˍ₋gamma, t) - _jac(J, du, u, p, ˍ₋gamma, t) = jac_iip(J, du, u, p, ˍ₋gamma, t) + _jac = GeneratedFunctionWrapper{(3, 5, is_split(sys))}(jac_oop, jac_iip) else _jac = nothing end @@ -496,8 +488,7 @@ function DiffEqBase.DDEFunction{iip}(sys::AbstractODESystem, dvs = unknowns(sys) expression_module = eval_module, checkbounds = checkbounds, kwargs...) f_oop, f_iip = eval_or_rgf.(f_gen; eval_expression, eval_module) - f(u, h, p, t) = f_oop(u, h, p, t) - f(du, u, h, p, t) = f_iip(du, u, h, p, t) + f = GeneratedFunctionWrapper{(3, 4, is_split(sys))}(f_oop, f_iip) DDEFunction{iip}(f; sys = sys, initialization_data) end @@ -521,14 +512,12 @@ function DiffEqBase.SDDEFunction{iip}(sys::AbstractODESystem, dvs = unknowns(sys expression_module = eval_module, checkbounds = checkbounds, kwargs...) f_oop, f_iip = eval_or_rgf.(f_gen; eval_expression, eval_module) - f(u, h, p, t) = f_oop(u, h, p, t) - f(du, u, h, p, t) = f_iip(du, u, h, p, t) + f = GeneratedFunctionWrapper{(3, 4, is_split(sys))}(f_oop, f_iip) g_gen = generate_diffusion_function(sys, dvs, ps; expression = Val{true}, isdde = true, kwargs...) g_oop, g_iip = eval_or_rgf.(g_gen; eval_expression, eval_module) - g(u, h, p, t) = g_oop(u, h, p, t) - g(du, u, h, p, t) = g_iip(du, u, h, p, t) + g = GeneratedFunctionWrapper{(3, 4, is_split(sys))}(g_oop, g_iip) SDDEFunction{iip}(f, g; sys = sys, initialization_data) end @@ -549,13 +538,6 @@ variable and parameter vectors, respectively. """ struct ODEFunctionExpr{iip, specialize} end -struct ODEFunctionClosure{O, I} <: Function - f_oop::O - f_iip::I -end -(f::ODEFunctionClosure)(u, p, t) = f.f_oop(u, p, t) -(f::ODEFunctionClosure)(du, u, p, t) = f.f_iip(du, u, p, t) - function ODEFunctionExpr{iip, specialize}(sys::AbstractODESystem, dvs = unknowns(sys), ps = parameters(sys), u0 = nothing; version = nothing, tgrad = false, @@ -572,13 +554,14 @@ function ODEFunctionExpr{iip, specialize}(sys::AbstractODESystem, dvs = unknowns f_oop, f_iip = generate_function(sys, dvs, ps; expression = Val{true}, kwargs...) fsym = gensym(:f) - _f = :($fsym = $ODEFunctionClosure($f_oop, $f_iip)) + _f = :($fsym = $(GeneratedFunctionWrapper{(2, 3, is_split(sys))})($f_oop, $f_iip)) tgradsym = gensym(:tgrad) if tgrad tgrad_oop, tgrad_iip = generate_tgrad(sys, dvs, ps; simplify = simplify, expression = Val{true}, kwargs...) - _tgrad = :($tgradsym = $ODEFunctionClosure($tgrad_oop, $tgrad_iip)) + _tgrad = :($tgradsym = $(GeneratedFunctionWrapper{(2, 3, is_split(sys))})( + $tgrad_oop, $tgrad_iip)) else _tgrad = :($tgradsym = nothing) end @@ -588,7 +571,8 @@ function ODEFunctionExpr{iip, specialize}(sys::AbstractODESystem, dvs = unknowns jac_oop, jac_iip = generate_jacobian(sys, dvs, ps; sparse = sparse, simplify = simplify, expression = Val{true}, kwargs...) - _jac = :($jacsym = $ODEFunctionClosure($jac_oop, $jac_iip)) + _jac = :($jacsym = $(GeneratedFunctionWrapper{(2, 3, is_split(sys))})( + $jac_oop, $jac_iip)) else _jac = :($jacsym = nothing) end @@ -647,13 +631,6 @@ variable and parameter vectors, respectively. """ struct DAEFunctionExpr{iip} end -struct DAEFunctionClosure{O, I} <: Function - f_oop::O - f_iip::I -end -(f::DAEFunctionClosure)(du, u, p, t) = f.f_oop(du, u, p, t) -(f::DAEFunctionClosure)(out, du, u, p, t) = f.f_iip(out, du, u, p, t) - function DAEFunctionExpr{iip}(sys::AbstractODESystem, dvs = unknowns(sys), ps = parameters(sys), u0 = nothing; version = nothing, tgrad = false, @@ -667,7 +644,7 @@ function DAEFunctionExpr{iip}(sys::AbstractODESystem, dvs = unknowns(sys), f_oop, f_iip = generate_function(sys, dvs, ps; expression = Val{true}, implicit_dae = true, kwargs...) fsym = gensym(:f) - _f = :($fsym = $DAEFunctionClosure($f_oop, $f_iip)) + _f = :($fsym = $(GeneratedFunctionWrapper{(3, 4, is_split(sys))})($f_oop, $f_iip)) ex = quote $_f ODEFunction{$iip}($fsym) @@ -708,6 +685,7 @@ function SymbolicTstops( expression = Val{true}, p_start = 1, p_end = length(rps), add_observed = false, force_SA = true) tstops = eval_or_rgf(tstops; eval_expression, eval_module) + tstops = GeneratedFunctionWrapper{(1, 3, is_split(sys))}(tstops, nothing) return SymbolicTstops(tstops) end diff --git a/src/systems/diffeqs/odesystem.jl b/src/systems/diffeqs/odesystem.jl index 76fcb9b823..2c4dc04622 100644 --- a/src/systems/diffeqs/odesystem.jl +++ b/src/systems/diffeqs/odesystem.jl @@ -521,9 +521,14 @@ function build_explicit_observed_function(sys, ts; output_type, mkarray, try_namespaced = true, expression = Val{true}) if fns isa Tuple oop, iip = eval_or_rgf.(fns; eval_expression, eval_module) - return return_inplace ? (oop, iip) : oop + f = GeneratedFunctionWrapper{( + p_start, length(args) - length(ps) + 1, is_split(sys))}(oop, iip) + return return_inplace ? (f, f) : f else - return eval_or_rgf(fns; eval_expression, eval_module) + f = eval_or_rgf(fns; eval_expression, eval_module) + f = GeneratedFunctionWrapper{( + p_start, length(args) - length(ps) + 1, is_split(sys))}(f, nothing) + return f end end diff --git a/src/systems/diffeqs/sdesystem.jl b/src/systems/diffeqs/sdesystem.jl index 00265cfc67..bc3fd6b73e 100644 --- a/src/systems/diffeqs/sdesystem.jl +++ b/src/systems/diffeqs/sdesystem.jl @@ -604,18 +604,14 @@ function DiffEqBase.SDEFunction{iip, specialize}(sys::SDESystem, dvs = unknowns( kwargs...) g_oop, g_iip = eval_or_rgf.(g_gen; eval_expression, eval_module) - f(u, p, t) = f_oop(u, p, t) - f(du, u, p, t) = f_iip(du, u, p, t) - g(u, p, t) = g_oop(u, p, t) - g(du, u, p, t) = g_iip(du, u, p, t) + f = GeneratedFunctionWrapper{(2, 3, is_split(sys))}(f_oop, f_iip) + g = GeneratedFunctionWrapper{(2, 3, is_split(sys))}(g_oop, g_iip) if tgrad tgrad_gen = generate_tgrad(sys, dvs, ps; expression = Val{true}, kwargs...) tgrad_oop, tgrad_iip = eval_or_rgf.(tgrad_gen; eval_expression, eval_module) - - _tgrad(u, p, t) = tgrad_oop(u, p, t) - _tgrad(J, u, p, t) = tgrad_iip(J, u, p, t) + _tgrad = GeneratedFunctionWrapper{(2, 3, is_split(sys))}(tgrad_oop, tgrad_iip) else _tgrad = nothing end @@ -625,8 +621,7 @@ function DiffEqBase.SDEFunction{iip, specialize}(sys::SDESystem, dvs = unknowns( sparse = sparse, kwargs...) jac_oop, jac_iip = eval_or_rgf.(jac_gen; eval_expression, eval_module) - _jac(u, p, t) = jac_oop(u, p, t) - _jac(J, u, p, t) = jac_iip(J, u, p, t) + _jac = GeneratedFunctionWrapper{(2, 3, is_split(sys))}(jac_oop, jac_iip) else _jac = nothing end @@ -637,10 +632,8 @@ function DiffEqBase.SDEFunction{iip, specialize}(sys::SDESystem, dvs = unknowns( Wfact_oop, Wfact_iip = eval_or_rgf.(tmp_Wfact; eval_expression, eval_module) Wfact_oop_t, Wfact_iip_t = eval_or_rgf.(tmp_Wfact_t; eval_expression, eval_module) - _Wfact(u, p, dtgamma, t) = Wfact_oop(u, p, dtgamma, t) - _Wfact(W, u, p, dtgamma, t) = Wfact_iip(W, u, p, dtgamma, t) - _Wfact_t(u, p, dtgamma, t) = Wfact_oop_t(u, p, dtgamma, t) - _Wfact_t(W, u, p, dtgamma, t) = Wfact_iip_t(W, u, p, dtgamma, t) + _Wfact = GeneratedFunctionWrapper{(2, 4, is_split(sys))}(Wfact_oop, Wfact_iip) + _Wfact_t = GeneratedFunctionWrapper{(2, 4, is_split(sys))}(Wfact_oop_t, Wfact_iip_t) else _Wfact, _Wfact_t = nothing, nothing end diff --git a/src/systems/discrete_system/discrete_system.jl b/src/systems/discrete_system/discrete_system.jl index cb33f5d339..5af8e24c58 100644 --- a/src/systems/discrete_system/discrete_system.jl +++ b/src/systems/discrete_system/discrete_system.jl @@ -354,8 +354,7 @@ function SciMLBase.DiscreteFunction{iip, specialize}( f_gen = generate_function(sys, dvs, ps; expression = Val{true}, expression_module = eval_module, kwargs...) f_oop, f_iip = eval_or_rgf.(f_gen; eval_expression, eval_module) - f(u, p, t) = f_oop(u, p, t) - f(du, u, p, t) = f_iip(du, u, p, t) + f = GeneratedFunctionWrapper{(2, 3, is_split(sys))}(f_oop, f_iip) if specialize === SciMLBase.FunctionWrapperSpecialize && iip if u0 === nothing || p === nothing || t === nothing diff --git a/src/systems/jumps/jumpsystem.jl b/src/systems/jumps/jumpsystem.jl index 164c38e161..81f0e1371d 100644 --- a/src/systems/jumps/jumpsystem.jl +++ b/src/systems/jumps/jumpsystem.jl @@ -283,7 +283,7 @@ function generate_rate_function(js::JumpSystem, rate) rate = substitute(rate, csubs) end p = reorder_parameters(js) - rf = build_function_wrapper(js, rate, unknowns(js), p..., + build_function_wrapper(js, rate, unknowns(js), p..., get_iv(js), expression = Val{true}) end @@ -302,7 +302,7 @@ end function assemble_vrj( js, vrj, unknowntoid; eval_expression = false, eval_module = @__MODULE__) rate = eval_or_rgf(generate_rate_function(js, vrj.rate); eval_expression, eval_module) - + 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); @@ -326,7 +326,7 @@ end function assemble_crj( js, crj, unknowntoid; eval_expression = false, eval_module = @__MODULE__) rate = eval_or_rgf(generate_rate_function(js, crj.rate); eval_expression, eval_module) - + 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); diff --git a/src/systems/nonlinear/nonlinearsystem.jl b/src/systems/nonlinear/nonlinearsystem.jl index 6f5de67fa7..b026543af8 100644 --- a/src/systems/nonlinear/nonlinearsystem.jl +++ b/src/systems/nonlinear/nonlinearsystem.jl @@ -343,16 +343,14 @@ function SciMLBase.NonlinearFunction{iip}(sys::NonlinearSystem, dvs = unknowns(s end f_gen = generate_function(sys, dvs, ps; expression = Val{true}, kwargs...) f_oop, f_iip = eval_or_rgf.(f_gen; eval_expression, eval_module) - f(u, p) = f_oop(u, p) - f(du, u, p) = f_iip(du, u, p) + f = GeneratedFunctionWrapper{(2, 2, is_split(sys))}(f_oop, f_iip) if jac jac_gen = generate_jacobian(sys, dvs, ps; simplify = simplify, sparse = sparse, expression = Val{true}, kwargs...) jac_oop, jac_iip = eval_or_rgf.(jac_gen; eval_expression, eval_module) - _jac(u, p) = jac_oop(u, p) - _jac(J, u, p) = jac_iip(J, u, p) + _jac = GeneratedFunctionWrapper{(2, 2, is_split(sys))}(jac_oop, jac_iip) else _jac = nothing end @@ -397,6 +395,7 @@ function SciMLBase.IntervalNonlinearFunction( f_gen = generate_function( sys, dvs, ps; expression = Val{true}, scalar = true, kwargs...) f = eval_or_rgf(f_gen; eval_expression, eval_module) + f = GeneratedFunctionWrapper{(2, 2, is_split(sys))}(f, nothing) observedfun = ObservedFunctionCache( sys; eval_expression, eval_module, checkbounds = get(kwargs, :checkbounds, false)) @@ -431,13 +430,14 @@ function NonlinearFunctionExpr{iip}(sys::NonlinearSystem, dvs = unknowns(sys), if !iscomplete(sys) error("A completed `NonlinearSystem` is required. Call `complete` or `structural_simplify` on the system before creating a `NonlinearFunctionExpr`") end - idx = iip ? 2 : 1 - f = generate_function(sys, dvs, ps; expression = Val{true}, kwargs...)[idx] + f_oop, f_iip = generate_function(sys, dvs, ps; expression = Val{true}, kwargs...) + f = :($(GeneratedFunctionWrapper{(2, 2, is_split(sys))})($f_oop, $f_iip)) if jac - _jac = generate_jacobian(sys, dvs, ps; + jac_oop, jac_iip = generate_jacobian(sys, dvs, ps; sparse = sparse, simplify = simplify, - expression = Val{true}, kwargs...)[idx] + expression = Val{true}, kwargs...) + _jac = :($(GeneratedFunctionWrapper{(2, 2, is_split(sys))})($jac_oop, $jac_iip)) else _jac = :nothing end @@ -484,8 +484,7 @@ function IntervalNonlinearFunctionExpr( end f = generate_function(sys, dvs, ps; expression = Val{true}, scalar = true, kwargs...) - - IntervalNonlinearFunction{false}(f; sys = sys) + f = :($(GeneratedFunctionWrapper{2, 2, is_split(sys)})($f, nothing)) ex = quote f = $f @@ -584,7 +583,9 @@ function CacheWriter(sys::AbstractSystem, buffer_types::Vector{TypeT}, DestructuredArgs.(rps)...; p_start = 3, p_end = length(rps) + 2, expression = Val{true}, add_observed = false, extra_assignments = [obs_assigns; body]) - return CacheWriter(eval_or_rgf(fn; eval_expression, eval_module)) + fn = eval_or_rgf(fn; eval_expression, eval_module) + fn = GeneratedFunctionWrapper{(3, 3, is_split(sys))}(fn, nothing) + return CacheWriter(fn) end struct SCCNonlinearFunction{iip} end @@ -603,9 +604,7 @@ function SCCNonlinearFunction{iip}( p_end = length(rps) + length(cachesyms) + 1, add_observed = false, extra_assignments = obs_assignments, expression = Val{true}) f_oop, f_iip = eval_or_rgf.(f_gen; eval_expression, eval_module) - - f(u, p) = f_oop(u, p) - f(resid, u, p) = f_iip(resid, u, p) + f = GeneratedFunctionWrapper{(2, 2, is_split(sys))}(f_oop, f_iip) subsys = NonlinearSystem(_eqs, _dvs, ps; observed = _obs, parameter_dependencies = parameter_dependencies(sys), name = nameof(sys)) From 11e394597098559152a622956aa1e06873d1ee08 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Sun, 16 Feb 2025 18:19:46 +0530 Subject: [PATCH 0954/1337] test: update tests to not wrap parameter vector in tuple --- test/jumpsystem.jl | 4 ++-- test/labelledarrays.jl | 2 +- test/odesystem.jl | 4 ++-- test/sdesystem.jl | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/test/jumpsystem.jl b/test/jumpsystem.jl index 4cd5135939..0fd6dc4af0 100644 --- a/test/jumpsystem.jl +++ b/test/jumpsystem.jl @@ -104,8 +104,8 @@ sol = solve(jprob, SSAStepper(); saveat = 1.0) rate2(u, p, t) = 0.01u[2] jump2 = ConstantRateJump(rate2, affect2!) mtjumps = jprob.discrete_jump_aggregation -@test abs(mtjumps.rates[1](u, (p,), tf) - jump1.rate(u, p, tf)) < 10 * eps() -@test abs(mtjumps.rates[2](u, (p,), tf) - jump2.rate(u, p, tf)) < 10 * eps() +@test abs(mtjumps.rates[1](u, p, tf) - jump1.rate(u, p, tf)) < 10 * eps() +@test abs(mtjumps.rates[2](u, p, tf) - jump2.rate(u, p, tf)) < 10 * eps() mtjumps.affects![1](mtintegrator) jump1.affect!(integrator) @test all(integrator.u .== mtintegrator.u) diff --git a/test/labelledarrays.jl b/test/labelledarrays.jl index 75d5de6d56..c9ee7ee50b 100644 --- a/test/labelledarrays.jl +++ b/test/labelledarrays.jl @@ -19,7 +19,7 @@ ff = ODEFunction(de, [x, y, z], [σ, ρ, β], jac = true) a = @SVector [1.0, 2.0, 3.0] b = SLVector(x = 1.0, y = 2.0, z = 3.0) c = [1.0, 2.0, 3.0] -p = (SLVector(σ = 10.0, ρ = 26.0, β = 8 / 3),) +p = SLVector(σ = 10.0, ρ = 26.0, β = 8 / 3) @test ff(a, p, 0.0) isa SVector @test typeof(ff(b, p, 0.0)) <: SLArray @test ff(c, p, 0.0) isa Vector diff --git a/test/odesystem.jl b/test/odesystem.jl index 65ceaabc16..de166ef0a1 100644 --- a/test/odesystem.jl +++ b/test/odesystem.jl @@ -235,7 +235,7 @@ end prob = ODEProblem(ODEFunction{false}(lotka), [1.0, 1.0], (0.0, 1.0), [1.5, 1.0, 3.0, 1.0]) de = complete(modelingtoolkitize(prob)) -ODEFunction(de)(similar(prob.u0), prob.u0, (prob.p,), 0.1) +ODEFunction(de)(similar(prob.u0), prob.u0, prob.p, 0.1) function lotka(du, u, p, t) x = u[1] @@ -247,7 +247,7 @@ end prob = ODEProblem(lotka, [1.0, 1.0], (0.0, 1.0), [1.5, 1.0, 3.0, 1.0]) de = complete(modelingtoolkitize(prob)) -ODEFunction(de)(similar(prob.u0), prob.u0, (prob.p,), 0.1) +ODEFunction(de)(similar(prob.u0), prob.u0, prob.p, 0.1) # automatic unknown detection for DAEs @parameters k₁ k₂ k₃ diff --git a/test/sdesystem.jl b/test/sdesystem.jl index 0a30982bac..b031a2f5ab 100644 --- a/test/sdesystem.jl +++ b/test/sdesystem.jl @@ -24,7 +24,7 @@ noiseeqs = [0.1 * x, @named de = SDESystem(eqs, noiseeqs, tt, [x, y, z], [σ, ρ, β], tspan = (0.0, 10.0)) de = complete(de) f = eval(generate_diffusion_function(de)[1]) -@test f(ones(3), (rand(3),), nothing) == 0.1ones(3) +@test f(ones(3), rand(3), nothing) == 0.1ones(3) f = SDEFunction(de) prob = SDEProblem(de, [1.0, 0.0, 0.0], (0.0, 100.0), [10.0, 26.0, 2.33]) From baefe858e9f34c57c67b0053227ab82e420e98ea Mon Sep 17 00:00:00 2001 From: Christopher Rackauckas Date: Sun, 16 Feb 2025 11:12:28 -0500 Subject: [PATCH 0955/1337] Update Project.toml --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index ce3ece8e9a..3fbf220ef8 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "ModelingToolkit" uuid = "961ee093-0014-501f-94e3-6117800e7a78" authors = ["Yingbo Ma ", "Chris Rackauckas and contributors"] -version = "9.63.0" +version = "9.64.0" [deps] AbstractTrees = "1520ce14-60c1-5f80-bbc7-55ef81b5835c" From 33caa99dfb3196f61eccdb93f5a228cca1b47b58 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 17 Feb 2025 12:53:26 +0530 Subject: [PATCH 0956/1337] fix: handle null state problem in `ReconstructInitializeprob` --- src/systems/nonlinear/initializesystem.jl | 5 +++-- test/initializationsystem.jl | 9 +++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/systems/nonlinear/initializesystem.jl b/src/systems/nonlinear/initializesystem.jl index 38cc65daf6..0301276183 100644 --- a/src/systems/nonlinear/initializesystem.jl +++ b/src/systems/nonlinear/initializesystem.jl @@ -322,7 +322,8 @@ function (rip::ReconstructInitializeprob)(srcvalp, dstvalp) if state_values(dstvalp) === nothing return nothing, newp end - T = eltype(state_values(srcvalp)) + srcu0 = state_values(srcvalp) + T = srcu0 === nothing || isempty(srcu0) ? Union{} : eltype(srcu0) if parameter_values(dstvalp) isa MTKParameters if !isempty(newp.tunable) T = promote_type(eltype(newp.tunable), T) @@ -332,7 +333,7 @@ function (rip::ReconstructInitializeprob)(srcvalp, dstvalp) end if T == eltype(state_values(dstvalp)) u0 = state_values(dstvalp) - else + elseif T != Union{} u0 = T.(state_values(dstvalp)) end buf, repack, alias = SciMLStructures.canonicalize(SciMLStructures.Tunable(), newp) diff --git a/test/initializationsystem.jl b/test/initializationsystem.jl index 0990101069..a2348c6647 100644 --- a/test/initializationsystem.jl +++ b/test/initializationsystem.jl @@ -1361,3 +1361,12 @@ end prob = ODEProblem(sys, [], (0.0, 1.0)) @test prob.f.initialization_data !== nothing end + +@testset "`ReconstructInitializeprob` with `nothing` state" begin + @parameters p + @variables x(t) + @mtkbuild sys = ODESystem(x ~ p * t, t) + prob = @test_nowarn ODEProblem(sys, [], (0.0, 1.0), [p => 1.0]) + @test_nowarn remake(prob, p = [p => 1.0]) + @test_nowarn remake(prob, p = [p => ForwardDiff.Dual(1.0)]) +end From a7ffae5fb42f566fb558622d41e146755c9f10ea Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 17 Feb 2025 13:00:14 +0530 Subject: [PATCH 0957/1337] fix: throw when observed variable not present in system --- src/systems/diffeqs/odesystem.jl | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/systems/diffeqs/odesystem.jl b/src/systems/diffeqs/odesystem.jl index 2c4dc04622..2dcb703d96 100644 --- a/src/systems/diffeqs/odesystem.jl +++ b/src/systems/diffeqs/odesystem.jl @@ -481,6 +481,11 @@ function build_explicit_observed_function(sys, ts; newvar = get(ns_map, var, nothing) if newvar !== nothing namespace_subs[var] = newvar + var = newvar + end + if throw && !(var in allsyms) && + (!iscall(var) || operation(var) !== getindex || !(arguments(var)[1] in allsyms)) + Base.throw(ArgumentError("Symbol $var is not present in the system.")) end end ts = fast_substitute(ts, namespace_subs) From 96dd399680c55ece42b6c2e4abc33911883de01f Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 17 Feb 2025 13:53:44 +0530 Subject: [PATCH 0958/1337] fix: handle edge case in ChainRulesCoreExt --- ext/MTKChainRulesCoreExt.jl | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/ext/MTKChainRulesCoreExt.jl b/ext/MTKChainRulesCoreExt.jl index 635f9d12cf..f153ee77de 100644 --- a/ext/MTKChainRulesCoreExt.jl +++ b/ext/MTKChainRulesCoreExt.jl @@ -16,7 +16,12 @@ end function subset_idxs(idxs, portion, template) ntuple(Val(length(template))) do subi - [Base.tail(idx.idx) for idx in idxs if idx.portion == portion && idx.idx[1] == subi] + result = [Base.tail(idx.idx) + for idx in idxs if idx.portion == portion && idx.idx[1] == subi] + if isempty(result) + result = [] + end + result end end From cfc5b099b7e89506cebf816d04d112c85441e4c2 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 17 Feb 2025 13:53:52 +0530 Subject: [PATCH 0959/1337] test: fix AD test --- test/extensions/ad.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/extensions/ad.jl b/test/extensions/ad.jl index 7b5c939b0a..0e72b2b7b7 100644 --- a/test/extensions/ad.jl +++ b/test/extensions/ad.jl @@ -25,7 +25,7 @@ prob = ODEProblem(sys, u0, tspan, ps) sol = solve(prob, Tsit5()) mtkparams = parameter_values(prob) -new_p = rand(10) +new_p = rand(14) gs = gradient(new_p) do new_p new_params = SciMLStructures.replace(SciMLStructures.Tunable(), mtkparams, new_p) new_prob = remake(prob, p = new_params) From 79363b21804a73117534fb7f9dcbb940db01a3ef Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 17 Feb 2025 15:26:33 +0530 Subject: [PATCH 0960/1337] test: try and make FMI test more consistent --- test/fmi/fmi.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/fmi/fmi.jl b/test/fmi/fmi.jl index f3ab3549cf..3db1c3f0e8 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-4, + Val(2); fmu, type = :CS, communication_step_size = 1e-6, reinitializealg = BrownFullBasicInit()) @test MTK.isinput(adder.a) @test MTK.isinput(adder.b) From ce1584fa09d008da95c49f86c3c4ecb55940f75e Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 17 Feb 2025 17:58:40 +0530 Subject: [PATCH 0961/1337] fix: check symbolic type in `maketerm` for `Initial` --- src/systems/abstractsystem.jl | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index 8baad025d1..0aed6f81e7 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -669,7 +669,11 @@ end # This is required so `fast_substitute` works function SymbolicUtils.maketerm(::Type{<:BasicSymbolic}, ::Initial, args, meta) - return metadata(Initial()(args...), meta) + val = Initial()(args...) + if symbolic_type(val) == NotSymbolic() + return val + end + return metadata(val, meta) end function add_initialization_parameters(sys::AbstractSystem) From ae4e6f70200221e736bdeea732f50c9c8fbeced2 Mon Sep 17 00:00:00 2001 From: vyudu Date: Mon, 17 Feb 2025 11:44:51 -0500 Subject: [PATCH 0962/1337] add tests --- src/systems/problem_utils.jl | 32 ++++++++++++++++++++------------ test/odesystem.jl | 15 +++++++++++++++ 2 files changed, 35 insertions(+), 12 deletions(-) diff --git a/src/systems/problem_utils.jl b/src/systems/problem_utils.jl index 8541056272..3d5b00c418 100644 --- a/src/systems/problem_utils.jl +++ b/src/systems/problem_utils.jl @@ -687,11 +687,11 @@ function process_SciMLProblem( u0map = to_varmap(u0map, dvs) symbols_to_symbolics!(sys, u0map) - check_keys(sys, u0map) pmap = to_varmap(pmap, ps) symbols_to_symbolics!(sys, pmap) - check_keys(sys, pmap) + + check_inputmap_keys(sys, u0map, pmap) defs = add_toterms(recursive_unwrap(defaults(sys))) cmap, cs = get_cmap(sys) @@ -783,29 +783,37 @@ end # Check that the keys of a u0map or pmap are valid # (i.e. are symbolic keys, and are defined for the system.) -function check_keys(sys, map) - badkeys = Any[] - for k in keys(map) +function check_inputmap_keys(sys, u0map, pmap) + badvarkeys = Any[] + for k in keys(u0map) if symbolic_type(k) === NotSymbolic() - push!(badkeys, k) + push!(badvarkeys, k) end end - isempty(badkeys) || throw(BadKeyError(collect(badkeys))) + badparamkeys = Any[] + for k in keys(pmap) + if symbolic_type(k) === NotSymbolic() + push!(badparamkeys, k) + end + end + (isempty(badvarkeys) && isempty(badparamkeys)) || throw(InvalidKeyError(collect(badvarkeys), collect(badparamkeys))) end const BAD_KEY_MESSAGE = """ - Undefined keys found in the parameter or initial condition maps. - The following keys are either invalid or not parameters/states of the system: + Undefined keys found in the parameter or initial condition maps. Check if symbolic variable names have been reassigned. + The following keys are invalid: """ -struct BadKeyError <: Exception +struct InvalidKeyError <: Exception vars::Any + params::Any end -function Base.showerror(io::IO, e::BadKeyError) +function Base.showerror(io::IO, e::InvalidKeyError) println(io, BAD_KEY_MESSAGE) - println(io, join(e.vars, ", ")) + println(io, "u0map: $(join(e.vars, ", "))") + println(io, "pmap: $(join(e.params, ", "))") end diff --git a/test/odesystem.jl b/test/odesystem.jl index a635c3dad9..7b4d580718 100644 --- a/test/odesystem.jl +++ b/test/odesystem.jl @@ -1626,3 +1626,18 @@ end prob = ODEProblem{false}(lowered_dae_sys; u0_constructor = x -> SVector(x...)) @test prob.u0 isa SVector end + +@testset "input map validation" begin + import ModelingToolkit: InvalidKeyError + @variables x(t) y(t) z(t) + @parameters a b c d + eqs = [D(x) ~ x*a, D(y) ~ y*c, D(z) ~ b + d] + @mtkbuild sys = ODESystem(eqs, t) + pmap = [a => 1, b => 2, c => 3, d => 4, "b" => 2] + u0map = [x => 1, y => 2, z => 3] + @test_throws InvalidKeyError ODEProblem(sys, u0map, (0., 1.), pmap) + + pmap = [a => 1, b => 2, c => 3, d => 4] + u0map = [x => 1, y => 2, z => 3, :0 => 3] + @test_throws InvalidKeyError ODEProblem(sys, u0map, (0., 1.), pmap) +end From 83f572c42a3787a395675bd5a0de4daa5aa30063 Mon Sep 17 00:00:00 2001 From: vyudu Date: Mon, 17 Feb 2025 18:25:45 -0500 Subject: [PATCH 0963/1337] refactor tests --- test/odesystem.jl | 15 --------------- test/problem_validation.jl | 21 +++++++++++++++------ 2 files changed, 15 insertions(+), 21 deletions(-) diff --git a/test/odesystem.jl b/test/odesystem.jl index 62bcf4c355..de166ef0a1 100644 --- a/test/odesystem.jl +++ b/test/odesystem.jl @@ -1673,18 +1673,3 @@ end prob = ODEProblem{false}(lowered_dae_sys; u0_constructor = x -> SVector(x...)) @test prob.u0 isa SVector end - -@testset "input map validation" begin - import ModelingToolkit: InvalidKeyError - @variables x(t) y(t) z(t) - @parameters a b c d - eqs = [D(x) ~ x*a, D(y) ~ y*c, D(z) ~ b + d] - @mtkbuild sys = ODESystem(eqs, t) - pmap = [a => 1, b => 2, c => 3, d => 4, "b" => 2] - u0map = [x => 1, y => 2, z => 3] - @test_throws InvalidKeyError ODEProblem(sys, u0map, (0., 1.), pmap) - - pmap = [a => 1, b => 2, c => 3, d => 4] - u0map = [x => 1, y => 2, z => 3, :0 => 3] - @test_throws InvalidKeyError ODEProblem(sys, u0map, (0., 1.), pmap) -end diff --git a/test/problem_validation.jl b/test/problem_validation.jl index f871327ae8..bce39b51d2 100644 --- a/test/problem_validation.jl +++ b/test/problem_validation.jl @@ -2,6 +2,7 @@ using ModelingToolkit using ModelingToolkit: t_nounits as t, D_nounits as D @testset "Input map validation" begin + import ModelingToolkit: InvalidKeyError, MissingParametersError @variables X(t) @parameters p d eqs = [D(X) ~ p - d*X] @@ -10,16 +11,24 @@ using ModelingToolkit: t_nounits as t, D_nounits as D p = "I accidentally renamed p" u0 = [X => 1.0] ps = [p => 1.0, d => 0.5] - @test_throws ModelingToolkit.BadKeyError oprob = ODEProblem(osys, u0, (0.0, 1.0), ps) + @test_throws MissingParametersError oprob = ODEProblem(osys, u0, (0.0, 1.0), ps) @parameters p d ps = [p => 1.0, d => 0.5, "Random stuff" => 3.0] - @test_throws ModelingToolkit.BadKeyError oprob = ODEProblem(osys, u0, (0.0, 1.0), ps) + @test_throws InvalidKeyError oprob = ODEProblem(osys, u0, (0.0, 1.0), ps) u0 = [:X => 1.0, "random" => 3.0] - @test_throws ModelingToolkit.BadKeyError oprob = ODEProblem(osys, u0, (0.0, 1.0), ps) + @test_throws InvalidKeyError oprob = ODEProblem(osys, u0, (0.0, 1.0), ps) - @parameters k - ps = [p => 1., d => 0.5, k => 3.] - @test_throws ModelingToolkit.BadKeyError oprob = ODEProblem(osys, u0, (0.0, 1.0), ps) + @variables x(t) y(t) z(t) + @parameters a b c d + eqs = [D(x) ~ x*a, D(y) ~ y*c, D(z) ~ b + d] + @mtkbuild sys = ODESystem(eqs, t) + pmap = [a => 1, b => 2, c => 3, d => 4, "b" => 2] + u0map = [x => 1, y => 2, z => 3] + @test_throws InvalidKeyError ODEProblem(sys, u0map, (0., 1.), pmap) + + pmap = [a => 1, b => 2, c => 3, d => 4] + u0map = [x => 1, y => 2, z => 3, :0 => 3] + @test_throws InvalidKeyError ODEProblem(sys, u0map, (0., 1.), pmap) end From 2a97325d0b4e9b70f3a18de8049d14559504880d Mon Sep 17 00:00:00 2001 From: vyudu Date: Tue, 18 Feb 2025 15:19:38 -0500 Subject: [PATCH 0964/1337] feat: initialization of DiscreteSystem --- .../symbolics_tearing.jl | 114 +++++++++++++----- src/structural_transformation/utils.jl | 55 ++++++--- .../discrete_system/discrete_system.jl | 8 +- src/variables.jl | 16 ++- test/discrete_system.jl | 68 ++++++++--- 5 files changed, 193 insertions(+), 68 deletions(-) diff --git a/src/structural_transformation/symbolics_tearing.jl b/src/structural_transformation/symbolics_tearing.jl index a166bb064b..bbb6853a7c 100644 --- a/src/structural_transformation/symbolics_tearing.jl +++ b/src/structural_transformation/symbolics_tearing.jl @@ -341,14 +341,18 @@ in order to properly generate the difference equations. In the system x(k) ~ x(k-1) + x(k-2), becomes Shift(t, 1)(x(t)) ~ x(t) + Shift(t, -1)(x(t)) -The lowest-order term is Shift(t, k)(x(t)), instead of x(t). -As such we actually want dummy variables for the k-1 lowest order terms instead of the k-1 highest order terms. +The lowest-order term is Shift(t, k)(x(t)), instead of x(t). As such we actually want +dummy variables for the k-1 lowest order terms instead of the k-1 highest order terms. Shift(t, -1)(x(t)) -> x\_{t-1}(t) -Since Shift(t, -1)(x) is not a derivative, it is directly substituted in `fullvars`. No equation or variable is added for it. +Since Shift(t, -1)(x) is not a derivative, it is directly substituted in `fullvars`. +No equation or variable is added for it. -For ODESystems D(D(D(x))) in equations is recursively substituted as D(x) ~ x_t, D(x_t) ~ x_tt, etc. The analogue for discrete systems, Shift(t, 1)(Shift(t,1)(Shift(t,1)(Shift(t, -3)(x(t))))) does not actually appear. So `total_sub` in generate_system_equations` is directly initialized with all of the lowered variables `Shift(t, -3)(x) -> x_t-3(t)`, etc. +For ODESystems D(D(D(x))) in equations is recursively substituted as D(x) ~ x_t, D(x_t) ~ x_tt, etc. +The analogue for discrete systems, Shift(t, 1)(Shift(t,1)(Shift(t,1)(Shift(t, -3)(x(t))))) +does not actually appear. So `total_sub` in generate_system_equations` is directly +initialized with all of the lowered variables `Shift(t, -3)(x) -> x_t-3(t)`, etc. =# """ Generate new derivative variables for the system. @@ -358,7 +362,7 @@ Effects on the system structure: - neweqs: add the identity equations for the new variables, D(x) ~ x_t - graph: update graph with the new equations and variables, and their connections - solvable_graph: -- var_eq_matching: match D(x) to the added identity equation +- var_eq_matching: match D(x) to the added identity equation D(x) ~ x_t """ function generate_derivative_variables!(ts::TearingState, neweqs, var_eq_matching; mm = nothing, iv = nothing, D = nothing) @unpack fullvars, sys, structure = ts @@ -406,7 +410,7 @@ function generate_derivative_variables!(ts::TearingState, neweqs, var_eq_matchin end """ -Check if there's `D(x) = x_t` already. +Check if there's `D(x) ~ x_t` already. """ function find_duplicate_dd(dv, solvable_graph, diff_to_var, linear_eqs, mm) for eq in 𝑑neighbors(solvable_graph, dv) @@ -427,6 +431,10 @@ function find_duplicate_dd(dv, solvable_graph, diff_to_var, linear_eqs, mm) return nothing end +""" +Add a dummy derivative variable x_t corresponding to symbolic variable D(x) +which has index dv in `fullvars`. Return the new index of x_t. +""" function add_dd_variable!(s::SystemStructure, fullvars, x_t, dv) push!(fullvars, simplify_shifts(x_t)) v_t = length(fullvars) @@ -439,7 +447,11 @@ function add_dd_variable!(s::SystemStructure, fullvars, x_t, dv) v_t end -# dv = index of D(x), v_t = index of x_t +""" +Add the equation D(x) - x_t ~ 0 to `neweqs`. `dv` and `v_t` are the indices +of the higher-order derivative variable and the newly-introduced dummy +derivative variable. Return the index of the new equation in `neweqs`. +""" function add_dd_equation!(s::SystemStructure, neweqs, eq, dv, v_t) push!(neweqs, eq) add_vertex!(s.graph, SRC) @@ -452,8 +464,33 @@ function add_dd_equation!(s::SystemStructure, neweqs, eq, dv, v_t) end """ -Solve the solvable equations of the system and generate differential (or discrete) -equations in terms of the selected states. +Solve the equations in `neweqs` to obtain the final equations of the +system. + +For each equation of `neweqs`, do one of the following: + 1. If the equation is solvable for a differentiated variable D(x), + then solve for D(x), and add D(x) ~ sol as a differential equation + of the system. + 2. If the equation is solvable for an un-differentiated variable x, + solve for x and then add x ~ sol as a solved equation. These will + become observables. + 3. If the equation is not solvable, add it as an algebraic equation. + +Solved equations are added to `total_sub`. Occurrences of differential +or solved variables on the RHS of the final equations will get substituted. +The topological sort of the equations ensures that variables are solved for +before they appear in equations. + +Reorder the equations and unknowns to be: + [diffeqs; ...] + [diffvars; ...] +such that the mass matrix is: + [I 0 + 0 0]. + +Order the new equations and variables such that the differential equations +and variables come first. Return the new equations, the solved equations, +the new orderings, and the number of solved variables and equations. """ function generate_system_equations!(state::TearingState, neweqs, var_eq_matching; simplify = false, iv = nothing, D = nothing) @unpack fullvars, sys, structure = state @@ -550,6 +587,9 @@ function generate_system_equations!(state::TearingState, neweqs, var_eq_matching return neweqs, solved_eqs, eq_ordering, var_ordering, length(solved_vars), length(solved_vars_set) end +""" +Occurs when a variable D(x) occurs in a non-differential system. +""" struct UnexpectedDifferentialError eq::Equation end @@ -558,12 +598,20 @@ function Base.showerror(io::IO, err::UnexpectedDifferentialError) error("Differential found in a non-differential system. Likely this is a bug in the construction of an initialization system. Please report this issue with a reproducible example. Offending equation: $(err.eq)") end +""" +Generate a first-order differential equation whose LHS is `dx`. + +`var` and `dx` represent the same variable, but `var` may be a higher-order differential and `dx` is always first-order. For example, if `var` is D(D(x)), then `dx` would be `D(x_t)`. Solve `eq` for `var`, substitute previously solved variables, and return the differential equation. +""" function make_differential_equation(var, dx, eq, total_sub) dx ~ simplify_shifts(Symbolics.fixpoint_sub( Symbolics.symbolic_linear_solve(eq, var), total_sub; operator = ModelingToolkit.Shift)) end +""" +Generate an algebraic equation. Substitute solved variables into `eq` and return the equation. +""" function make_algebraic_equation(eq, total_sub) rhs = eq.rhs if !(eq.lhs isa Number && eq.lhs == 0) @@ -572,6 +620,9 @@ function make_algebraic_equation(eq, total_sub) 0 ~ simplify_shifts(Symbolics.fixpoint_sub(rhs, total_sub)) end +""" +Solve equation `eq` for `var`, substitute previously solved variables, and return the solved equation. +""" function make_solved_equation(var, eq, total_sub; simplify = false) residual = eq.lhs - eq.rhs a, b, islinear = linear_expansion(residual, var) @@ -591,17 +642,13 @@ function make_solved_equation(var, eq, total_sub; simplify = false) end """ -Reorder the equations and unknowns to be: - [diffeqs; ...] - [diffvars; ...] -such that the mass matrix is: - [I 0 - 0 0]. - -Update the state to account for the new ordering and equations. +Given the ordering returned by `generate_system_equations!`, update the +tearing state to account for the new order. Permute the variables and equations. +Eliminate the solved variables and equations from the graph and permute the +graph's vertices to account for the new variable/equation ordering. """ # TODO: BLT sorting -function reorder_vars!(state::TearingState, var_eq_matching, eq_ordering, var_ordering, nelim_eq, nelim_var) +function reorder_vars!(state::TearingState, var_eq_matching, eq_ordering, var_ordering, nsolved_eq, nsolved_var) @unpack solvable_graph, var_to_diff, eq_to_diff, graph = state.structure eqsperm = zeros(Int, nsrcs(graph)) @@ -616,7 +663,7 @@ function reorder_vars!(state::TearingState, var_eq_matching, eq_ordering, var_or # Contract the vertices in the structure graph to make the structure match # the new reality of the system we've just created. new_graph = contract_variables(graph, var_eq_matching, varsperm, eqsperm, - nelim_eq, nelim_var) + nsolved_eq, nsolved_var) new_var_to_diff = complete(DiffGraph(length(var_ordering))) for (v, d) in enumerate(var_to_diff) @@ -643,7 +690,7 @@ function reorder_vars!(state::TearingState, var_eq_matching, eq_ordering, var_or end """ -Set the system equations, unknowns, observables post-tearing. +Update the system equations, unknowns, and observables after simplification. """ function update_simplified_system!(state::TearingState, neweqs, solved_eqs, dummy_sub, var_eq_matching, extra_unknowns; cse_hack = true, array_hack = true) @@ -685,16 +732,10 @@ function update_simplified_system!(state::TearingState, neweqs, solved_eqs, dumm sys = schedule(sys) end -# Terminology and Definition: -# A general DAE is in the form of `F(u'(t), u(t), p, t) == 0`. We can -# characterize variables in `u(t)` into two classes: differential variables -# (denoted `v(t)`) and algebraic variables (denoted `z(t)`). Differential -# variables are marked as `SelectedState` and they are differentiated in the -# DAE system, i.e. `v'(t)` are all the variables in `u'(t)` that actually -# appear in the system. Algebraic variables are variables that are not -# differential variables. -# Give the order of the variable indexed by dv +""" +Give the order of the variable indexed by dv. +""" function var_order(dv, diff_to_var) order = 0 while (dv′ = diff_to_var[dv]) !== nothing @@ -704,6 +745,21 @@ function var_order(dv, diff_to_var) order, dv end +""" +Main internal function for structural simplification for DAE systems and discrete systems. +Generate dummy derivative variables, new equations in terms of variables, return updated +system and tearing state. + +Terminology and Definition: + +A general DAE is in the form of `F(u'(t), u(t), p, t) == 0`. We can +characterize variables in `u(t)` into two classes: differential variables +(denoted `v(t)`) and algebraic variables (denoted `z(t)`). Differential +variables are marked as `SelectedState` and they are differentiated in the +DAE system, i.e. `v'(t)` are all the variables in `u'(t)` that actually +appear in the system. Algebraic variables are variables that are not +differential variables. +""" function tearing_reassemble(state::TearingState, var_eq_matching, full_var_eq_matching = nothing; simplify = false, mm = nothing, cse_hack = true, array_hack = true) extra_vars = Int[] diff --git a/src/structural_transformation/utils.jl b/src/structural_transformation/utils.jl index f64a8da132..2753446a94 100644 --- a/src/structural_transformation/utils.jl +++ b/src/structural_transformation/utils.jl @@ -449,7 +449,12 @@ end ### Misc ### -# For discrete variables. Turn Shift(t, k)(x(t)) into xₜ₋ₖ(t) +""" +Handle renaming variable names for discrete structural simplification. Three cases: +- positive shift: do nothing +- zero shift: x(t) => Shift(t, 0)(x(t)) +- negative shift: rename the variable +""" function lower_shift_varname(var, iv) op = operation(var) op isa Shift || return Shift(iv, 0)(var, true) # hack to prevent simplification of x(t) - x(t) @@ -460,30 +465,46 @@ function lower_shift_varname(var, iv) end end -function shift2term(var) +""" +Rename a Shift variable with negative shift, Shift(t, k)(x(t)) to xₜ₋ₖ(t). +""" +function shift2term(var) backshift = operation(var).steps iv = operation(var).t - num = join(Char(0x2080 + d) for d in reverse!(digits(-backshift))) - ds = join([Char(0x209c), Char(0x208b), num]) - #ds = "$iv-$(-backshift)" - #d_separator = 'ˍ' - - if ModelingToolkit.isoperator(var, ModelingToolkit.Shift) - O = only(arguments(var)) - oldop = operation(O) - newname = Symbol(string(nameof(oldop)), ds) - else - O = var - oldop = operation(var) - varname = split(string(nameof(oldop)), d_separator)[1] - newname = Symbol(varname, d_separator, ds) - end + num = join(Char(0x2080 + d) for d in reverse!(digits(-backshift))) # subscripted number, e.g. ₁ + ds = join([Char(0x209c), Char(0x208b), num]) + # Char(0x209c) = ₜ + # Char(0x208b) = ₋ (subscripted minus) + + O = only(arguments(var)) + oldop = operation(O) + newname = Symbol(string(nameof(oldop)), ds) + newvar = maketerm(typeof(O), Symbolics.rename(oldop, newname), Symbolics.children(O), Symbolics.metadata(O)) newvar = setmetadata(newvar, Symbolics.VariableSource, (:variables, newname)) newvar = setmetadata(newvar, ModelingToolkit.VariableUnshifted, O) + newvar = setmetadata(newvar, ModelingToolkit.VariableShift, backshift) return newvar end +function term2shift(var) + var = Symbolics.unwrap(var) + name = Symbolics.getname(var) + O = only(arguments(var)) + oldop = operation(O) + iv = only(arguments(x)) + # Split on ₋ + if occursin(Char(0x208b), name) + substrings = split(name, Char(0x208b)) + shift = last(split(name, Char(0x208b))) + newname = join(substrings[1:end-1])[1:end-1] + newvar = maketerm(typeof(O), Symbolics.rename(oldop, newname), Symbolics.children(O), Symbolics.metadata(O)) + return Shift(iv, -shift)(newvar) + else + return var + end +end + function isdoubleshift(var) return ModelingToolkit.isoperator(var, ModelingToolkit.Shift) && ModelingToolkit.isoperator(arguments(var)[1], ModelingToolkit.Shift) diff --git a/src/systems/discrete_system/discrete_system.jl b/src/systems/discrete_system/discrete_system.jl index 9a9ac47853..40bf632339 100644 --- a/src/systems/discrete_system/discrete_system.jl +++ b/src/systems/discrete_system/discrete_system.jl @@ -270,15 +270,19 @@ function shift_u0map_forward(sys::DiscreteSystem, u0map, defs) v = u0map[k] if !((op = operation(k)) isa Shift) 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)).") + 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)))).") end + updated[Shift(iv, op.steps + 1)(arguments(k)[1])] = v end for var in unknowns(sys) op = operation(var) - haskey(updated, var) && continue root = getunshifted(var) + shift = getshift(var) isnothing(root) && continue - haskey(defs, root) || error("Initial condition for $root not provided.") + (haskey(updated, Shift(iv, shift)(root)) || haskey(updated, var)) && continue + haskey(defs, root) || error("Initial condition for $var not provided.") updated[var] = defs[root] end return updated diff --git a/src/variables.jl b/src/variables.jl index a29a119607..4e13ad2c5d 100644 --- a/src/variables.jl +++ b/src/variables.jl @@ -6,7 +6,9 @@ struct VariableOutput end struct VariableIrreducible end struct VariableStatePriority end struct VariableMisc end +# Metadata for renamed shift variables xₜ₋₁ struct VariableUnshifted end +struct VariableShift end Symbolics.option_to_metadata_type(::Val{:unit}) = VariableUnit Symbolics.option_to_metadata_type(::Val{:connect}) = VariableConnectType Symbolics.option_to_metadata_type(::Val{:input}) = VariableInput @@ -15,6 +17,7 @@ Symbolics.option_to_metadata_type(::Val{:irreducible}) = VariableIrreducible Symbolics.option_to_metadata_type(::Val{:state_priority}) = VariableStatePriority Symbolics.option_to_metadata_type(::Val{:misc}) = VariableMisc Symbolics.option_to_metadata_type(::Val{:unshifted}) = VariableUnshifted +Symbolics.option_to_metadata_type(::Val{:shift}) = VariableShift """ dump_variable_metadata(var) @@ -97,7 +100,7 @@ struct Stream <: AbstractConnectType end # special stream connector Get the connect type of x. See also [`hasconnect`](@ref). """ -getconnect(x) = getconnect(unwrap(x)) +getconnect(x::Num) = getconnect(unwrap(x)) getconnect(x::Symbolic) = Symbolics.getmetadata(x, VariableConnectType, nothing) """ hasconnect(x) @@ -264,7 +267,7 @@ end end struct IsHistory end -ishistory(x) = ishistory(unwrap(x)) +ishistory(x::Num) = ishistory(unwrap(x)) ishistory(x::Symbolic) = getmetadata(x, IsHistory, false) hist(x, t) = wrap(hist(unwrap(x), t)) function hist(x::Symbolic, t) @@ -575,7 +578,7 @@ end Fetch any miscellaneous data associated with symbolic variable `x`. See also [`hasmisc(x)`](@ref). """ -getmisc(x) = getmisc(unwrap(x)) +getmisc(x::Num) = getmisc(unwrap(x)) getmisc(x::Symbolic) = Symbolics.getmetadata(x, VariableMisc, nothing) """ hasmisc(x) @@ -594,7 +597,7 @@ setmisc(x, miscdata) = setmetadata(x, VariableMisc, miscdata) Fetch the unit associated with variable `x`. This function is a metadata getter for an individual variable, while `get_unit` is used for unit inference on more complicated sdymbolic expressions. """ -getunit(x) = getunit(unwrap(x)) +getunit(x::Num) = getunit(unwrap(x)) getunit(x::Symbolic) = Symbolics.getmetadata(x, VariableUnit, nothing) """ hasunit(x) @@ -603,5 +606,8 @@ Check if the variable `x` has a unit. """ hasunit(x) = getunit(x) !== nothing -getunshifted(x) = getunshifted(unwrap(x)) +getunshifted(x::Num) = getunshifted(unwrap(x)) getunshifted(x::Symbolic) = Symbolics.getmetadata(x, VariableUnshifted, nothing) + +getshift(x::Num) = getshift(unwrap(x)) +getshift(x::Symbolic) = Symbolics.getmetadata(x, VariableShift, 0) diff --git a/test/discrete_system.jl b/test/discrete_system.jl index b63f66d4e4..fa7ba993e6 100644 --- a/test/discrete_system.jl +++ b/test/discrete_system.jl @@ -220,21 +220,6 @@ sol = solve(prob, FunctionMap()) @test reduce(vcat, sol.u) == 1:11 -# test that default values apply to the entire history -@variables x(t) = 1.0 -@mtkbuild de = DiscreteSystem([x ~ x(k - 1) + x(k - 2)], t) -prob = DiscreteProblem(de, [], (0, 10)) -@test prob[x] == 2.0 -@test prob[x(k - 1)] == 1.0 - -# must provide initial conditions for history -@test_throws ErrorException DiscreteProblem(de, [x => 2.0], (0, 10)) - -# initial values only affect _that timestep_, not the entire history -prob = DiscreteProblem(de, [x(k - 1) => 2.0], (0, 10)) -@test prob[x] == 3.0 -@test prob[x(k - 1)] == 2.0 - # Issue#2585 getdata(buffer, t) = buffer[mod1(Int(t), length(buffer))] @register_symbolic getdata(buffer::Vector, t) @@ -272,6 +257,7 @@ k = ShiftIndex(t) @named sys = DiscreteSystem([x ~ x^2 + y^2, y ~ x(k - 1) + y(k - 1)], t) @test_throws ["algebraic equations", "not yet supported"] structural_simplify(sys) + @testset "Passing `nothing` to `u0`" begin @variables x(t) = 1 k = ShiftIndex() @@ -279,3 +265,55 @@ k = ShiftIndex(t) prob = @test_nowarn DiscreteProblem(sys, nothing, (0.0, 1.0)) @test_nowarn solve(prob, FunctionMap()) end + +@testset "Initialization" begin + # test that default values apply to the entire history + @variables x(t) = 1.0 + @mtkbuild de = DiscreteSystem([x ~ x(k - 1) + x(k - 2)], t) + prob = DiscreteProblem(de, [], (0, 10)) + @test prob[x] == 2.0 + @test prob[x(k - 1)] == 1.0 + + # must provide initial conditions for history + @test_throws ErrorException DiscreteProblem(de, [x => 2.0], (0, 10)) + @test_throws ErrorException DiscreteProblem(de, [x(k+1) => 2.], (0, 10)) + + # initial values only affect _that timestep_, not the entire history + prob = DiscreteProblem(de, [x(k - 1) => 2.0], (0, 10)) + @test prob[x] == 3.0 + @test prob[x(k - 1)] == 2.0 + @test prob[xₜ₋₁] == 2.0 + + # Test initial assignment with lowered variable + @variables xₜ₋₁(t) + prob = DiscreteProblem(de, [xₜ₋₁(k-1) => 4.0], (0, 10)) + @test prob[x(k-1)] == prob[xₜ₋₁] == 1.0 + @test prob[x] == 5. + + # Test missing initial throws error + @variables x(t) + @mtkbuild de = DiscreteSystem([x ~ x(k-1) + x(k-2)*x(k-3)], t) + @test_throws ErrorException prob = DiscreteProblem(de, [x(k-3) => 2.], (0, 10)) + @test_throws ErrorException prob = DiscreteProblem(de, [x(k-3) => 2., x(k-1) => 3.], (0, 10)) + + # Test non-assigned initials are given default value + @variables x(t) = 2. + prob = DiscreteProblem(de, [x(k-3) => 12.], (0, 10)) + @test prob[x] == 26.0 + @test prob[x(k-1)] == 2.0 + @test prob[x(k-2)] == 2.0 + + # Elaborate test + eqs = [x ~ x(k-1) + z(k-2), + z ~ x(k-2) * x(k-3) - z(k-1)^2] + @mtkbuild de = DiscreteSystem(eqs, t) + @variables xₜ₋₂(t) zₜ₋₁(t) + u0 = [x(k-1) => 3, + xₜ₋₂(k-1) => 4, + x(k-2) => 1, + z(k-1) => 5, + zₜ₋₁(k-1) => 12] + prob = DiscreteProblem(de, u0, (0, 10)) + @test prob[x] == 15 + @test prob[z] == -21 +end From fc2a309f3d9f8e607329b9ee640672ca4f872eb3 Mon Sep 17 00:00:00 2001 From: vyudu Date: Wed, 19 Feb 2025 10:12:41 -0800 Subject: [PATCH 0965/1337] fix tests and shift2term --- src/structural_transformation/utils.jl | 31 ++++++------------- .../discrete_system/discrete_system.jl | 7 +++-- test/discrete_system.jl | 13 ++++++-- 3 files changed, 25 insertions(+), 26 deletions(-) diff --git a/src/structural_transformation/utils.jl b/src/structural_transformation/utils.jl index 2753446a94..96f7f78f99 100644 --- a/src/structural_transformation/utils.jl +++ b/src/structural_transformation/utils.jl @@ -469,16 +469,21 @@ end Rename a Shift variable with negative shift, Shift(t, k)(x(t)) to xₜ₋ₖ(t). """ function shift2term(var) - backshift = operation(var).steps - iv = operation(var).t + op = operation(var) + iv = op.t + arg = only(arguments(var)) + is_lowered = !isnothing(ModelingToolkit.getunshifted(arg)) + + backshift = is_lowered ? op.steps + ModelingToolkit.getshift(arg) : op.steps + num = join(Char(0x2080 + d) for d in reverse!(digits(-backshift))) # subscripted number, e.g. ₁ ds = join([Char(0x209c), Char(0x208b), num]) # Char(0x209c) = ₜ # Char(0x208b) = ₋ (subscripted minus) - O = only(arguments(var)) + O = is_lowered ? ModelingToolkit.getunshifted(arg) : arg oldop = operation(O) - newname = Symbol(string(nameof(oldop)), ds) + newname = backshift != 0 ? Symbol(string(nameof(oldop)), ds) : Symbol(string(nameof(oldop))) newvar = maketerm(typeof(O), Symbolics.rename(oldop, newname), Symbolics.children(O), Symbolics.metadata(O)) newvar = setmetadata(newvar, Symbolics.VariableSource, (:variables, newname)) @@ -487,24 +492,6 @@ function shift2term(var) return newvar end -function term2shift(var) - var = Symbolics.unwrap(var) - name = Symbolics.getname(var) - O = only(arguments(var)) - oldop = operation(O) - iv = only(arguments(x)) - # Split on ₋ - if occursin(Char(0x208b), name) - substrings = split(name, Char(0x208b)) - shift = last(split(name, Char(0x208b))) - newname = join(substrings[1:end-1])[1:end-1] - newvar = maketerm(typeof(O), Symbolics.rename(oldop, newname), Symbolics.children(O), Symbolics.metadata(O)) - return Shift(iv, -shift)(newvar) - else - return var - end -end - function isdoubleshift(var) return ModelingToolkit.isoperator(var, ModelingToolkit.Shift) && ModelingToolkit.isoperator(arguments(var)[1], ModelingToolkit.Shift) diff --git a/src/systems/discrete_system/discrete_system.jl b/src/systems/discrete_system/discrete_system.jl index 40bf632339..3f8d9e85c6 100644 --- a/src/systems/discrete_system/discrete_system.jl +++ b/src/systems/discrete_system/discrete_system.jl @@ -269,12 +269,15 @@ function shift_u0map_forward(sys::DiscreteSystem, u0map, defs) for k in collect(keys(u0map)) v = u0map[k] if !((op = operation(k)) isa Shift) - 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)).") + 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)).") + + updated[Shift(iv, 1)(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)))).") + else + updated[Shift(iv, op.steps + 1)(only(arguments(k)))] = v end - updated[Shift(iv, op.steps + 1)(arguments(k)[1])] = v end for var in unknowns(sys) op = operation(var) diff --git a/test/discrete_system.jl b/test/discrete_system.jl index fa7ba993e6..756e5bca48 100644 --- a/test/discrete_system.jl +++ b/test/discrete_system.jl @@ -282,10 +282,10 @@ end prob = DiscreteProblem(de, [x(k - 1) => 2.0], (0, 10)) @test prob[x] == 3.0 @test prob[x(k - 1)] == 2.0 + @variables xₜ₋₁(t) @test prob[xₜ₋₁] == 2.0 # Test initial assignment with lowered variable - @variables xₜ₋₁(t) prob = DiscreteProblem(de, [xₜ₋₁(k-1) => 4.0], (0, 10)) @test prob[x(k-1)] == prob[xₜ₋₁] == 1.0 @test prob[x] == 5. @@ -298,16 +298,17 @@ end # Test non-assigned initials are given default value @variables x(t) = 2. + @mtkbuild de = DiscreteSystem([x ~ x(k-1) + x(k-2)*x(k-3)], t) prob = DiscreteProblem(de, [x(k-3) => 12.], (0, 10)) @test prob[x] == 26.0 @test prob[x(k-1)] == 2.0 @test prob[x(k-2)] == 2.0 # Elaborate test + @variables xₜ₋₂(t) zₜ₋₁(t) z(t) eqs = [x ~ x(k-1) + z(k-2), z ~ x(k-2) * x(k-3) - z(k-1)^2] @mtkbuild de = DiscreteSystem(eqs, t) - @variables xₜ₋₂(t) zₜ₋₁(t) u0 = [x(k-1) => 3, xₜ₋₂(k-1) => 4, x(k-2) => 1, @@ -316,4 +317,12 @@ end prob = DiscreteProblem(de, u0, (0, 10)) @test prob[x] == 15 @test prob[z] == -21 + + import ModelingToolkit: shift2term + # unknowns(de) = xₜ₋₁, x, zₜ₋₁, xₜ₋₂, z + vars = ModelingToolkit.value.(unknowns(de)) + @test isequal(shift2term(Shift(t, 1)(vars[1])), vars[2]) + @test isequal(shift2term(Shift(t, 1)(vars[4])), vars[1]) + @test isequal(shift2term(Shift(t, -1)(vars[5])), vars[3]) + @test isequal(shift2term(Shift(t, -2)(vars[2])), vars[4]) end From 3d1c2ffa9a33bb9095e885dd866ec17b7407b7c9 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 17 Feb 2025 18:47:32 +0530 Subject: [PATCH 0966/1337] fix: skip inplace check in BifurcationKit --- ext/MTKBifurcationKitExt.jl | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/ext/MTKBifurcationKitExt.jl b/ext/MTKBifurcationKitExt.jl index e5b57efc5a..0b9f104d9b 100644 --- a/ext/MTKBifurcationKitExt.jl +++ b/ext/MTKBifurcationKitExt.jl @@ -97,7 +97,10 @@ function BifurcationKit.BifurcationProblem(nsys::NonlinearSystem, @set! nsys.index_cache = nothing # force usage of a parameter vector instead of `MTKParameters` # Creates F and J functions. ofun = NonlinearFunction(nsys; jac = jac) - F = ofun.f + F = let f = ofun.f + _f(resid, u, p) = (f(resid, u, p); resid) + _f(u, p) = f(u, p) + end J = jac ? ofun.jac : nothing # Converts the input state guess. @@ -136,6 +139,7 @@ function BifurcationKit.BifurcationProblem(nsys::NonlinearSystem, args...; record_from_solution = record_from_solution, J = J, + inplace = true, kwargs...) end From 71fd4b2d9e882b4bdde3e570c6d3c430bdc71252 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 17 Feb 2025 19:42:00 +0530 Subject: [PATCH 0967/1337] build: bump OrdinaryDiffEqNonlinearSolve compat --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 3fbf220ef8..51e8f3acb2 100644 --- a/Project.toml +++ b/Project.toml @@ -128,7 +128,7 @@ OrderedCollections = "1" OrdinaryDiffEq = "6.82.0" OrdinaryDiffEqCore = "1.15.0" OrdinaryDiffEqDefault = "1.2" -OrdinaryDiffEqNonlinearSolve = "1.3.0" +OrdinaryDiffEqNonlinearSolve = "1.5.0" PrecompileTools = "1" REPL = "1" RecursiveArrayTools = "3.26" From 3192166ca0c7763a93b7d60130c9d845869b827f Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 18 Feb 2025 11:53:43 +0530 Subject: [PATCH 0968/1337] fix: don't unnecessarily construct arrays for unknowns --- src/structural_transformation/symbolics_tearing.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/src/structural_transformation/symbolics_tearing.jl b/src/structural_transformation/symbolics_tearing.jl index 92e121c028..bf5c51797b 100644 --- a/src/structural_transformation/symbolics_tearing.jl +++ b/src/structural_transformation/symbolics_tearing.jl @@ -719,6 +719,7 @@ function cse_and_array_hacks(sys, obs, subeqs, unknowns, neweqs; cse = true, arr Symbolics.shape(sym) != Symbolics.Unknown() || continue arg1 = arguments(sym)[1] cnt = get(arr_obs_occurrences, arg1, 0) + cnt == 0 && continue arr_obs_occurrences[arg1] = cnt + 1 end for eq in neweqs From f5176c6d3fd496dee519e45a58ecd9afc337781e Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 18 Feb 2025 12:03:26 +0530 Subject: [PATCH 0969/1337] test: avoid spamming stderr in debugging tests --- Project.toml | 8 +++++--- test/debugging.jl | 13 +++++++------ 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/Project.toml b/Project.toml index 51e8f3acb2..0563e28bcf 100644 --- a/Project.toml +++ b/Project.toml @@ -104,11 +104,11 @@ DynamicQuantities = "^0.11.2, 0.12, 0.13, 1" EnumX = "1.0.4" ExprTools = "0.1.10" Expronicon = "0.8" +FMI = "0.14" FindFirstFunctions = "1" ForwardDiff = "0.10.3" FunctionWrappers = "1.1" FunctionWrappersWrappers = "0.1" -FMI = "0.14" Graphs = "1.5.2" HomotopyContinuation = "2.11" InfiniteOpt = "0.5" @@ -119,6 +119,7 @@ LabelledArrays = "1.3" Latexify = "0.11, 0.12, 0.13, 0.14, 0.15, 0.16" Libdl = "1" LinearAlgebra = "1" +Logging = "1" MLStyle = "0.4.17" ModelingToolkitStandardLibrary = "2.19" NaNMath = "0.3, 1" @@ -143,8 +144,8 @@ SimpleNonlinearSolve = "0.1.0, 1, 2" SparseArrays = "1" SpecialFunctions = "0.7, 0.8, 0.9, 0.10, 1.0, 2" StaticArrays = "0.10, 0.11, 0.12, 1.0" -StochasticDiffEq = "6.72.1" StochasticDelayDiffEq = "1.8.1" +StochasticDiffEq = "6.72.1" SymbolicIndexingInterface = "0.3.37" SymbolicUtils = "3.14" Symbolics = "6.29" @@ -164,6 +165,7 @@ ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210" Ipopt = "b6b21f68-93f8-5de0-b562-5493be1d77c9" Ipopt_jll = "9cc047cb-c261-5740-88fc-0cf96f7bdcc7" JET = "c3a54625-cd67-489e-a8e7-0a5a0ff4e31b" +Logging = "56ddb016-857b-54e1-b83d-db4d58db5568" ModelingToolkitStandardLibrary = "16a59e39-deab-5bd0-87e4-056b12336739" NonlinearSolve = "8913a72c-1f9b-4ce2-8d82-65094dcecaec" Optimization = "7f7a1694-90dd-40f0-9382-eb1efda571ba" @@ -187,4 +189,4 @@ Sundials = "c3572dad-4567-51f8-b174-8c6c989267f4" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [targets] -test = ["AmplNLWriter", "BenchmarkTools", "ControlSystemsBase", "DataInterpolations", "DelayDiffEq", "NonlinearSolve", "ForwardDiff", "Ipopt", "Ipopt_jll", "ModelingToolkitStandardLibrary", "Optimization", "OptimizationOptimJL", "OptimizationMOI", "OrdinaryDiffEq", "OrdinaryDiffEqCore", "OrdinaryDiffEqDefault", "REPL", "Random", "ReferenceTests", "SafeTestsets", "StableRNGs", "Statistics", "SteadyStateDiffEq", "Test", "StochasticDiffEq", "Sundials", "StochasticDelayDiffEq", "Pkg", "JET", "OrdinaryDiffEqNonlinearSolve"] +test = ["AmplNLWriter", "BenchmarkTools", "ControlSystemsBase", "DataInterpolations", "DelayDiffEq", "NonlinearSolve", "ForwardDiff", "Ipopt", "Ipopt_jll", "ModelingToolkitStandardLibrary", "Optimization", "OptimizationOptimJL", "OptimizationMOI", "OrdinaryDiffEq", "OrdinaryDiffEqCore", "OrdinaryDiffEqDefault", "REPL", "Random", "ReferenceTests", "SafeTestsets", "StableRNGs", "Statistics", "SteadyStateDiffEq", "Test", "StochasticDiffEq", "Sundials", "StochasticDelayDiffEq", "Pkg", "JET", "OrdinaryDiffEqNonlinearSolve", "Logging"] diff --git a/test/debugging.jl b/test/debugging.jl index aad36eb995..a55684737c 100644 --- a/test/debugging.jl +++ b/test/debugging.jl @@ -1,4 +1,5 @@ using ModelingToolkit, OrdinaryDiffEq, StochasticDiffEq, SymbolicIndexingInterface +import Logging using ModelingToolkit: t_nounits as t, D_nounits as D, ASSERTION_LOG_VARIABLE @variables x(t) @@ -25,11 +26,11 @@ end dsys = debug_system(sys; functions = []) @test is_parameter(dsys, ASSERTION_LOG_VARIABLE) prob = Problem(dsys, [x => 0.1], (0.0, 5.0)) - sol = solve(prob, alg) - @test !SciMLBase.successful_retcode(sol) - prob.ps[ASSERTION_LOG_VARIABLE] = true sol = @test_logs (:error, r"ohno") match_mode=:any solve(prob, alg) @test !SciMLBase.successful_retcode(sol) + prob.ps[ASSERTION_LOG_VARIABLE] = false + sol = @test_logs min_level=Logging.Error solve(prob, alg) + @test !SciMLBase.successful_retcode(sol) end end @@ -41,10 +42,10 @@ end dsys = debug_system(outer; functions = []) @test is_parameter(dsys, ASSERTION_LOG_VARIABLE) prob = Problem(dsys, [inner.x => 0.1], (0.0, 5.0)) - sol = solve(prob, alg) - @test !SciMLBase.successful_retcode(sol) - prob.ps[ASSERTION_LOG_VARIABLE] = true sol = @test_logs (:error, r"ohno") match_mode=:any solve(prob, alg) @test !SciMLBase.successful_retcode(sol) + prob.ps[ASSERTION_LOG_VARIABLE] = false + sol = @test_logs min_level=Logging.Error solve(prob, alg) + @test !SciMLBase.successful_retcode(sol) end end From 5b62aee9de092aa7b7710329e54e68d4e01d095a Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 18 Feb 2025 12:45:05 +0530 Subject: [PATCH 0970/1337] fix: improve validation of variables in `build_explicit_observed_function` --- src/systems/diffeqs/odesystem.jl | 4 ++-- src/utils.jl | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/src/systems/diffeqs/odesystem.jl b/src/systems/diffeqs/odesystem.jl index 2dcb703d96..bd40eeb853 100644 --- a/src/systems/diffeqs/odesystem.jl +++ b/src/systems/diffeqs/odesystem.jl @@ -476,6 +476,7 @@ function build_explicit_observed_function(sys, ts; end end allsyms = Set(all_symbols(sys)) + iv = has_iv(sys) ? get_iv(sys) : nothing for var in vs var = unwrap(var) newvar = get(ns_map, var, nothing) @@ -483,8 +484,7 @@ function build_explicit_observed_function(sys, ts; namespace_subs[var] = newvar var = newvar end - if throw && !(var in allsyms) && - (!iscall(var) || operation(var) !== getindex || !(arguments(var)[1] in allsyms)) + if throw && !var_in_varlist(var, allsyms, iv) Base.throw(ArgumentError("Symbol $var is not present in the system.")) end end diff --git a/src/utils.jl b/src/utils.jl index fc6e49d894..962801622a 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -1267,3 +1267,22 @@ function symbol_to_symbolic(sys::AbstractSystem, sym; allsyms = all_symbols(sys) end return sym end + +""" + $(TYPEDSIGNATURES) + +Check if `var` is present in `varlist`. `iv` is the independent variable of the system, +and should be `nothing` if not applicable. +""" +function var_in_varlist(var, varlist::AbstractSet, iv) + var = unwrap(var) + # simple case + return var in varlist || + # indexed array symbolic, unscalarized array present + (iscall(var) && operation(var) === getindex && arguments(var)[1] in varlist) || + # unscalarized sized array symbolic, all scalarized elements present + (symbolic_type(var) == ArraySymbolic() && is_sized_array_symbolic(var) && + all(x -> x in varlist, collect(var))) || + # delayed variables + (isdelay(var, iv) && var_in_varlist(operation(var)(iv), varlist, iv)) +end From 3dd2810c2843a4b3c27edc4cf12d04201bb37561 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 18 Feb 2025 12:45:19 +0530 Subject: [PATCH 0971/1337] test: fix hack test --- test/structural_transformation/utils.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/structural_transformation/utils.jl b/test/structural_transformation/utils.jl index 621aa8b8a8..863e091aad 100644 --- a/test/structural_transformation/utils.jl +++ b/test/structural_transformation/utils.jl @@ -92,7 +92,7 @@ end @mtkbuild sys = ODESystem( [D(y) ~ foo(x), D(x) ~ sum(y), zeros(2) ~ foo(prod(z))], t) @test length(equations(sys)) == 5 - @test length(observed(sys)) == 4 + @test length(observed(sys)) == 2 prob = ODEProblem( sys, [y => ones(2), z => 2ones(2), x => 3.0], (0.0, 1.0), [foo => _tmp_fn2]) @test_nowarn prob.f(prob.u0, prob.p, 0.0) From eb6bc157fdaf542b0fdd01c132f3902d2a835208 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 18 Feb 2025 14:11:06 +0530 Subject: [PATCH 0972/1337] fix: appropriately map array variables to scalarized versions in `add_fallbacks!` --- src/systems/problem_utils.jl | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/systems/problem_utils.jl b/src/systems/problem_utils.jl index be05d8fcff..60650e9ee9 100644 --- a/src/systems/problem_utils.jl +++ b/src/systems/problem_utils.jl @@ -117,6 +117,7 @@ Variables as they are specified in `vars` will take priority over their `toterm` function add_fallbacks!( varmap::AnyDict, vars::Vector, fallbacks::Dict; toterm = default_toterm) missingvars = Set() + arrvars = Set() for var in vars haskey(varmap, var) && continue ttvar = toterm(var) @@ -157,6 +158,7 @@ function add_fallbacks!( fallbacks, arrvar, nothing) get(fallbacks, ttarrvar, nothing) Some(nothing) if val !== nothing val = val[idxs...] + is_sized_array_symbolic(arrvar) && push!(arrvars, arrvar) end else val = nothing @@ -170,6 +172,10 @@ function add_fallbacks!( end end + for arrvar in arrvars + varmap[arrvar] = collect(arrvar) + end + return missingvars end From e735b4f70c0d707141e22658fac2d1d1464f0ba6 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 18 Feb 2025 14:11:28 +0530 Subject: [PATCH 0973/1337] fix: handle unscalarized defaults for scalarized observed variables --- src/systems/problem_utils.jl | 4 ++-- test/initial_values.jl | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/systems/problem_utils.jl b/src/systems/problem_utils.jl index 60650e9ee9..0be41ea6f7 100644 --- a/src/systems/problem_utils.jl +++ b/src/systems/problem_utils.jl @@ -275,9 +275,9 @@ entry for `eq.lhs`, insert the reverse mapping if `eq.rhs` is not a number. """ function add_observed_equations!(varmap::AbstractDict, eqs) for eq in eqs - if haskey(varmap, eq.lhs) + if var_in_varlist(eq.lhs, keys(varmap), nothing) eq.rhs isa Number && continue - haskey(varmap, eq.rhs) && continue + var_in_varlist(eq.rhs, keys(varmap), nothing) && continue !iscall(eq.rhs) || issym(operation(eq.rhs)) || continue varmap[eq.rhs] = eq.lhs else diff --git a/test/initial_values.jl b/test/initial_values.jl index 4543f3549e..2db552c3ba 100644 --- a/test/initial_values.jl +++ b/test/initial_values.jl @@ -1,5 +1,6 @@ using ModelingToolkit using ModelingToolkit: t_nounits as t, D_nounits as D, get_u0 +using OrdinaryDiffEq using SymbolicIndexingInterface: getu @variables x(t)[1:3]=[1.0, 2.0, 3.0] y(t) z(t)[1:2] @@ -183,3 +184,18 @@ end @test_throws ModelingToolkit.MissingParametersError ODEProblem( sys, [x => ones(2)], (0.0, 1.0)) end + +@testset "Unscalarized default for scalarized observed variable" begin + @parameters p[1:4] = rand(4) + @variables x(t)[1:4] y(t)[1:2] + eqs = [ + D(x) ~ x, + y[1] ~ x[3], + y[2] ~ x[4] + ] + @mtkbuild sys = ODESystem(eqs, t; defaults = [x => vcat(ones(2), y), y => x[1:2] ./ 2]) + prob = ODEProblem(sys, [], (0.0, 1.0)) + sol = solve(prob) + @test SciMLBase.successful_retcode(sol) + @test sol.u[1] ≈ [1.0, 1.0, 0.5, 0.5] +end From 6665eb0540e63894040bfd1569eda2cd80ba7576 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 18 Feb 2025 16:34:25 +0530 Subject: [PATCH 0974/1337] feat: allow swapping out argument name function in `array_variable_assignments` --- src/systems/codegen_utils.jl | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/systems/codegen_utils.jl b/src/systems/codegen_utils.jl index 910d0d1c1e..4dce62827d 100644 --- a/src/systems/codegen_utils.jl +++ b/src/systems/codegen_utils.jl @@ -12,8 +12,14 @@ end Given the arguments to `build_function_wrapper`, return a list of assignments which reconstruct array variables if they are present scalarized in `args`. + +# Keyword Arguments + +- `argument_name` a function of the form `(::Int) -> Symbol` which takes the index of + an argument to the generated function and returns the name of the argument in the + generated function. """ -function array_variable_assignments(args...) +function array_variable_assignments(args...; argument_name = generated_argument_name) # map array symbolic to an identically sized array where each element is (buffer_idx, idx_in_buffer) var_to_arridxs = Dict{BasicSymbolic, Array{Tuple{Int, Int}}}() for (i, arg) in enumerate(args) @@ -60,12 +66,12 @@ function array_variable_assignments(args...) end # view and reshape - expr = term(reshape, term(view, generated_argument_name(buffer_idx), idxs), + expr = term(reshape, term(view, argument_name(buffer_idx), idxs), size(arrvar)) else elems = map(idxs) do idx i, j = idx - term(getindex, generated_argument_name(i), j) + term(getindex, argument_name(i), j) end # use `MakeArray` syntax and generate a stack-allocated array expr = term(SymbolicUtils.Code.create_array, SArray, nothing, From b49628d065788e9f0a6269d4ab42d404e30cf68c Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 18 Feb 2025 16:34:42 +0530 Subject: [PATCH 0975/1337] fix: handle scalarized array arguments to `CacheWriter` --- src/systems/nonlinear/nonlinearsystem.jl | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/systems/nonlinear/nonlinearsystem.jl b/src/systems/nonlinear/nonlinearsystem.jl index b026543af8..c68fefa7d1 100644 --- a/src/systems/nonlinear/nonlinearsystem.jl +++ b/src/systems/nonlinear/nonlinearsystem.jl @@ -578,11 +578,19 @@ function CacheWriter(sys::AbstractSystem, buffer_types::Vector{TypeT}, Symbol(:tmp, i) ← SetArray(true, :(out[$i]), get(exprs, T, [])) end + function argument_name(i::Int) + if i <= length(solsyms) + return :($(generated_argument_name(1))[$i]) + end + return generated_argument_name(i - length(solsyms)) + end + array_assignments = array_variable_assignments(solsyms...; argument_name) fn = build_function_wrapper( - sys, nothing, :out, DestructuredArgs(DestructuredArgs.(solsyms)), - DestructuredArgs.(rps)...; p_start = 3, p_end = length(rps) + 2, + sys, nothing, :out, + DestructuredArgs(DestructuredArgs.(solsyms), generated_argument_name(1)), + rps...; p_start = 3, p_end = length(rps) + 2, expression = Val{true}, add_observed = false, - extra_assignments = [obs_assigns; body]) + extra_assignments = [array_assignments; obs_assigns; body]) fn = eval_or_rgf(fn; eval_expression, eval_module) fn = GeneratedFunctionWrapper{(3, 3, is_split(sys))}(fn, nothing) return CacheWriter(fn) From 93387c2ee7319faf168b2302d8a174f906f60271 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 19 Feb 2025 12:09:11 +0530 Subject: [PATCH 0976/1337] fix: fix parameter object aliasing after `remake` --- src/systems/nonlinear/initializesystem.jl | 5 +++-- test/initializationsystem.jl | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/src/systems/nonlinear/initializesystem.jl b/src/systems/nonlinear/initializesystem.jl index 0301276183..81f5af5ddc 100644 --- a/src/systems/nonlinear/initializesystem.jl +++ b/src/systems/nonlinear/initializesystem.jl @@ -512,9 +512,10 @@ end function SciMLBase.late_binding_update_u0_p( prob, sys::AbstractSystem, u0, p, t0, newu0, newp) - u0 === missing && return newu0, newp - eltype(u0) <: Pair || return newu0, newp + u0 === missing && return newu0, (p === missing ? copy(newp) : newp) + eltype(u0) <: Pair || return newu0, (p === missing ? copy(newp) : newp) + newp = p === missing ? copy(newp) : newp newu0 = DiffEqBase.promote_u0(newu0, newp, t0) tunables, repack, alias = SciMLStructures.canonicalize(SciMLStructures.Tunable(), newp) tunables = DiffEqBase.promote_u0(tunables, newu0, t0) diff --git a/test/initializationsystem.jl b/test/initializationsystem.jl index a2348c6647..45afa4163a 100644 --- a/test/initializationsystem.jl +++ b/test/initializationsystem.jl @@ -1370,3 +1370,21 @@ end @test_nowarn remake(prob, p = [p => 1.0]) @test_nowarn remake(prob, p = [p => ForwardDiff.Dual(1.0)]) end + +@testset "`late_binding_update_u0_p` copies `newp`" begin + @parameters k1 k2 + @variables X1(t) X2(t) + @parameters Γ[1:1]=missing [guess = [1.0]] + eqs = [ + D(X1) ~ k1 * (Γ[1] - X1) - k2 * X1 + ] + obs = [X2 ~ Γ[1] - X1] + @mtkbuild osys = ODESystem(eqs, t, [X1, X2], [k1, k2, Γ]; observed = obs) + u0 = [X1 => 1.0, X2 => 2.0] + ps = [k1 => 0.1, k2 => 0.2] + + oprob1 = ODEProblem(osys, u0, 1.0, ps) + oprob2 = remake(oprob1, u0 = [X1 => 10.0]) + integ1 = init(oprob1) + @test integ1[X1] ≈ 1.0 +end From a3b4bdd13e1cb2e76e83673c0c360777c652d17d Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 19 Feb 2025 16:27:58 +0530 Subject: [PATCH 0977/1337] fix: handle DDEs in `GeneratedFunctionWrapper` call in `build_explicit_observed_function` --- src/systems/diffeqs/odesystem.jl | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/systems/diffeqs/odesystem.jl b/src/systems/diffeqs/odesystem.jl index bd40eeb853..1590fd6ebd 100644 --- a/src/systems/diffeqs/odesystem.jl +++ b/src/systems/diffeqs/odesystem.jl @@ -527,12 +527,14 @@ function build_explicit_observed_function(sys, ts; if fns isa Tuple oop, iip = eval_or_rgf.(fns; eval_expression, eval_module) f = GeneratedFunctionWrapper{( - p_start, length(args) - length(ps) + 1, is_split(sys))}(oop, iip) + p_start + is_dde(sys), length(args) - length(ps) + 1 + is_dde(sys), is_split(sys))}( + oop, iip) return return_inplace ? (f, f) : f else f = eval_or_rgf(fns; eval_expression, eval_module) f = GeneratedFunctionWrapper{( - p_start, length(args) - length(ps) + 1, is_split(sys))}(f, nothing) + p_start + is_dde(sys), length(args) - length(ps) + 1 + is_dde(sys), is_split(sys))}( + f, nothing) return f end end From 267eee9b6a3d3aa192ecdeb3d75e2594bbac47c0 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 19 Feb 2025 16:28:11 +0530 Subject: [PATCH 0978/1337] test: fix initialization of inversemodel tests --- test/downstream/inversemodel.jl | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/test/downstream/inversemodel.jl b/test/downstream/inversemodel.jl index 8573964551..a0b72c6b2c 100644 --- a/test/downstream/inversemodel.jl +++ b/test/downstream/inversemodel.jl @@ -157,15 +157,21 @@ nsys = get_named_sensitivity(model, :y; op) # Test that we get the same result w # Test the same thing for comp sensitivities -Sf, simplified_sys = Blocks.get_comp_sensitivity_function(model, :y; op); # This should work without providing an operating opint containing a dummy derivative +# This should work without providing an operating opint containing a dummy derivative +Sf, simplified_sys = Blocks.get_comp_sensitivity_function( + model, :y; op, initialization_solver_alg = NewtonRaphson()); x = state_values(Sf) p = parameter_values(Sf) # If this somehow passes, mention it on # https://github.com/SciML/ModelingToolkit.jl/issues/2786 matrices1 = Sf(x, p, 0) -matrices2, _ = Blocks.get_comp_sensitivity(model, :y; op) # Test that we get the same result when calling the higher-level API +# Test that we get the same result when calling the higher-level API +matrices2, _ = Blocks.get_comp_sensitivity( + model, :y; op, initialization_solver_alg = NewtonRaphson()) @test matrices1.f_x ≈ matrices2.A[1:7, 1:7] -nsys = get_named_comp_sensitivity(model, :y; op) # Test that we get the same result when calling an even higher-level API +# Test that we get the same result when calling an even higher-level API +nsys = get_named_comp_sensitivity( + model, :y; op, initialization_solver_alg = NewtonRaphson()) @test matrices2.A ≈ nsys.A @testset "Issue #3319" begin From f29266420eb8d18c683be80501662f136d73d3e6 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 19 Feb 2025 16:32:09 +0530 Subject: [PATCH 0979/1337] test: fix inversemodel tests Changing state realization in ControlSystemsBase made it unstable, so filter order was reduced. Co-authored-by: Fredrik Bagge Carlson --- test/downstream/inversemodel.jl | 42 ++++++++++----------------------- 1 file changed, 13 insertions(+), 29 deletions(-) diff --git a/test/downstream/inversemodel.jl b/test/downstream/inversemodel.jl index a0b72c6b2c..32d5ee87ec 100644 --- a/test/downstream/inversemodel.jl +++ b/test/downstream/inversemodel.jl @@ -27,20 +27,17 @@ rc = 0.25 # Reference concentration k0 = 1.05e14 ϵ = 34.2894 end - @variables begin gamma(t), [description = "Reaction speed"] xc(t) = c0, [description = "Concentration"] xT(t) = T0, [description = "Temperature"] xT_c(t), [description = "Cooling temperature"] end - @components begin T_c = RealInput() c = RealOutput() T = RealOutput() end - begin τ0 = 60 wk0 = k0 / c0 @@ -57,20 +54,18 @@ rc = 0.25 # Reference concentration gamma ~ xc * wk0 * exp(-wϵ / xT) D(xc) ~ -wa11 * xc - wa12 * gamma + wa13 D(xT) ~ -wa21 * xT + wa22 * gamma + wa23 + wb * xT_c - xc ~ c.u xT ~ T.u xT_c ~ T_c.u end end - begin - Ftf = tf(1, [(100), 1])^3 + Ftf = tf(1, [(100), 1])^2 Fss = ss(Ftf) - # Create an MTK-compatible constructor function RefFilter(; name) sys = ODESystem(Fss; name) + "Compute initial state that yields y0 as output" empty!(ModelingToolkit.get_defaults(sys)) return sys end @@ -82,7 +77,6 @@ end x10 = 0.42 x20 = 0.01 u0 = -0.0224 - c_start = c0 * (1 - x10) # Initial concentration T_start = T0 * (1 + x20) # Initial temperature c_high_start = c0 * (1 - 0.72) # Reference concentration @@ -104,22 +98,13 @@ end @equations begin connect(ref.output, :r, filter.input) connect(filter.output, inverse_tank.c) - connect(inverse_tank.T_c, ff_gain.input) connect(ff_gain.output, :uff, limiter.input) connect(limiter.output, add.input1) - connect(controller.ctr_output, :u, add.input2) - - #connect(add.output, :u_tot, limiter.input) - #connect(limiter.output, :v, tank.T_c) - connect(add.output, :u_tot, tank.T_c) - connect(inverse_tank.T, feedback.input1) - connect(tank.T, :y, noise_filter.input) - connect(noise_filter.output, feedback.input2) connect(feedback.output, :e, controller.err_input) end @@ -128,8 +113,10 @@ end; ssys = structural_simplify(model) cm = complete(model) -op = Dict(D(cm.inverse_tank.xT) => 1, - cm.tank.xc => 0.65) +op = Dict( + cm.filter.y.u => 0.8 * (1 - 0.42), + D(cm.filter.y.u) => 0 +) tspan = (0.0, 1000.0) # https://github.com/SciML/ModelingToolkit.jl/issues/2786 prob = ODEProblem(ssys, op, tspan) @@ -151,37 +138,34 @@ p = parameter_values(Sf) # https://github.com/SciML/ModelingToolkit.jl/issues/2786 matrices1 = Sf(x, p, 0) matrices2, _ = Blocks.get_sensitivity(model, :y; op); # Test that we get the same result when calling the higher-level API -@test matrices1.f_x ≈ matrices2.A[1:7, 1:7] +@test matrices1.f_x ≈ matrices2.A[1:6, 1:6] nsys = get_named_sensitivity(model, :y; op) # Test that we get the same result when calling an even higher-level API @test matrices2.A ≈ nsys.A # Test the same thing for comp sensitivities # This should work without providing an operating opint containing a dummy derivative -Sf, simplified_sys = Blocks.get_comp_sensitivity_function( - model, :y; op, initialization_solver_alg = NewtonRaphson()); +Sf, simplified_sys = Blocks.get_comp_sensitivity_function(model, :y; op); x = state_values(Sf) p = parameter_values(Sf) # If this somehow passes, mention it on # https://github.com/SciML/ModelingToolkit.jl/issues/2786 matrices1 = Sf(x, p, 0) # Test that we get the same result when calling the higher-level API -matrices2, _ = Blocks.get_comp_sensitivity( - model, :y; op, initialization_solver_alg = NewtonRaphson()) -@test matrices1.f_x ≈ matrices2.A[1:7, 1:7] +matrices2, _ = Blocks.get_comp_sensitivity(model, :y; op) +@test matrices1.f_x ≈ matrices2.A[1:6, 1:6] # Test that we get the same result when calling an even higher-level API -nsys = get_named_comp_sensitivity( - model, :y; op, initialization_solver_alg = NewtonRaphson()) +nsys = get_named_comp_sensitivity(model, :y; op) @test matrices2.A ≈ nsys.A @testset "Issue #3319" begin op1 = Dict( - D(cm.inverse_tank.xT) => 1, + cm.filter.y.u => 0.8 * (1 - 0.42), cm.tank.xc => 0.65 ) op2 = Dict( - D(cm.inverse_tank.xT) => 1, + cm.filter.y.u => 0.8 * (1 - 0.42), cm.tank.xc => 0.45 ) From b257e20cd13822577b389cfedbb0b774b87ca355 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 19 Feb 2025 17:48:23 +0530 Subject: [PATCH 0980/1337] refactor: don't do CSE hack for operators Prevents annoying equations with `Initial` parameters --- src/structural_transformation/symbolics_tearing.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/src/structural_transformation/symbolics_tearing.jl b/src/structural_transformation/symbolics_tearing.jl index bf5c51797b..fe3ce45430 100644 --- a/src/structural_transformation/symbolics_tearing.jl +++ b/src/structural_transformation/symbolics_tearing.jl @@ -647,6 +647,7 @@ function cse_and_array_hacks(sys, obs, subeqs, unknowns, neweqs; cse = true, arr # HACK 1 if cse && is_getindexed_array(rhs) rhs_arr = arguments(rhs)[1] + iscall(rhs_arr) && operation(rhs_arr) isa Symbolics.Operator && continue if !haskey(rhs_to_tempvar, rhs_arr) tempvar = gensym(Symbol(lhs)) N = length(rhs_arr) From 0c84697d7c4bdf29c177ead1e117625c505558bf Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 20 Feb 2025 13:16:30 +0530 Subject: [PATCH 0981/1337] fix: filter `nothing` values in `u0map/parammap` in `InitializationProblem` --- src/systems/diffeqs/abstractodesystem.jl | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index 04f7b468d2..9944d92a6e 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -1291,7 +1291,9 @@ function InitializationProblem{iip, specialize}(sys::AbstractSystem, guesses = Dict() end - u0map = merge(ModelingToolkit.guesses(sys), todict(guesses), todict(u0map)) + filter_missing_values!(u0map) + filter_missing_values!(parammap) + u0map = merge(ModelingToolkit.guesses(sys), todict(guesses), u0map) fullmap = merge(u0map, parammap) u0T = Union{} From 7c92d22afd42f50dcfe48b6b4c4897cc230b57b8 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 20 Feb 2025 13:16:45 +0530 Subject: [PATCH 0982/1337] fix: support `CheckInit` in `linearization_function` --- src/linearization.jl | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/linearization.jl b/src/linearization.jl index 469c2cd766..f83efc1642 100644 --- a/src/linearization.jl +++ b/src/linearization.jl @@ -170,8 +170,8 @@ function (linfun::LinearizationFunction)(u, p, t) if u !== nothing # Handle systems without unknowns linfun.num_states == length(u) || error("Number of unknown variables ($(linfun.num_states)) does not match the number of input unknowns ($(length(u)))") - integ_cache = linfun.caches - integ = MockIntegrator{true}(u, p, t, integ_cache) + integ_cache = (linfun.caches,) + integ = MockIntegrator{true}(u, p, t, integ_cache, nothing) u, p, success = SciMLBase.get_initial_values( linfun.prob, integ, fun, linfun.initializealg, Val(true); linfun.initialize_kwargs...) @@ -218,7 +218,7 @@ Mock `DEIntegrator` to allow using `CheckInit` without having to create a new in $(TYPEDFIELDS) """ -struct MockIntegrator{iip, U, P, T, C} <: SciMLBase.DEIntegrator{Nothing, iip, U, T} +struct MockIntegrator{iip, U, P, T, C, O} <: SciMLBase.DEIntegrator{Nothing, iip, U, T} """ The state vector. """ @@ -235,10 +235,14 @@ struct MockIntegrator{iip, U, P, T, C} <: SciMLBase.DEIntegrator{Nothing, iip, U The integrator cache. """ cache::C + """ + Integrator "options" for `CheckInit`. + """ + opts::O end -function MockIntegrator{iip}(u::U, p::P, t::T, cache::C) where {iip, U, P, T, C} - return MockIntegrator{iip, U, P, T, C}(u, p, t, cache) +function MockIntegrator{iip}(u::U, p::P, t::T, cache::C, opts::O) where {iip, U, P, T, C, O} + return MockIntegrator{iip, U, P, T, C, O}(u, p, t, cache, opts) end SymbolicIndexingInterface.state_values(integ::MockIntegrator) = integ.u From 6e15eec40b67eb2855c5649777d37ca15a01d613 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 20 Feb 2025 14:44:54 +0530 Subject: [PATCH 0983/1337] ci: add MTKNeuralNets to downstream CI --- .github/workflows/Downstream.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/Downstream.yml b/.github/workflows/Downstream.yml index 0debdbdc7c..9c5d05e022 100644 --- a/.github/workflows/Downstream.yml +++ b/.github/workflows/Downstream.yml @@ -37,6 +37,7 @@ jobs: - {user: SciML, repo: MethodOfLines.jl, group: Interface} - {user: SciML, repo: MethodOfLines.jl, group: 2D_Diffusion} - {user: SciML, repo: MethodOfLines.jl, group: DAE} + - {user: SciML, repo: ModelingToolkitNeuralNets.jl, group: All} steps: - uses: actions/checkout@v4 - uses: julia-actions/setup-julia@v1 From 069b096c62723567c30ff702b6a860d6cb149df7 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 20 Feb 2025 14:45:14 +0530 Subject: [PATCH 0984/1337] build: bump Symbolics compat --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 0563e28bcf..1ecd1f792f 100644 --- a/Project.toml +++ b/Project.toml @@ -148,7 +148,7 @@ StochasticDelayDiffEq = "1.8.1" StochasticDiffEq = "6.72.1" SymbolicIndexingInterface = "0.3.37" SymbolicUtils = "3.14" -Symbolics = "6.29" +Symbolics = "6.29.1" URIs = "1" UnPack = "0.1, 1.0" Unitful = "1.1" From ceb9cdc7493f8c622fcfb9821437de2d74ef5e18 Mon Sep 17 00:00:00 2001 From: Fredrik Bagge Carlson Date: Fri, 7 Feb 2025 09:20:28 +0100 Subject: [PATCH 0985/1337] add utilities and tests for disturbance modeling Apply suggestions from code review Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> rm plot --- src/systems/analysis_points.jl | 33 ++++ src/systems/diffeqs/odesystem.jl | 13 +- test/downstream/test_disturbance_model.jl | 215 ++++++++++++++++++++++ test/runtests.jl | 1 + 4 files changed, 261 insertions(+), 1 deletion(-) create mode 100644 test/downstream/test_disturbance_model.jl diff --git a/src/systems/analysis_points.jl b/src/systems/analysis_points.jl index 022a0909ed..135bf81729 100644 --- a/src/systems/analysis_points.jl +++ b/src/systems/analysis_points.jl @@ -960,3 +960,36 @@ Compute the (linearized) loop-transfer function in analysis point `ap`, from `ap See also [`get_sensitivity`](@ref), [`get_comp_sensitivity`](@ref), [`open_loop`](@ref). """ get_looptransfer +# + +""" + generate_control_function(sys::ModelingToolkit.AbstractODESystem, input_ap_name::Union{Symbol, Vector{Symbol}, AnalysisPoint, Vector{AnalysisPoint}}, dist_ap_name::Union{Symbol, Vector{Symbol}, AnalysisPoint, Vector{AnalysisPoint}}; system_modifier = identity, kwargs) + +When called with analysis points as input arguments, we assume that all analysis points corresponds to connections that should be opened (broken). The use case for this is to get rid of input signal blocks, such as `Step` or `Sine`, since these are useful for simulation but are not needed when using the plant model in a controller or state estimator. +""" +function generate_control_function( + sys::ModelingToolkit.AbstractODESystem, input_ap_name::Union{ + Symbol, Vector{Symbol}, AnalysisPoint, Vector{AnalysisPoint}}, + dist_ap_name::Union{ + Nothing, Symbol, Vector{Symbol}, AnalysisPoint, Vector{AnalysisPoint}} = nothing; + system_modifier = identity, + kwargs...) + input_ap_name = canonicalize_ap(sys, input_ap_name) + u = [] + for input_ap in input_ap_name + sys, (du, _) = open_loop(sys, input_ap) + push!(u, du) + end + if dist_ap_name === nothing + return ModelingToolkit.generate_control_function(system_modifier(sys), u; kwargs...) + end + + dist_ap_name = canonicalize_ap(sys, dist_ap_name) + d = [] + for dist_ap in dist_ap_name + sys, (du, _) = open_loop(sys, dist_ap) + push!(d, du) + end + + ModelingToolkit.generate_control_function(system_modifier(sys), u, d; kwargs...) +end diff --git a/src/systems/diffeqs/odesystem.jl b/src/systems/diffeqs/odesystem.jl index 1590fd6ebd..20d1e495dc 100644 --- a/src/systems/diffeqs/odesystem.jl +++ b/src/systems/diffeqs/odesystem.jl @@ -436,6 +436,8 @@ an array of inputs `inputs` is given, and `param_only` is false for a time-depen """ function build_explicit_observed_function(sys, ts; inputs = nothing, + disturbance_inputs = nothing, + disturbance_argument = false, expression = false, eval_expression = false, eval_module = @__MODULE__, @@ -512,13 +514,22 @@ function build_explicit_observed_function(sys, ts; ps = setdiff(ps, inputs) # Inputs have been converted to parameters by io_preprocessing, remove those from the parameter list inputs = (inputs,) end + if disturbance_inputs !== nothing + # Disturbance inputs may or may not be included as inputs, depending on disturbance_argument + ps = setdiff(ps, disturbance_inputs) + end + if disturbance_argument + disturbance_inputs = (disturbance_inputs,) + else + disturbance_inputs = () + end ps = reorder_parameters(sys, ps) iv = if is_time_dependent(sys) (get_iv(sys),) else () end - args = (dvs..., inputs..., ps..., iv...) + args = (dvs..., inputs..., ps..., iv..., disturbance_inputs...) p_start = length(dvs) + length(inputs) + 1 p_end = length(dvs) + length(inputs) + length(ps) fns = build_function_wrapper( diff --git a/test/downstream/test_disturbance_model.jl b/test/downstream/test_disturbance_model.jl new file mode 100644 index 0000000000..10fcf9fc1f --- /dev/null +++ b/test/downstream/test_disturbance_model.jl @@ -0,0 +1,215 @@ +#= +This file implements and tests a typical workflow for state estimation with disturbance models +The primary subject of the tests is the analysis-point features and the +analysis-point specific method for `generate_control_function`. +=# +using ModelingToolkit, OrdinaryDiffEq, LinearAlgebra, Test +using ModelingToolkitStandardLibrary.Mechanical.Rotational +using ModelingToolkitStandardLibrary.Blocks +using ModelingToolkit: connect +# using Plots + +using ModelingToolkit: t_nounits as t, D_nounits as D + +indexof(sym, syms) = findfirst(isequal(sym), syms) + +## Build the system model ====================================================== +@mtkmodel SystemModel begin + @parameters begin + m1 = 1 + m2 = 1 + k = 10 # Spring stiffness + c = 3 # Damping coefficient + end + @components begin + inertia1 = Inertia(; J = m1, phi = 0, w = 0) + inertia2 = Inertia(; J = m2, phi = 0, w = 0) + spring = Spring(; c = k) + damper = Damper(; d = c) + torque = Torque(use_support = false) + end + @equations begin + connect(torque.flange, inertia1.flange_a) + connect(inertia1.flange_b, spring.flange_a, damper.flange_a) + connect(inertia2.flange_a, spring.flange_b, damper.flange_b) + end +end + +@mtkmodel ModelWithInputs begin + @components begin + input_signal = Blocks.Sine(frequency = 1, amplitude = 1) + disturbance_signal1 = Blocks.Constant(k = 0) + disturbance_signal2 = Blocks.Constant(k = 0) + disturbance_torque1 = Torque(use_support = false) + disturbance_torque2 = Torque(use_support = false) + system_model = SystemModel() + end + @equations begin + connect(input_signal.output, :u, system_model.torque.tau) + connect(disturbance_signal1.output, :d1, disturbance_torque1.tau) + connect(disturbance_signal2.output, :d2, disturbance_torque2.tau) + connect(disturbance_torque1.flange, system_model.inertia1.flange_b) + connect(disturbance_torque2.flange, system_model.inertia2.flange_b) + end +end + +@named model = ModelWithInputs() # Model with load disturbance +ssys = structural_simplify(model) +prob = ODEProblem(ssys, [], (0.0, 10.0)) +sol = solve(prob, Tsit5()) +# plot(sol) + +## +using ControlSystemsBase, ControlSystemsMTK +cmodel = complete(model) +P = cmodel.system_model +lsys = named_ss( + model, [:u, :d1], [P.inertia1.phi, P.inertia2.phi, P.inertia1.w, P.inertia2.w]) + +## +# If we now want to add a disturbance model, we cannot do that since we have already connected a constant to the disturbance input. We have also already used the name `d` for an analysis point, but that might not be an issue since we create an outer model and get a new namespace. + +s = tf("s") +dist(; name) = ODESystem(1 / s; name) + +@mtkmodel SystemModelWithDisturbanceModel begin + @components begin + input_signal = Blocks.Sine(frequency = 1, amplitude = 1) + disturbance_signal1 = Blocks.Constant(k = 0) + disturbance_signal2 = Blocks.Constant(k = 0) + disturbance_torque1 = Torque(use_support = false) + disturbance_torque2 = Torque(use_support = false) + disturbance_model = dist() + system_model = SystemModel() + end + @equations begin + connect(input_signal.output, :u, system_model.torque.tau) + connect(disturbance_signal1.output, :d1, disturbance_model.input) + connect(disturbance_model.output, disturbance_torque1.tau) + connect(disturbance_signal2.output, :d2, disturbance_torque2.tau) + connect(disturbance_torque1.flange, system_model.inertia1.flange_b) + connect(disturbance_torque2.flange, system_model.inertia2.flange_b) + end +end + +@named model_with_disturbance = SystemModelWithDisturbanceModel() +# ssys = structural_simplify(open_loop(model_with_disturbance, :d)) # Open loop worked, but it's a bit awkward that we have to use it here +# lsys2 = named_ss(model_with_disturbance, [:u, :d1], +# [P.inertia1.phi, P.inertia2.phi, P.inertia1.w, P.inertia2.w]) +ssys = structural_simplify(model_with_disturbance) +prob = ODEProblem(ssys, [], (0.0, 10.0)) +sol = solve(prob, Tsit5()) +@test SciMLBase.successful_retcode(sol) +# plot(sol) + +## +# Now we only have an integrating disturbance affecting inertia1, what if we want both integrating and direct Gaussian? We'd need a "PI controller" disturbancemodel. If we add the disturbance model (s+1)/s we get the integrating and non-integrating noises being correlated which is fine, it reduces the dimensions of the sigma point by 1. + +dist3(; name) = ODESystem(ss(1 + 10 / s, balance = false); name) + +@mtkmodel SystemModelWithDisturbanceModel begin + @components begin + input_signal = Blocks.Sine(frequency = 1, amplitude = 1) + disturbance_signal1 = Blocks.Constant(k = 0) + disturbance_signal2 = Blocks.Constant(k = 0) + disturbance_torque1 = Torque(use_support = false) + disturbance_torque2 = Torque(use_support = false) + disturbance_model = dist3() + system_model = SystemModel() + + y = Blocks.Add() + angle_sensor = AngleSensor() + output_disturbance = Blocks.Constant(k = 0) + end + @equations begin + connect(input_signal.output, :u, system_model.torque.tau) + connect(disturbance_signal1.output, :d1, disturbance_model.input) + connect(disturbance_model.output, disturbance_torque1.tau) + connect(disturbance_signal2.output, :d2, disturbance_torque2.tau) + connect(disturbance_torque1.flange, system_model.inertia1.flange_b) + connect(disturbance_torque2.flange, system_model.inertia2.flange_b) + + connect(system_model.inertia1.flange_b, angle_sensor.flange) + connect(angle_sensor.phi, y.input1) + connect(output_disturbance.output, :dy, y.input2) + end +end + +@named model_with_disturbance = SystemModelWithDisturbanceModel() +# ssys = structural_simplify(open_loop(model_with_disturbance, :d)) # Open loop worked, but it's a bit awkward that we have to use it here +# lsys3 = named_ss(model_with_disturbance, [:u, :d1], +# [P.inertia1.phi, P.inertia2.phi, P.inertia1.w, P.inertia2.w]) +ssys = structural_simplify(model_with_disturbance) +prob = ODEProblem(ssys, [], (0.0, 10.0)) +sol = solve(prob, Tsit5()) +@test SciMLBase.successful_retcode(sol) +# plot(sol) + +## Generate function for an augmented Unscented Kalman Filter ===================== +# temp = open_loop(model_with_disturbance, :d) +outputs = [P.inertia1.phi, P.inertia2.phi, P.inertia1.w, P.inertia2.w] +(f_oop1, f_ip), x_sym, p_sym, io_sys = ModelingToolkit.generate_control_function( + model_with_disturbance, [:u], [:d1, :d2, :dy], split = false) + +(f_oop2, f_ip2), x_sym, p_sym, io_sys = ModelingToolkit.generate_control_function( + model_with_disturbance, [:u], [:d1, :d2, :dy], + disturbance_argument = true, split = false) + +measurement = ModelingToolkit.build_explicit_observed_function( + io_sys, outputs, inputs = ModelingToolkit.inputs(io_sys)[1:1]) +measurement2 = ModelingToolkit.build_explicit_observed_function( + io_sys, [io_sys.y.output.u], inputs = ModelingToolkit.inputs(io_sys)[1:1], + disturbance_inputs = ModelingToolkit.inputs(io_sys)[2:end], + disturbance_argument = true) + +op = ModelingToolkit.inputs(io_sys) .=> 0 +x0, p = ModelingToolkit.get_u0_p(io_sys, op, op) +x = zeros(5) +u = zeros(1) +d = zeros(3) +@test f_oop2(x, u, p, t, d) == zeros(5) +@test measurement(x, u, p, 0.0) == [0, 0, 0, 0] +@test measurement2(x, u, p, 0.0, d) == [0] + +# Add to the integrating disturbance input +d = [1, 0, 0] +@test sort(f_oop2(x, u, p, 0.0, d)) == [0, 0, 0, 1, 1] # Affects disturbance state and one velocity +@test measurement2(x, u, p, 0.0, d) == [0] + +d = [0, 1, 0] +@test sort(f_oop2(x, u, p, 0.0, d)) == [0, 0, 0, 0, 1] # Affects one velocity +@test measurement(x, u, p, 0.0) == [0, 0, 0, 0] +@test measurement2(x, u, p, 0.0, d) == [0] + +d = [0, 0, 1] +@test sort(f_oop2(x, u, p, 0.0, d)) == [0, 0, 0, 0, 0] # Affects nothing +@test measurement(x, u, p, 0.0) == [0, 0, 0, 0] +@test measurement2(x, u, p, 0.0, d) == [1] # We have now disturbed the output + +## Further downstream tests that the functions generated above actually have the properties required to use for state estimation +# +# using LowLevelParticleFilters, SeeToDee +# Ts = 0.001 +# discrete_dynamics = SeeToDee.Rk4(f_oop2, Ts) +# nx = length(x_sym) +# nu = 1 +# nw = 2 +# ny = length(outputs) +# R1 = Diagonal([1e-5, 1e-5]) +# R2 = 0.1 * I(ny) +# op = ModelingToolkit.inputs(io_sys) .=> 0 +# x0, p = ModelingToolkit.get_u0_p(io_sys, op, op) +# d0 = LowLevelParticleFilters.SimpleMvNormal(x0, 10.0I(nx)) +# measurement_model = UKFMeasurementModel{Float64, false, false}(measurement, R2; nx, ny) +# kf = UnscentedKalmanFilter{false, false, true, false}( +# discrete_dynamics, measurement_model, R1, d0; nu, Ts, p) + +# tvec = 0:Ts:sol.t[end] +# u = vcat.(Array(sol(tvec, idxs = P.torque.tau.u))) +# y = collect.(eachcol(Array(sol(tvec, idxs = outputs)) .+ 1e-2 .* randn.())) + +# inds = 1:5805 +# res = forward_trajectory(kf, u, y) + +# plot(res, size = (1000, 1000)); +# plot!(sol, idxs = x_sym, sp = (1:nx)', l = :dash); diff --git a/test/runtests.jl b/test/runtests.jl index 966b02cacb..11c78e43ca 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -120,6 +120,7 @@ end @safetestset "Linearization Dummy Derivative Tests" include("downstream/linearization_dd.jl") @safetestset "Inverse Models Test" include("downstream/inversemodel.jl") @safetestset "Analysis Points Test" include("downstream/analysis_points.jl") + @safetestset "Analysis Points Test" include("downstream/test_disturbance_model.jl") end if GROUP == "All" || GROUP == "FMI" From 43172c9ec98c383d42fe58a1c81754b3ce7b45a8 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 21 Feb 2025 11:23:37 +0530 Subject: [PATCH 0986/1337] build: bump patch version --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 1ecd1f792f..ba47d83eb6 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "ModelingToolkit" uuid = "961ee093-0014-501f-94e3-6117800e7a78" authors = ["Yingbo Ma ", "Chris Rackauckas and contributors"] -version = "9.64.0" +version = "9.64.1" [deps] AbstractTrees = "1520ce14-60c1-5f80-bbc7-55ef81b5835c" From 56b74353dec77231dc3f671c7358bba2a4e23734 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 14 Feb 2025 17:43:32 +0530 Subject: [PATCH 0987/1337] refactor: remove `MTKHomotopyContinuationExt` --- Project.toml | 3 - ext/MTKHomotopyContinuationExt.jl | 225 ------------------------------ 2 files changed, 228 deletions(-) delete mode 100644 ext/MTKHomotopyContinuationExt.jl diff --git a/Project.toml b/Project.toml index ba47d83eb6..3196235379 100644 --- a/Project.toml +++ b/Project.toml @@ -65,7 +65,6 @@ BifurcationKit = "0f109fa4-8a5d-4b75-95aa-f515264e7665" ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4" DeepDiffs = "ab62b9b5-e342-54a8-a765-a90f495de1a6" FMI = "14a09403-18e3-468f-ad8a-74f8dda2d9ac" -HomotopyContinuation = "f213a82b-91d6-5c5d-acf7-10f1c761b327" InfiniteOpt = "20393b10-9daf-11e9-18c9-8db751c92c57" LabelledArrays = "2ee39098-c373-598a-b85f-a56591580800" @@ -74,7 +73,6 @@ MTKBifurcationKitExt = "BifurcationKit" MTKChainRulesCoreExt = "ChainRulesCore" MTKDeepDiffsExt = "DeepDiffs" MTKFMIExt = "FMI" -MTKHomotopyContinuationExt = "HomotopyContinuation" MTKInfiniteOptExt = "InfiniteOpt" MTKLabelledArraysExt = "LabelledArrays" @@ -110,7 +108,6 @@ ForwardDiff = "0.10.3" FunctionWrappers = "1.1" FunctionWrappersWrappers = "0.1" Graphs = "1.5.2" -HomotopyContinuation = "2.11" InfiniteOpt = "0.5" InteractiveUtils = "1" JuliaFormatter = "1.0.47" diff --git a/ext/MTKHomotopyContinuationExt.jl b/ext/MTKHomotopyContinuationExt.jl deleted file mode 100644 index 8f17c05b18..0000000000 --- a/ext/MTKHomotopyContinuationExt.jl +++ /dev/null @@ -1,225 +0,0 @@ -module MTKHomotopyContinuationExt - -using ModelingToolkit -using ModelingToolkit.SciMLBase -using ModelingToolkit.Symbolics: unwrap, symtype, BasicSymbolic, simplify_fractions -using ModelingToolkit.SymbolicIndexingInterface -using ModelingToolkit.DocStringExtensions -using HomotopyContinuation -using ModelingToolkit: iscomplete, parameters, has_index_cache, get_index_cache, get_u0, - get_u0_p, check_eqs_u0, CommonSolve - -const MTK = ModelingToolkit - -""" -$(TYPEDSIGNATURES) - -Convert `expr` from a symbolics expression to one that uses `HomotopyContinuation.ModelKit`. -""" -function symbolics_to_hc(expr) - if iscall(expr) - if operation(expr) == getindex - args = arguments(expr) - return ModelKit.Variable(getname(args[1]), args[2:end]...) - else - return operation(expr)(symbolics_to_hc.(arguments(expr))...) - end - elseif symbolic_type(expr) == NotSymbolic() - return expr - else - return ModelKit.Variable(getname(expr)) - end -end - -""" -$(TYPEDEF) - -A subtype of `HomotopyContinuation.AbstractSystem` used to solve `HomotopyContinuationProblem`s. -""" -struct MTKHomotopySystem{F, P, J, V} <: HomotopyContinuation.AbstractSystem - """ - The generated function for the residual of the polynomial system. In-place. - """ - f::F - """ - The parameter object. - """ - p::P - """ - The generated function for the jacobian of the polynomial system. In-place. - """ - jac::J - """ - The `HomotopyContinuation.ModelKit.Variable` representation of the unknowns of - the system. - """ - vars::V - """ - The number of polynomials in the system. Must also be equal to `length(vars)`. - """ - nexprs::Int -end - -Base.size(sys::MTKHomotopySystem) = (sys.nexprs, length(sys.vars)) -ModelKit.variables(sys::MTKHomotopySystem) = sys.vars - -function (sys::MTKHomotopySystem)(x, p = nothing) - sys.f(x, sys.p) -end - -function ModelKit.evaluate!(u, sys::MTKHomotopySystem, x, p = nothing) - sys.f(u, x, sys.p) -end - -function ModelKit.evaluate_and_jacobian!(u, U, sys::MTKHomotopySystem, x, p = nothing) - sys.f(u, x, sys.p) - sys.jac(U, x, sys.p) -end - -SymbolicIndexingInterface.parameter_values(s::MTKHomotopySystem) = s.p - -""" - $(TYPEDSIGNATURES) - -Create a `HomotopyContinuationProblem` from a `NonlinearSystem` with polynomial equations. -The problem will be solved by HomotopyContinuation.jl. The resultant `NonlinearSolution` -will contain the polynomial root closest to the point specified by `u0map` (if real roots -exist for the system). - -Keyword arguments: -- `eval_expression`: Whether to `eval` the generated functions or use a `RuntimeGeneratedFunction`. -- `eval_module`: The module to use for `eval`/`@RuntimeGeneratedFunction` -- `warn_parametric_exponent`: Whether to warn if the system contains a parametric - exponent preventing the homotopy from being cached. - -All other keyword arguments are forwarded to `HomotopyContinuation.solver_startsystems`. -""" -function MTK.HomotopyContinuationProblem( - sys::NonlinearSystem, u0map, parammap = nothing; kwargs...) - prob = MTK._safe_HomotopyContinuationProblem(sys, u0map, parammap; kwargs...) - prob isa MTK.HomotopyContinuationProblem || throw(prob) - return prob -end - -function MTK._safe_HomotopyContinuationProblem(sys, u0map, parammap = nothing; - fraction_cancel_fn = SymbolicUtils.simplify_fractions, kwargs...) - if !iscomplete(sys) - error("A completed `NonlinearSystem` is required. Call `complete` or `structural_simplify` on the system before creating a `HomotopyContinuationProblem`") - end - transformation = MTK.PolynomialTransformation(sys) - if transformation isa MTK.NotPolynomialError - return transformation - end - result = MTK.transform_system(sys, transformation; fraction_cancel_fn) - if result isa MTK.NotPolynomialError - return result - end - MTK.HomotopyContinuationProblem(sys, transformation, result, u0map, parammap; kwargs...) -end - -function MTK.HomotopyContinuationProblem( - sys::MTK.NonlinearSystem, transformation::MTK.PolynomialTransformation, - result::MTK.PolynomialTransformationResult, u0map, - parammap = nothing; eval_expression = false, - eval_module = ModelingToolkit, warn_parametric_exponent = true, kwargs...) - sys2 = result.sys - denoms = result.denominators - polydata = transformation.polydata - new_dvs = transformation.new_dvs - all_solutions = transformation.all_solutions - - _, u0, p = MTK.process_SciMLProblem( - MTK.EmptySciMLFunction, sys, u0map, parammap; eval_expression, eval_module) - nlfn = NonlinearFunction{true}(sys2; jac = true, eval_expression, eval_module) - - denominator = MTK.build_explicit_observed_function(sys2, denoms) - unpack_solution = MTK.build_explicit_observed_function(sys2, all_solutions) - - hvars = symbolics_to_hc.(new_dvs) - mtkhsys = MTKHomotopySystem(nlfn.f, p, nlfn.jac, hvars, length(new_dvs)) - - obsfn = MTK.ObservedFunctionCache(sys; eval_expression, eval_module) - - has_parametric_exponents = any(d -> d.has_parametric_exponent, polydata) - if has_parametric_exponents - if warn_parametric_exponent - @warn """ - The system has parametric exponents, preventing caching of the homotopy. \ - This will cause `solve` to be slower. Pass `warn_parametric_exponent \ - = false` to turn off this warning - """ - end - solver_and_starts = nothing - else - solver_and_starts = HomotopyContinuation.solver_startsolutions(mtkhsys; kwargs...) - end - return MTK.HomotopyContinuationProblem( - u0, mtkhsys, denominator, sys, obsfn, solver_and_starts, unpack_solution) -end - -""" -$(TYPEDSIGNATURES) - -Solve a `HomotopyContinuationProblem`. Ignores the algorithm passed to it, and always -uses `HomotopyContinuation.jl`. The original solution as returned by -`HomotopyContinuation.jl` will be available in the `.original` field of the returned -`NonlinearSolution`. - -All keyword arguments except the ones listed below are forwarded to -`HomotopyContinuation.solve`. Note that the solver and start solutions are precomputed, -and only keyword arguments related to the solve process are valid. All keyword -arguments have their default values in HomotopyContinuation.jl, except `show_progress` -which defaults to `false`. - -Extra keyword arguments: -- `denominator_abstol`: In case `prob` is solving a rational function, roots which cause - the denominator to be below `denominator_abstol` will be discarded. -""" -function CommonSolve.solve(prob::MTK.HomotopyContinuationProblem, - alg = nothing; show_progress = false, denominator_abstol = 1e-7, kwargs...) - if prob.solver_and_starts === nothing - sol = HomotopyContinuation.solve( - prob.homotopy_continuation_system; show_progress, kwargs...) - else - solver, starts = prob.solver_and_starts - sol = HomotopyContinuation.solve(solver, starts; show_progress, kwargs...) - end - realsols = HomotopyContinuation.results(sol; only_real = true) - if isempty(realsols) - u = state_values(prob) - retcode = SciMLBase.ReturnCode.ConvergenceFailure - resid = prob.homotopy_continuation_system(u) - else - T = eltype(state_values(prob)) - distance = T(Inf) - u = state_values(prob) - resid = nothing - for result in realsols - if any(<=(denominator_abstol), - prob.denominator(real.(result.solution), parameter_values(prob))) - continue - end - for truesol in prob.unpack_solution(result.solution, parameter_values(prob)) - dist = norm(truesol - state_values(prob)) - if dist < distance - distance = dist - u = T.(real.(truesol)) - resid = T.(real.(prob.homotopy_continuation_system(result.solution))) - end - end - end - # all roots cause denominator to be zero - if isinf(distance) - u = state_values(prob) - resid = prob.homotopy_continuation_system(u) - retcode = SciMLBase.ReturnCode.Infeasible - else - retcode = SciMLBase.ReturnCode.Success - end - end - - return SciMLBase.build_solution( - prob, :HomotopyContinuation, u, resid; retcode, original = sol) -end - -end From ef5e7a00b3c22f051b2f53fc1a66a92588c2a9f2 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 14 Feb 2025 17:43:54 +0530 Subject: [PATCH 0988/1337] refactor: remove `use_homotopy_continuation` keyword from `NonlinearProblem` --- src/systems/nonlinear/nonlinearsystem.jl | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/systems/nonlinear/nonlinearsystem.jl b/src/systems/nonlinear/nonlinearsystem.jl index c68fefa7d1..73484eb0dd 100644 --- a/src/systems/nonlinear/nonlinearsystem.jl +++ b/src/systems/nonlinear/nonlinearsystem.jl @@ -512,17 +512,10 @@ end function DiffEqBase.NonlinearProblem{iip}(sys::NonlinearSystem, u0map, parammap = DiffEqBase.NullParameters(); - check_length = true, use_homotopy_continuation = false, kwargs...) where {iip} + check_length = true, kwargs...) where {iip} if !iscomplete(sys) error("A completed `NonlinearSystem` is required. Call `complete` or `structural_simplify` on the system before creating a `NonlinearProblem`") end - if use_homotopy_continuation - prob = safe_HomotopyContinuationProblem( - sys, u0map, parammap; check_length, kwargs...) - if prob isa HomotopyContinuationProblem - return prob - end - end f, u0, p = process_SciMLProblem(NonlinearFunction{iip}, sys, u0map, parammap; check_length, kwargs...) pt = something(get_metadata(sys), StandardNonlinearProblem()) From 18029477936545a07ab95da6f8cb2f296bc9a1be Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 14 Feb 2025 17:44:36 +0530 Subject: [PATCH 0989/1337] refactor: rewrite `HomotopyContinuationProblem` to target NonlinearSolveHomotopyContinuation.jl --- .../nonlinear/homotopy_continuation.jl | 179 +++++++++--------- test/extensions/homotopy_continuation.jl | 114 ++++++----- 2 files changed, 142 insertions(+), 151 deletions(-) diff --git a/src/systems/nonlinear/homotopy_continuation.jl b/src/systems/nonlinear/homotopy_continuation.jl index 09e11199e8..474224eacd 100644 --- a/src/systems/nonlinear/homotopy_continuation.jl +++ b/src/systems/nonlinear/homotopy_continuation.jl @@ -1,93 +1,3 @@ -""" -$(TYPEDEF) - -A type of Nonlinear problem which specializes on polynomial systems and uses -HomotopyContinuation.jl to solve the system. Requires importing HomotopyContinuation.jl to -create and solve. -""" -struct HomotopyContinuationProblem{uType, H, D, O, SS, U} <: - SciMLBase.AbstractNonlinearProblem{uType, true} - """ - The initial values of states in the system. If there are multiple real roots of - the system, the one closest to this point is returned. - """ - u0::uType - """ - A subtype of `HomotopyContinuation.AbstractSystem` to solve. Also contains the - parameter object. - """ - homotopy_continuation_system::H - """ - A function with signature `(u, p) -> resid`. In case of rational functions, this - is used to rule out roots of the system which would cause the denominator to be - zero. - """ - denominator::D - """ - The `NonlinearSystem` used to create this problem. Used for symbolic indexing. - """ - sys::NonlinearSystem - """ - A function which generates and returns observed expressions for the given system. - """ - obsfn::O - """ - The HomotopyContinuation.jl solver and start system, obtained through - `HomotopyContinuation.solver_startsystems`. - """ - solver_and_starts::SS - """ - A function which takes a solution of the transformed system, and returns a vector - of solutions for the original system. This is utilized when converting systems - to polynomials. - """ - unpack_solution::U -end - -function HomotopyContinuationProblem(::AbstractSystem, _u0, _p; kwargs...) - error("HomotopyContinuation.jl is required to create and solve `HomotopyContinuationProblem`s. Please run `Pkg.add(\"HomotopyContinuation\")` to continue.") -end - -""" - $(TYPEDSIGNATURES) - -Utility function for `safe_HomotopyContinuationProblem`, implemented in the extension. -""" -function _safe_HomotopyContinuationProblem end - -""" - $(TYPEDSIGNATURES) - -Return a `HomotopyContinuationProblem` if the extension is loaded and the system is -polynomial. If the extension is not loaded, return `nothing`. If the system is not -polynomial, return the appropriate `NotPolynomialError`. -""" -function safe_HomotopyContinuationProblem(sys::NonlinearSystem, args...; kwargs...) - if Base.get_extension(ModelingToolkit, :MTKHomotopyContinuationExt) === nothing - return nothing - end - return _safe_HomotopyContinuationProblem(sys, args...; kwargs...) -end - -SymbolicIndexingInterface.symbolic_container(p::HomotopyContinuationProblem) = p.sys -SymbolicIndexingInterface.state_values(p::HomotopyContinuationProblem) = p.u0 -function SymbolicIndexingInterface.set_state!(p::HomotopyContinuationProblem, args...) - set_state!(p.u0, args...) -end -function SymbolicIndexingInterface.parameter_values(p::HomotopyContinuationProblem) - parameter_values(p.homotopy_continuation_system) -end -function SymbolicIndexingInterface.set_parameter!(p::HomotopyContinuationProblem, args...) - set_parameter!(parameter_values(p), args...) -end -function SymbolicIndexingInterface.observed(p::HomotopyContinuationProblem, sym) - if p.obsfn !== nothing - return p.obsfn(sym) - else - return SymbolicIndexingInterface.observed(p.sys, sym) - end -end - function contains_variable(x, wrt) any(y -> occursin(y, x), wrt) end @@ -562,3 +472,92 @@ function handle_rational_polynomials(x, wrt; fraction_cancel_fn = simplify_fract end return num, den end + +function SciMLBase.HomotopyNonlinearFunction(sys::NonlinearSystem, args...; kwargs...) + ODEFunction{true}(sys, args...; kwargs...) +end + +function SciMLBase.HomotopyNonlinearFunction{true}(sys::NonlinearSystem, args...; + kwargs...) + ODEFunction{true, SciMLBase.AutoSpecialize}(sys, args...; kwargs...) +end + +function SciMLBase.HomotopyNonlinearFunction{false}(sys::NonlinearSystem, args...; + kwargs...) + ODEFunction{false, SciMLBase.FullSpecialize}(sys, args...; kwargs...) +end + +function SciMLBase.HomotopyNonlinearFunction{iip, specialize}( + sys::NonlinearSystem, args...; eval_expression = false, eval_module = @__MODULE__, + p = nothing, fraction_cancel_fn = SymbolicUtils.simplify_fractions, + kwargs...) where {iip, specialize} + if !iscomplete(sys) + error("A completed `NonlinearSystem` is required. Call `complete` or `structural_simplify` on the system before creating a `HomotopyContinuationFunction`") + end + transformation = PolynomialTransformation(sys) + if transformation isa NotPolynomialError + throw(transformation) + end + result = transform_system(sys, transformation; fraction_cancel_fn) + if result isa NotPolynomialError + throw(result) + end + + sys2 = result.sys + denoms = result.denominators + polydata = transformation.polydata + new_dvs = transformation.new_dvs + all_solutions = transformation.all_solutions + + # we want to create f, jac etc. according to `sys2` since that will do the solving + # but the `sys` inside for symbolic indexing should be the non-polynomial system + fn = NonlinearFunction{iip}(sys2; eval_expression, eval_module, kwargs...) + obsfn = ObservedFunctionCache( + sys; eval_expression, eval_module, checkbounds = get(kwargs, :checkbounds, false)) + fn = remake(fn; sys = sys, observed = obsfn) + + denominator = build_explicit_observed_function(sys2, denoms) + unpolynomialize = build_explicit_observed_function(sys2, all_solutions) + + inv_mapping = Dict(v => k for (k, v) in transformation.substitution_rules) + polynomialize_terms = [get(inv_mapping, var, var) for var in unknowns(sys2)] + polynomialize = build_explicit_observed_function(sys, polynomialize_terms) + + return HomotopyNonlinearFunction{iip, specialize}( + fn; polynomialize, unpolynomialize, denominator) +end + +struct HomotopyContinuationProblem{iip, specialization} end + +function HomotopyContinuationProblem(sys::NonlinearSystem, args...; kwargs...) + HomotopyContinuationProblem{true}(sys, args...; kwargs...) +end + +function HomotopyContinuationProblem(sys::NonlinearSystem, t, + u0map::StaticArray, + args...; + kwargs...) + HomotopyContinuationProblem{false, SciMLBase.FullSpecialize}( + sys, t, u0map, args...; kwargs...) +end + +function HomotopyContinuationProblem{true}(sys::NonlinearSystem, args...; kwargs...) + HomotopyContinuationProblem{true, SciMLBase.AutoSpecialize}(sys, args...; kwargs...) +end + +function HomotopyContinuationProblem{false}(sys::NonlinearSystem, args...; kwargs...) + HomotopyContinuationProblem{false, SciMLBase.FullSpecialize}(sys, args...; kwargs...) +end + +function HomotopyContinuationProblem{iip, spec}( + sys::NonlinearSystem, u0map, pmap = SciMLBase.NullParameters(); + kwargs...) where {iip, spec} + if !iscomplete(sys) + error("A completed `NonlinearSystem` is required. Call `complete` or `structural_simplify` on the system before creating a `HomotopyContinuationProblem`") + end + f, u0, p = process_SciMLProblem( + HomotopyNonlinearFunction{iip, spec}, sys, u0map, pmap; kwargs...) + + kwargs = filter_kwargs(kwargs) + return NonlinearProblem{iip}(f, u0, p; kwargs...) +end diff --git a/test/extensions/homotopy_continuation.jl b/test/extensions/homotopy_continuation.jl index 9e15ea857e..ed630ea8e3 100644 --- a/test/extensions/homotopy_continuation.jl +++ b/test/extensions/homotopy_continuation.jl @@ -1,20 +1,32 @@ -using ModelingToolkit, NonlinearSolve, SymbolicIndexingInterface +using ModelingToolkit, NonlinearSolve, NonlinearSolveHomotopyContinuation, + SymbolicIndexingInterface using SymbolicUtils import ModelingToolkit as MTK using LinearAlgebra using Test -@testset "Safe HCProblem" begin - @variables x y z - eqs = [0 ~ x^2 + y^2 + 2x * y - 0 ~ x^2 + 4x + 4 - 0 ~ y * z + 4x^2] - @mtkbuild sys = NonlinearSystem(eqs) - prob = MTK.safe_HomotopyContinuationProblem(sys, [x => 1.0, y => 1.0, z => 1.0], []) - @test prob === nothing +allrootsalg = HomotopyContinuationJL{true}(; threading = false) +singlerootalg = HomotopyContinuationJL{false}(; threading = false) + +function test_single_root(sol; atol = 1e-10) + @test SciMLBase.successful_retcode(sol) + @test norm(sol.resid)≈0.0 atol=atol end -import HomotopyContinuation +function test_all_roots(sol; atol = 1e-10) + @test sol.converged + for nlsol in sol.u + @test SciMLBase.successful_retcode(nlsol) + @test norm(nlsol.resid)≈0.0 atol=1e-10 + end +end + +function solve_allroots_closest(prob) + sol = solve(prob, allrootsalg) + return argmin(sol.u) do nlsol + return norm(nlsol.u - prob.u0) + end +end @testset "No parameters" begin @variables x y z @@ -24,19 +36,13 @@ import HomotopyContinuation @mtkbuild sys = NonlinearSystem(eqs) u0 = [x => 1.0, y => 1.0, z => 1.0] prob = HomotopyContinuationProblem(sys, u0) + @test prob isa NonlinearProblem @test prob[x] == prob[y] == prob[z] == 1.0 @test prob[x + y] == 2.0 - sol = solve(prob; threading = false) - @test SciMLBase.successful_retcode(sol) - @test norm(sol.resid)≈0.0 atol=1e-10 - - prob2 = NonlinearProblem(sys, u0; use_homotopy_continuation = true) - @test prob2 isa HomotopyContinuationProblem - sol = solve(prob2; threading = false) - @test SciMLBase.successful_retcode(sol) - @test norm(sol.resid)≈0.0 atol=1e-10 - - @test NonlinearProblem(sys, u0; use_homotopy_continuation = false) isa NonlinearProblem + sol = solve(prob, singlerootalg) + test_single_root(sol) + sol = solve(prob, allrootsalg) + test_all_roots(sol) end struct Wrapper @@ -61,9 +67,10 @@ end @test prob.ps[q] == 4 @test prob.ps[r].x == [1.0 1.0; 0.0 0.0] @test prob.ps[p * q] == 8.0 - sol = solve(prob; threading = false) - @test SciMLBase.successful_retcode(sol) - @test norm(sol.resid)≈0.0 atol=1e-10 + sol = solve(prob, singlerootalg) + test_single_root(sol) + sol = solve(prob, allrootsalg) + test_all_roots(sol) end @testset "Array variables" begin @@ -79,7 +86,7 @@ end @test prob[x] == 2ones(3) prob.ps[p] = [2, 3, 4] @test prob.ps[p] == [2, 3, 4] - sol = @test_nowarn solve(prob; threading = false) + sol = @test_nowarn solve(prob, singlerootalg) @test sol.retcode == ReturnCode.ConvergenceFailure end @@ -87,11 +94,11 @@ end @variables x = 1.0 @parameters n::Integer = 4 @mtkbuild sys = NonlinearSystem([x^n + x^2 - 1 ~ 0]) - prob = @test_warn ["parametric", "exponent"] HomotopyContinuationProblem(sys, []) - @test prob.solver_and_starts === nothing - @test_nowarn HomotopyContinuationProblem(sys, []; warn_parametric_exponent = false) - sol = solve(prob; threading = false) - @test SciMLBase.successful_retcode(sol) + prob = HomotopyContinuationProblem(sys, []) + sol = solve(prob, singlerootalg) + test_single_root(sol) + sol = solve(prob, allrootsalg) + test_all_roots(sol) end @testset "Polynomial check and warnings" begin @@ -100,45 +107,31 @@ end @test_throws ["Cannot convert", "Unable", "symbolically solve", "Exponent", "not an integer", "not a polynomial"] HomotopyContinuationProblem( sys, []) - @test MTK.safe_HomotopyContinuationProblem(sys, []) isa MTK.NotPolynomialError - @test NonlinearProblem(sys, []) isa NonlinearProblem @mtkbuild sys = NonlinearSystem([x^x - x ~ 0]) @test_throws ["Cannot convert", "Unable", "symbolically solve", "Exponent", "unknowns", "not a polynomial"] HomotopyContinuationProblem( sys, []) - @test MTK.safe_HomotopyContinuationProblem(sys, []) isa MTK.NotPolynomialError - @test NonlinearProblem(sys, []) isa NonlinearProblem @mtkbuild sys = NonlinearSystem([((x^2) / sin(x))^2 + x ~ 0]) @test_throws ["Cannot convert", "both polynomial", "non-polynomial", "recognized", "sin", "not a polynomial"] HomotopyContinuationProblem( sys, []) - @test MTK.safe_HomotopyContinuationProblem(sys, []) isa MTK.NotPolynomialError - @test NonlinearProblem(sys, []) isa NonlinearProblem @variables y = 2.0 @mtkbuild sys = NonlinearSystem([x^2 + y^2 + 2 ~ 0, y ~ sin(x)]) @test_throws ["Cannot convert", "recognized", "sin", "not a polynomial"] HomotopyContinuationProblem( sys, []) - @test MTK.safe_HomotopyContinuationProblem(sys, []) isa MTK.NotPolynomialError - @test NonlinearProblem(sys, []) isa NonlinearProblem @mtkbuild sys = NonlinearSystem([x^2 + y^2 - 2 ~ 0, sin(x + y) ~ 0]) @test_throws ["Cannot convert", "function of multiple unknowns"] HomotopyContinuationProblem( sys, []) - @test MTK.safe_HomotopyContinuationProblem(sys, []) isa MTK.NotPolynomialError - @test NonlinearProblem(sys, []) isa NonlinearProblem @mtkbuild sys = NonlinearSystem([sin(x)^2 + 1 ~ 0, cos(y) - cos(x) - 1 ~ 0]) @test_throws ["Cannot convert", "multiple non-polynomial terms", "same unknown"] HomotopyContinuationProblem( sys, []) - @test MTK.safe_HomotopyContinuationProblem(sys, []) isa MTK.NotPolynomialError - @test NonlinearProblem(sys, []) isa NonlinearProblem @mtkbuild sys = NonlinearSystem([sin(x^2)^2 + sin(x^2) - 1 ~ 0]) @test_throws ["import Nemo"] HomotopyContinuationProblem(sys, []) - @test MTK.safe_HomotopyContinuationProblem(sys, []) isa MTK.NotPolynomialError - @test NonlinearProblem(sys, []) isa NonlinearProblem end import Nemo @@ -148,7 +141,8 @@ import Nemo @mtkbuild sys = NonlinearSystem([sin(x^2)^2 + sin(x^2) - 1 ~ 0]) prob = HomotopyContinuationProblem(sys, []) @test prob[1] ≈ 2.0 - sol = solve(prob; threading = false) + # singlerootalg doesn't converge + sol = solve(prob, allrootsalg).u[1] _x = sol[1] @test sin(_x^2)^2 + sin(_x^2) - 1≈0.0 atol=1e-12 end @@ -162,9 +156,7 @@ end prob = HomotopyContinuationProblem(sys, []) @test prob[x] ≈ 0.25 @test prob[y] ≈ 0.125 - sol = solve(prob; threading = false) - # can't replicate the solve failure locally, so CI logs might help - @show sol.u sol.original.path_results + sol = solve(prob, allrootsalg).u[1] @test SciMLBase.successful_retcode(sol) @test sol[a]≈0.5 atol=1e-6 @test sol[b]≈0.25 atol=1e-6 @@ -177,12 +169,12 @@ end 0 ~ (x^2 - n * x + n) * (x - 1) / (x - 2) / (x - 3) ]) prob = HomotopyContinuationProblem(sys, []) - sol = solve(prob; threading = false) + sol = solve_allroots_closest(prob) @test sol[x] ≈ 1.0 p = parameter_values(prob) for invalid in [2.0, 3.0] for err in [-9e-8, 0, 9e-8] - @test any(<=(1e-7), prob.denominator([invalid + err, 2.0], p)) + @test any(<=(1e-7), prob.f.denominator([invalid + err, 2.0], p)) end end @@ -195,7 +187,7 @@ end [n]) sys = complete(sys) prob = HomotopyContinuationProblem(sys, []) - sol = solve(prob; threading = false) + sol = solve(prob, singlerootalg) disallowed_x = [4, 5.5] disallowed_y = [7, 5, 4] @test all(!isapprox(sol[x]; atol = 1e-8), disallowed_x) @@ -205,30 +197,30 @@ end p = parameter_values(prob) for val in disallowed_x for err in [-9e-8, 0, 9e-8] - @test any(<=(1e-7), prob.denominator([val + err, 2.0], p)) + @test any(<=(1e-7), prob.f.denominator([val + err, 2.0], p)) end end for val in disallowed_y for err in [-9e-8, 0, 9e-8] - @test any(<=(1e-7), prob.denominator([2.0, val + err], p)) + @test any(<=(1e-7), prob.f.denominator([2.0, val + err], p)) end end - @test prob.denominator([2.0, 4.0], p)[1] <= 1e-8 + @test prob.f.denominator([2.0, 4.0], p)[1] <= 1e-8 @testset "Rational function in observed" begin @variables x=1 y=1 @mtkbuild sys = NonlinearSystem([x^2 + y^2 - 2x - 2 ~ 0, y ~ (x - 1) / (x - 2)]) prob = HomotopyContinuationProblem(sys, []) - @test any(prob.denominator([2.0], parameter_values(prob)) .≈ 0.0) - @test SciMLBase.successful_retcode(solve(prob; threading = false)) + @test any(prob.f.denominator([2.0], parameter_values(prob)) .≈ 0.0) + @test SciMLBase.successful_retcode(solve(prob, singlerootalg)) end @testset "Rational function forced to common denominators" begin @variables x = 1 @mtkbuild sys = NonlinearSystem([0 ~ 1 / (1 + x) - x]) prob = HomotopyContinuationProblem(sys, []) - @test any(prob.denominator([-1.0], parameter_values(prob)) .≈ 0.0) - sol = solve(prob; threading = false) + @test any(prob.f.denominator([-1.0], parameter_values(prob)) .≈ 0.0) + sol = solve(prob, singlerootalg) @test SciMLBase.successful_retcode(sol) @test 1 / (1 + sol.u[1]) - sol.u[1]≈0.0 atol=1e-10 end @@ -238,7 +230,7 @@ end @variables x=1 y @mtkbuild sys = NonlinearSystem([x^2 - 2 ~ 0, y ~ sin(x)]) prob = HomotopyContinuationProblem(sys, []) - sol = @test_nowarn solve(prob; threading = false) + sol = @test_nowarn solve(prob, singlerootalg) @test sol[x] ≈ √2.0 @test sol[y] ≈ sin(√2.0) end @@ -251,10 +243,10 @@ end @testset "`simplify_fractions`" begin prob = HomotopyContinuationProblem(sys, []) - @test prob.denominator([0.0], parameter_values(prob)) ≈ [4.0] + @test prob.f.denominator([0.0], parameter_values(prob)) ≈ [4.0] end @testset "`nothing`" begin prob = HomotopyContinuationProblem(sys, []; fraction_cancel_fn = nothing) - @test sort(prob.denominator([0.0], parameter_values(prob))) ≈ [2.0, 4.0^3] + @test sort(prob.f.denominator([0.0], parameter_values(prob))) ≈ [2.0, 4.0^3] end end From 486bba7cc982bea2784b99553e67b21212651c75 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 14 Feb 2025 22:51:17 +0530 Subject: [PATCH 0990/1337] test: add NonlinearSolveHomotopyContinuation to `test/extensions` env --- test/extensions/Project.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/test/extensions/Project.toml b/test/extensions/Project.toml index 5f7afe222a..5b0de73cdf 100644 --- a/test/extensions/Project.toml +++ b/test/extensions/Project.toml @@ -10,6 +10,7 @@ JuMP = "4076af6c-e467-56ae-b986-b466b2749572" LabelledArrays = "2ee39098-c373-598a-b85f-a56591580800" ModelingToolkit = "961ee093-0014-501f-94e3-6117800e7a78" Nemo = "2edaba10-b0f1-5616-af89-8c11ac63239a" +NonlinearSolveHomotopyContinuation = "2ac3b008-d579-4536-8c91-a1a5998c2f8b" OrdinaryDiffEq = "1dea7af3-3e70-54e6-95c3-0bf5283fa5ed" SciMLSensitivity = "1ed8b502-d754-442c-8d5d-10ac956f44a1" SciMLStructures = "53ae85a6-f571-4167-b2af-e1d143709226" From 18e766efa165188ea6df5b2ea9a095c85c9ff40d Mon Sep 17 00:00:00 2001 From: vyudu Date: Fri, 21 Feb 2025 11:06:26 -0800 Subject: [PATCH 0991/1337] fix bug --- src/systems/problem_utils.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/systems/problem_utils.jl b/src/systems/problem_utils.jl index e7345f5d5d..1f40f61eed 100644 --- a/src/systems/problem_utils.jl +++ b/src/systems/problem_utils.jl @@ -755,7 +755,7 @@ function process_SciMLProblem( u0map = to_varmap(u0map, dvs) symbols_to_symbolics!(sys, u0map) - pmap = to_varmap(pmap, ps) + pmap = to_varmap(pmap, parameters(sys)) symbols_to_symbolics!(sys, pmap) check_inputmap_keys(sys, u0map, pmap) From bb1522869cd80da051b7c3930f69384ef1880af5 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 14 Feb 2025 13:28:35 +0530 Subject: [PATCH 0992/1337] feat: add `available_vars` to `observed_equations_used_by` --- src/utils.jl | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/utils.jl b/src/utils.jl index 962801622a..1fe7da603d 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -1068,14 +1068,24 @@ Keyword arguments: providing this keyword is not necessary and is only useful to avoid repeatedly calling `vars(exprs)` - `obs`: the list of observed equations. +- `available_vars`: If `exprs` involves a variable `x[1]`, this function will look for + observed equations whose LHS is `x[1]` OR `x`. Sometimes, the latter is not required + since `x[1]` might already be present elsewhere in the generated code (e.g. an argument + to the function) but other elements of `x` are part of the observed equations, thus + requiring them to be obtained from the equation for `x`. Any variable present in + `available_vars` will not be searched for in the observed equations. """ function observed_equations_used_by(sys::AbstractSystem, exprs; - involved_vars = vars(exprs; op = Union{Shift, Differential}), obs = observed(sys)) + involved_vars = vars(exprs; op = Union{Shift, Differential}), obs = observed(sys), available_vars = []) obsvars = getproperty.(obs, :lhs) graph = observed_dependency_graph(obs) + if !(available_vars isa Set) + available_vars = Set(available_vars) + end obsidxs = BitSet() for sym in involved_vars + sym in available_vars && continue arrsym = iscall(sym) && operation(sym) === getindex ? arguments(sym)[1] : nothing idx = findfirst(v -> isequal(v, sym) || isequal(v, arrsym), obsvars) idx === nothing && continue From 7ee87a5f4c9d81895099aeacb57d3b232877be88 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 14 Feb 2025 13:28:50 +0530 Subject: [PATCH 0993/1337] fix: fix array varables split across SCCs in SCCNonlinearProblem --- src/systems/nonlinear/nonlinearsystem.jl | 17 ++++++++++------- test/scc_nonlinear_problem.jl | 10 ++++++++++ 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/src/systems/nonlinear/nonlinearsystem.jl b/src/systems/nonlinear/nonlinearsystem.jl index c68fefa7d1..e0aa84dbdb 100644 --- a/src/systems/nonlinear/nonlinearsystem.jl +++ b/src/systems/nonlinear/nonlinearsystem.jl @@ -676,23 +676,23 @@ function SciMLBase.SCCNonlinearProblem{iip}(sys::NonlinearSystem, u0map, scc_cacheexprs = Dict{TypeT, Vector{Any}}[] scc_eqs = Vector{Equation}[] scc_obs = Vector{Equation}[] + # variables solved in previous SCCs + available_vars = Set() for (i, (escc, vscc)) in enumerate(zip(eq_sccs, var_sccs)) # subset unknowns and equations _dvs = dvs[vscc] _eqs = eqs[escc] # get observed equations required by this SCC - obsidxs = observed_equations_used_by(sys, _eqs) + union!(available_vars, _dvs) + obsidxs = observed_equations_used_by(sys, _eqs; available_vars) # the ones used by previous SCCs can be precomputed into the cache setdiff!(obsidxs, prevobsidxs) _obs = obs[obsidxs] + union!(available_vars, getproperty.(_obs, (:lhs,))) # get all subexpressions in the RHS which we can precompute in the cache # precomputed subexpressions should not contain `banned_vars` banned_vars = Set{Any}(vcat(_dvs, getproperty.(_obs, (:lhs,)))) - filter!(banned_vars) do var - symbolic_type(var) != ArraySymbolic() || - all(j -> var[j] in banned_vars, eachindex(var)) - end state = Dict() for i in eachindex(_obs) _obs[i] = _obs[i].lhs ~ subexpressions_not_involving_vars!( @@ -753,9 +753,12 @@ function SciMLBase.SCCNonlinearProblem{iip}(sys::NonlinearSystem, u0map, _obs = scc_obs[i] cachevars = scc_cachevars[i] cacheexprs = scc_cacheexprs[i] + available_vars = [dvs[reduce(vcat, var_sccs[1:(i - 1)]; init = Int[])]; + getproperty.( + reduce(vcat, scc_obs[1:(i - 1)]; init = []), (:lhs,))] _prevobsidxs = vcat(_prevobsidxs, - observed_equations_used_by(sys, reduce(vcat, values(cacheexprs); init = []))) - + observed_equations_used_by( + sys, reduce(vcat, values(cacheexprs); init = []); available_vars)) if isempty(cachevars) push!(explicitfuns, Returns(nothing)) else diff --git a/test/scc_nonlinear_problem.jl b/test/scc_nonlinear_problem.jl index 57f3d72fb7..ef4638cdb0 100644 --- a/test/scc_nonlinear_problem.jl +++ b/test/scc_nonlinear_problem.jl @@ -253,3 +253,13 @@ import ModelingToolkitStandardLibrary.Hydraulic.IsothermalCompressible as IC sol = solve(prob) @test SciMLBase.successful_retcode(sol) end + +@testset "Array variables split across SCCs" begin + @variables x[1:3] + @parameters (f::Function)(..) + @mtkbuild sys = NonlinearSystem([ + 0 ~ x[1]^2 - 9, x[2] ~ 2x[1], 0 ~ x[3]^2 - x[1]^2 + f(x)]) + prob = SCCNonlinearProblem(sys, [x => ones(3)], [f => sum]) + sol = solve(prob, NewtonRaphson()) + @test SciMLBase.successful_retcode(sol) +end From cd1f01e4e0ef7b35d15413584e8f7f14bb95b9f0 Mon Sep 17 00:00:00 2001 From: Martijn Visser Date: Fri, 24 Jan 2025 17:26:30 +0100 Subject: [PATCH 0994/1337] Replace Expronicon with Moshi --- Project.toml | 6 ++-- src/ModelingToolkit.jl | 7 +++-- src/clock.jl | 28 +++++++------------ src/discretedomain.jl | 12 ++++---- src/systems/abstractsystem.jl | 4 +-- src/systems/clock_inference.jl | 6 ++-- src/systems/systemstructure.jl | 3 +- test/clock.jl | 50 +++++++++++++++++----------------- 8 files changed, 56 insertions(+), 60 deletions(-) diff --git a/Project.toml b/Project.toml index b3bc7ed18c..ffd5cd5bf8 100644 --- a/Project.toml +++ b/Project.toml @@ -23,7 +23,6 @@ DomainSets = "5b8099bc-c8ec-5219-889f-1d9e522a28bf" DynamicQuantities = "06fc5a27-2a28-4c7c-a15d-362465fb6821" EnumX = "4e289a0a-7415-4d19-859d-a7e5c4648b56" ExprTools = "e2ba6199-217a-4e67-a87a-7c52f15ade04" -Expronicon = "6b7a57c9-7cc1-4fdf-b7f5-e857abae3636" FindFirstFunctions = "64ca27bc-2ba2-4a57-88aa-44e436879224" ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210" FunctionWrappers = "069b7b12-0de2-55c6-9aab-29f3d0a68a2e" @@ -36,6 +35,7 @@ Latexify = "23fbe1c1-3f47-55db-b15f-69d7ec21a316" Libdl = "8f399da3-3557-5675-b5ff-fb832c97cbdb" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" MLStyle = "d8e11817-5142-5d16-987a-aa16d5891078" +Moshi = "2e0e35c7-a2e4-4343-998d-7ef72827ed2d" NaNMath = "77ba4419-2d1f-58cd-9bb1-8ffee604a2e3" NonlinearSolve = "8913a72c-1f9b-4ce2-8d82-65094dcecaec" OffsetArrays = "6fe1bfb0-de20-5000-8ca7-80f57d26f881" @@ -105,7 +105,6 @@ DomainSets = "0.6, 0.7" DynamicQuantities = "^0.11.2, 0.12, 0.13, 1" EnumX = "1.0.4" ExprTools = "0.1.10" -Expronicon = "0.8" FMI = "0.14" FindFirstFunctions = "1" ForwardDiff = "0.10.3" @@ -124,6 +123,7 @@ LinearAlgebra = "1" Logging = "1" MLStyle = "0.4.17" ModelingToolkitStandardLibrary = "2.19" +Moshi = "0.3" NaNMath = "0.3, 1" NonlinearSolve = "4.3" OffsetArrays = "1" @@ -138,7 +138,7 @@ RecursiveArrayTools = "3.26" Reexport = "0.2, 1" RuntimeGeneratedFunctions = "0.5.9" SCCNonlinearSolve = "1.0.0" -SciMLBase = "2.73" +SciMLBase = "2.75" SciMLStructures = "1.0" Serialization = "1" Setfield = "0.7, 0.8, 1" diff --git a/src/ModelingToolkit.jl b/src/ModelingToolkit.jl index b683e132a2..e6e29cf736 100644 --- a/src/ModelingToolkit.jl +++ b/src/ModelingToolkit.jl @@ -45,10 +45,13 @@ using Compat using AbstractTrees using DiffEqBase, SciMLBase, ForwardDiff using SciMLBase: StandardODEProblem, StandardNonlinearProblem, handle_varmap, TimeDomain, - PeriodicClock, Clock, SolverStepClock, Continuous, OverrideInit, NoInit + PeriodicClock, Clock, SolverStepClock, ContinuousClock, OverrideInit, + NoInit using Distributed import JuliaFormatter using MLStyle +import Moshi +using Moshi.Data: @data using NonlinearSolve import SCCNonlinearSolve using Reexport @@ -290,7 +293,7 @@ export @variables, @parameters, @independent_variables, @constants, @brownian export @named, @nonamespace, @namespace, extend, compose, complete export debug_system -#export Continuous, Discrete, sampletime, input_timedomain, output_timedomain +#export ContinuousClock, Discrete, sampletime, input_timedomain, output_timedomain #export has_discrete_domain, has_continuous_domain #export is_discrete_domain, is_continuous_domain, is_hybrid_domain export Sample, Hold, Shift, ShiftIndex, sampletime, SampleTime diff --git a/src/clock.jl b/src/clock.jl index 2cd00a24bc..1c9ed89128 100644 --- a/src/clock.jl +++ b/src/clock.jl @@ -1,20 +1,12 @@ -module InferredClock - -export InferredTimeDomain - -using Expronicon.ADT: @adt, @match -using SciMLBase: TimeDomain - -@adt InferredTimeDomain begin +@data InferredClock begin Inferred InferredDiscrete end -Base.Broadcast.broadcastable(x::InferredTimeDomain) = Ref(x) +const InferredTimeDomain = InferredClock.Type +using .InferredClock: Inferred, InferredDiscrete -end - -using .InferredClock +Base.Broadcast.broadcastable(x::InferredTimeDomain) = Ref(x) struct VariableTimeDomain end Symbolics.option_to_metadata_type(::Val{:timedomain}) = VariableTimeDomain @@ -29,7 +21,7 @@ true if `x` contains only continuous-domain signals. See also [`has_continuous_domain`](@ref) """ function is_continuous_domain(x) - issym(x) && return getmetadata(x, VariableTimeDomain, false) == Continuous() + issym(x) && return getmetadata(x, VariableTimeDomain, false) == ContinuousClock() !has_discrete_domain(x) && has_continuous_domain(x) end @@ -50,7 +42,7 @@ has_time_domain(_, x) = has_time_domain(x) Determine if variable `x` has a time-domain attributed to it. """ function has_time_domain(x::Symbolic) - # getmetadata(x, Continuous, nothing) !== nothing || + # getmetadata(x, ContinuousClock, nothing) !== nothing || # getmetadata(x, Discrete, nothing) !== nothing getmetadata(x, VariableTimeDomain, nothing) !== nothing end @@ -58,8 +50,8 @@ has_time_domain(x::Num) = has_time_domain(value(x)) has_time_domain(x) = false for op in [Differential] - @eval input_timedomain(::$op, arg = nothing) = Continuous() - @eval output_timedomain(::$op, arg = nothing) = Continuous() + @eval input_timedomain(::$op, arg = nothing) = ContinuousClock() + @eval output_timedomain(::$op, arg = nothing) = ContinuousClock() end """ @@ -104,8 +96,8 @@ function is_discrete_domain(x) !has_discrete_domain(x) && has_continuous_domain(x) end -sampletime(c) = @match c begin - PeriodicClock(dt, _...) => dt +sampletime(c) = Moshi.Match.@match c begin + PeriodicClock(dt) => dt _ => nothing end diff --git a/src/discretedomain.jl b/src/discretedomain.jl index c7f90007c5..5693347a13 100644 --- a/src/discretedomain.jl +++ b/src/discretedomain.jl @@ -226,28 +226,28 @@ Base.:-(k::ShiftIndex, i::Int) = k + (-i) """ input_timedomain(op::Operator) -Return the time-domain type (`Continuous` or `InferredDiscrete`) that `op` operates on. +Return the time-domain type (`ContinuousClock()` or `InferredDiscrete()`) that `op` operates on. """ function input_timedomain(s::Shift, arg = nothing) if has_time_domain(arg) return get_time_domain(arg) end - InferredDiscrete + InferredDiscrete() end """ output_timedomain(op::Operator) -Return the time-domain type (`Continuous` or `InferredDiscrete`) that `op` results in. +Return the time-domain type (`ContinuousClock()` or `InferredDiscrete()`) that `op` results in. """ function output_timedomain(s::Shift, arg = nothing) if has_time_domain(t, arg) return get_time_domain(t, arg) end - InferredDiscrete + InferredDiscrete() end -input_timedomain(::Sample, _ = nothing) = Continuous() +input_timedomain(::Sample, _ = nothing) = ContinuousClock() output_timedomain(s::Sample, _ = nothing) = s.clock function input_timedomain(h::Hold, arg = nothing) @@ -256,7 +256,7 @@ function input_timedomain(h::Hold, arg = nothing) end InferredDiscrete # the Hold accepts any discrete end -output_timedomain(::Hold, _ = nothing) = Continuous() +output_timedomain(::Hold, _ = nothing) = ContinuousClock() sampletime(op::Sample, _ = nothing) = sampletime(op.clock) sampletime(op::ShiftIndex, _ = nothing) = sampletime(op.clock) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index 35685d5c73..8fdd36e291 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -631,8 +631,8 @@ SymbolicUtils.promote_symtype(::Type{Initial}, T) = T SymbolicUtils.isbinop(::Initial) = false Base.nameof(::Initial) = :Initial Base.show(io::IO, x::Initial) = print(io, "Initial") -input_timedomain(::Initial, _ = nothing) = Continuous() -output_timedomain(::Initial, _ = nothing) = Continuous() +input_timedomain(::Initial, _ = nothing) = ContinuousClock() +output_timedomain(::Initial, _ = nothing) = ContinuousClock() function (f::Initial)(x) # wrap output if wrapped input diff --git a/src/systems/clock_inference.jl b/src/systems/clock_inference.jl index 611f8e2fae..b535773061 100644 --- a/src/systems/clock_inference.jl +++ b/src/systems/clock_inference.jl @@ -8,8 +8,8 @@ end function ClockInference(ts::TransformationState) @unpack structure = ts @unpack graph = structure - eq_domain = TimeDomain[Continuous() for _ in 1:nsrcs(graph)] - var_domain = TimeDomain[Continuous() for _ in 1:ndsts(graph)] + eq_domain = TimeDomain[ContinuousClock() for _ in 1:nsrcs(graph)] + var_domain = TimeDomain[ContinuousClock() for _ in 1:ndsts(graph)] inferred = BitSet() for (i, v) in enumerate(get_fullvars(ts)) d = get_time_domain(ts, v) @@ -151,7 +151,7 @@ function split_system(ci::ClockInference{S}) where {S} get!(clock_to_id, d) do cid = (cid_counter[] += 1) push!(id_to_clock, d) - if d == Continuous() + if d == ContinuousClock() continuous_id[] = cid end cid diff --git a/src/systems/systemstructure.jl b/src/systems/systemstructure.jl index 1bdc11f06a..a8650d9bc8 100644 --- a/src/systems/systemstructure.jl +++ b/src/systems/systemstructure.jl @@ -662,7 +662,8 @@ function structural_simplify!(state::TearingState, io = nothing; simplify = fals Dict(v => 0.0 for v in Iterators.flatten(inputs))) end ps = [sym isa CallWithMetadata ? sym : - setmetadata(sym, VariableTimeDomain, get(time_domains, sym, Continuous())) + setmetadata( + sym, VariableTimeDomain, get(time_domains, sym, ContinuousClock())) for sym in get_ps(sys)] @set! sys.ps = ps else diff --git a/test/clock.jl b/test/clock.jl index 67f469e1a6..961c7af181 100644 --- a/test/clock.jl +++ b/test/clock.jl @@ -1,5 +1,5 @@ using ModelingToolkit, Test, Setfield, OrdinaryDiffEq, DiffEqCallbacks -using ModelingToolkit: Continuous +using ModelingToolkit: ContinuousClock using ModelingToolkit: t_nounits as t, D_nounits as D function infer_clocks(sys) @@ -77,19 +77,19 @@ k = ShiftIndex(d) d = Clock(dt) # Note that TearingState reorders the equations -@test eqmap[1] == Continuous() +@test eqmap[1] == ContinuousClock() @test eqmap[2] == d @test eqmap[3] == d @test eqmap[4] == d -@test eqmap[5] == Continuous() -@test eqmap[6] == Continuous() +@test eqmap[5] == ContinuousClock() +@test eqmap[6] == ContinuousClock() @test varmap[yd] == d @test varmap[ud] == d @test varmap[r] == d -@test varmap[x] == Continuous() -@test varmap[y] == Continuous() -@test varmap[u] == Continuous() +@test varmap[x] == ContinuousClock() +@test varmap[y] == ContinuousClock() +@test varmap[u] == ContinuousClock() @info "Testing shift normalization" dt = 0.1 @@ -192,10 +192,10 @@ eqs = [yd ~ Sample(dt)(y) @test varmap[ud1] == d @test varmap[yd2] == d2 @test varmap[ud2] == d2 - @test varmap[r] == Continuous() - @test varmap[x] == Continuous() - @test varmap[y] == Continuous() - @test varmap[u] == Continuous() + @test varmap[r] == ContinuousClock() + @test varmap[x] == ContinuousClock() + @test varmap[y] == ContinuousClock() + @test varmap[u] == ContinuousClock() @info "test composed systems" @@ -241,14 +241,14 @@ eqs = [yd ~ Sample(dt)(y) ci, varmap = infer_clocks(cl) @test varmap[f.x] == Clock(0.5) - @test varmap[p.x] == Continuous() - @test varmap[p.y] == Continuous() + @test varmap[p.x] == ContinuousClock() + @test varmap[p.y] == ContinuousClock() @test varmap[c.ud] == Clock(0.5) @test varmap[c.yd] == Clock(0.5) - @test varmap[c.y] == Continuous() + @test varmap[c.y] == ContinuousClock() @test varmap[f.y] == Clock(0.5) @test varmap[f.u] == Clock(0.5) - @test varmap[p.u] == Continuous() + @test varmap[p.u] == ContinuousClock() @test varmap[c.r] == Clock(0.5) ## Multiple clock rates @@ -281,9 +281,9 @@ eqs = [yd ~ Sample(dt)(y) @test varmap[ud1] == d @test varmap[yd2] == d2 @test varmap[ud2] == d2 - @test varmap[x] == Continuous() - @test varmap[y] == Continuous() - @test varmap[u] == Continuous() + @test varmap[x] == ContinuousClock() + @test varmap[y] == ContinuousClock() + @test varmap[u] == ContinuousClock() ss = structural_simplify(cl) ss_nosplit = structural_simplify(cl; split = false) @@ -398,13 +398,13 @@ eqs = [yd ~ Sample(dt)(y) ci, varmap = infer_clocks(expand_connections(_model)) - @test varmap[_model.plant.input.u] == Continuous() - @test varmap[_model.plant.u] == Continuous() - @test varmap[_model.plant.x] == Continuous() - @test varmap[_model.plant.y] == Continuous() - @test varmap[_model.plant.output.u] == Continuous() - @test varmap[_model.holder.output.u] == Continuous() - @test varmap[_model.sampler.input.u] == Continuous() + @test varmap[_model.plant.input.u] == ContinuousClock() + @test varmap[_model.plant.u] == ContinuousClock() + @test varmap[_model.plant.x] == ContinuousClock() + @test varmap[_model.plant.y] == ContinuousClock() + @test varmap[_model.plant.output.u] == ContinuousClock() + @test varmap[_model.holder.output.u] == ContinuousClock() + @test varmap[_model.sampler.input.u] == ContinuousClock() @test varmap[_model.controller.u] == d @test varmap[_model.holder.input.u] == d @test varmap[_model.controller.output.u] == d From a797008ab49846fcfa659842a2168c070866b31d Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 24 Feb 2025 18:11:46 +0530 Subject: [PATCH 0995/1337] refactor: format --- src/systems/diffeqs/abstractodesystem.jl | 14 +- src/systems/diffeqs/odesystem.jl | 35 ++-- src/systems/problem_utils.jl | 7 +- test/bvproblem.jl | 193 ++++++++++++----------- test/odesystem.jl | 12 +- test/problem_validation.jl | 22 +-- 6 files changed, 150 insertions(+), 133 deletions(-) diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index 1f6014a357..205d7e4601 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -841,35 +841,35 @@ function SciMLBase.BVProblem{iip, specialize}(sys::AbstractODESystem, u0map = [] eval_expression = false, eval_module = @__MODULE__, kwargs...) where {iip, specialize} - if !iscomplete(sys) error("A completed system is required. Call `complete` or `structural_simplify` on the system before creating an `BVProblem`") end !isnothing(callback) && error("BVP solvers do not support callbacks.") - has_alg_eqs(sys) && error("The BVProblem constructor currently does not support ODESystems with algebraic equations.") # Remove this when the BVDAE solvers get updated, the codegen should work when it does. + has_alg_eqs(sys) && + error("The BVProblem constructor currently does not support ODESystems with algebraic equations.") # Remove this when the BVDAE solvers get updated, the codegen should work when it does. sts = unknowns(sys) ps = parameters(sys) constraintsys = get_constraintsystem(sys) if !isnothing(constraintsys) - (length(constraints(constraintsys)) + length(u0map) > length(sts)) && - @warn "The BVProblem is overdetermined. The total number of conditions (# constraints + # fixed initial values given by u0map) exceeds the total number of states. The BVP solvers will default to doing a nonlinear least-squares optimization." + (length(constraints(constraintsys)) + length(u0map) > length(sts)) && + @warn "The BVProblem is overdetermined. The total number of conditions (# constraints + # fixed initial values given by u0map) exceeds the total number of states. The BVP solvers will default to doing a nonlinear least-squares optimization." end # ODESystems without algebraic equations should use both fixed values + guesses # for initialization. - _u0map = has_alg_eqs(sys) ? u0map : merge(Dict(u0map), Dict(guesses)) + _u0map = has_alg_eqs(sys) ? u0map : merge(Dict(u0map), Dict(guesses)) f, u0, p = process_SciMLProblem(ODEFunction{iip, specialize}, sys, _u0map, parammap; t = tspan !== nothing ? tspan[1] : tspan, guesses, check_length, warn_initialize_determined, eval_expression, eval_module, kwargs...) stidxmap = Dict([v => i for (i, v) in enumerate(sts)]) - u0_idxs = has_alg_eqs(sys) ? collect(1:length(sts)) : [stidxmap[k] for (k,v) in u0map] + u0_idxs = has_alg_eqs(sys) ? collect(1:length(sts)) : [stidxmap[k] for (k, v) in u0map] fns = generate_function_bc(sys, u0, u0_idxs, tspan) - bc_oop, bc_iip = eval_or_rgf.(fns; eval_expression, eval_module) + bc_oop, bc_iip = eval_or_rgf.(fns; eval_expression, eval_module) bc(sol, p, t) = bc_oop(sol, p, t) bc(resid, u, p, t) = bc_iip(resid, u, p, t) diff --git a/src/systems/diffeqs/odesystem.jl b/src/systems/diffeqs/odesystem.jl index 37f921303c..33cdc59909 100644 --- a/src/systems/diffeqs/odesystem.jl +++ b/src/systems/diffeqs/odesystem.jl @@ -193,7 +193,8 @@ struct ODESystem <: AbstractODESystem """ parent::Any - function ODESystem(tag, deqs, iv, dvs, ps, tspan, var_to_name, ctrls, observed, constraints, tgrad, + function ODESystem( + tag, deqs, iv, dvs, ps, tspan, var_to_name, ctrls, observed, constraints, tgrad, jac, ctrl_jac, Wfact, Wfact_t, name, description, systems, defaults, guesses, torn_matching, initializesystem, initialization_eqs, schedule, connector_type, preface, cevents, @@ -214,7 +215,8 @@ struct ODESystem <: AbstractODESystem u = __get_unit_type(dvs, ps, iv) check_units(u, deqs) end - new(tag, deqs, iv, dvs, ps, tspan, var_to_name, ctrls, observed, constraints, tgrad, jac, + new(tag, deqs, iv, dvs, ps, tspan, var_to_name, + ctrls, observed, constraints, tgrad, jac, ctrl_jac, Wfact, Wfact_t, name, description, systems, defaults, guesses, torn_matching, initializesystem, initialization_eqs, schedule, connector_type, preface, cevents, devents, parameter_dependencies, assertions, metadata, @@ -300,16 +302,16 @@ function ODESystem(deqs::AbstractVector{<:Equation}, iv, dvs, ps; if is_dde === nothing is_dde = _check_if_dde(deqs, iv′, systems) end - + if !isempty(systems) && !isnothing(constraintsystem) conssystems = ConstraintsSystem[] for sys in systems cons = get_constraintsystem(sys) - cons !== nothing && push!(conssystems, cons) + cons !== nothing && push!(conssystems, cons) end @show conssystems @set! constraintsystem.systems = conssystems - end + end assertions = Dict{BasicSymbolic, Any}(unwrap(k) => v for (k, v) in assertions) @@ -359,9 +361,9 @@ function ODESystem(eqs, iv; constraints = Equation[], kwargs...) if !isempty(constraints) constraintsystem = process_constraint_system(constraints, allunknowns, new_ps, iv) for st in get_unknowns(constraintsystem) - iscall(st) ? - !in(operation(st)(iv), allunknowns) && push!(consvars, st) : - !in(st, allunknowns) && push!(consvars, st) + iscall(st) ? + !in(operation(st)(iv), allunknowns) && push!(consvars, st) : + !in(st, allunknowns) && push!(consvars, st) end for p in parameters(constraintsystem) !in(p, new_ps) && push!(new_ps, p) @@ -712,7 +714,8 @@ end # Validate that all the variables in the BVP constraints are well-formed states or parameters. # - Callable/delay variables (e.g. of the form x(0.6) should be unknowns of the system (and have one arg, etc.) # - Callable/delay parameters should be parameters of the system (and have one arg, etc.) -function process_constraint_system(constraints::Vector{Equation}, sts, ps, iv; consname = :cons) +function process_constraint_system( + constraints::Vector{Equation}, sts, ps, iv; consname = :cons) isempty(constraints) && return nothing constraintsts = OrderedSet() @@ -725,22 +728,26 @@ function process_constraint_system(constraints::Vector{Equation}, sts, ps, iv; c # Validate the states. for var in constraintsts if !iscall(var) - occursin(iv, var) && (var ∈ sts || throw(ArgumentError("Time-dependent variable $var is not an unknown of the system."))) + occursin(iv, var) && (var ∈ sts || + throw(ArgumentError("Time-dependent variable $var is not an unknown of the system."))) elseif length(arguments(var)) > 1 throw(ArgumentError("Too many arguments for variable $var.")) elseif length(arguments(var)) == 1 arg = only(arguments(var)) - operation(var)(iv) ∈ sts || + operation(var)(iv) ∈ sts || throw(ArgumentError("Variable $var is not a variable of the ODESystem. Called variables must be variables of the ODESystem.")) - isequal(arg, iv) || isparameter(arg) || arg isa Integer || arg isa AbstractFloat || + isequal(arg, iv) || isparameter(arg) || arg isa Integer || + arg isa AbstractFloat || throw(ArgumentError("Invalid argument specified for variable $var. The argument of the variable should be either $iv, a parameter, or a value specifying the time that the constraint holds.")) isparameter(arg) && push!(constraintps, arg) else - var ∈ sts && @warn "Variable $var has no argument. It will be interpreted as $var($iv), and the constraint will apply to the entire interval." + var ∈ sts && + @warn "Variable $var has no argument. It will be interpreted as $var($iv), and the constraint will apply to the entire interval." end end - ConstraintsSystem(constraints, collect(constraintsts), collect(constraintps); name = consname) + ConstraintsSystem( + constraints, collect(constraintsts), collect(constraintps); name = consname) end diff --git a/src/systems/problem_utils.jl b/src/systems/problem_utils.jl index 1f40f61eed..77f4229696 100644 --- a/src/systems/problem_utils.jl +++ b/src/systems/problem_utils.jl @@ -872,7 +872,8 @@ function check_inputmap_keys(sys, u0map, pmap) push!(badparamkeys, k) end end - (isempty(badvarkeys) && isempty(badparamkeys)) || throw(InvalidKeyError(collect(badvarkeys), collect(badparamkeys))) + (isempty(badvarkeys) && isempty(badparamkeys)) || + throw(InvalidKeyError(collect(badvarkeys), collect(badparamkeys))) end const BAD_KEY_MESSAGE = """ @@ -885,14 +886,12 @@ struct InvalidKeyError <: Exception params::Any end -function Base.showerror(io::IO, e::InvalidKeyError) +function Base.showerror(io::IO, e::InvalidKeyError) println(io, BAD_KEY_MESSAGE) println(io, "u0map: $(join(e.vars, ", "))") println(io, "pmap: $(join(e.params, ", "))") end - - ############## # Legacy functions for backward compatibility ############## diff --git a/test/bvproblem.jl b/test/bvproblem.jl index f05be90281..c5451f681b 100644 --- a/test/bvproblem.jl +++ b/test/bvproblem.jl @@ -12,70 +12,73 @@ solvers = [MIRK4] daesolvers = [Ascher2, Ascher4, Ascher6] let - @parameters α=7.5 β=4.0 γ=8.0 δ=5.0 - @variables x(t)=1.0 y(t)=2.0 - - eqs = [D(x) ~ α * x - β * x * y, - D(y) ~ -γ * y + δ * x * y] - - u0map = [x => 1.0, y => 2.0] - parammap = [α => 7.5, β => 4, γ => 8.0, δ => 5.0] - tspan = (0.0, 10.0) - - @mtkbuild lotkavolterra = ODESystem(eqs, t) - op = ODEProblem(lotkavolterra, u0map, tspan, parammap) - osol = solve(op, Vern9()) - - bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(lotkavolterra, u0map, tspan, parammap) - - for solver in solvers - sol = solve(bvp, solver(), dt = 0.01) - @test isapprox(sol.u[end], osol.u[end]; atol = 0.01) - @test sol.u[1] == [1.0, 2.0] - end - - # Test out of place - bvp2 = SciMLBase.BVProblem{false, SciMLBase.AutoSpecialize}(lotkavolterra, u0map, tspan, parammap) - - for solver in solvers - sol = solve(bvp2, solver(), dt = 0.01) - @test isapprox(sol.u[end], osol.u[end]; atol = 0.01) - @test sol.u[1] == [1.0, 2.0] - end + @parameters α=7.5 β=4.0 γ=8.0 δ=5.0 + @variables x(t)=1.0 y(t)=2.0 + + eqs = [D(x) ~ α * x - β * x * y, + D(y) ~ -γ * y + δ * x * y] + + u0map = [x => 1.0, y => 2.0] + parammap = [α => 7.5, β => 4, γ => 8.0, δ => 5.0] + tspan = (0.0, 10.0) + + @mtkbuild lotkavolterra = ODESystem(eqs, t) + op = ODEProblem(lotkavolterra, u0map, tspan, parammap) + osol = solve(op, Vern9()) + + bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}( + lotkavolterra, u0map, tspan, parammap) + + for solver in solvers + sol = solve(bvp, solver(), dt = 0.01) + @test isapprox(sol.u[end], osol.u[end]; atol = 0.01) + @test sol.u[1] == [1.0, 2.0] + end + + # Test out of place + bvp2 = SciMLBase.BVProblem{false, SciMLBase.AutoSpecialize}( + lotkavolterra, u0map, tspan, parammap) + + for solver in solvers + sol = solve(bvp2, solver(), dt = 0.01) + @test isapprox(sol.u[end], osol.u[end]; atol = 0.01) + @test sol.u[1] == [1.0, 2.0] + end end ### Testing on pendulum let - @parameters g=9.81 L=1.0 - @variables θ(t) = π / 2 θ_t(t) - - eqs = [D(θ) ~ θ_t - D(θ_t) ~ -(g / L) * sin(θ)] - - @mtkbuild pend = ODESystem(eqs, t) - - u0map = [θ => π / 2, θ_t => π / 2] - parammap = [:L => 1.0, :g => 9.81] - tspan = (0.0, 6.0) - - op = ODEProblem(pend, u0map, tspan, parammap) - osol = solve(op, Vern9()) - - bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(pend, u0map, tspan, parammap) - for solver in solvers - sol = solve(bvp, solver(), dt = 0.01) - @test isapprox(sol.u[end], osol.u[end]; atol = 0.01) - @test sol.u[1] == [π / 2, π / 2] - end - - # Test out-of-place - bvp2 = SciMLBase.BVProblem{false, SciMLBase.FullSpecialize}(pend, u0map, tspan, parammap) - - for solver in solvers - sol = solve(bvp2, solver(), dt = 0.01) - @test isapprox(sol.u[end], osol.u[end]; atol = 0.01) - @test sol.u[1] == [π / 2, π / 2] - end + @parameters g=9.81 L=1.0 + @variables θ(t)=π / 2 θ_t(t) + + eqs = [D(θ) ~ θ_t + D(θ_t) ~ -(g / L) * sin(θ)] + + @mtkbuild pend = ODESystem(eqs, t) + + u0map = [θ => π / 2, θ_t => π / 2] + parammap = [:L => 1.0, :g => 9.81] + tspan = (0.0, 6.0) + + op = ODEProblem(pend, u0map, tspan, parammap) + osol = solve(op, Vern9()) + + bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(pend, u0map, tspan, parammap) + for solver in solvers + sol = solve(bvp, solver(), dt = 0.01) + @test isapprox(sol.u[end], osol.u[end]; atol = 0.01) + @test sol.u[1] == [π / 2, π / 2] + end + + # Test out-of-place + bvp2 = SciMLBase.BVProblem{false, SciMLBase.FullSpecialize}( + pend, u0map, tspan, parammap) + + for solver in solvers + sol = solve(bvp2, solver(), dt = 0.01) + @test isapprox(sol.u[end], osol.u[end]; atol = 0.01) + @test sol.u[1] == [π / 2, π / 2] + end end ################################################################## @@ -87,40 +90,42 @@ let @parameters α=1.5 β=1.0 γ=3.0 δ=1.0 @variables x(..) y(..) eqs = [D(x(t)) ~ α * x(t) - β * x(t) * y(t), - D(y(t)) ~ -γ * y(t) + δ * x(t) * y(t)] - - tspan = (0., 1.) + D(y(t)) ~ -γ * y(t) + δ * x(t) * y(t)] + + tspan = (0.0, 1.0) @mtkbuild lksys = ODESystem(eqs, t) - function lotkavolterra!(du, u, p, t) - du[1] = p[1]*u[1] - p[2]*u[1]*u[2] - du[2] = -p[4]*u[2] + p[3]*u[1]*u[2] + function lotkavolterra!(du, u, p, t) + du[1] = p[1] * u[1] - p[2] * u[1] * u[2] + du[2] = -p[4] * u[2] + p[3] * u[1] * u[2] end - function lotkavolterra(u, p, t) - [p[1]*u[1] - p[2]*u[1]*u[2], -p[4]*u[2] + p[3]*u[1]*u[2]] + function lotkavolterra(u, p, t) + [p[1] * u[1] - p[2] * u[1] * u[2], -p[4] * u[2] + p[3] * u[1] * u[2]] end # Test with a constraint. - constr = [y(0.5) ~ 2.] + constr = [y(0.5) ~ 2.0] @mtkbuild lksys = ODESystem(eqs, t; constraints = constr) - function bc!(resid, u, p, t) - resid[1] = u(0.0)[1] - 1. - resid[2] = u(0.5)[2] - 2. + function bc!(resid, u, p, t) + resid[1] = u(0.0)[1] - 1.0 + resid[2] = u(0.5)[2] - 2.0 end function bc(u, p, t) - [u(0.0)[1] - 1., u(0.5)[2] - 2.] + [u(0.0)[1] - 1.0, u(0.5)[2] - 2.0] end - u0 = [1., 1.] - tspan = (0., 1.) - p = [1.5, 1., 1., 3.] + u0 = [1.0, 1.0] + tspan = (0.0, 1.0) + p = [1.5, 1.0, 1.0, 3.0] bvpi1 = SciMLBase.BVProblem(lotkavolterra!, bc!, u0, tspan, p) bvpi2 = SciMLBase.BVProblem(lotkavolterra, bc, u0, tspan, p) - bvpi3 = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(lksys, [x(t) => 1.], tspan; guesses = [y(t) => 1.]) - bvpi4 = SciMLBase.BVProblem{false, SciMLBase.FullSpecialize}(lksys, [x(t) => 1.], tspan; guesses = [y(t) => 1.]) - + bvpi3 = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}( + lksys, [x(t) => 1.0], tspan; guesses = [y(t) => 1.0]) + bvpi4 = SciMLBase.BVProblem{false, SciMLBase.FullSpecialize}( + lksys, [x(t) => 1.0], tspan; guesses = [y(t) => 1.0]) + sol1 = @btime solve($bvpi1, MIRK4(), dt = 0.01) sol2 = @btime solve($bvpi2, MIRK4(), dt = 0.01) sol3 = @btime solve($bvpi3, MIRK4(), dt = 0.01) @@ -128,12 +133,15 @@ let @test sol1 ≈ sol2 ≈ sol3 ≈ sol4 # don't get true equality here, not sure why end -function test_solvers(solvers, prob, u0map, constraints, equations = []; dt = 0.05, atol = 1e-2) +function test_solvers( + solvers, prob, u0map, constraints, equations = []; dt = 0.05, atol = 1e-2) for solver in solvers println("Solver: $solver") sol = @btime solve($prob, $solver(), dt = $dt, abstol = $atol) @test SciMLBase.successful_retcode(sol.retcode) - p = prob.p; t = sol.t; bc = prob.f.bc + p = prob.p + t = sol.t + bc = prob.f.bc ns = length(prob.u0) if isinplace(prob.f) resid = zeros(ns) @@ -148,7 +156,7 @@ function test_solvers(solvers, prob, u0map, constraints, equations = []; dt = 0. for (k, v) in u0map @test sol[k][1] == v end - + # for cons in constraints # @test sol[cons.rhs - cons.lhs] ≈ 0 # end @@ -163,28 +171,31 @@ end let @parameters α=1.5 β=1.0 γ=3.0 δ=1.0 @variables x(..) y(..) - + eqs = [D(x(t)) ~ α * x(t) - β * x(t) * y(t), - D(y(t)) ~ -γ * y(t) + δ * x(t) * y(t)] - + D(y(t)) ~ -γ * y(t) + δ * x(t) * y(t)] + u0map = [] tspan = (0.0, 1.0) guess = [x(t) => 4.0, y(t) => 2.0] - constr = [x(.6) ~ 3.5, x(.3) ~ 7.] + constr = [x(0.6) ~ 3.5, x(0.3) ~ 7.0] @mtkbuild lksys = ODESystem(eqs, t; constraints = constr) - bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(lksys, u0map, tspan; guesses = guess) + bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}( + lksys, u0map, tspan; guesses = guess) test_solvers(solvers, bvp, u0map, constr; dt = 0.05) # Testing that more complicated constraints give correct solutions. - constr = [y(.2) + x(.8) ~ 3., y(.3) ~ 2.] + constr = [y(0.2) + x(0.8) ~ 3.0, y(0.3) ~ 2.0] @mtkbuild lksys = ODESystem(eqs, t; constraints = constr) - bvp = SciMLBase.BVProblem{false, SciMLBase.FullSpecialize}(lksys, u0map, tspan; guesses = guess) + bvp = SciMLBase.BVProblem{false, SciMLBase.FullSpecialize}( + lksys, u0map, tspan; guesses = guess) test_solvers(solvers, bvp, u0map, constr; dt = 0.05) - constr = [α * β - x(.6) ~ 0.0, y(.2) ~ 3.] + constr = [α * β - x(0.6) ~ 0.0, y(0.2) ~ 3.0] @mtkbuild lksys = ODESystem(eqs, t; constraints = constr) - bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(lksys, u0map, tspan; guesses = guess) + bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}( + lksys, u0map, tspan; guesses = guess) test_solvers(solvers, bvp, u0map, constr) end diff --git a/test/odesystem.jl b/test/odesystem.jl index ae39aa4c5b..01df23ede8 100644 --- a/test/odesystem.jl +++ b/test/odesystem.jl @@ -1676,9 +1676,9 @@ end @testset "Constraint system construction" begin @variables x(..) y(..) z(..) - @parameters a b c d e - eqs = [D(x(t)) ~ 3*a*y(t), D(y(t)) ~ x(t) - z(t), D(z(t)) ~ e*x(t)^2] - cons = [x(0.3) ~ c*d, y(0.7) ~ 3] + @parameters a b c d e + eqs = [D(x(t)) ~ 3 * a * y(t), D(y(t)) ~ x(t) - z(t), D(z(t)) ~ e * x(t)^2] + cons = [x(0.3) ~ c * d, y(0.7) ~ 3] # Test variables + parameters infer correctly. @mtkbuild sys = ODESystem(eqs, t; constraints = cons) @@ -1688,12 +1688,12 @@ end @parameters t_c cons = [x(t_c) ~ 3] @mtkbuild sys = ODESystem(eqs, t; constraints = cons) - @test issetequal(parameters(sys), [a, e, t_c]) + @test issetequal(parameters(sys), [a, e, t_c]) @parameters g(..) h i cons = [g(h, i) * x(3) ~ c] @mtkbuild sys = ODESystem(eqs, t; constraints = cons) - @test issetequal(parameters(sys), [g, h, i, a, e, c]) + @test issetequal(parameters(sys), [g, h, i, a, e, c]) # Test that bad constraints throw errors. cons = [x(3, 4) ~ 3] # unknowns cannot have multiple args. @@ -1716,7 +1716,7 @@ end 2 0 0 2 1 0 0 2 0 5] eqs = D(x(t)) ~ mat * x(t) - cons = [x(3) ~ [2,3,3,5,4]] + cons = [x(3) ~ [2, 3, 3, 5, 4]] @mtkbuild ode = ODESystem(D(x(t)) ~ mat * x(t), t; constraints = cons) @test length(constraints(ModelingToolkit.get_constraintsystem(ode))) == 5 end diff --git a/test/problem_validation.jl b/test/problem_validation.jl index bce39b51d2..a0a7afaf3c 100644 --- a/test/problem_validation.jl +++ b/test/problem_validation.jl @@ -2,33 +2,33 @@ using ModelingToolkit using ModelingToolkit: t_nounits as t, D_nounits as D @testset "Input map validation" begin - import ModelingToolkit: InvalidKeyError, MissingParametersError + import ModelingToolkit: InvalidKeyError, MissingParametersError @variables X(t) @parameters p d - eqs = [D(X) ~ p - d*X] + eqs = [D(X) ~ p - d * X] @mtkbuild osys = ODESystem(eqs, t) - + p = "I accidentally renamed p" u0 = [X => 1.0] ps = [p => 1.0, d => 0.5] - @test_throws MissingParametersError oprob = ODEProblem(osys, u0, (0.0, 1.0), ps) - + @test_throws MissingParametersError oprob=ODEProblem(osys, u0, (0.0, 1.0), ps) + @parameters p d ps = [p => 1.0, d => 0.5, "Random stuff" => 3.0] - @test_throws InvalidKeyError oprob = ODEProblem(osys, u0, (0.0, 1.0), ps) + @test_throws InvalidKeyError oprob=ODEProblem(osys, u0, (0.0, 1.0), ps) u0 = [:X => 1.0, "random" => 3.0] - @test_throws InvalidKeyError oprob = ODEProblem(osys, u0, (0.0, 1.0), ps) + @test_throws InvalidKeyError oprob=ODEProblem(osys, u0, (0.0, 1.0), ps) @variables x(t) y(t) z(t) - @parameters a b c d - eqs = [D(x) ~ x*a, D(y) ~ y*c, D(z) ~ b + d] + @parameters a b c d + eqs = [D(x) ~ x * a, D(y) ~ y * c, D(z) ~ b + d] @mtkbuild sys = ODESystem(eqs, t) pmap = [a => 1, b => 2, c => 3, d => 4, "b" => 2] u0map = [x => 1, y => 2, z => 3] - @test_throws InvalidKeyError ODEProblem(sys, u0map, (0., 1.), pmap) + @test_throws InvalidKeyError ODEProblem(sys, u0map, (0.0, 1.0), pmap) pmap = [a => 1, b => 2, c => 3, d => 4] u0map = [x => 1, y => 2, z => 3, :0 => 3] - @test_throws InvalidKeyError ODEProblem(sys, u0map, (0., 1.), pmap) + @test_throws InvalidKeyError ODEProblem(sys, u0map, (0.0, 1.0), pmap) end From f29824e829041f01f60a32ada65c5249f91a9746 Mon Sep 17 00:00:00 2001 From: vyudu Date: Mon, 24 Feb 2025 09:25:21 -0500 Subject: [PATCH 0996/1337] Format --- .../StructuralTransformations.jl | 6 ++- .../symbolics_tearing.jl | 54 +++++++++++-------- src/structural_transformation/utils.jl | 8 +-- .../discrete_system/discrete_system.jl | 6 +-- src/systems/systemstructure.jl | 6 +-- src/variables.jl | 2 +- test/discrete_system.jl | 44 +++++++-------- 7 files changed, 69 insertions(+), 57 deletions(-) diff --git a/src/structural_transformation/StructuralTransformations.jl b/src/structural_transformation/StructuralTransformations.jl index 7d2b9afa26..f0124d7f4b 100644 --- a/src/structural_transformation/StructuralTransformations.jl +++ b/src/structural_transformation/StructuralTransformations.jl @@ -11,7 +11,8 @@ using SymbolicUtils: maketerm, iscall using ModelingToolkit using ModelingToolkit: ODESystem, AbstractSystem, var_from_nested_derivative, Differential, - unknowns, equations, vars, Symbolic, diff2term_with_unit, shift2term_with_unit, value, + unknowns, equations, vars, Symbolic, diff2term_with_unit, + shift2term_with_unit, value, operation, arguments, Sym, Term, simplify, symbolic_linear_solve, isdiffeq, isdifferential, isirreducible, empty_substitutions, get_substitutions, @@ -22,7 +23,8 @@ using ModelingToolkit: ODESystem, AbstractSystem, var_from_nested_derivative, Di get_postprocess_fbody, vars!, IncrementalCycleTracker, add_edge_checked!, topological_sort, invalidate_cache!, Substitutions, get_or_construct_tearing_state, - filter_kwargs, lower_varname_with_unit, lower_shift_varname_with_unit, setio, SparseMatrixCLIL, + filter_kwargs, lower_varname_with_unit, + lower_shift_varname_with_unit, setio, SparseMatrixCLIL, get_fullvars, has_equations, observed, Schedule, schedule diff --git a/src/structural_transformation/symbolics_tearing.jl b/src/structural_transformation/symbolics_tearing.jl index bbb6853a7c..ea594f2172 100644 --- a/src/structural_transformation/symbolics_tearing.jl +++ b/src/structural_transformation/symbolics_tearing.jl @@ -248,7 +248,8 @@ called dummy derivatives. State selection is done. All non-differentiated variables are algebraic variables, and all variables that appear differentiated are differential variables. """ -function substitute_derivatives_algevars!(ts::TearingState, neweqs, var_eq_matching, dummy_sub; iv = nothing, D = nothing) +function substitute_derivatives_algevars!( + ts::TearingState, neweqs, var_eq_matching, dummy_sub; iv = nothing, D = nothing) @unpack fullvars, sys, structure = ts @unpack solvable_graph, var_to_diff, eq_to_diff, graph = structure diff_to_var = invview(var_to_diff) @@ -288,7 +289,7 @@ end #= There are three cases where we want to generate new variables to convert the system into first order (semi-implicit) ODEs. - + 1. To first order: Whenever higher order differentiated variable like `D(D(D(x)))` appears, we introduce new variables `x_t`, `x_tt`, and `x_ttt` and new equations @@ -364,7 +365,8 @@ Effects on the system structure: - solvable_graph: - var_eq_matching: match D(x) to the added identity equation D(x) ~ x_t """ -function generate_derivative_variables!(ts::TearingState, neweqs, var_eq_matching; mm = nothing, iv = nothing, D = nothing) +function generate_derivative_variables!( + ts::TearingState, neweqs, var_eq_matching; mm = nothing, iv = nothing, D = nothing) @unpack fullvars, sys, structure = ts @unpack solvable_graph, var_to_diff, eq_to_diff, graph = structure eq_var_matching = invview(var_eq_matching) @@ -395,7 +397,7 @@ function generate_derivative_variables!(ts::TearingState, neweqs, var_eq_matchin dx = fullvars[dv] order, lv = var_order(dv, diff_to_var) x_t = is_discrete ? lower_shift_varname_with_unit(fullvars[dv], iv) : - lower_varname_with_unit(fullvars[lv], iv, order) + lower_varname_with_unit(fullvars[lv], iv, order) # Add `x_t` to the graph v_t = add_dd_variable!(structure, fullvars, x_t, dv) @@ -405,7 +407,7 @@ function generate_derivative_variables!(ts::TearingState, neweqs, var_eq_matchin # Update matching push!(var_eq_matching, unassigned) var_eq_matching[dv] = unassigned - eq_var_matching[dummy_eq] = dv + eq_var_matching[dummy_eq] = dv end end @@ -428,7 +430,7 @@ function find_duplicate_dd(dv, solvable_graph, diff_to_var, linear_eqs, mm) return eq, v_t end end - return nothing + return nothing end """ @@ -492,8 +494,9 @@ Order the new equations and variables such that the differential equations and variables come first. Return the new equations, the solved equations, the new orderings, and the number of solved variables and equations. """ -function generate_system_equations!(state::TearingState, neweqs, var_eq_matching; simplify = false, iv = nothing, D = nothing) - @unpack fullvars, sys, structure = state +function generate_system_equations!(state::TearingState, neweqs, var_eq_matching; + simplify = false, iv = nothing, D = nothing) + @unpack fullvars, sys, structure = state @unpack solvable_graph, var_to_diff, eq_to_diff, graph = structure eq_var_matching = invview(var_eq_matching) diff_to_var = invview(var_to_diff) @@ -502,11 +505,12 @@ function generate_system_equations!(state::TearingState, neweqs, var_eq_matching if is_only_discrete(structure) for (i, v) in enumerate(fullvars) op = operation(v) - op isa Shift && (op.steps < 0) && begin - lowered = lower_shift_varname_with_unit(v, iv) - total_sub[v] = lowered - fullvars[i] = lowered - end + op isa Shift && (op.steps < 0) && + begin + lowered = lower_shift_varname_with_unit(v, iv) + total_sub[v] = lowered + fullvars[i] = lowered + end end end @@ -581,10 +585,11 @@ function generate_system_equations!(state::TearingState, neweqs, var_eq_matching end solved_vars_set = BitSet(solved_vars) var_ordering = [diff_vars; - setdiff!(setdiff(1:ndsts(graph), diff_vars_set), - solved_vars_set)] + setdiff!(setdiff(1:ndsts(graph), diff_vars_set), + solved_vars_set)] - return neweqs, solved_eqs, eq_ordering, var_ordering, length(solved_vars), length(solved_vars_set) + return neweqs, solved_eqs, eq_ordering, var_ordering, length(solved_vars), + length(solved_vars_set) end """ @@ -648,7 +653,8 @@ Eliminate the solved variables and equations from the graph and permute the graph's vertices to account for the new variable/equation ordering. """ # TODO: BLT sorting -function reorder_vars!(state::TearingState, var_eq_matching, eq_ordering, var_ordering, nsolved_eq, nsolved_var) +function reorder_vars!(state::TearingState, var_eq_matching, eq_ordering, + var_ordering, nsolved_eq, nsolved_var) @unpack solvable_graph, var_to_diff, eq_to_diff, graph = state.structure eqsperm = zeros(Int, nsrcs(graph)) @@ -692,7 +698,8 @@ end """ Update the system equations, unknowns, and observables after simplification. """ -function update_simplified_system!(state::TearingState, neweqs, solved_eqs, dummy_sub, var_eq_matching, extra_unknowns; +function update_simplified_system!( + state::TearingState, neweqs, solved_eqs, dummy_sub, var_eq_matching, extra_unknowns; cse_hack = true, array_hack = true) @unpack solvable_graph, var_to_diff, eq_to_diff, graph = state.structure diff_to_var = invview(var_to_diff) @@ -732,7 +739,6 @@ function update_simplified_system!(state::TearingState, neweqs, solved_eqs, dumm sys = schedule(sys) end - """ Give the order of the variable indexed by dv. """ @@ -790,12 +796,14 @@ function tearing_reassemble(state::TearingState, var_eq_matching, generate_derivative_variables!(state, neweqs, var_eq_matching; mm, iv, D) - neweqs, solved_eqs, eq_ordering, var_ordering, nelim_eq, nelim_var = - generate_system_equations!(state, neweqs, var_eq_matching; simplify, iv, D) + neweqs, solved_eqs, eq_ordering, var_ordering, nelim_eq, nelim_var = generate_system_equations!( + state, neweqs, var_eq_matching; simplify, iv, D) - state = reorder_vars!(state, var_eq_matching, eq_ordering, var_ordering, nelim_eq, nelim_var) + state = reorder_vars!( + state, var_eq_matching, eq_ordering, var_ordering, nelim_eq, nelim_var) - sys = update_simplified_system!(state, neweqs, solved_eqs, dummy_sub, var_eq_matching, extra_unknowns; cse_hack, array_hack) + sys = update_simplified_system!(state, neweqs, solved_eqs, dummy_sub, var_eq_matching, + extra_unknowns; cse_hack, array_hack) @set! state.sys = sys @set! sys.tearing_state = state diff --git a/src/structural_transformation/utils.jl b/src/structural_transformation/utils.jl index 96f7f78f99..f3cea9c7ba 100644 --- a/src/structural_transformation/utils.jl +++ b/src/structural_transformation/utils.jl @@ -477,15 +477,17 @@ function shift2term(var) backshift = is_lowered ? op.steps + ModelingToolkit.getshift(arg) : op.steps num = join(Char(0x2080 + d) for d in reverse!(digits(-backshift))) # subscripted number, e.g. ₁ - ds = join([Char(0x209c), Char(0x208b), num]) + ds = join([Char(0x209c), Char(0x208b), num]) # Char(0x209c) = ₜ # Char(0x208b) = ₋ (subscripted minus) O = is_lowered ? ModelingToolkit.getunshifted(arg) : arg oldop = operation(O) - newname = backshift != 0 ? Symbol(string(nameof(oldop)), ds) : Symbol(string(nameof(oldop))) + newname = backshift != 0 ? Symbol(string(nameof(oldop)), ds) : + Symbol(string(nameof(oldop))) - newvar = maketerm(typeof(O), Symbolics.rename(oldop, newname), Symbolics.children(O), Symbolics.metadata(O)) + newvar = maketerm(typeof(O), Symbolics.rename(oldop, newname), + Symbolics.children(O), Symbolics.metadata(O)) newvar = setmetadata(newvar, Symbolics.VariableSource, (:variables, newname)) newvar = setmetadata(newvar, ModelingToolkit.VariableUnshifted, O) newvar = setmetadata(newvar, ModelingToolkit.VariableShift, backshift) diff --git a/src/systems/discrete_system/discrete_system.jl b/src/systems/discrete_system/discrete_system.jl index 3f8d9e85c6..e4949d812f 100644 --- a/src/systems/discrete_system/discrete_system.jl +++ b/src/systems/discrete_system/discrete_system.jl @@ -269,15 +269,15 @@ function shift_u0map_forward(sys::DiscreteSystem, u0map, defs) for k in collect(keys(u0map)) 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)).") - + 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)).") + updated[Shift(iv, 1)(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)))).") else updated[Shift(iv, op.steps + 1)(only(arguments(k)))] = v end - end for var in unknowns(sys) op = operation(var) diff --git a/src/systems/systemstructure.jl b/src/systems/systemstructure.jl index 6fee78cfd6..0643f32ec4 100644 --- a/src/systems/systemstructure.jl +++ b/src/systems/systemstructure.jl @@ -473,9 +473,9 @@ function shift_discrete_system(ts::TearingState) end iv = get_iv(sys) - discmap = Dict(k => StructuralTransformations.simplify_shifts(Shift(iv, 1)(k)) - for k in discvars - if any(isequal(k), fullvars) && !isa(operation(k), Union{Sample, Hold})) + discmap = Dict(k => StructuralTransformations.simplify_shifts(Shift(iv, 1)(k)) + for k in discvars + if any(isequal(k), fullvars) && !isa(operation(k), Union{Sample, Hold})) for i in eachindex(fullvars) fullvars[i] = StructuralTransformations.simplify_shifts(fast_substitute( diff --git a/src/variables.jl b/src/variables.jl index 4e13ad2c5d..a7dde165f9 100644 --- a/src/variables.jl +++ b/src/variables.jl @@ -138,7 +138,7 @@ function default_toterm(x) if iscall(x) && (op = operation(x)) isa Operator if !(op isa Differential) if op isa Shift && op.steps < 0 - return shift2term(x) + return shift2term(x) end x = normalize_to_differential(op)(arguments(x)...) end diff --git a/test/discrete_system.jl b/test/discrete_system.jl index 756e5bca48..f232faaf81 100644 --- a/test/discrete_system.jl +++ b/test/discrete_system.jl @@ -257,7 +257,6 @@ k = ShiftIndex(t) @named sys = DiscreteSystem([x ~ x^2 + y^2, y ~ x(k - 1) + y(k - 1)], t) @test_throws ["algebraic equations", "not yet supported"] structural_simplify(sys) - @testset "Passing `nothing` to `u0`" begin @variables x(t) = 1 k = ShiftIndex() @@ -273,11 +272,11 @@ end prob = DiscreteProblem(de, [], (0, 10)) @test prob[x] == 2.0 @test prob[x(k - 1)] == 1.0 - + # must provide initial conditions for history @test_throws ErrorException DiscreteProblem(de, [x => 2.0], (0, 10)) - @test_throws ErrorException DiscreteProblem(de, [x(k+1) => 2.], (0, 10)) - + @test_throws ErrorException DiscreteProblem(de, [x(k + 1) => 2.0], (0, 10)) + # initial values only affect _that timestep_, not the entire history prob = DiscreteProblem(de, [x(k - 1) => 2.0], (0, 10)) @test prob[x] == 3.0 @@ -286,34 +285,35 @@ end @test prob[xₜ₋₁] == 2.0 # Test initial assignment with lowered variable - prob = DiscreteProblem(de, [xₜ₋₁(k-1) => 4.0], (0, 10)) - @test prob[x(k-1)] == prob[xₜ₋₁] == 1.0 - @test prob[x] == 5. + prob = DiscreteProblem(de, [xₜ₋₁(k - 1) => 4.0], (0, 10)) + @test prob[x(k - 1)] == prob[xₜ₋₁] == 1.0 + @test prob[x] == 5.0 # Test missing initial throws error @variables x(t) - @mtkbuild de = DiscreteSystem([x ~ x(k-1) + x(k-2)*x(k-3)], t) - @test_throws ErrorException prob = DiscreteProblem(de, [x(k-3) => 2.], (0, 10)) - @test_throws ErrorException prob = DiscreteProblem(de, [x(k-3) => 2., x(k-1) => 3.], (0, 10)) + @mtkbuild de = DiscreteSystem([x ~ x(k - 1) + x(k - 2) * x(k - 3)], t) + @test_throws ErrorException prob=DiscreteProblem(de, [x(k - 3) => 2.0], (0, 10)) + @test_throws ErrorException prob=DiscreteProblem( + de, [x(k - 3) => 2.0, x(k - 1) => 3.0], (0, 10)) # Test non-assigned initials are given default value - @variables x(t) = 2. - @mtkbuild de = DiscreteSystem([x ~ x(k-1) + x(k-2)*x(k-3)], t) - prob = DiscreteProblem(de, [x(k-3) => 12.], (0, 10)) + @variables x(t) = 2.0 + @mtkbuild de = DiscreteSystem([x ~ x(k - 1) + x(k - 2) * x(k - 3)], t) + prob = DiscreteProblem(de, [x(k - 3) => 12.0], (0, 10)) @test prob[x] == 26.0 - @test prob[x(k-1)] == 2.0 - @test prob[x(k-2)] == 2.0 + @test prob[x(k - 1)] == 2.0 + @test prob[x(k - 2)] == 2.0 # Elaborate test @variables xₜ₋₂(t) zₜ₋₁(t) z(t) - eqs = [x ~ x(k-1) + z(k-2), - z ~ x(k-2) * x(k-3) - z(k-1)^2] + eqs = [x ~ x(k - 1) + z(k - 2), + z ~ x(k - 2) * x(k - 3) - z(k - 1)^2] @mtkbuild de = DiscreteSystem(eqs, t) - u0 = [x(k-1) => 3, - xₜ₋₂(k-1) => 4, - x(k-2) => 1, - z(k-1) => 5, - zₜ₋₁(k-1) => 12] + u0 = [x(k - 1) => 3, + xₜ₋₂(k - 1) => 4, + x(k - 2) => 1, + z(k - 1) => 5, + zₜ₋₁(k - 1) => 12] prob = DiscreteProblem(de, u0, (0, 10)) @test prob[x] == 15 @test prob[z] == -21 From b40c2a1dbda36d481067ffa5450599540cefdc40 Mon Sep 17 00:00:00 2001 From: vyudu Date: Mon, 24 Feb 2025 11:58:05 -0500 Subject: [PATCH 0997/1337] fix Complex typecheck --- src/utils.jl | 16 ++++++++++++---- test/odesystem.jl | 4 ++++ 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/utils.jl b/src/utils.jl index 962801622a..4685e76ebc 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -222,7 +222,8 @@ function collect_ivs_from_nested_operator!(ivs, x, target_op) end function iv_from_nested_derivative(x, op = Differential) - if iscall(x) && operation(x) == getindex + if iscall(x) && + (operation(x) == getindex || operation(x) == real || operation(x) == imag) iv_from_nested_derivative(arguments(x)[1], op) elseif iscall(x) operation(x) isa op ? iv_from_nested_derivative(arguments(x)[1], op) : @@ -1204,7 +1205,7 @@ end Find all the unknowns and parameters from the equations of a SDESystem or ODESystem. Return re-ordered equations, differential variables, all variables, and parameters. """ function process_equations(eqs, iv) - eqs = collect(eqs) + eqs = collect(Iterators.flatten(eqs)) diffvars = OrderedSet() allunknowns = OrderedSet() @@ -1237,8 +1238,8 @@ function process_equations(eqs, iv) throw(ArgumentError("An ODESystem can only have one independent variable.")) diffvar in diffvars && throw(ArgumentError("The differential variable $diffvar is not unique in the system of equations.")) - !(symtype(diffvar) === Real || eltype(symtype(diffvar)) === Real) && - throw(ArgumentError("Differential variable $diffvar has type $(symtype(diffvar)). Differential variables should not be concretely typed.")) + !has_diffvar_type(diffvar) && + throw(ArgumentError("Differential variable $diffvar has type $(symtype(diffvar)). Differential variables should be of a continuous, non-concrete number type: Real, Complex, AbstractFloat, or Number.")) push!(diffvars, diffvar) end push!(diffeq, eq) @@ -1250,6 +1251,13 @@ function process_equations(eqs, iv) diffvars, allunknowns, ps, Equation[diffeq; algeeq; compressed_eqs] end +function has_diffvar_type(diffvar) + st = symtype(diffvar) + st === Real || eltype(st) === Real || st === Complex || eltype(st) === Complex || + st === Number || eltype(st) === Number || st === AbstractFloat || + eltype(st) === AbstractFloat +end + """ $(TYPEDSIGNATURES) diff --git a/test/odesystem.jl b/test/odesystem.jl index 01df23ede8..b9a41e1c3d 100644 --- a/test/odesystem.jl +++ b/test/odesystem.jl @@ -1589,6 +1589,10 @@ end @variables Y(t)[1:3]::String eq = D(Y) ~ [p, p, p] @test_throws ArgumentError @mtkbuild osys = ODESystem([eq], t) + + @variables X(t)::Complex + eq = D(X) ~ p - d * X + @test_nowarn @named osys = ODESystem([eq], t) end # Test `isequal` From 956079768e8a8c1785440a8b370b3acb5b1fb756 Mon Sep 17 00:00:00 2001 From: vyudu Date: Mon, 24 Feb 2025 12:27:10 -0500 Subject: [PATCH 0998/1337] up --- src/utils.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/utils.jl b/src/utils.jl index 4685e76ebc..950af5a1e2 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -1205,7 +1205,8 @@ end Find all the unknowns and parameters from the equations of a SDESystem or ODESystem. Return re-ordered equations, differential variables, all variables, and parameters. """ function process_equations(eqs, iv) - eqs = collect(Iterators.flatten(eqs)) + eltype(eqs) <: Vector && (eqs = vcat(eqs...)) + eqs = collect(eqs) diffvars = OrderedSet() allunknowns = OrderedSet() From 6fb59de1cbec6ba8a2662202b3f5a8135efb3b9c Mon Sep 17 00:00:00 2001 From: vyudu Date: Tue, 25 Feb 2025 19:25:08 -0500 Subject: [PATCH 0999/1337] feat: implement --- src/structural_transformation/utils.jl | 35 +++++++++++++++++++ .../implicit_discrete_system.jl | 2 +- src/systems/systemstructure.jl | 2 +- test/implicit_discrete_system.jl | 15 +++++++- test/structural_transformation/utils.jl | 20 +++++++++++ 5 files changed, 71 insertions(+), 3 deletions(-) diff --git a/src/structural_transformation/utils.jl b/src/structural_transformation/utils.jl index db79c8a124..fb78d707d9 100644 --- a/src/structural_transformation/utils.jl +++ b/src/structural_transformation/utils.jl @@ -507,3 +507,38 @@ function simplify_shifts(var) unwrap(var).metadata) end end + +function distribute_shift(var) + var = unwrap(var) + var isa Equation && return distribute_shift(var.lhs) ~ distribute_shift(var.rhs) + + ModelingToolkit.hasshift(var) || return var + shift = operation(var) + shift isa Shift || return var + + shift = operation(var) + expr = only(arguments(var)) + if expr isa Equation + return distribute_shift(shift(expr.lhs)) ~ distribute_shift(shift(expr.rhs)) + end + shiftexpr = _distribute_shift(expr, shift) + return simplify_shifts(shiftexpr) +end + +function _distribute_shift(expr, shift) + if iscall(expr) + op = operation(expr) + args = arguments(expr) + + if ModelingToolkit.isvariable(expr) + (length(args) == 1 && isequal(shift.t, only(args))) ? (return shift(expr)) : (return expr) + elseif op isa Shift + return shift(expr) + else + return maketerm(typeof(expr), operation(expr), Base.Fix2(_distribute_shift, shift).(args), + unwrap(expr).metadata) + end + else + return expr + end +end diff --git a/src/systems/discrete_system/implicit_discrete_system.jl b/src/systems/discrete_system/implicit_discrete_system.jl index e0450a1ada..5ff9631b3e 100644 --- a/src/systems/discrete_system/implicit_discrete_system.jl +++ b/src/systems/discrete_system/implicit_discrete_system.jl @@ -265,7 +265,7 @@ function generate_function( sys::ImplicitDiscreteSystem, dvs = unknowns(sys), ps = parameters(sys); wrap_code = identity, kwargs...) iv = get_iv(sys) exprs = map(equations(sys)) do eq - _iszero(eq.lhs) ? eq.rhs : (simplify_shifts(Shift(iv, -1)(eq.rhs)) - simplify_shifts(Shift(iv, -1)(eq.lhs))) + _iszero(eq.lhs) ? eq.rhs : (distribute_shift(Shift(iv, -1)(eq.rhs)) - distribute_shift(Shift(iv, -1)(eq.lhs))) end u_next = dvs diff --git a/src/systems/systemstructure.jl b/src/systems/systemstructure.jl index a6e6b1d218..9bd3d6c8ab 100644 --- a/src/systems/systemstructure.jl +++ b/src/systems/systemstructure.jl @@ -440,7 +440,7 @@ function TearingState(sys; quick_cancel = false, check = true) SystemStructure(complete(var_to_diff), complete(eq_to_diff), complete(graph), nothing, var_types, sys isa AbstractDiscreteSystem), Any[]) - if sys isa DiscreteSystem + if sys isa AbstractDiscreteSystem ts = shift_discrete_system(ts) end return ts diff --git a/test/implicit_discrete_system.jl b/test/implicit_discrete_system.jl index 88e79acc77..ffe4ce500f 100644 --- a/test/implicit_discrete_system.jl +++ b/test/implicit_discrete_system.jl @@ -18,7 +18,6 @@ resid = rand(2) f(resid, u_next, [2.,3.], [], t) @test resid ≈ [3., -3.] -# Initialization cases. prob = ImplicitDiscreteProblem(sys, [x(k-1) => 3.], tspan) @test prob.u0 == [3., 1.] prob = ImplicitDiscreteProblem(sys, [], tspan) @@ -28,3 +27,17 @@ prob = ImplicitDiscreteProblem(sys, [], tspan) @test_throws ErrorException prob = ImplicitDiscreteProblem(sys, [], tspan) # Test solvers +@testset "System with algebraic equations" begin + @variables x(t) y(t) + eqs = [x(k) ~ x(k-1) + x(k-2), + x^2 ~ 1 - y^2] + @mtkbuild sys = ImplicitDiscreteSystem(eqs, t) +end + +@testset "System with algebraic equations, implicit difference equations, explicit difference equations" begin + @variables x(t) y(t) + eqs = [x(k) ~ x(k-1) + x(k-2), + y(k) ~ x(k) + x(k-2)*y(k-1)] + @mtkbuild sys = ImplicitDiscreteSystem(eqs, t) +end + diff --git a/test/structural_transformation/utils.jl b/test/structural_transformation/utils.jl index 4da3d1e924..b7cc4b720b 100644 --- a/test/structural_transformation/utils.jl +++ b/test/structural_transformation/utils.jl @@ -162,3 +162,23 @@ end structural_simplify(sys; additional_passes = [pass]) @test value[] == 1 end + +@testset "Distribute shifts" begin + @variables x(t) y(t) z(t) + @parameters a b c + + # Expand shifts + @test isequal(ST.distribute_shift(Shift(t, -1)(x + y)), Shift(t, -1)(x) + Shift(t, -1)(y)) + + expr = a * Shift(t, -2)(x) + Shift(t, 2)(y) + b + @test isequal(ST.simplify_shifts(ST.distribute_shift(Shift(t, 2)(expr))), + a*x + Shift(t, 4)(y) + b) + @test isequal(ST.distribute_shift(Shift(t, 2)(exp(z))), exp(Shift(t, 2)(z))) + @test isequal(ST.distribute_shift(Shift(t, 2)(exp(a) + b)), exp(a) + b) + + expr = a^x - log(b*y) + z*x + @test isequal(ST.distribute_shift(Shift(t, -3)(expr)), a^(Shift(t, -3)(x)) - log(b * Shift(t, -3)(y)) + Shift(t, -3)(z)*Shift(t, -3)(x)) + + expr = x(k+1) ~ x + x(k-1) + @test isequal(ST.distribute_shift(Shift(t, -1)(expr)), x ~ x(k-1) + x(k-2)) +end From 00a8222960a6f3bcb147c0ae7e43e37c44bda3f7 Mon Sep 17 00:00:00 2001 From: vyudu Date: Tue, 25 Feb 2025 22:55:34 -0500 Subject: [PATCH 1000/1337] add shift2term for positive shifts --- src/discretedomain.jl | 13 ++++ .../StructuralTransformations.jl | 2 +- src/structural_transformation/utils.jl | 20 ++++-- test/implicit_discrete_system.jl | 62 ++++++++++++------- 4 files changed, 71 insertions(+), 26 deletions(-) diff --git a/src/discretedomain.jl b/src/discretedomain.jl index c7f90007c5..508cfcd216 100644 --- a/src/discretedomain.jl +++ b/src/discretedomain.jl @@ -70,6 +70,19 @@ Base.literal_pow(f::typeof(^), D::Shift, ::Val{n}) where {n} = Shift(D.t, D.step hasshift(eq::Equation) = hasshift(eq.lhs) || hasshift(eq.rhs) +""" + Next(x) + +An alias for Shift(t, 1)(x). +""" +Next(x) = Shift(t, 1)(x) +""" + Prev(x) + +An alias for Shift(t, -1)(x). +""" +Prev(x) = Shift(t, -1)(x) + """ hasshift(O) diff --git a/src/structural_transformation/StructuralTransformations.jl b/src/structural_transformation/StructuralTransformations.jl index e6be30891f..2e600e86e4 100644 --- a/src/structural_transformation/StructuralTransformations.jl +++ b/src/structural_transformation/StructuralTransformations.jl @@ -65,7 +65,7 @@ export torn_system_jacobian_sparsity export full_equations export but_ordered_incidence, lowest_order_variable_mask, highest_order_variable_mask export computed_highest_diff_variables -export shift2term, lower_shift_varname, simplify_shifts +export shift2term, lower_shift_varname, simplify_shifts, distribute_shift include("utils.jl") include("pantelides.jl") diff --git a/src/structural_transformation/utils.jl b/src/structural_transformation/utils.jl index 31c58e797d..031ef6b2d8 100644 --- a/src/structural_transformation/utils.jl +++ b/src/structural_transformation/utils.jl @@ -457,7 +457,7 @@ Handle renaming variable names for discrete structural simplification. Three cas """ function lower_shift_varname(var, iv) op = operation(var) - if op isa Shift && op.steps < 0 + if op isa Shift return shift2term(var) else return Shift(iv, 0)(var, true) @@ -475,10 +475,14 @@ function shift2term(var) backshift = is_lowered ? op.steps + ModelingToolkit.getshift(arg) : op.steps - num = join(Char(0x2080 + d) for d in reverse!(digits(-backshift))) # subscripted number, e.g. ₁ - ds = join([Char(0x209c), Char(0x208b), num]) - # Char(0x209c) = ₜ # Char(0x208b) = ₋ (subscripted minus) + # Char(0x208a) = ₊ (subscripted plus) + pm = backshift > 0 ? Char(0x208a) : Char(0x208b) + # subscripted number, e.g. ₁ + num = join(Char(0x2080 + d) for d in reverse!(digits(abs(backshift)))) + # Char(0x209c) = ₜ + # ds = ₜ₋₁ + ds = join([Char(0x209c), pm, num]) O = is_lowered ? ModelingToolkit.getunshifted(arg) : arg oldop = operation(O) @@ -498,6 +502,9 @@ function isdoubleshift(var) ModelingToolkit.isoperator(arguments(var)[1], ModelingToolkit.Shift) end +""" +Simplify multiple shifts: Shift(t, k1)(Shift(t, k2)(x)) becomes Shift(t, k1+k2)(x). +""" function simplify_shifts(var) ModelingToolkit.hasshift(var) || return var var isa Equation && return simplify_shifts(var.lhs) ~ simplify_shifts(var.rhs) @@ -518,6 +525,11 @@ function simplify_shifts(var) end end +""" +Distribute a shift applied to a whole expression or equation. +Shift(t, 1)(x + y) will become Shift(t, 1)(x) + Shift(t, 1)(y). +Only shifts variables whose independent variable is the same t that appears in the Shift (i.e. constants, time-independent parameters, etc. do not get shifted). +""" function distribute_shift(var) var = unwrap(var) var isa Equation && return distribute_shift(var.lhs) ~ distribute_shift(var.rhs) diff --git a/test/implicit_discrete_system.jl b/test/implicit_discrete_system.jl index ffe4ce500f..f2e7deaa32 100644 --- a/test/implicit_discrete_system.jl +++ b/test/implicit_discrete_system.jl @@ -1,30 +1,34 @@ using ModelingToolkit, Test using ModelingToolkit: t_nounits as t +using StableRNGs k = ShiftIndex(t) -@variables x(t) = 1 -@mtkbuild sys = ImplicitDiscreteSystem([x(k) ~ x(k)*x(k-1) - 3], t) -tspan = (0, 10) +rng = StableRNG(22525) # Shift(t, -1)(x(t)) - x_{t-1}(t) # -3 - x(t) + x(t)*x_{t-1} -f = ImplicitDiscreteFunction(sys) -u_next = [3., 1.5] -@test f(u_next, [2.,3.], [], t) ≈ [0., 0.] -u_next = [0., 0.] -@test f(u_next, [2.,3.], [], t) ≈ [3., -3.] - -resid = rand(2) -f(resid, u_next, [2.,3.], [], t) -@test resid ≈ [3., -3.] - -prob = ImplicitDiscreteProblem(sys, [x(k-1) => 3.], tspan) -@test prob.u0 == [3., 1.] -prob = ImplicitDiscreteProblem(sys, [], tspan) -@test prob.u0 == [1., 1.] -@variables x(t) -@mtkbuild sys = ImplicitDiscreteSystem([x(k) ~ x(k)*x(k-1) - 3], t) -@test_throws ErrorException prob = ImplicitDiscreteProblem(sys, [], tspan) +@testset "Correct ImplicitDiscreteFunction" begin + @variables x(t) = 1 + @mtkbuild sys = ImplicitDiscreteSystem([x(k) ~ x(k)*x(k-1) - 3], t) + tspan = (0, 10) + f = ImplicitDiscreteFunction(sys) + u_next = [3., 1.5] + @test f(u_next, [2.,3.], [], t) ≈ [0., 0.] + u_next = [0., 0.] + @test f(u_next, [2.,3.], [], t) ≈ [3., -3.] + + resid = rand(2) + f(resid, u_next, [2.,3.], [], t) + @test resid ≈ [3., -3.] + + prob = ImplicitDiscreteProblem(sys, [x(k-1) => 3.], tspan) + @test prob.u0 == [3., 1.] + prob = ImplicitDiscreteProblem(sys, [], tspan) + @test prob.u0 == [1., 1.] + @variables x(t) + @mtkbuild sys = ImplicitDiscreteSystem([x(k) ~ x(k)*x(k-1) - 3], t) + @test_throws ErrorException prob = ImplicitDiscreteProblem(sys, [], tspan) +end # Test solvers @testset "System with algebraic equations" begin @@ -32,6 +36,23 @@ prob = ImplicitDiscreteProblem(sys, [], tspan) eqs = [x(k) ~ x(k-1) + x(k-2), x^2 ~ 1 - y^2] @mtkbuild sys = ImplicitDiscreteSystem(eqs, t) + f = ImplicitDiscreteFunction(sys) + + function correct_f(u_next, u, p, t) + [u[2] - u_next[1], + u[1] + u[2] - u_next[2], + 1 - (u_next[1]+u_next[2])^2 - u_next[3]^2] + end + + for _ in 1:10 + u_next = rand(rng, 3) + u = rand(rng, 3) + @test correct_f(u_next, u, [], 0.) ≈ f(u_next, u, [], 0.) + end + + # Initialization is satisfied. + prob = ImplicitDiscreteProblem(sys, [x(k-1) => 3.], tspan) + @test (prob.u0[1] + prob.u0[2])^2 + prob.u0[3]^2 ≈ 1 end @testset "System with algebraic equations, implicit difference equations, explicit difference equations" begin @@ -40,4 +61,3 @@ end y(k) ~ x(k) + x(k-2)*y(k-1)] @mtkbuild sys = ImplicitDiscreteSystem(eqs, t) end - From ddf0d12c06e9afc7777d83777f6537bdb214757c Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 26 Feb 2025 22:08:02 +0530 Subject: [PATCH 1001/1337] feat: mark `getproperty(::AbstractSystem, ::Symbol)` as non-differentiable --- ext/MTKChainRulesCoreExt.jl | 2 ++ test/extensions/ad.jl | 10 ++++++++++ 2 files changed, 12 insertions(+) diff --git a/ext/MTKChainRulesCoreExt.jl b/ext/MTKChainRulesCoreExt.jl index f153ee77de..9cf17d203d 100644 --- a/ext/MTKChainRulesCoreExt.jl +++ b/ext/MTKChainRulesCoreExt.jl @@ -103,4 +103,6 @@ function ChainRulesCore.rrule( newbuf, pullback end +ChainRulesCore.@non_differentiable Base.getproperty(sys::MTK.AbstractSystem, x::Symbol) + end diff --git a/test/extensions/ad.jl b/test/extensions/ad.jl index 0e72b2b7b7..adaf6117c6 100644 --- a/test/extensions/ad.jl +++ b/test/extensions/ad.jl @@ -124,3 +124,13 @@ fwd, back = ChainRulesCore.rrule(remake_buffer, sys, ps, idxs, vals) nsol = solve(nprob, NewtonRaphson()) @test nsol[1] ≈ 10.0 / 1.0 + 9.81 * 1.0 / 2 # anal free fall solution is y = v0*t - g*t^2/2 -> v0 = y/t + g*t/2 end + +@testset "`sys.var` is non-differentiable" begin + @variables x(t) + @mtkbuild sys = ODESystem(D(x) ~ x, t) + prob = ODEProblem(sys, [x => 1.0], (0.0, 1.0)) + + grad = Zygote.gradient(prob) do prob + prob[sys.x] + end +end From f289c5a5934766376e4b05bab4a0b777499a931c Mon Sep 17 00:00:00 2001 From: David Widmann Date: Wed, 26 Feb 2025 19:31:41 +0100 Subject: [PATCH 1002/1337] Replace `vcat(eqs...)` with `reduce(vcat, eqs)` --- src/utils.jl | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/utils.jl b/src/utils.jl index 22ef4dc160..78c665ffca 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -1217,7 +1217,9 @@ end Find all the unknowns and parameters from the equations of a SDESystem or ODESystem. Return re-ordered equations, differential variables, all variables, and parameters. """ function process_equations(eqs, iv) - eltype(eqs) <: Vector && (eqs = vcat(eqs...)) + if eltype(eqs) <: AbstractVector + eqs = reduce(vcat, eqs) + end eqs = collect(eqs) diffvars = OrderedSet() From 3d9a8d847560717a8eab615f4ad16bb42451947c Mon Sep 17 00:00:00 2001 From: Christopher Rackauckas Date: Wed, 26 Feb 2025 10:57:59 -0800 Subject: [PATCH 1003/1337] Update Project.toml --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 14ea5743fa..8c4a58045e 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "ModelingToolkit" uuid = "961ee093-0014-501f-94e3-6117800e7a78" authors = ["Yingbo Ma ", "Chris Rackauckas and contributors"] -version = "9.64.1" +version = "9.64.2" [deps] AbstractTrees = "1520ce14-60c1-5f80-bbc7-55ef81b5835c" From c504aefa92bf974e24d3f1d4625a65ae25070157 Mon Sep 17 00:00:00 2001 From: vyudu Date: Wed, 26 Feb 2025 16:06:10 -0500 Subject: [PATCH 1004/1337] fix: make the codegen work with shifted observables --- .../symbolics_tearing.jl | 4 +++ .../implicit_discrete_system.jl | 32 ++++++++++++++----- src/systems/systemstructure.jl | 2 +- test/implicit_discrete_system.jl | 25 +++++++++++---- 4 files changed, 47 insertions(+), 16 deletions(-) diff --git a/src/structural_transformation/symbolics_tearing.jl b/src/structural_transformation/symbolics_tearing.jl index 9818bba361..f1cdd7ce9c 100644 --- a/src/structural_transformation/symbolics_tearing.jl +++ b/src/structural_transformation/symbolics_tearing.jl @@ -558,6 +558,10 @@ function generate_system_equations!(state::TearingState, neweqs, var_eq_matching end total_sub[simplify_shifts(neweq.lhs)] = neweq.rhs + # Substitute unshifted variables x(k), y(k) on RHS of implicit equations + if is_only_discrete(structure) + var_to_diff[iv] === nothing && (total_sub[var] = neweq.rhs) + end push!(diff_eqs, neweq) push!(diffeq_idxs, ieq) push!(diff_vars, diff_to_var[iv]) diff --git a/src/systems/discrete_system/implicit_discrete_system.jl b/src/systems/discrete_system/implicit_discrete_system.jl index 5ff9631b3e..5f13e87be4 100644 --- a/src/systems/discrete_system/implicit_discrete_system.jl +++ b/src/systems/discrete_system/implicit_discrete_system.jl @@ -264,13 +264,20 @@ end function generate_function( sys::ImplicitDiscreteSystem, dvs = unknowns(sys), ps = parameters(sys); wrap_code = identity, kwargs...) iv = get_iv(sys) + # Algebraic equations get shifted forward 1, to match with differential equations exprs = map(equations(sys)) do eq - _iszero(eq.lhs) ? eq.rhs : (distribute_shift(Shift(iv, -1)(eq.rhs)) - distribute_shift(Shift(iv, -1)(eq.lhs))) + _iszero(eq.lhs) ? distribute_shift(Shift(iv, 1)(eq.rhs)) : (eq.rhs - eq.lhs) end - u_next = dvs - u = map(Shift(iv, -1), u_next) - build_function_wrapper(sys, exprs, u_next, u, ps..., iv; p_start = 3, kwargs...) + # Handle observables in algebraic equations, since they are shifted + obs = observed(sys) + shifted_obs = [distribute_shift(Shift(iv, 1)(eq)) for eq in obs] + obsidxs = observed_equations_used_by(sys, exprs; obs = shifted_obs) + extra_assignments = [Assignment(shifted_obs[i].lhs, shifted_obs[i].rhs) for i in obsidxs] + + u_next = map(Shift(iv, 1), dvs) + u = dvs + build_function_wrapper(sys, exprs, u_next, u, ps..., iv; p_start = 3, extra_assignments, kwargs...) end function shift_u0map_forward(sys::ImplicitDiscreteSystem, u0map, defs) @@ -279,15 +286,22 @@ function shift_u0map_forward(sys::ImplicitDiscreteSystem, u0map, defs) for k in collect(keys(u0map)) v = u0map[k] if !((op = operation(k)) isa Shift) - 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)).") + 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)).") + + 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)(k))).") + else + updated[k] = v end - updated[shift2term(k)] = v end for var in unknowns(sys) op = operation(var) - haskey(updated, var) && continue root = getunshifted(var) + shift = getshift(var) isnothing(root) && continue + (haskey(updated, Shift(iv, shift)(root)) || haskey(updated, var)) && continue haskey(defs, root) || error("Initial condition for $var not provided.") updated[var] = defs[root] end @@ -317,7 +331,9 @@ function SciMLBase.ImplicitDiscreteProblem( u0map = to_varmap(u0map, dvs) u0map = shift_u0map_forward(sys, u0map, defaults(sys)) f, u0, p = process_SciMLProblem( - ImplicitDiscreteFunction, sys, u0map, parammap; eval_expression, eval_module) + ImplicitDiscreteFunction, sys, u0map, parammap; eval_expression, eval_module, kwargs...) + + kwargs = filter_kwargs(kwargs) ImplicitDiscreteProblem(f, u0, tspan, p; kwargs...) end diff --git a/src/systems/systemstructure.jl b/src/systems/systemstructure.jl index e7d5fa71f1..e5909ed892 100644 --- a/src/systems/systemstructure.jl +++ b/src/systems/systemstructure.jl @@ -440,7 +440,7 @@ function TearingState(sys; quick_cancel = false, check = true) SystemStructure(complete(var_to_diff), complete(eq_to_diff), complete(graph), nothing, var_types, sys isa AbstractDiscreteSystem), Any[]) - if sys isa AbstractDiscreteSystem + if sys isa DiscreteSystem ts = shift_discrete_system(ts) end return ts diff --git a/test/implicit_discrete_system.jl b/test/implicit_discrete_system.jl index f2e7deaa32..f298f43e7c 100644 --- a/test/implicit_discrete_system.jl +++ b/test/implicit_discrete_system.jl @@ -5,12 +5,13 @@ using StableRNGs k = ShiftIndex(t) rng = StableRNG(22525) -# Shift(t, -1)(x(t)) - x_{t-1}(t) -# -3 - x(t) + x(t)*x_{t-1} @testset "Correct ImplicitDiscreteFunction" begin @variables x(t) = 1 @mtkbuild sys = ImplicitDiscreteSystem([x(k) ~ x(k)*x(k-1) - 3], t) tspan = (0, 10) + + # u[2] - u_next[1] + # -3 - u_next[2] + u_next[2]*u_next[1] f = ImplicitDiscreteFunction(sys) u_next = [3., 1.5] @test f(u_next, [2.,3.], [], t) ≈ [0., 0.] @@ -30,7 +31,6 @@ rng = StableRNG(22525) @test_throws ErrorException prob = ImplicitDiscreteProblem(sys, [], tspan) end -# Test solvers @testset "System with algebraic equations" begin @variables x(t) y(t) eqs = [x(k) ~ x(k-1) + x(k-2), @@ -51,13 +51,24 @@ end end # Initialization is satisfied. - prob = ImplicitDiscreteProblem(sys, [x(k-1) => 3.], tspan) + prob = ImplicitDiscreteProblem(sys, [x(k-1) => .3, x(k-2) => .4], (0, 10), guesses = [y => 1]) @test (prob.u0[1] + prob.u0[2])^2 + prob.u0[3]^2 ≈ 1 end -@testset "System with algebraic equations, implicit difference equations, explicit difference equations" begin - @variables x(t) y(t) +@testset "Handle observables in function codegen" begin + # Observable appears in differential equation + @variables x(t) y(t) z(t) eqs = [x(k) ~ x(k-1) + x(k-2), - y(k) ~ x(k) + x(k-2)*y(k-1)] + y(k) ~ x(k) + x(k-2)*z(k-1), + x + y + z ~ 2] + @mtkbuild sys = ImplicitDiscreteSystem(eqs, t) + @test length(unknowns(sys)) == length(equations(sys)) == 3 + @test occursin("var\"y(t)\"", string(ImplicitDiscreteFunctionExpr(sys))) + + # Shifted observable that appears in algebraic equation is properly handled. + eqs = [z(k) ~ x(k) + sin(x(k)), + y(k) ~ x(k-1) + x(k-2), + z(k) * x(k) ~ 3] @mtkbuild sys = ImplicitDiscreteSystem(eqs, t) + @test occursin("var\"Shift(t, 1)(z(t))\"", string(ImplicitDiscreteFunctionExpr(sys))) end From 48ec55c15ed46d03d6a5e1e8d0627fd026dde311 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 25 Feb 2025 13:22:23 +0530 Subject: [PATCH 1005/1337] feat: add `map_variables_to_equations` --- src/ModelingToolkit.jl | 1 + src/systems/systems.jl | 54 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/src/ModelingToolkit.jl b/src/ModelingToolkit.jl index b683e132a2..48070f244a 100644 --- a/src/ModelingToolkit.jl +++ b/src/ModelingToolkit.jl @@ -276,6 +276,7 @@ export TearingState export BipartiteGraph, equation_dependencies, variable_dependencies export eqeq_dependencies, varvar_dependencies export asgraph, asdigraph +export map_variables_to_equations export toexpr, get_variables export simplify, substitute diff --git a/src/systems/systems.jl b/src/systems/systems.jl index ef0f966eee..6151ffa515 100644 --- a/src/systems/systems.jl +++ b/src/systems/systems.jl @@ -158,3 +158,57 @@ function __structural_simplify(sys::AbstractSystem, io = nothing; simplify = fal guesses = guesses(sys), initialization_eqs = initialization_equations(sys)) end end + +""" + $(TYPEDSIGNATURES) + +Given a system that has been simplified via `structural_simplify`, return a `Dict` mapping +variables of the system to equations that are used to solve for them. This includes +observed variables. + +# Keyword Arguments + +- `rename_dummy_derivatives`: Whether to rename dummy derivative variable keys into their + `Differential` forms. For example, this would turn the key `yˍt(t)` into + `Differential(t)(y(t))`. +""" +function map_variables_to_equations(sys::AbstractSystem; rename_dummy_derivatives = true) + if !has_tearing_state(sys) + throw(ArgumentError("$(typeof(sys)) is not supported.")) + end + ts = get_tearing_state(sys) + if ts === nothing + throw(ArgumentError("`map_variables_to_equations` requires a simplified system. Call `structural_simplify` on the system before calling this function.")) + end + + dummy_sub = Dict() + if rename_dummy_derivatives && has_schedule(sys) && (sc = get_schedule(sys)) !== nothing + dummy_sub = Dict(v => k for (k, v) in sc.dummy_sub if isequal(default_toterm(k), v)) + end + + mapping = Dict{Union{Num, BasicSymbolic}, Equation}() + eqs = equations(sys) + for eq in eqs + isdifferential(eq.lhs) || continue + var = arguments(eq.lhs)[1] + var = get(dummy_sub, var, var) + mapping[var] = eq + end + + graph = ts.structure.graph + algvars = BitSet(findall( + Base.Fix1(StructuralTransformations.isalgvar, ts.structure), 1:ndsts(graph))) + algeqs = BitSet(findall(1:nsrcs(graph)) do eq + all(!Base.Fix1(isdervar, ts.structure), 𝑠neighbors(graph, eq)) + end) + alge_var_eq_matching = complete(maximal_matching(graph, in(algeqs), in(algvars))) + for (i, eq) in enumerate(alge_var_eq_matching) + eq isa Unassigned && continue + mapping[get(dummy_sub, ts.fullvars[i], ts.fullvars[i])] = eqs[eq] + end + for eq in observed(sys) + mapping[get(dummy_sub, eq.lhs, eq.lhs)] = eq + end + + return mapping +end From 74de88b8acfb0efb9045801604969e682bc3504d Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 25 Feb 2025 13:22:30 +0530 Subject: [PATCH 1006/1337] test: test `map_variables_to_equations` --- test/structural_transformation/utils.jl | 98 ++++++++++++++++++++++++- 1 file changed, 96 insertions(+), 2 deletions(-) diff --git a/test/structural_transformation/utils.jl b/test/structural_transformation/utils.jl index 4da3d1e924..24cfb98d45 100644 --- a/test/structural_transformation/utils.jl +++ b/test/structural_transformation/utils.jl @@ -3,8 +3,8 @@ using ModelingToolkit using Graphs using SparseArrays using UnPack -using ModelingToolkit: t_nounits as t, D_nounits as D -const ST = StructuralTransformations +using ModelingToolkit: t_nounits as t, D_nounits as D, default_toterm +using Symbolics: unwrap # Define some variables @parameters L g @@ -162,3 +162,97 @@ end structural_simplify(sys; additional_passes = [pass]) @test value[] == 1 end + +@testset "`map_variables_to_equations`" begin + @testset "Not supported for systems without `.tearing_state`" begin + @variables x + @mtkbuild sys = OptimizationSystem(x^2) + @test_throws ArgumentError map_variables_to_equations(sys) + end + @testset "Requires simplified system" begin + @variables x(t) y(t) + @named sys = ODESystem([D(x) ~ x, y ~ 2x], t) + sys = complete(sys) + @test_throws ArgumentError map_variables_to_equations(sys) + end + @testset "`ODESystem`" begin + @variables x(t) y(t) z(t) + @mtkbuild sys = ODESystem([D(x) ~ 2x + y, y ~ x + z, z^3 + x^3 ~ 12], t) + mapping = map_variables_to_equations(sys) + @test mapping[x] == (D(x) ~ 2x + y) + @test mapping[y] == (y ~ x + z) + @test mapping[z] == (0 ~ 12 - z^3 - x^3) + @test length(mapping) == 3 + + @testset "With dummy derivatives" begin + @parameters g + @variables x(t) y(t) [state_priority = 10] λ(t) + eqs = [D(D(x)) ~ λ * x + D(D(y)) ~ λ * y - g + x^2 + y^2 ~ 1] + @mtkbuild sys = ODESystem(eqs, t) + mapping = map_variables_to_equations(sys) + + yt = default_toterm(unwrap(D(y))) + xt = default_toterm(unwrap(D(x))) + xtt = default_toterm(unwrap(D(D(x)))) + @test mapping[x] == (0 ~ 1 - x^2 - y^2) + @test mapping[y] == (D(y) ~ yt) + @test mapping[D(y)] == (D(yt) ~ -g + y * λ) + @test mapping[D(x)] == (0 ~ -2xt * x - 2yt * y) + @test mapping[D(D(x))] == (xtt ~ x * λ) + @test length(mapping) == 5 + + @testset "`rename_dummy_derivatives = false`" begin + mapping = map_variables_to_equations(sys; rename_dummy_derivatives = false) + + @test mapping[x] == (0 ~ 1 - x^2 - y^2) + @test mapping[y] == (D(y) ~ yt) + @test mapping[yt] == (D(yt) ~ -g + y * λ) + @test mapping[xt] == (0 ~ -2xt * x - 2yt * y) + @test mapping[xtt] == (xtt ~ x * λ) + @test length(mapping) == 5 + end + end + @testset "DDEs" begin + function oscillator(; name, k = 1.0, τ = 0.01) + @parameters k=k τ=τ + @variables x(..)=0.1 y(t)=0.1 jcn(t)=0.0 delx(t) + eqs = [D(x(t)) ~ y, + D(y) ~ -k * x(t - τ) + jcn, + delx ~ x(t - τ)] + return System(eqs, t; name = name) + end + + systems = @named begin + osc1 = oscillator(k = 1.0, τ = 0.01) + osc2 = oscillator(k = 2.0, τ = 0.04) + end + eqs = [osc1.jcn ~ osc2.delx, + osc2.jcn ~ osc1.delx] + @named coupledOsc = System(eqs, t) + @mtkbuild sys = compose(coupledOsc, systems) + mapping = map_variables_to_equations(sys) + x1 = operation(unwrap(osc1.x)) + x2 = operation(unwrap(osc2.x)) + @test mapping[osc1.x] == (D(osc1.x) ~ osc1.y) + @test mapping[osc1.y] == (D(osc1.y) ~ osc1.jcn - osc1.k * x1(t - osc1.τ)) + @test mapping[osc1.delx] == (osc1.delx ~ x1(t - osc1.τ)) + @test mapping[osc1.jcn] == (osc1.jcn ~ osc2.delx) + @test mapping[osc2.x] == (D(osc2.x) ~ osc2.y) + @test mapping[osc2.y] == (D(osc2.y) ~ osc2.jcn - osc2.k * x2(t - osc2.τ)) + @test mapping[osc2.delx] == (osc2.delx ~ x2(t - osc2.τ)) + @test mapping[osc2.jcn] == (osc2.jcn ~ osc1.delx) + @test length(mapping) == 8 + end + end + @testset "`NonlinearSystem`" begin + @variables x y z + @mtkbuild sys = NonlinearSystem([x^2 ~ 2y^2 + 1, sin(z) ~ y, z^3 + 4z + 1 ~ 0]) + mapping = map_variables_to_equations(sys) + @test mapping[x] == (0 ~ 2y^2 + 1 - x^2) + @test mapping[y] == (y ~ sin(z)) + @test mapping[z] == (0 ~ -1 - 4z - z^3) + @test length(mapping) == 3 + end +end From 55601e07318531341c118de9b3f4b38416a03373 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 25 Feb 2025 13:24:28 +0530 Subject: [PATCH 1007/1337] docs: add `map_variables_to_equations` to docs --- docs/src/basics/DependencyGraphs.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/src/basics/DependencyGraphs.md b/docs/src/basics/DependencyGraphs.md index 67de5ea8f1..73fbbca9be 100644 --- a/docs/src/basics/DependencyGraphs.md +++ b/docs/src/basics/DependencyGraphs.md @@ -22,3 +22,9 @@ asdigraph eqeq_dependencies varvar_dependencies ``` + +# Miscellaneous + +```@docs +map_variables_to_equations +``` From 615c7fc37ff0d76e2c6f066c5e490716a5aecaae Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 24 Feb 2025 16:24:12 +0530 Subject: [PATCH 1008/1337] fix: allow passing guesses to `linearization_function` --- src/linearization.jl | 10 +++++----- test/downstream/linearize.jl | 9 ++++++--- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/linearization.jl b/src/linearization.jl index f83efc1642..01d8ee2f4a 100644 --- a/src/linearization.jl +++ b/src/linearization.jl @@ -41,6 +41,7 @@ function linearization_function(sys::AbstractSystem, inputs, initialization_solver_alg = TrustRegion(), eval_expression = false, eval_module = @__MODULE__, warn_initialize_determined = true, + guesses = Dict(), kwargs...) op = Dict(op) inputs isa AbstractVector || (inputs = [inputs]) @@ -66,11 +67,10 @@ function linearization_function(sys::AbstractSystem, inputs, initializealg = initialize ? OverrideInit() : NoInit() end - fun, u0, p = process_SciMLProblem( - ODEFunction{true, SciMLBase.FullSpecialize}, sys, op, p; - t = 0.0, build_initializeprob = initializealg isa OverrideInit, - allow_incomplete = true, algebraic_only = true) - prob = ODEProblem(fun, u0, (nothing, nothing), p) + prob = ODEProblem{true, SciMLBase.FullSpecialize}( + sys, op, (nothing, nothing), p; allow_incomplete = true, + algebraic_only = true, guesses) + u0 = state_values(prob) ps = parameters(sys) h = build_explicit_observed_function(sys, outputs; eval_expression, eval_module) diff --git a/test/downstream/linearize.jl b/test/downstream/linearize.jl index 3cab641d26..898040fb4f 100644 --- a/test/downstream/linearize.jl +++ b/test/downstream/linearize.jl @@ -317,12 +317,15 @@ matrices = linfun([1.0], Dict(p => 3.0), 1.0) @test matrices.f_u[] == 3.0 end -@testset "Issue #2941" begin - @variables x(t) y(t) [guess = 1.0] +@testset "Issue #2941 and #3400" begin + @variables x(t) y(t) @parameters p eqs = [0 ~ x * log(y) - p] @named sys = ODESystem(eqs, t; defaults = [p => 1.0]) sys = complete(sys) - @test_nowarn linearize( + @test_throws ModelingToolkit.MissingVariablesError linearize( sys, [x], []; op = Dict(x => 1.0), allow_input_derivatives = true) + @test_nowarn linearize( + sys, [x], []; op = Dict(x => 1.0), guesses = Dict(y => 1.0), + allow_input_derivatives = true) end From 23231d6095628ae4199537b1183f56b4b589daaa Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 24 Feb 2025 16:26:35 +0530 Subject: [PATCH 1009/1337] feat: add `Base.show` methods for `LinearizationFunction` and `LinearizationProblem` --- src/linearization.jl | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/linearization.jl b/src/linearization.jl index 01d8ee2f4a..411620e8f3 100644 --- a/src/linearization.jl +++ b/src/linearization.jl @@ -148,6 +148,12 @@ function SymbolicIndexingInterface.parameter_values(f::LinearizationFunction) end SymbolicIndexingInterface.current_time(f::LinearizationFunction) = current_time(f.prob) +function Base.show(io::IO, mime::MIME"text/plain", lf::LinearizationFunction) + printstyled(io, "LinearizationFunction"; bold = true, color = :blue) + println(io, " which wraps:") + show(io, mime, lf.prob) +end + """ $(TYPEDSIGNATURES) @@ -265,6 +271,12 @@ mutable struct LinearizationProblem{F <: LinearizationFunction, T} t::T end +function Base.show(io::IO, mime::MIME"text/plain", prob::LinearizationProblem) + printstyled(io, "LinearizationProblem"; bold = true, color = :blue) + println(io, " at time ", prob.t, " which wraps:") + show(io, mime, prob.f.prob) +end + """ $(TYPEDSIGNATURES) From bbfb34d6678b1f504ef4a5e0102b8412ec7c8766 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 24 Feb 2025 16:26:49 +0530 Subject: [PATCH 1010/1337] docs: link to MTK's linear analysis doc page rather than the standard library --- docs/src/basics/Linearization.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/src/basics/Linearization.md b/docs/src/basics/Linearization.md index 0b29beec2f..5338deda49 100644 --- a/docs/src/basics/Linearization.md +++ b/docs/src/basics/Linearization.md @@ -43,7 +43,7 @@ using ModelingToolkit: inputs, outputs !!! note "Inputs must be unconnected" - The model above has 4 variables but only three equations, there is no equation specifying the value of `r` since `r` is an input. This means that only unbalanced models can be linearized, or in other words, models that are balanced and can be simulated _cannot_ be linearized. To learn more about this, see [How to linearize a ModelingToolkit model (YouTube)](https://www.youtube.com/watch?v=-XOux-2XDGI&t=395s). Also see [ModelingToolkitStandardLibrary: Linear analysis](https://docs.sciml.ai/ModelingToolkitStandardLibrary/stable/API/linear_analysis/) for utilities that make linearization of completed models easier. + The model above has 4 variables but only three equations, there is no equation specifying the value of `r` since `r` is an input. This means that only unbalanced models can be linearized, or in other words, models that are balanced and can be simulated _cannot_ be linearized. To learn more about this, see [How to linearize a ModelingToolkit model (YouTube)](https://www.youtube.com/watch?v=-XOux-2XDGI&t=395s). Also see [ModelingToolkitStandardLibrary: Linear analysis](https://docs.sciml.ai/ModelingToolkit/stable/tutorials/linear_analysis/) for utilities that make linearization of completed models easier. !!! note "Un-simplified system" @@ -75,7 +75,7 @@ If the modeled system is actually proper (but MTK failed to find a proper realiz ## Tools for linear analysis -[ModelingToolkitStandardLibrary](https://docs.sciml.ai/ModelingToolkitStandardLibrary/stable/) contains a set of [tools for more advanced linear analysis](https://docs.sciml.ai/ModelingToolkitStandardLibrary/stable/API/linear_analysis/). These can be used to make it easier to work with and analyze causal models, such as control and signal-processing systems. +ModelingToolkit contains a set of [tools for more advanced linear analysis](https://docs.sciml.ai/ModelingToolkit/stable/tutorials/linear_analysis/). These can be used to make it easier to work with and analyze causal models, such as control and signal-processing systems. Also see [ControlSystemsMTK.jl](https://juliacontrol.github.io/ControlSystemsMTK.jl/dev/) for an interface to [ControlSystems.jl](https://github.com/JuliaControl/ControlSystems.jl) that contains tools for linear analysis and frequency-domain analysis. From c17ce0a3a2bcbdb79f47c38098f8114a5af2d439 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 24 Feb 2025 18:03:14 +0530 Subject: [PATCH 1011/1337] feat: export `CommonSolve.solve` --- src/ModelingToolkit.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ModelingToolkit.jl b/src/ModelingToolkit.jl index b683e132a2..27d398e633 100644 --- a/src/ModelingToolkit.jl +++ b/src/ModelingToolkit.jl @@ -262,6 +262,7 @@ export independent_variable, equations, controls, observed, full_equations export initialization_equations, guesses, defaults, parameter_dependencies, hierarchy export structural_simplify, expand_connections, linearize, linearization_function, LinearizationProblem +export solve export calculate_jacobian, generate_jacobian, generate_function, generate_custom_function export calculate_control_jacobian, generate_control_jacobian From 027fe713f3e5f17003f562b24e6338e52d50484d Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 24 Feb 2025 18:06:54 +0530 Subject: [PATCH 1012/1337] docs: add doc example for faster linearization --- docs/src/basics/Linearization.md | 71 +++++++++++++++++++++++++++++++- 1 file changed, 70 insertions(+), 1 deletion(-) diff --git a/docs/src/basics/Linearization.md b/docs/src/basics/Linearization.md index 5338deda49..1c06ce72d4 100644 --- a/docs/src/basics/Linearization.md +++ b/docs/src/basics/Linearization.md @@ -22,7 +22,7 @@ The `linearize` function expects the user to specify the inputs ``u`` and the ou ```@example LINEARIZE using ModelingToolkit using ModelingToolkit: t_nounits as t, D_nounits as D -@variables x(t)=0 y(t)=0 u(t)=0 r(t)=0 +@variables x(t)=0 y(t) u(t) r(t)=0 @parameters kp = 1 eqs = [u ~ kp * (r - y) # P controller @@ -57,6 +57,74 @@ The operating point to linearize around can be specified with the keyword argume If linearization is to be performed around multiple operating points, the simplification of the system has to be carried out a single time only. To facilitate this, the lower-level function [`ModelingToolkit.linearization_function`](@ref) is available. This function further allows you to obtain separate Jacobians for the differential and algebraic parts of the model. For ODE models without algebraic equations, the statespace representation above is available from the output of `linearization_function` as `A, B, C, D = f_x, f_u, h_x, h_u`. +All variables that will be fixed by an operating point _must_ be provided in the operating point to `linearization_function`. For example, if the operating points fix the value of +`x`, `y` and `z` then an operating point with constant values for these variables (e.g. `Dict(x => 1.0, y => 1.0, z => 1.0)`) must be provided. The constant values themselves +do not matter and can be changed by subsequent operating points. + +One approach to batch linearization would be to call `linearize` in a loop, providing a different operating point each time. For example: + +```@example LINEARIZE +using ModelingToolkitStandardLibrary +using ModelingToolkitStandardLibrary.Blocks + +@parameters k=10 k3=2 c=1 +@variables x(t)=0 [bounds = (-0.5, 1.5)] +@variables v(t) = 0 + +@named y = Blocks.RealOutput() +@named u = Blocks.RealInput() + +eqs = [D(x) ~ v + D(v) ~ -k * x - k3 * x^3 - c * v + 10u.u + y.u ~ x] + +@named duffing = ODESystem(eqs, t, systems = [y, u], defaults = [u.u => 0]) + +# pass a constant value for `x`, since it is the variable we will change in operating points +linfun, simplified_sys = linearization_function(duffing, [u.u], [y.u]; op = Dict(x => NaN)); + +println(linearize(simplified_sys, linfun; op = Dict(x => 1.0))) +println(linearize(simplified_sys, linfun; op = Dict(x => 0.0))) + +@time linearize(simplified_sys, linfun; op = Dict(x => 0.0)) + +nothing # hide +``` + +However, this route is still expensive since it has to repeatedly process the symbolic map provided to `op`. `linearize` is simply a wrapper for creating and solving a +[`ModelingToolkit.LinearizationProblem`](@ref). This object is symbolically indexable, and can thus integrate with SymbolicIndexingInterface.jl for fast updates. + +```@example LINEARIZE +using SymbolicIndexingInterface + +# The second argument is the value of the independent variable `t`. +linprob = LinearizationProblem(linfun, 1.0) +# It can be mutated +linprob.t = 0.0 +# create a setter function to update `x` efficiently +setter! = setu(linprob, x) + +function fast_linearize!(problem, setter!, value) + setter!(problem, value) + solve(problem) +end + +println(fast_linearize!(linprob, setter!, 1.0)) +println(fast_linearize!(linprob, setter!, 0.0)) + +@time fast_linearize!(linprob, setter!, 1.0) + +nothing # hide +``` + +Note that `linprob` above can be interacted with similar to a normal `ODEProblem`. + +```@repl LINEARIZE +prob[x] +prob[x] = 1.5 +prob[x] +``` + ## Symbolic linearization The function [`ModelingToolkit.linearize_symbolic`](@ref) works similar to [`ModelingToolkit.linearize`](@ref) but returns symbolic rather than numeric Jacobians. Symbolic linearization have several limitations and no all systems that can be linearized numerically can be linearized symbolically. @@ -89,4 +157,5 @@ Pages = ["Linearization.md"] linearize ModelingToolkit.linearize_symbolic ModelingToolkit.linearization_function +ModelingToolkit.LinearizationProblem ``` From 64427a9df3cc994304ed6c67499b4084a7a5e2db Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 27 Feb 2025 11:52:49 +0530 Subject: [PATCH 1013/1337] fix: fix `SCCNonlinearProblem` reordering parameters --- src/systems/nonlinear/nonlinearsystem.jl | 3 ++- test/scc_nonlinear_problem.jl | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/systems/nonlinear/nonlinearsystem.jl b/src/systems/nonlinear/nonlinearsystem.jl index 06e843d6be..ad3fbfdeef 100644 --- a/src/systems/nonlinear/nonlinearsystem.jl +++ b/src/systems/nonlinear/nonlinearsystem.jl @@ -792,7 +792,8 @@ function SciMLBase.SCCNonlinearProblem{iip}(sys::NonlinearSystem, u0map, new_eqs = eqs[reduce(vcat, eq_sccs)] @set! sys.unknowns = new_dvs @set! sys.eqs = new_eqs - sys = complete(sys) + @set! sys.index_cache = subset_unknowns_observed( + get_index_cache(sys), sys, new_dvs, getproperty.(obs, (:lhs,))) return SCCNonlinearProblem(subprobs, explicitfuns, p, true; sys) end diff --git a/test/scc_nonlinear_problem.jl b/test/scc_nonlinear_problem.jl index ef4638cdb0..92030a84aa 100644 --- a/test/scc_nonlinear_problem.jl +++ b/test/scc_nonlinear_problem.jl @@ -263,3 +263,22 @@ end sol = solve(prob, NewtonRaphson()) @test SciMLBase.successful_retcode(sol) end + +@testset "SCCNonlinearProblem retains parameter order" begin + @variables x y z + @parameters σ β ρ + @mtkbuild fullsys = NonlinearSystem( + [0 ~ x^3 * β + y^3 * ρ - σ, 0 ~ x^2 + 2x * y + y^2, 0 ~ z^2 - 4z + 4], + [x, y, z], [σ, β, ρ]) + + u0 = [x => 1.0, + y => 0.0, + z => 0.0] + + p = [σ => 28.0, + ρ => 10.0, + β => 8 / 3] + + sccprob = SCCNonlinearProblem(fullsys, u0, p) + @test isequal(parameters(fullsys), parameters(sccprob.f.sys)) +end From c82fc9b0fdd9b23370ad71846873d75922bb26f8 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 27 Feb 2025 15:16:12 +0530 Subject: [PATCH 1014/1337] build: bump patch version --- Project.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Project.toml b/Project.toml index 8c4a58045e..8061f6f1ef 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "ModelingToolkit" uuid = "961ee093-0014-501f-94e3-6117800e7a78" authors = ["Yingbo Ma ", "Chris Rackauckas and contributors"] -version = "9.64.2" +version = "9.64.3" [deps] AbstractTrees = "1520ce14-60c1-5f80-bbc7-55ef81b5835c" @@ -190,4 +190,4 @@ Sundials = "c3572dad-4567-51f8-b174-8c6c989267f4" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [targets] -test = ["AmplNLWriter", "BenchmarkTools", "BoundaryValueDiffEqMIRK", "BoundaryValueDiffEqAscher", "ControlSystemsBase", "DataInterpolations", "DelayDiffEq", "NonlinearSolve", "ForwardDiff", "Ipopt", "Ipopt_jll", "ModelingToolkitStandardLibrary", "Optimization", "OptimizationOptimJL", "OptimizationMOI", "OrdinaryDiffEq", "OrdinaryDiffEqCore", "OrdinaryDiffEqDefault", "REPL", "Random", "ReferenceTests", "SafeTestsets", "StableRNGs", "Statistics", "SteadyStateDiffEq", "Test", "StochasticDiffEq", "Sundials", "StochasticDelayDiffEq", "Pkg", "JET", "OrdinaryDiffEqNonlinearSolve", "Logging"] \ No newline at end of file +test = ["AmplNLWriter", "BenchmarkTools", "BoundaryValueDiffEqMIRK", "BoundaryValueDiffEqAscher", "ControlSystemsBase", "DataInterpolations", "DelayDiffEq", "NonlinearSolve", "ForwardDiff", "Ipopt", "Ipopt_jll", "ModelingToolkitStandardLibrary", "Optimization", "OptimizationOptimJL", "OptimizationMOI", "OrdinaryDiffEq", "OrdinaryDiffEqCore", "OrdinaryDiffEqDefault", "REPL", "Random", "ReferenceTests", "SafeTestsets", "StableRNGs", "Statistics", "SteadyStateDiffEq", "Test", "StochasticDiffEq", "Sundials", "StochasticDelayDiffEq", "Pkg", "JET", "OrdinaryDiffEqNonlinearSolve", "Logging"] From e5245e1256f732a2951af0ff1564d115553c01fe Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 21 Feb 2025 13:39:35 +0530 Subject: [PATCH 1015/1337] feat: run trivial initialization in problem constructor --- src/systems/diffeqs/abstractodesystem.jl | 13 ++++++------ src/systems/diffeqs/sdesystem.jl | 4 ++-- src/systems/nonlinear/nonlinearsystem.jl | 4 ++-- test/initializationsystem.jl | 26 ++++++++++++++++++++++++ 4 files changed, 37 insertions(+), 10 deletions(-) diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index 205d7e4601..e8f826eb49 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -759,7 +759,7 @@ function DiffEqBase.ODEProblem{iip, specialize}(sys::AbstractODESystem, u0map = kwargs1 = merge(kwargs1, (; tstops)) end - return ODEProblem{iip}(f, u0, tspan, p, pt; kwargs1..., kwargs...) + return remake(ODEProblem{iip}(f, u0, tspan, p, pt; kwargs1..., kwargs...)) end get_callback(prob::ODEProblem) = prob.kwargs[:callback] @@ -963,8 +963,9 @@ function DiffEqBase.DAEProblem{iip}(sys::AbstractODESystem, du0map, u0map, tspan kwargs1 = merge(kwargs1, (; tstops)) end - DAEProblem{iip}(f, du0, u0, tspan, p; differential_vars = differential_vars, - kwargs..., kwargs1...) + return remake(DAEProblem{iip}( + f, du0, u0, tspan, p; differential_vars = differential_vars, + kwargs..., kwargs1...)) end function generate_history(sys::AbstractODESystem, u0; expression = Val{false}, kwargs...) @@ -1008,7 +1009,7 @@ function DiffEqBase.DDEProblem{iip}(sys::AbstractODESystem, u0map = [], if cbs !== nothing kwargs1 = merge(kwargs1, (callback = cbs,)) end - DDEProblem{iip}(f, u0, h, tspan, p; kwargs1..., kwargs...) + return remake(DDEProblem{iip}(f, u0, h, tspan, p; kwargs1..., kwargs...)) end function DiffEqBase.SDDEProblem(sys::AbstractODESystem, args...; kwargs...) @@ -1057,9 +1058,9 @@ function DiffEqBase.SDDEProblem{iip}(sys::AbstractODESystem, u0map = [], else noise_rate_prototype = zeros(eltype(u0), size(noiseeqs)) end - SDDEProblem{iip}(f, f.g, u0, h, tspan, p; + return remake(SDDEProblem{iip}(f, f.g, u0, h, tspan, p; noise_rate_prototype = - noise_rate_prototype, kwargs1..., kwargs...) + noise_rate_prototype, kwargs1..., kwargs...)) end """ diff --git a/src/systems/diffeqs/sdesystem.jl b/src/systems/diffeqs/sdesystem.jl index bc3fd6b73e..70ace68d15 100644 --- a/src/systems/diffeqs/sdesystem.jl +++ b/src/systems/diffeqs/sdesystem.jl @@ -792,8 +792,8 @@ function DiffEqBase.SDEProblem{iip, specialize}( kwargs = filter_kwargs(kwargs) - SDEProblem{iip}(f, u0, tspan, p; callback = cbs, noise, - noise_rate_prototype = noise_rate_prototype, kwargs...) + return remake(SDEProblem{iip}(f, u0, tspan, p; callback = cbs, noise, + noise_rate_prototype = noise_rate_prototype, kwargs...)) end function DiffEqBase.SDEProblem(sys::ODESystem, args...; kwargs...) diff --git a/src/systems/nonlinear/nonlinearsystem.jl b/src/systems/nonlinear/nonlinearsystem.jl index ad3fbfdeef..a74fe0b8e6 100644 --- a/src/systems/nonlinear/nonlinearsystem.jl +++ b/src/systems/nonlinear/nonlinearsystem.jl @@ -519,7 +519,7 @@ function DiffEqBase.NonlinearProblem{iip}(sys::NonlinearSystem, u0map, f, u0, p = process_SciMLProblem(NonlinearFunction{iip}, sys, u0map, parammap; check_length, kwargs...) pt = something(get_metadata(sys), StandardNonlinearProblem()) - NonlinearProblem{iip}(f, u0, p, pt; filter_kwargs(kwargs)...) + return remake(NonlinearProblem{iip}(f, u0, p, pt; filter_kwargs(kwargs)...)) end """ @@ -548,7 +548,7 @@ function DiffEqBase.NonlinearLeastSquaresProblem{iip}(sys::NonlinearSystem, u0ma f, u0, p = process_SciMLProblem(NonlinearFunction{iip}, sys, u0map, parammap; check_length, kwargs...) pt = something(get_metadata(sys), StandardNonlinearProblem()) - NonlinearLeastSquaresProblem{iip}(f, u0, p; filter_kwargs(kwargs)...) + return remake(NonlinearLeastSquaresProblem{iip}(f, u0, p; filter_kwargs(kwargs)...)) end const TypeT = Union{DataType, UnionAll} diff --git a/test/initializationsystem.jl b/test/initializationsystem.jl index 45afa4163a..aaf7bdfce8 100644 --- a/test/initializationsystem.jl +++ b/test/initializationsystem.jl @@ -1388,3 +1388,29 @@ end integ1 = init(oprob1) @test integ1[X1] ≈ 1.0 end + +@testset "Trivial initialization is run on problem construction" begin + @variables _x(..) y(t) + @brownian a + @parameters tot + x = _x(t) + @testset "$Problem" for (Problem, lhs, rhs) in [ + (ODEProblem, D, 0.0), + (SDEProblem, D, a), + (DDEProblem, D, _x(t - 0.1)), + (SDDEProblem, D, _x(t - 0.1) + a) + ] + @mtkbuild sys = ModelingToolkit.System([lhs(x) ~ x + rhs, x + y ~ tot], t; + guesses = [tot => 1.0], defaults = [tot => missing]) + prob = Problem(sys, [x => 1.0, y => 1.0], (0.0, 1.0)) + @test prob.ps[tot] ≈ 2.0 + end + @testset "$Problem" for Problem in [NonlinearProblem, NonlinearLeastSquaresProblem] + @parameters p1 p2 + @mtkbuild sys = NonlinearSystem([x^2 + y^2 ~ p1, (x - 1)^2 + (y - 1)^2 ~ p2]; + parameter_dependencies = [p2 ~ 2p1], + guesses = [p1 => 0.0], defaults = [p1 => missing]) + prob = Problem(sys, [x => 1.0, y => 1.0], [p2 => 6.0]) + @test prob.ps[p1] ≈ 3.0 + end +end From 6ed57907526a4cd8e14e53e1fed2ee8da8a4a7ca Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 21 Feb 2025 17:52:45 +0530 Subject: [PATCH 1016/1337] fix: retain `u0_constructor` in `initializeprobmap` --- src/systems/problem_utils.jl | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/systems/problem_utils.jl b/src/systems/problem_utils.jl index 77f4229696..1cd8304eb9 100644 --- a/src/systems/problem_utils.jl +++ b/src/systems/problem_utils.jl @@ -601,7 +601,7 @@ All other keyword arguments are forwarded to `InitializationProblem`. """ function maybe_build_initialization_problem( sys::AbstractSystem, op::AbstractDict, u0map, pmap, t, defs, - guesses, missing_unknowns; implicit_dae = false, kwargs...) + guesses, missing_unknowns; implicit_dae = false, u0_constructor = identity, kwargs...) guesses = merge(ModelingToolkit.guesses(sys), todict(guesses)) if t === nothing && is_time_dependent(sys) @@ -615,7 +615,7 @@ function maybe_build_initialization_problem( if is_time_dependent(sys) all_init_syms = Set(all_symbols(initializeprob)) solved_unknowns = filter(var -> var in all_init_syms, unknowns(sys)) - initializeprobmap = getu(initializeprob, solved_unknowns) + initializeprobmap = u0_constructor ∘ getu(initializeprob, solved_unknowns) else initializeprobmap = nothing end @@ -781,7 +781,8 @@ function process_SciMLProblem( eval_expression, eval_module, fully_determined, warn_cyclic_dependency, check_units = check_initialization_units, circular_dependency_max_cycle_length, circular_dependency_max_cycles, use_scc, - force_time_independent = force_initialization_time_independent, algebraic_only, allow_incomplete) + force_time_independent = force_initialization_time_independent, algebraic_only, allow_incomplete, + u0_constructor) kwargs = merge(kwargs, kws) end From 76d022a3673332c088132d743a8e572b74e83dc7 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 24 Feb 2025 11:37:46 +0530 Subject: [PATCH 1017/1337] docs: add comments explaining why `remake` is called in the problem constructor --- src/systems/diffeqs/abstractodesystem.jl | 4 ++++ src/systems/diffeqs/sdesystem.jl | 1 + src/systems/nonlinear/nonlinearsystem.jl | 2 ++ 3 files changed, 7 insertions(+) diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index e8f826eb49..5140c0635f 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -759,6 +759,7 @@ function DiffEqBase.ODEProblem{iip, specialize}(sys::AbstractODESystem, u0map = kwargs1 = merge(kwargs1, (; tstops)) end + # Call `remake` so it runs initialization if it is trivial return remake(ODEProblem{iip}(f, u0, tspan, p, pt; kwargs1..., kwargs...)) end get_callback(prob::ODEProblem) = prob.kwargs[:callback] @@ -963,6 +964,7 @@ function DiffEqBase.DAEProblem{iip}(sys::AbstractODESystem, du0map, u0map, tspan kwargs1 = merge(kwargs1, (; tstops)) end + # Call `remake` so it runs initialization if it is trivial return remake(DAEProblem{iip}( f, du0, u0, tspan, p; differential_vars = differential_vars, kwargs..., kwargs1...)) @@ -1009,6 +1011,7 @@ function DiffEqBase.DDEProblem{iip}(sys::AbstractODESystem, u0map = [], if cbs !== nothing kwargs1 = merge(kwargs1, (callback = cbs,)) end + # Call `remake` so it runs initialization if it is trivial return remake(DDEProblem{iip}(f, u0, h, tspan, p; kwargs1..., kwargs...)) end @@ -1058,6 +1061,7 @@ function DiffEqBase.SDDEProblem{iip}(sys::AbstractODESystem, u0map = [], else noise_rate_prototype = zeros(eltype(u0), size(noiseeqs)) end + # Call `remake` so it runs initialization if it is trivial return remake(SDDEProblem{iip}(f, f.g, u0, h, tspan, p; noise_rate_prototype = noise_rate_prototype, kwargs1..., kwargs...)) diff --git a/src/systems/diffeqs/sdesystem.jl b/src/systems/diffeqs/sdesystem.jl index 70ace68d15..d8ac52dd1a 100644 --- a/src/systems/diffeqs/sdesystem.jl +++ b/src/systems/diffeqs/sdesystem.jl @@ -792,6 +792,7 @@ function DiffEqBase.SDEProblem{iip, specialize}( kwargs = filter_kwargs(kwargs) + # Call `remake` so it runs initialization if it is trivial return remake(SDEProblem{iip}(f, u0, tspan, p; callback = cbs, noise, noise_rate_prototype = noise_rate_prototype, kwargs...)) end diff --git a/src/systems/nonlinear/nonlinearsystem.jl b/src/systems/nonlinear/nonlinearsystem.jl index a74fe0b8e6..42ef380337 100644 --- a/src/systems/nonlinear/nonlinearsystem.jl +++ b/src/systems/nonlinear/nonlinearsystem.jl @@ -519,6 +519,7 @@ function DiffEqBase.NonlinearProblem{iip}(sys::NonlinearSystem, u0map, f, u0, p = process_SciMLProblem(NonlinearFunction{iip}, sys, u0map, parammap; check_length, kwargs...) pt = something(get_metadata(sys), StandardNonlinearProblem()) + # Call `remake` so it runs initialization if it is trivial return remake(NonlinearProblem{iip}(f, u0, p, pt; filter_kwargs(kwargs)...)) end @@ -548,6 +549,7 @@ function DiffEqBase.NonlinearLeastSquaresProblem{iip}(sys::NonlinearSystem, u0ma f, u0, p = process_SciMLProblem(NonlinearFunction{iip}, sys, u0map, parammap; check_length, kwargs...) pt = something(get_metadata(sys), StandardNonlinearProblem()) + # Call `remake` so it runs initialization if it is trivial return remake(NonlinearLeastSquaresProblem{iip}(f, u0, p; filter_kwargs(kwargs)...)) end From e2a4b31084528820bc35668c5bc34878da9d18ea Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 24 Feb 2025 14:38:40 +0530 Subject: [PATCH 1018/1337] fix: handle `u0` provided as `StaticArray` with no `u0_constructor` --- src/systems/problem_utils.jl | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/systems/problem_utils.jl b/src/systems/problem_utils.jl index 1cd8304eb9..85dfded21c 100644 --- a/src/systems/problem_utils.jl +++ b/src/systems/problem_utils.jl @@ -774,6 +774,10 @@ function process_SciMLProblem( op, missing_unknowns, missing_pars = build_operating_point!(sys, u0map, pmap, defs, cmap, dvs, ps) + if u0_constructor === identity && u0Type <: StaticArray + u0_constructor = vals -> SymbolicUtils.Code.create_array( + u0Type, eltype(vals), Val(1), Val(length(vals)), vals...) + end if build_initializeprob kws = maybe_build_initialization_problem( sys, op, u0map, pmap, t, defs, guesses, missing_unknowns; From 3207fc7afc15044e9dfafa6a0c6fc606d79cee38 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 24 Feb 2025 14:40:30 +0530 Subject: [PATCH 1019/1337] test: update tests to account for eager initialization --- test/initializationsystem.jl | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/initializationsystem.jl b/test/initializationsystem.jl index aaf7bdfce8..236f9e5ce3 100644 --- a/test/initializationsystem.jl +++ b/test/initializationsystem.jl @@ -720,7 +720,8 @@ end @parameters x0 y0 @mtkbuild sys = ODESystem([x ~ x0, y ~ y0, s ~ x + y], t; guesses = [y0 => 0.0]) prob = ODEProblem(sys, [s => 1.0], (0.0, 1.0), [x0 => 0.3, y0 => missing]) - @test prob.ps[y0] ≈ 0.0 + # trivial initialization run immediately + @test prob.ps[y0] ≈ 0.7 @test init(prob, Tsit5()).ps[y0] ≈ 0.7 @test solve(prob, Tsit5()).ps[y0] ≈ 0.7 end @@ -745,7 +746,8 @@ end systems = [fixed, spring, mass, gravity, constant, damper], guesses = [spring.s_rel0 => 1.0]) prob = ODEProblem(sys, [], (0.0, 1.0), [spring.s_rel0 => missing]) - @test prob.ps[spring.s_rel0] ≈ 0.0 + # trivial initialization run immediately + @test prob.ps[spring.s_rel0] ≈ -3.905 @test init(prob, Tsit5()).ps[spring.s_rel0] ≈ -3.905 @test solve(prob, Tsit5()).ps[spring.s_rel0] ≈ -3.905 end From c768c3e62f71fa278f57b982dd899ac82494c564 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 27 Feb 2025 16:00:56 +0530 Subject: [PATCH 1020/1337] fix: pass `u0_constructor` to `process_SciMLProblem` in `DDEProblem`, `SDDEProblem` --- src/systems/diffeqs/abstractodesystem.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index 5140c0635f..01f847d5d6 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -994,7 +994,7 @@ function DiffEqBase.DDEProblem{iip}(sys::AbstractODESystem, u0map = [], end f, u0, p = process_SciMLProblem(DDEFunction{iip}, sys, u0map, parammap; t = tspan !== nothing ? tspan[1] : tspan, - symbolic_u0 = true, + symbolic_u0 = true, u0_constructor, check_length, eval_expression, eval_module, kwargs...) h_gen = generate_history(sys, u0; expression = Val{true}) h_oop, h_iip = eval_or_rgf.(h_gen; eval_expression, eval_module) @@ -1033,7 +1033,7 @@ function DiffEqBase.SDDEProblem{iip}(sys::AbstractODESystem, u0map = [], end f, u0, p = process_SciMLProblem(SDDEFunction{iip}, sys, u0map, parammap; t = tspan !== nothing ? tspan[1] : tspan, - symbolic_u0 = true, eval_expression, eval_module, + symbolic_u0 = true, eval_expression, eval_module, u0_constructor, check_length, kwargs...) h_gen = generate_history(sys, u0; expression = Val{true}) h_oop, h_iip = eval_or_rgf.(h_gen; eval_expression, eval_module) From 312983a6f638530a79c40c9a9e857711b37b4075 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 21 Feb 2025 17:42:32 +0530 Subject: [PATCH 1021/1337] docs: add doc page for FMU import capability --- docs/Project.toml | 2 + docs/pages.jl | 3 +- docs/src/tutorials/fmi.md | 226 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 230 insertions(+), 1 deletion(-) create mode 100644 docs/src/tutorials/fmi.md diff --git a/docs/Project.toml b/docs/Project.toml index 1f43934352..5e9cc200de 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -6,6 +6,8 @@ DataInterpolations = "82cc6244-b520-54b8-b5a6-8a565e85f1d0" Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f" Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" DynamicQuantities = "06fc5a27-2a28-4c7c-a15d-362465fb6821" +FMI = "14a09403-18e3-468f-ad8a-74f8dda2d9ac" +FMIZoo = "724179cf-c260-40a9-bd27-cccc6fe2f195" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" ModelingToolkit = "961ee093-0014-501f-94e3-6117800e7a78" ModelingToolkitStandardLibrary = "16a59e39-deab-5bd0-87e4-056b12336739" diff --git a/docs/pages.jl b/docs/pages.jl index 6e18f403dd..f6c49a0de3 100644 --- a/docs/pages.jl +++ b/docs/pages.jl @@ -14,7 +14,8 @@ pages = [ "tutorials/SampledData.md", "tutorials/domain_connections.md", "tutorials/callable_params.md", - "tutorials/linear_analysis.md"], + "tutorials/linear_analysis.md", + "tutorials/fmi.md"], "Examples" => Any[ "Basic Examples" => Any["examples/higher_order.md", "examples/spring_mass.md", diff --git a/docs/src/tutorials/fmi.md b/docs/src/tutorials/fmi.md new file mode 100644 index 0000000000..ef00477c78 --- /dev/null +++ b/docs/src/tutorials/fmi.md @@ -0,0 +1,226 @@ +# Importing FMUs + +ModelingToolkit is able to import FMUs following the [FMI Standard](https://fmi-standard.org/) versions 2 and 3. +This integration is done through [FMI.jl](https://github.com/ThummeTo/FMI.jl) and requires importing it to +enable the relevant functionality. Currently Model Exchange (ME) and CoSimulation (CS) FMUs are supported. +Events, non-floating-point variables and array variables are not supported. Additionally, calculating the +time derivatives of FMU states/outputs is not supported. + +!!! danger "Experimental" + + This functionality is currently experimental and subject to change without a breaking release of + ModelingToolkit.jl. + +## FMUs of full models + +Here, we will demonstrate the usage of an FMU of an entire model (as opposed to a single component). +First, the required libraries must be imported and the FMU loaded using FMI.jl. + +```@example fmi +using ModelingToolkit, FMI, FMIZoo, OrdinaryDiffEq +using ModelingToolkit: t_nounits as t, D_nounits as D + +# This is a spring-pendulum FMU from FMIZoo.jl. It is a v2 FMU +# and we are importing it in ModelExchange format. +fmu = loadFMU("SpringPendulum1D", "Dymola", "2022x"; type = :ME) +``` + +Following are the variables in the FMU (both states and parameters): + +```@example fmi +fmu.modelDescription.modelVariables +``` + +Next, [`FMIComponent`](@ref) is used to import the FMU as an MTK component. We provide the FMI +major version as a `Val` to the constructor, along with the loaded FMU and the type as keyword +arguments. + +```@example fmi +@named model = ModelingToolkit.FMIComponent(Val(2); fmu, type = :ME) +``` + +Note how hierarchical names in the FMU (e.g. `mass.m` or `spring.f`) are turned into flattened +names, with `__` being the namespace separator (`mass__m` and `spring__f`). + +!!! note + + Eventually we plan to reconstruct a hierarchical system structure mirroring the one indicated + by the variables in the FMU. This would allow accessing the above mentioned variables as + `model.mass.m` and `model.spring.f` instead of `model.mass__m` and `model.spring__f` respectively. + +Derivative variables such as `der(mass.v)` use the dummy derivative notation, and are hence transformed +into a form similar to `mass__vˍt`. However, they can still be referred to as `D(model.mass__v)`. + +```@example fmi +equations(model) +``` + +Since the FMI spec allows multiple names to alias the same quantity, ModelingToolkit.jl creates +equations to alias them. For example, it can be seen above that `der(mass.v)` and `mass.a` have the +same reference, and hence refer to the same quantity. Correspondingly, there is an equation +`mass__vˍt(t) ~ mass__a(t)` in the system. + +!!! note + + Any variables and/or parameters that are not part of the FMU should be ignored, as ModelingToolkit + creates them to manage the FMU. Unexpected usage of these variables/parameters can lead to errors. + +```@example fmi +defaults(model) +``` + +All parameters in the FMU are given a default equal to their start value, if present. Unknowns are not +assigned defaults even if a start value is present, as this would conflict with ModelingToolkit's own +initialization semantics. + +We can simulate this model like any other ModelingToolkit system. + +```@repl fmi +sys = structural_simplify(model) +prob = ODEProblem(sys, [sys.mass__s => 0.5, sys.mass__v => 0.0], (0.0, 5.0)) +sol = solve(prob, Tsit5()) +``` + +We can interpolate the solution object to obtain values at arbitrary time points in the solved interval, +just like a normal solution. + +```@repl fmi +sol(0.0:0.1:1.0; idxs = sys.mass_a) +``` + +FMUs following version 3 of the specification can be simulated with almost the same process. This time, +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) +``` + +This FMU has fewer equations, partly due to missing aliasing variables and partly due to being a CS FMU. +CoSimulation FMUs are bundled with an integrator. As such, they do not function like ME FMUs. Instead, +a callback steps the FMU at periodic intervals in time and obtains the updated state. This state is held +constant until the next time the callback triggers. The periodic interval must be specified through the +`communication_step_size` keyword argument. A smaller step size typically leads to less error but is +more computationally expensive. + +This model alone does not have any differential variables, and calling `structural_simplify` will lead +to an `ODESystem` with no unknowns. + +```@example fmi +structural_simplify(inner) +``` + +Simulating this model will cause the OrdinaryDiffEq integrator to immediately finish, and will not +trigger the callback. Thus, we wrap this system in a trivial system with a differential variable. + +```@example fmi +@variables x(t) = 1.0 +@mtkbuild sys = ODESystem([D(x) ~ x], t; systems = [inner]) +``` + +We can now simulate `sys`. + +```@example fmi +prob = ODEProblem(sys, [sys.inner.mass__s => 0.5, sys.inner.mass__v => 0.0], (0.0, 5.0)) +sol = solve(prob, Tsit5()) +``` + +The variables of the FMU are discrete, and their timeseries can be obtained at intervals of +`communication_step_size`. + +```@example fmi +sol[sys.inner.mass__s] +``` + +## FMUs of components + +FMUs can also be imported as individual components. For this example, we will use custom FMUs used +in the test suite of ModelingToolkit.jl. + +```@example fmi +fmu = loadFMU( + joinpath(@__DIR__, "..", "..", "..", "test", "fmi", "fmus", "SimpleAdder.fmu"); + type = :ME) +fmu.modelDescription.modelVariables +``` + +This FMU is equivalent to the following model: + +```julia +@mtkmodel SimpleAdder begin + @variables begin + a(t) + b(t) + c(t) + out(t) + out2(t) + end + @parameters begin + value = 1.0 + end + @equations begin + out ~ a + b + value + D(c) ~ out + out2 ~ 2c + end +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); +isinput(adder.a) +isinput(adder.b) +isoutput(adder.out) +isoutput(adder.out2) +``` + +ModelingToolkit recognizes input and output variables of the component, and attaches the appropriate +metadata. We can now use this component as a subcomponent of a larger system. + +```@repl fmi +@variables a(t) b(t) c(t) [guess = 1.0]; +@mtkbuild sys = ODESystem( + [adder.a ~ a, adder.b ~ b, D(a) ~ t, + D(b) ~ adder.out + adder.c, c^2 ~ adder.out + adder.value], + t; + systems = [adder]) +equations(sys) +``` + +Note how the output `adder.out` is used in an algebraic equation of the system. We have also given +`sys.c` a guess, expecting it to be solved for by initialization. ModelingToolkit is able to use +FMUs in initialization to solve for initial states. As mentioned earlier, we cannot differentiate +through an FMU. Thus, automatic differentiation has to be disabled for the solver. + +```@example fmi +prob = ODEProblem(sys, [sys.adder.c => 2.0, sys.a => 1.0, sys.b => 1.0], + (0.0, 1.0), [sys.adder.value => 2.0]) +solve(prob, Rodas5P(autodiff = false)) +``` + +CoSimulation FMUs follow a nearly identical process. Since CoSimulation FMUs operate using callbacks, +after triggering the callbacks and altering the discrete state the algebraic equations may no longer +be satisfied. To resolve for the values of algebraic variables, we use the `reinitializealg` keyword +of `FMIComponent`. This is a DAE initialization algorithm to use at the end of every callback. Since +CoSimulation FMUs are not directly involved in the RHS of the system - instead operating through +callbacks - we can use a solver with automatic differentiation. + +```@example fmi +fmu = loadFMU( + joinpath(@__DIR__, "..", "..", "..", "test", "fmi", "fmus", "SimpleAdder.fmu"); + type = :CS) +@named adder = ModelingToolkit.FMIComponent( + Val(2); fmu, type = :CS, communication_step_size = 1e-3, + 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], + t; + systems = [adder]) +prob = ODEProblem(sys, [sys.adder.c => 2.0, sys.a => 1.0, sys.b => 1.0], + (0.0, 1.0), [sys.adder.value => 2.0]) +solve(prob, Rodas5P()) +``` From 77774ae589eaf320ff165168043937f000fd63d7 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 24 Feb 2025 11:59:24 +0530 Subject: [PATCH 1022/1337] docs: fix `DiffCache` aliasing `odeprob.p.tunable` in `remake` doc example --- docs/src/examples/remake.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/src/examples/remake.md b/docs/src/examples/remake.md index 47ac02ee0a..91dba4d7ae 100644 --- a/docs/src/examples/remake.md +++ b/docs/src/examples/remake.md @@ -98,8 +98,10 @@ using SymbolicIndexingInterface optfn = OptimizationFunction(loss, Optimization.AutoForwardDiff()) # function to set the parameters we are optimizing setter = setp(odeprob, [α, β, γ, δ]) -# `DiffCache` to avoid allocations -diffcache = DiffCache(canonicalize(Tunable(), parameter_values(odeprob))[1]) +# `DiffCache` to avoid allocations. +# `copy` prevents the buffer stored by `DiffCache` from aliasing the one in +# `parameter_values(odeprob)`. +diffcache = DiffCache(copy(canonicalize(Tunable(), parameter_values(odeprob))[1])) # parameter object is a tuple, to store differently typed objects together optprob = OptimizationProblem( optfn, rand(4), (odeprob, timesteps, data, setter, diffcache), From 0f04a5368edd0b8ab4d69263f320df105ab4bdc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Miclu=C8=9Ba-C=C3=A2mpeanu?= Date: Mon, 17 Feb 2025 12:00:36 +0000 Subject: [PATCH 1023/1337] test: add test for vector parameters in function args Co-authored-by: Aayush Sabharwal --- test/scc_nonlinear_problem.jl | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/test/scc_nonlinear_problem.jl b/test/scc_nonlinear_problem.jl index 92030a84aa..620347403a 100644 --- a/test/scc_nonlinear_problem.jl +++ b/test/scc_nonlinear_problem.jl @@ -282,3 +282,12 @@ end sccprob = SCCNonlinearProblem(fullsys, u0, p) @test isequal(parameters(fullsys), parameters(sccprob.f.sys)) end + +@testset "Vector parameters in function arguments" begin + @variables x y + @parameters p[1:2] (f::Function)(..) + + @mtkbuild sys = NonlinearSystem([x^2 - p[1]^2 ~ 0, y^2 ~ f(p)]) + prob = NonlinearProblem(sys, [x => 1.0, y => 1.0], [p => ones(2), f => sum]) + @test_nowarn solve(prob, NewtonRaphson()) +end From 69e5c80cb65b47fc8da415ca5cc006cb283effdb Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 28 Feb 2025 12:06:13 +0530 Subject: [PATCH 1024/1337] fix: fix vector parameter discovery when used both unscalarized and partially scalarized --- src/systems/nonlinear/nonlinearsystem.jl | 6 ++++++ test/nonlinearsystem.jl | 10 ++++++++++ 2 files changed, 16 insertions(+) diff --git a/src/systems/nonlinear/nonlinearsystem.jl b/src/systems/nonlinear/nonlinearsystem.jl index ad3fbfdeef..8b893b6cd2 100644 --- a/src/systems/nonlinear/nonlinearsystem.jl +++ b/src/systems/nonlinear/nonlinearsystem.jl @@ -219,6 +219,12 @@ function NonlinearSystem(eqs; kwargs...) push!(new_ps, p) end else + if symbolic_type(p) == ArraySymbolic() && + Symbolics.shape(unwrap(p)) != Symbolics.Unknown() + for i in eachindex(p) + delete!(new_ps, p[i]) + end + end push!(new_ps, p) end end diff --git a/test/nonlinearsystem.jl b/test/nonlinearsystem.jl index c6318b1314..c8dda530e2 100644 --- a/test/nonlinearsystem.jl +++ b/test/nonlinearsystem.jl @@ -4,6 +4,7 @@ using DiffEqBase, SparseArrays using Test using NonlinearSolve using ForwardDiff +using SymbolicIndexingInterface using ModelingToolkit: value using ModelingToolkit: get_default_or_guess, MTKParameters @@ -380,3 +381,12 @@ end @test_throws ["single equation", "unknown"] IntervalNonlinearFunctionExpr( sys, (0.0, 1.0)) end + +@testset "Vector parameter used unscalarized and partially scalarized" begin + @variables x y + @parameters p[1:2] (f::Function)(..) + + @mtkbuild sys = NonlinearSystem([x^2 - p[1]^2 ~ 0, y^2 ~ f(p)]) + @test !any(isequal(p[1]), parameters(sys)) + @test is_parameter(sys, p) +end From d64b2126b27f093f79f766637af53ca03e01aaad Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 28 Feb 2025 12:06:39 +0530 Subject: [PATCH 1025/1337] test: use `SCCNonlinearProblem` in test --- test/scc_nonlinear_problem.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/scc_nonlinear_problem.jl b/test/scc_nonlinear_problem.jl index 620347403a..b2b326d090 100644 --- a/test/scc_nonlinear_problem.jl +++ b/test/scc_nonlinear_problem.jl @@ -288,6 +288,6 @@ end @parameters p[1:2] (f::Function)(..) @mtkbuild sys = NonlinearSystem([x^2 - p[1]^2 ~ 0, y^2 ~ f(p)]) - prob = NonlinearProblem(sys, [x => 1.0, y => 1.0], [p => ones(2), f => sum]) + prob = SCCNonlinearProblem(sys, [x => 1.0, y => 1.0], [p => ones(2), f => sum]) @test_nowarn solve(prob, NewtonRaphson()) end From a0019928f14e7e8e80fca9f2c34700d2bf1c4244 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 28 Feb 2025 16:47:02 +0530 Subject: [PATCH 1026/1337] fix: fix transition to Moshi.jl ADT --- src/discretedomain.jl | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/discretedomain.jl b/src/discretedomain.jl index c7f90007c5..67bef42c9e 100644 --- a/src/discretedomain.jl +++ b/src/discretedomain.jl @@ -85,7 +85,7 @@ $(TYPEDEF) Represents a sample operator. A discrete-time signal is created by sampling a continuous-time signal. # Constructors -`Sample(clock::Union{TimeDomain, InferredTimeDomain} = InferredDiscrete)` +`Sample(clock::Union{TimeDomain, InferredTimeDomain} = InferredDiscrete())` `Sample(dt::Real)` `Sample(x::Num)`, with a single argument, is shorthand for `Sample()(x)`. @@ -106,7 +106,7 @@ julia> Δ = Sample(0.01) """ struct Sample <: Operator clock::Any - Sample(clock::Union{TimeDomain, InferredTimeDomain} = InferredDiscrete) = new(clock) + Sample(clock::Union{TimeDomain, InferredTimeDomain} = InferredDiscrete()) = new(clock) end function Sample(arg::Real) @@ -190,7 +190,7 @@ struct ShiftIndex clock::Union{InferredTimeDomain, TimeDomain, IntegerSequence} steps::Int function ShiftIndex( - clock::Union{TimeDomain, InferredTimeDomain, IntegerSequence} = Inferred, steps::Int = 0) + clock::Union{TimeDomain, InferredTimeDomain, IntegerSequence} = Inferred(), steps::Int = 0) new(clock, steps) end ShiftIndex(dt::Real, steps::Int = 0) = new(Clock(dt), steps) @@ -232,7 +232,7 @@ function input_timedomain(s::Shift, arg = nothing) if has_time_domain(arg) return get_time_domain(arg) end - InferredDiscrete + InferredDiscrete() end """ @@ -244,7 +244,7 @@ function output_timedomain(s::Shift, arg = nothing) if has_time_domain(t, arg) return get_time_domain(t, arg) end - InferredDiscrete + InferredDiscrete() end input_timedomain(::Sample, _ = nothing) = Continuous() @@ -254,7 +254,7 @@ function input_timedomain(h::Hold, arg = nothing) if has_time_domain(arg) return get_time_domain(arg) end - InferredDiscrete # the Hold accepts any discrete + InferredDiscrete() # the Hold accepts any discrete end output_timedomain(::Hold, _ = nothing) = Continuous() From 97c5324ca03c25a2abd11df69170895a07e400dd Mon Sep 17 00:00:00 2001 From: vyudu Date: Fri, 28 Feb 2025 09:30:26 -0500 Subject: [PATCH 1027/1337] feat: add --- src/ModelingToolkit.jl | 2 +- src/systems/abstractsystem.jl | 16 ++++++++++++++++ test/variable_utils.jl | 13 +++++++++++++ 3 files changed, 30 insertions(+), 1 deletion(-) diff --git a/src/ModelingToolkit.jl b/src/ModelingToolkit.jl index 9fb5ff1299..099ce97324 100644 --- a/src/ModelingToolkit.jl +++ b/src/ModelingToolkit.jl @@ -286,7 +286,7 @@ export toexpr, get_variables export simplify, substitute export build_function export modelingtoolkitize -export generate_initializesystem, Initial +export generate_initializesystem, Initial, isInitial export alg_equations, diff_equations, has_alg_equations, has_diff_equations export get_alg_eqs, get_diff_eqs, has_alg_eqs, has_diff_eqs diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index 8fdd36e291..a1c6056492 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -717,6 +717,22 @@ function add_initialization_parameters(sys::AbstractSystem) return sys end +""" +Returns true if the variable or parameter `var` is of the form `Initial(x)`. +""" +function isInitial(var) + var = unwrap(var) + if iscall(var) + operation(var) isa Initial && return true + if operation(var) === getindex + operation(arguments(var)[1]) isa Initial && return true + end + else + return false + end + return false +end + """ $(TYPEDSIGNATURES) diff --git a/test/variable_utils.jl b/test/variable_utils.jl index ecc2421955..53515bd991 100644 --- a/test/variable_utils.jl +++ b/test/variable_utils.jl @@ -145,3 +145,16 @@ end @test isequal(parse_variable(sys, str), var) end end + +@testset "isInitial" begin + t = ModelingToolkit.to_nounits + @variables x(t) z(t)[1:5] + @parameters a b c[1:4] + @test isInitial(Initial(z)) + @test isInitial(Initial(x)) + @test isInitial(Initial(a)) + @test isInitial(Initial(z[1])) + @test isInitial(Initial(c[4])) + @test !isInitial(c) + @test !isInitial(x) +end From 5a56aa98afd54079438ed44bc3ab56fdf791b2ac Mon Sep 17 00:00:00 2001 From: vyudu Date: Fri, 28 Feb 2025 09:31:32 -0500 Subject: [PATCH 1028/1337] fix docstring --- src/systems/abstractsystem.jl | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index a1c6056492..f5b067ff0c 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -718,14 +718,14 @@ function add_initialization_parameters(sys::AbstractSystem) end """ -Returns true if the variable or parameter `var` is of the form `Initial(x)`. -""" -function isInitial(var) - var = unwrap(var) - if iscall(var) - operation(var) isa Initial && return true - if operation(var) === getindex - operation(arguments(var)[1]) isa Initial && return true +Returns true if the parameter `p` is of the form `Initial(x)`. +""" +function isInitial(p) + p = unwrap(p) + if iscall(p) + operation(p) isa Initial && return true + if operation(p) === getindex + operation(arguments(p)[1]) isa Initial && return true end else return false From 3036eaa3d55af4167c1f0fab383586e5803d22c2 Mon Sep 17 00:00:00 2001 From: vyudu Date: Fri, 28 Feb 2025 09:32:06 -0500 Subject: [PATCH 1029/1337] fix typo --- test/variable_utils.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/variable_utils.jl b/test/variable_utils.jl index 53515bd991..c21dd1765c 100644 --- a/test/variable_utils.jl +++ b/test/variable_utils.jl @@ -147,7 +147,7 @@ end end @testset "isInitial" begin - t = ModelingToolkit.to_nounits + t = ModelingToolkit.t_nounits @variables x(t) z(t)[1:5] @parameters a b c[1:4] @test isInitial(Initial(z)) From fc538244f1a9fff8afd3155646dbd33b4774902e Mon Sep 17 00:00:00 2001 From: vyudu Date: Fri, 28 Feb 2025 10:17:48 -0500 Subject: [PATCH 1030/1337] add Prev and Next to docs --- docs/src/systems/DiscreteSystem.md | 7 +++++++ docs/src/systems/ImplicitDiscreteSystem.md | 7 +++++++ docs/src/tutorials/SampledData.md | 13 +++++++++++-- src/ModelingToolkit.jl | 2 +- 4 files changed, 26 insertions(+), 3 deletions(-) diff --git a/docs/src/systems/DiscreteSystem.md b/docs/src/systems/DiscreteSystem.md index 5ede50c62a..1b2e6e81c5 100644 --- a/docs/src/systems/DiscreteSystem.md +++ b/docs/src/systems/DiscreteSystem.md @@ -26,3 +26,10 @@ structural_simplify DiscreteProblem(sys::DiscreteSystem, u0map, tspan) DiscreteFunction(sys::DiscreteSystem, args...) ``` + +## Discrete Domain +```@docs; canonical=false +Shift +Prev +Next +``` diff --git a/docs/src/systems/ImplicitDiscreteSystem.md b/docs/src/systems/ImplicitDiscreteSystem.md index d02ccc5e42..189f4fe65c 100644 --- a/docs/src/systems/ImplicitDiscreteSystem.md +++ b/docs/src/systems/ImplicitDiscreteSystem.md @@ -26,3 +26,10 @@ structural_simplify ImplicitDiscreteProblem(sys::ImplicitDiscreteSystem, u0map, tspan) ImplicitDiscreteFunction(sys::ImplicitDiscreteSystem, args...) ``` + +## Discrete Domain +```@docs; canonical=false +Shift +Prev +Next +``` diff --git a/docs/src/tutorials/SampledData.md b/docs/src/tutorials/SampledData.md index a72fd1698b..c700bae5c2 100644 --- a/docs/src/tutorials/SampledData.md +++ b/docs/src/tutorials/SampledData.md @@ -25,8 +25,10 @@ The operators [`Sample`](@ref) and [`Hold`](@ref) are thus providing the interfa The [`ShiftIndex`](@ref) operator is used to refer to past and future values of discrete-time variables. The example below illustrates its use, implementing the discrete-time system ```math -x(k+1) = 0.5x(k) + u(k) -y(k) = x(k) +\begin{align} + x(k+1) &= 0.5x(k) + u(k) \\ + y(k) &= x(k) +\end{align} ``` ```@example clocks @@ -187,3 +189,10 @@ connections = [r ~ sin(t) # reference signal @named cl = ODESystem(connections, t, systems = [f, c, p]) ``` + +```@docs +Sample +Hold +ShiftIndex +Clock +``` diff --git a/src/ModelingToolkit.jl b/src/ModelingToolkit.jl index 8adabb238f..11e0c0a390 100644 --- a/src/ModelingToolkit.jl +++ b/src/ModelingToolkit.jl @@ -301,7 +301,7 @@ export debug_system #export ContinuousClock, Discrete, sampletime, input_timedomain, output_timedomain #export has_discrete_domain, has_continuous_domain #export is_discrete_domain, is_continuous_domain, is_hybrid_domain -export Sample, Hold, Shift, ShiftIndex, sampletime, SampleTime +export Sample, Hold, Shift, ShiftIndex, sampletime, SampleTime, Next, Prev export Clock, SolverStepClock, TimeDomain export MTKParameters, reorder_dimension_by_tunables!, reorder_dimension_by_tunables From 1e2e6672f8c555070202e7e81018e27ecca22460 Mon Sep 17 00:00:00 2001 From: vyudu Date: Fri, 28 Feb 2025 10:23:38 -0500 Subject: [PATCH 1031/1337] format --- docs/src/systems/DiscreteSystem.md | 1 + docs/src/systems/ImplicitDiscreteSystem.md | 1 + src/ModelingToolkit.jl | 3 +- src/structural_transformation/utils.jl | 8 ++- .../implicit_discrete_system.jl | 25 +++++--- test/implicit_discrete_system.jl | 57 ++++++++++--------- test/structural_transformation/utils.jl | 14 +++-- 7 files changed, 62 insertions(+), 47 deletions(-) diff --git a/docs/src/systems/DiscreteSystem.md b/docs/src/systems/DiscreteSystem.md index 1b2e6e81c5..c00cc7f3c9 100644 --- a/docs/src/systems/DiscreteSystem.md +++ b/docs/src/systems/DiscreteSystem.md @@ -28,6 +28,7 @@ DiscreteFunction(sys::DiscreteSystem, args...) ``` ## Discrete Domain + ```@docs; canonical=false Shift Prev diff --git a/docs/src/systems/ImplicitDiscreteSystem.md b/docs/src/systems/ImplicitDiscreteSystem.md index 189f4fe65c..268d8ec13c 100644 --- a/docs/src/systems/ImplicitDiscreteSystem.md +++ b/docs/src/systems/ImplicitDiscreteSystem.md @@ -28,6 +28,7 @@ ImplicitDiscreteFunction(sys::ImplicitDiscreteSystem, args...) ``` ## Discrete Domain + ```@docs; canonical=false Shift Prev diff --git a/src/ModelingToolkit.jl b/src/ModelingToolkit.jl index 11e0c0a390..328e5b6df0 100644 --- a/src/ModelingToolkit.jl +++ b/src/ModelingToolkit.jl @@ -237,7 +237,8 @@ export DAEFunctionExpr, DAEProblemExpr export SDESystem, SDEFunction, SDEFunctionExpr, SDEProblemExpr export SystemStructure export DiscreteSystem, DiscreteProblem, DiscreteFunction, DiscreteFunctionExpr -export ImplicitDiscreteSystem, ImplicitDiscreteProblem, ImplicitDiscreteFunction, ImplicitDiscreteFunctionExpr +export ImplicitDiscreteSystem, ImplicitDiscreteProblem, ImplicitDiscreteFunction, + ImplicitDiscreteFunctionExpr export JumpSystem export ODEProblem, SDEProblem export NonlinearFunction, NonlinearFunctionExpr diff --git a/src/structural_transformation/utils.jl b/src/structural_transformation/utils.jl index 031ef6b2d8..14628f2958 100644 --- a/src/structural_transformation/utils.jl +++ b/src/structural_transformation/utils.jl @@ -530,7 +530,7 @@ Distribute a shift applied to a whole expression or equation. Shift(t, 1)(x + y) will become Shift(t, 1)(x) + Shift(t, 1)(y). Only shifts variables whose independent variable is the same t that appears in the Shift (i.e. constants, time-independent parameters, etc. do not get shifted). """ -function distribute_shift(var) +function distribute_shift(var) var = unwrap(var) var isa Equation && return distribute_shift(var.lhs) ~ distribute_shift(var.rhs) @@ -553,11 +553,13 @@ function _distribute_shift(expr, shift) args = arguments(expr) if ModelingToolkit.isvariable(expr) - (length(args) == 1 && isequal(shift.t, only(args))) ? (return shift(expr)) : (return expr) + (length(args) == 1 && isequal(shift.t, only(args))) ? (return shift(expr)) : + (return expr) elseif op isa Shift return shift(expr) else - return maketerm(typeof(expr), operation(expr), Base.Fix2(_distribute_shift, shift).(args), + return maketerm( + typeof(expr), operation(expr), Base.Fix2(_distribute_shift, shift).(args), unwrap(expr).metadata) end else diff --git a/src/systems/discrete_system/implicit_discrete_system.jl b/src/systems/discrete_system/implicit_discrete_system.jl index bf4e6037d1..cbc2555c36 100644 --- a/src/systems/discrete_system/implicit_discrete_system.jl +++ b/src/systems/discrete_system/implicit_discrete_system.jl @@ -239,7 +239,9 @@ function ImplicitDiscreteSystem(eqs, iv; kwargs...) collect(allunknowns), collect(new_ps); kwargs...) end -ImplicitDiscreteSystem(eq::Equation, args...; kwargs...) = ImplicitDiscreteSystem([eq], args...; kwargs...) +function ImplicitDiscreteSystem(eq::Equation, args...; kwargs...) + ImplicitDiscreteSystem([eq], args...; kwargs...) +end function flatten(sys::ImplicitDiscreteSystem, noeqs = false) systems = get_systems(sys) @@ -273,11 +275,13 @@ function generate_function( obs = observed(sys) shifted_obs = Symbolics.Equation[distribute_shift(Next(eq)) for eq in obs] obsidxs = observed_equations_used_by(sys, exprs; obs = shifted_obs) - extra_assignments = [Assignment(shifted_obs[i].lhs, shifted_obs[i].rhs) for i in obsidxs] + extra_assignments = [Assignment(shifted_obs[i].lhs, shifted_obs[i].rhs) + for i in obsidxs] u_next = map(Next, dvs) u = dvs - build_function_wrapper(sys, exprs, u_next, u, ps..., iv; p_start = 3, extra_assignments, kwargs...) + build_function_wrapper( + sys, exprs, u_next, u, ps..., iv; p_start = 3, extra_assignments, kwargs...) end function shift_u0map_forward(sys::ImplicitDiscreteSystem, u0map, defs) @@ -341,11 +345,13 @@ function SciMLBase.ImplicitDiscreteFunction(sys::ImplicitDiscreteSystem, args... ImplicitDiscreteFunction{true}(sys, args...; kwargs...) end -function SciMLBase.ImplicitDiscreteFunction{true}(sys::ImplicitDiscreteSystem, args...; kwargs...) +function SciMLBase.ImplicitDiscreteFunction{true}( + sys::ImplicitDiscreteSystem, args...; kwargs...) ImplicitDiscreteFunction{true, SciMLBase.AutoSpecialize}(sys, args...; kwargs...) end -function SciMLBase.ImplicitDiscreteFunction{false}(sys::ImplicitDiscreteSystem, args...; kwargs...) +function SciMLBase.ImplicitDiscreteFunction{false}( + sys::ImplicitDiscreteSystem, args...; kwargs...) ImplicitDiscreteFunction{false, SciMLBase.FullSpecialize}(sys, args...; kwargs...) end function SciMLBase.ImplicitDiscreteFunction{iip, specialize}( @@ -360,7 +366,6 @@ function SciMLBase.ImplicitDiscreteFunction{iip, specialize}( eval_module = @__MODULE__, analytic = nothing, kwargs...) where {iip, specialize} - if !iscomplete(sys) error("A completed `ImplicitDiscreteSystem` is required. Call `complete` or `structural_simplify` on the system before creating a `ImplicitDiscreteProblem`") end @@ -404,9 +409,12 @@ struct ImplicitDiscreteFunctionClosure{O, I} <: Function f_iip::I end (f::ImplicitDiscreteFunctionClosure)(u_next, u, p, t) = f.f_oop(u_next, u, p, t) -(f::ImplicitDiscreteFunctionClosure)(resid, u_next, u, p, t) = f.f_iip(resid, u_next, u, p, t) +function (f::ImplicitDiscreteFunctionClosure)(resid, u_next, u, p, t) + f.f_iip(resid, u_next, u, p, t) +end -function ImplicitDiscreteFunctionExpr{iip}(sys::ImplicitDiscreteSystem, dvs = unknowns(sys), +function ImplicitDiscreteFunctionExpr{iip}( + sys::ImplicitDiscreteSystem, dvs = unknowns(sys), ps = parameters(sys), u0 = nothing; version = nothing, p = nothing, linenumbers = false, @@ -427,4 +435,3 @@ end function ImplicitDiscreteFunctionExpr(sys::ImplicitDiscreteSystem, args...; kwargs...) ImplicitDiscreteFunctionExpr{true}(sys, args...; kwargs...) end - diff --git a/test/implicit_discrete_system.jl b/test/implicit_discrete_system.jl index f298f43e7c..70cc0b792e 100644 --- a/test/implicit_discrete_system.jl +++ b/test/implicit_discrete_system.jl @@ -3,72 +3,73 @@ using ModelingToolkit: t_nounits as t using StableRNGs k = ShiftIndex(t) -rng = StableRNG(22525) +rng = StableRNG(22525) @testset "Correct ImplicitDiscreteFunction" begin @variables x(t) = 1 - @mtkbuild sys = ImplicitDiscreteSystem([x(k) ~ x(k)*x(k-1) - 3], t) + @mtkbuild sys = ImplicitDiscreteSystem([x(k) ~ x(k) * x(k - 1) - 3], t) tspan = (0, 10) # u[2] - u_next[1] # -3 - u_next[2] + u_next[2]*u_next[1] f = ImplicitDiscreteFunction(sys) - u_next = [3., 1.5] - @test f(u_next, [2.,3.], [], t) ≈ [0., 0.] - u_next = [0., 0.] - @test f(u_next, [2.,3.], [], t) ≈ [3., -3.] - + u_next = [3.0, 1.5] + @test f(u_next, [2.0, 3.0], [], t) ≈ [0.0, 0.0] + u_next = [0.0, 0.0] + @test f(u_next, [2.0, 3.0], [], t) ≈ [3.0, -3.0] + resid = rand(2) - f(resid, u_next, [2.,3.], [], t) - @test resid ≈ [3., -3.] - - prob = ImplicitDiscreteProblem(sys, [x(k-1) => 3.], tspan) - @test prob.u0 == [3., 1.] + f(resid, u_next, [2.0, 3.0], [], t) + @test resid ≈ [3.0, -3.0] + + prob = ImplicitDiscreteProblem(sys, [x(k - 1) => 3.0], tspan) + @test prob.u0 == [3.0, 1.0] prob = ImplicitDiscreteProblem(sys, [], tspan) - @test prob.u0 == [1., 1.] + @test prob.u0 == [1.0, 1.0] @variables x(t) - @mtkbuild sys = ImplicitDiscreteSystem([x(k) ~ x(k)*x(k-1) - 3], t) - @test_throws ErrorException prob = ImplicitDiscreteProblem(sys, [], tspan) + @mtkbuild sys = ImplicitDiscreteSystem([x(k) ~ x(k) * x(k - 1) - 3], t) + @test_throws ErrorException prob=ImplicitDiscreteProblem(sys, [], tspan) end @testset "System with algebraic equations" begin @variables x(t) y(t) - eqs = [x(k) ~ x(k-1) + x(k-2), - x^2 ~ 1 - y^2] + eqs = [x(k) ~ x(k - 1) + x(k - 2), + x^2 ~ 1 - y^2] @mtkbuild sys = ImplicitDiscreteSystem(eqs, t) f = ImplicitDiscreteFunction(sys) - function correct_f(u_next, u, p, t) + function correct_f(u_next, u, p, t) [u[2] - u_next[1], - u[1] + u[2] - u_next[2], - 1 - (u_next[1]+u_next[2])^2 - u_next[3]^2] + u[1] + u[2] - u_next[2], + 1 - (u_next[1] + u_next[2])^2 - u_next[3]^2] end for _ in 1:10 u_next = rand(rng, 3) u = rand(rng, 3) - @test correct_f(u_next, u, [], 0.) ≈ f(u_next, u, [], 0.) + @test correct_f(u_next, u, [], 0.0) ≈ f(u_next, u, [], 0.0) end # Initialization is satisfied. - prob = ImplicitDiscreteProblem(sys, [x(k-1) => .3, x(k-2) => .4], (0, 10), guesses = [y => 1]) + prob = ImplicitDiscreteProblem( + sys, [x(k - 1) => 0.3, x(k - 2) => 0.4], (0, 10), guesses = [y => 1]) @test (prob.u0[1] + prob.u0[2])^2 + prob.u0[3]^2 ≈ 1 end @testset "Handle observables in function codegen" begin # Observable appears in differential equation @variables x(t) y(t) z(t) - eqs = [x(k) ~ x(k-1) + x(k-2), - y(k) ~ x(k) + x(k-2)*z(k-1), - x + y + z ~ 2] + eqs = [x(k) ~ x(k - 1) + x(k - 2), + y(k) ~ x(k) + x(k - 2) * z(k - 1), + x + y + z ~ 2] @mtkbuild sys = ImplicitDiscreteSystem(eqs, t) @test length(unknowns(sys)) == length(equations(sys)) == 3 @test occursin("var\"y(t)\"", string(ImplicitDiscreteFunctionExpr(sys))) # Shifted observable that appears in algebraic equation is properly handled. - eqs = [z(k) ~ x(k) + sin(x(k)), - y(k) ~ x(k-1) + x(k-2), - z(k) * x(k) ~ 3] + eqs = [z(k) ~ x(k) + sin(x(k)), + y(k) ~ x(k - 1) + x(k - 2), + z(k) * x(k) ~ 3] @mtkbuild sys = ImplicitDiscreteSystem(eqs, t) @test occursin("var\"Shift(t, 1)(z(t))\"", string(ImplicitDiscreteFunctionExpr(sys))) end diff --git a/test/structural_transformation/utils.jl b/test/structural_transformation/utils.jl index 0013fb4bbe..646b9ce74e 100644 --- a/test/structural_transformation/utils.jl +++ b/test/structural_transformation/utils.jl @@ -169,19 +169,21 @@ end k = ShiftIndex(t) # Expand shifts - @test isequal(ST.distribute_shift(Shift(t, -1)(x + y)), Shift(t, -1)(x) + Shift(t, -1)(y)) + @test isequal( + ST.distribute_shift(Shift(t, -1)(x + y)), Shift(t, -1)(x) + Shift(t, -1)(y)) expr = a * Shift(t, -2)(x) + Shift(t, 2)(y) + b @test isequal(ST.simplify_shifts(ST.distribute_shift(Shift(t, 2)(expr))), - a*x + Shift(t, 4)(y) + b) + a * x + Shift(t, 4)(y) + b) @test isequal(ST.distribute_shift(Shift(t, 2)(exp(z))), exp(Shift(t, 2)(z))) @test isequal(ST.distribute_shift(Shift(t, 2)(exp(a) + b)), exp(a) + b) - expr = a^x - log(b*y) + z*x - @test isequal(ST.distribute_shift(Shift(t, -3)(expr)), a^(Shift(t, -3)(x)) - log(b * Shift(t, -3)(y)) + Shift(t, -3)(z)*Shift(t, -3)(x)) + expr = a^x - log(b * y) + z * x + @test isequal(ST.distribute_shift(Shift(t, -3)(expr)), + a^(Shift(t, -3)(x)) - log(b * Shift(t, -3)(y)) + Shift(t, -3)(z) * Shift(t, -3)(x)) - expr = x(k+1) ~ x + x(k-1) - @test isequal(ST.distribute_shift(Shift(t, -1)(expr)), x ~ x(k-1) + x(k-2)) + expr = x(k + 1) ~ x + x(k - 1) + @test isequal(ST.distribute_shift(Shift(t, -1)(expr)), x ~ x(k - 1) + x(k - 2)) end @testset "`map_variables_to_equations`" begin From 506522c7f3b8ca2fafe4d205b70542301f26b26d Mon Sep 17 00:00:00 2001 From: vyudu Date: Fri, 28 Feb 2025 11:01:27 -0500 Subject: [PATCH 1032/1337] fix missing var --- test/structural_transformation/utils.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/test/structural_transformation/utils.jl b/test/structural_transformation/utils.jl index 646b9ce74e..9bf239a574 100644 --- a/test/structural_transformation/utils.jl +++ b/test/structural_transformation/utils.jl @@ -5,6 +5,7 @@ using SparseArrays using UnPack using ModelingToolkit: t_nounits as t, D_nounits as D, default_toterm using Symbolics: unwrap +const ST = StructuralTransformations # Define some variables @parameters L g From af3995c842cca7d342a8a642797795ea83f859d4 Mon Sep 17 00:00:00 2001 From: vyudu Date: Fri, 28 Feb 2025 11:45:02 -0500 Subject: [PATCH 1033/1337] make initialization_data for IMplicitDIscreteFunction, fix bad initialization test --- src/systems/discrete_system/implicit_discrete_system.jl | 5 ++++- test/implicit_discrete_system.jl | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/systems/discrete_system/implicit_discrete_system.jl b/src/systems/discrete_system/implicit_discrete_system.jl index cbc2555c36..ae5524768b 100644 --- a/src/systems/discrete_system/implicit_discrete_system.jl +++ b/src/systems/discrete_system/implicit_discrete_system.jl @@ -334,8 +334,10 @@ function SciMLBase.ImplicitDiscreteProblem( u0map = to_varmap(u0map, dvs) u0map = shift_u0map_forward(sys, u0map, defaults(sys)) + @show u0map f, u0, p = process_SciMLProblem( ImplicitDiscreteFunction, sys, u0map, parammap; eval_expression, eval_module, kwargs...) + @show u0 kwargs = filter_kwargs(kwargs) ImplicitDiscreteProblem(f, u0, tspan, p; kwargs...) @@ -388,7 +390,8 @@ function SciMLBase.ImplicitDiscreteFunction{iip, specialize}( ImplicitDiscreteFunction{iip, specialize}(f; sys = sys, observed = observedfun, - analytic = analytic) + analytic = analytic, + kwargs...) end """ diff --git a/test/implicit_discrete_system.jl b/test/implicit_discrete_system.jl index 70cc0b792e..932b6c6981 100644 --- a/test/implicit_discrete_system.jl +++ b/test/implicit_discrete_system.jl @@ -53,7 +53,7 @@ end # Initialization is satisfied. prob = ImplicitDiscreteProblem( sys, [x(k - 1) => 0.3, x(k - 2) => 0.4], (0, 10), guesses = [y => 1]) - @test (prob.u0[1] + prob.u0[2])^2 + prob.u0[3]^2 ≈ 1 + @test length(equations(prob.f.initialization_data.initializeprob.f.sys)) == 1 end @testset "Handle observables in function codegen" begin From 79f05d1b3baea8260471bc9f7eac5b8cd1d9597e Mon Sep 17 00:00:00 2001 From: CompatHelper Julia Date: Sat, 1 Mar 2025 00:27:01 +0000 Subject: [PATCH 1034/1337] CompatHelper: add new compat entry for FMI at version 0.14 for package docs, (keep existing compat) --- docs/Project.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/Project.toml b/docs/Project.toml index 5e9cc200de..815b73c1a0 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -33,6 +33,7 @@ DataInterpolations = "6.5" Distributions = "0.25" Documenter = "1" DynamicQuantities = "^0.11.2, 0.12, 1" +FMI = "0.14" ModelingToolkit = "8.33, 9" ModelingToolkitStandardLibrary = "2.19" NonlinearSolve = "3, 4" From 3d4c64811219eecb17cdacaf375bce63dfbaf6f7 Mon Sep 17 00:00:00 2001 From: CompatHelper Julia Date: Sat, 1 Mar 2025 00:27:05 +0000 Subject: [PATCH 1035/1337] CompatHelper: add new compat entry for FMIZoo at version 1 for package docs, (keep existing compat) --- docs/Project.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/Project.toml b/docs/Project.toml index 5e9cc200de..dabef83a4a 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -33,6 +33,7 @@ DataInterpolations = "6.5" Distributions = "0.25" Documenter = "1" DynamicQuantities = "^0.11.2, 0.12, 1" +FMIZoo = "1" ModelingToolkit = "8.33, 9" ModelingToolkitStandardLibrary = "2.19" NonlinearSolve = "3, 4" From 09db7327f8840aecdc99ae0203a4a5a422335bfd Mon Sep 17 00:00:00 2001 From: vyudu Date: Sat, 1 Mar 2025 18:02:31 -0500 Subject: [PATCH 1036/1337] change name, add docs --- docs/src/tutorials/initialization.md | 10 ++++++++++ src/ModelingToolkit.jl | 2 +- src/systems/abstractsystem.jl | 2 +- test/variable_utils.jl | 16 ++++++++-------- 4 files changed, 20 insertions(+), 10 deletions(-) diff --git a/docs/src/tutorials/initialization.md b/docs/src/tutorials/initialization.md index 27eb2ae28d..33694c1b7d 100644 --- a/docs/src/tutorials/initialization.md +++ b/docs/src/tutorials/initialization.md @@ -536,3 +536,13 @@ sol[α * x - β * x * y] ```@example init plot(sol) ``` + +## Summary of Initialization API +```@docs; canonical=false +Initial +isinitial +generate_initializesystem +initialization_equations +guesses +defaults +``` diff --git a/src/ModelingToolkit.jl b/src/ModelingToolkit.jl index 099ce97324..e6b8130af3 100644 --- a/src/ModelingToolkit.jl +++ b/src/ModelingToolkit.jl @@ -286,7 +286,7 @@ export toexpr, get_variables export simplify, substitute export build_function export modelingtoolkitize -export generate_initializesystem, Initial, isInitial +export generate_initializesystem, Initial, isinitial export alg_equations, diff_equations, has_alg_equations, has_diff_equations export get_alg_eqs, get_diff_eqs, has_alg_eqs, has_diff_eqs diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index f5b067ff0c..ddc70bb10a 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -720,7 +720,7 @@ end """ Returns true if the parameter `p` is of the form `Initial(x)`. """ -function isInitial(p) +function isinitial(p) p = unwrap(p) if iscall(p) operation(p) isa Initial && return true diff --git a/test/variable_utils.jl b/test/variable_utils.jl index c21dd1765c..3204d28836 100644 --- a/test/variable_utils.jl +++ b/test/variable_utils.jl @@ -146,15 +146,15 @@ end end end -@testset "isInitial" begin +@testset "isinitial" begin t = ModelingToolkit.t_nounits @variables x(t) z(t)[1:5] @parameters a b c[1:4] - @test isInitial(Initial(z)) - @test isInitial(Initial(x)) - @test isInitial(Initial(a)) - @test isInitial(Initial(z[1])) - @test isInitial(Initial(c[4])) - @test !isInitial(c) - @test !isInitial(x) + @test isinitial(Initial(z)) + @test isinitial(Initial(x)) + @test isinitial(Initial(a)) + @test isinitial(Initial(z[1])) + @test isinitial(Initial(c[4])) + @test !isinitial(c) + @test !isinitial(x) end From f883b8eb486fd998282e67025f1123605864e9d6 Mon Sep 17 00:00:00 2001 From: vyudu Date: Mon, 3 Mar 2025 09:17:11 -0500 Subject: [PATCH 1037/1337] Format --- docs/src/tutorials/initialization.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/src/tutorials/initialization.md b/docs/src/tutorials/initialization.md index 33694c1b7d..ba733e0bfb 100644 --- a/docs/src/tutorials/initialization.md +++ b/docs/src/tutorials/initialization.md @@ -538,6 +538,7 @@ plot(sol) ``` ## Summary of Initialization API + ```@docs; canonical=false Initial isinitial From 5567ab22186cf423e486b909d5a284bf8b020091 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 4 Mar 2025 14:17:04 +0530 Subject: [PATCH 1038/1337] feat: separate initial parameters for non-initialization-systems --- src/systems/index_cache.jl | 45 ++++++++++++++++++++++- src/systems/nonlinear/initializesystem.jl | 9 +++++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/src/systems/index_cache.jl b/src/systems/index_cache.jl index d71352da9f..5e6867edb2 100644 --- a/src/systems/index_cache.jl +++ b/src/systems/index_cache.jl @@ -49,6 +49,7 @@ struct IndexCache # sym => (clockidx, idx_in_clockbuffer) callback_to_clocks::Dict{Any, Vector{Int}} tunable_idx::TunableIndexMap + initials_idx::TunableIndexMap constant_idx::ParamIndexMap nonnumeric_idx::NonnumericMap observed_syms_to_timeseries::Dict{BasicSymbolic, TimeseriesSetType} @@ -56,6 +57,7 @@ struct IndexCache Union{BasicSymbolic, CallWithMetadata}, TimeseriesSetType} discrete_buffer_sizes::Vector{Vector{BufferTemplate}} tunable_buffer_size::BufferTemplate + initials_buffer_size::BufferTemplate constant_buffer_sizes::Vector{BufferTemplate} nonnumeric_buffer_sizes::Vector{BufferTemplate} symbol_to_variable::Dict{Symbol, SymbolicParam} @@ -251,7 +253,9 @@ function IndexCache(sys::AbstractSystem) tunable_idxs = TunableIndexMap() tunable_buffer_size = 0 - for buffers in (tunable_buffers, initial_param_buffers) + bufferlist = is_initializesystem(sys) ? (tunable_buffers, initial_param_buffers) : + (tunable_buffers,) + for buffers in bufferlist for (i, (_, buf)) in enumerate(buffers) for (j, p) in enumerate(buf) idx = if size(p) == () @@ -271,6 +275,43 @@ function IndexCache(sys::AbstractSystem) end end + initials_idxs = TunableIndexMap() + initials_buffer_size = 0 + if !is_initializesystem(sys) + for (i, (_, buf)) in enumerate(initial_param_buffers) + for (j, p) in enumerate(buf) + idx = if size(p) == () + initials_buffer_size + 1 + else + reshape( + (initials_buffer_size + 1):(initials_buffer_size + length(p)), size(p)) + end + initials_buffer_size += length(p) + initials_idxs[p] = idx + initials_idxs[default_toterm(p)] = idx + if hasname(p) && (!iscall(p) || operation(p) !== getindex) + symbol_to_variable[getname(p)] = p + symbol_to_variable[getname(default_toterm(p))] = p + end + end + end + end + + for k in collect(keys(tunable_idxs)) + v = tunable_idxs[k] + v isa AbstractArray || continue + for (kk, vv) in zip(collect(k), v) + tunable_idxs[kk] = vv + end + end + for k in collect(keys(initials_idxs)) + v = initials_idxs[k] + v isa AbstractArray || continue + for (kk, vv) in zip(collect(k), v) + initials_idxs[kk] = vv + end + end + dependent_pars_to_timeseries = Dict{ Union{BasicSymbolic, CallWithMetadata}, TimeseriesSetType}() @@ -341,12 +382,14 @@ function IndexCache(sys::AbstractSystem) disc_idxs, callback_to_clocks, tunable_idxs, + initials_idxs, const_idxs, nonnumeric_idxs, observed_syms_to_timeseries, dependent_pars_to_timeseries, disc_buffer_templates, BufferTemplate(Real, tunable_buffer_size), + BufferTemplate(Real, initials_buffer_size), const_buffer_sizes, nonnumeric_buffer_sizes, symbol_to_variable diff --git a/src/systems/nonlinear/initializesystem.jl b/src/systems/nonlinear/initializesystem.jl index 81f5af5ddc..ff203f12cb 100644 --- a/src/systems/nonlinear/initializesystem.jl +++ b/src/systems/nonlinear/initializesystem.jl @@ -538,6 +538,15 @@ function SciMLBase.late_binding_update_u0_p( return newu0, newp end +""" + $(TYPEDSIGNATURES) + +Check if the given system is an initialization system. +""" +function is_initializesystem(sys::AbstractSystem) + sys isa NonlinearSystem && get_metadata(sys) isa InitializationSystemMetadata +end + """ Counteracts the CSE/array variable hacks in `symbolics_tearing.jl` so it works with initialization. From b52ecc203fc78ff4c0b056d9787ea1a6227a7882 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 4 Mar 2025 14:17:47 +0530 Subject: [PATCH 1039/1337] feat: add `Initials` portion to `MTKParameters` --- src/systems/parameter_buffer.jl | 72 +++++++++++++++++++++++++++++---- 1 file changed, 64 insertions(+), 8 deletions(-) diff --git a/src/systems/parameter_buffer.jl b/src/systems/parameter_buffer.jl index 0954d9a40c..b9c6e0d382 100644 --- a/src/systems/parameter_buffer.jl +++ b/src/systems/parameter_buffer.jl @@ -3,8 +3,9 @@ symconvert(::Type{T}, x) where {T} = convert(T, x) symconvert(::Type{Real}, x::Integer) = convert(Float64, x) symconvert(::Type{V}, x) where {V <: AbstractArray} = convert(V, symconvert.(eltype(V), x)) -struct MTKParameters{T, D, C, N, H} +struct MTKParameters{T, I, D, C, N, H} tunable::T + initials::I discrete::D constant::C nonnumeric::N @@ -65,6 +66,8 @@ function MTKParameters( tunable_buffer = Vector{ic.tunable_buffer_size.type}( undef, ic.tunable_buffer_size.length) + initials_buffer = Vector{ic.initials_buffer_size.type}( + undef, ic.initials_buffer_size.length) disc_buffer = Tuple(BlockedArray( Vector{subbuffer_sizes[1].type}( undef, sum(x -> x.length, subbuffer_sizes)), @@ -79,6 +82,9 @@ function MTKParameters( if haskey(ic.tunable_idx, sym) idx = ic.tunable_idx[sym] tunable_buffer[idx] = val + elseif haskey(ic.initials_idx, sym) + idx = ic.initials_idx[sym] + initials_buffer[idx] = val elseif haskey(ic.discrete_idx, sym) idx = ic.discrete_idx[sym] disc_buffer[idx.buffer_idx][idx.idx_in_buffer] = val @@ -124,15 +130,19 @@ function MTKParameters( if isempty(tunable_buffer) tunable_buffer = SizedVector{0, Float64}() end + initials_buffer = narrow_buffer_type(initials_buffer) + if isempty(initials_buffer) + initials_buffer = SizedVector{0, Float64}() + end disc_buffer = narrow_buffer_type.(disc_buffer) const_buffer = narrow_buffer_type.(const_buffer) # Don't narrow nonnumeric types nonnumeric_buffer = nonnumeric_buffer mtkps = MTKParameters{ - typeof(tunable_buffer), typeof(disc_buffer), typeof(const_buffer), - typeof(nonnumeric_buffer), typeof(())}(tunable_buffer, - disc_buffer, const_buffer, nonnumeric_buffer, ()) + typeof(tunable_buffer), typeof(initials_buffer), typeof(disc_buffer), + typeof(const_buffer), typeof(nonnumeric_buffer), typeof(())}(tunable_buffer, + initials_buffer, disc_buffer, const_buffer, nonnumeric_buffer, ()) return mtkps end @@ -252,6 +262,26 @@ function SciMLStructures.replace!(::SciMLStructures.Tunable, p::MTKParameters, n return nothing end +function SciMLStructures.canonicalize(::SciMLStructures.Initials, p::MTKParameters) + arr = p.initials + repack = let p = p + function (new_val) + return SciMLStructures.replace(SciMLStructures.Initials(), p, new_val) + end + end + return arr, repack, true +end + +function SciMLStructures.replace(::SciMLStructures.Initials, p::MTKParameters, newvals) + @set! p.initials = newvals + return p +end + +function SciMLStructures.replace!(::SciMLStructures.Initials, p::MTKParameters, newvals) + copyto!(p.initials, newvals) + return nothing +end + for (Portion, field, recurse) in [(SciMLStructures.Discrete, :discrete, 1) (SciMLStructures.Constants, :constant, 1) (Nonnumeric, :nonnumeric, 1) @@ -279,12 +309,14 @@ end function Base.copy(p::MTKParameters) tunable = copy(p.tunable) + initials = copy(p.initials) discrete = Tuple(eltype(buf) <: Real ? copy(buf) : copy.(buf) for buf in p.discrete) constant = Tuple(eltype(buf) <: Real ? copy(buf) : copy.(buf) for buf in p.constant) nonnumeric = copy.(p.nonnumeric) caches = copy.(p.caches) return MTKParameters( tunable, + initials, discrete, constant, nonnumeric, @@ -300,6 +332,9 @@ function _ducktyped_parameter_values(p, pind::ParameterIndex) if portion isa SciMLStructures.Tunable return idx isa Int ? p.tunable[idx] : view(p.tunable, idx) end + if portion isa SciMLStructures.Initials + return idx isa Int ? p.initials[idx] : view(p.initials, idx) + end i, j, k... = idx if portion isa SciMLStructures.Discrete return isempty(k) ? p.discrete[i][j] : p.discrete[i][j][k...] @@ -320,6 +355,11 @@ function SymbolicIndexingInterface.set_parameter!( throw(InvalidParameterSizeException(size(idx), size(val))) end p.tunable[idx] = val + elseif portion isa SciMLStructures.Initials + if validate_size && size(val) !== size(idx) + throw(InvalidParameterSizeException(size(idx), size(val))) + end + p.initials[idx] = val else i, j, k... = idx if portion isa SciMLStructures.Discrete @@ -394,7 +434,8 @@ end function validate_parameter_type(ic::IndexCache, idx::ParameterIndex, val) stype = get_buffer_template(ic, idx).type - if idx.portion == SciMLStructures.Tunable() && !(idx.idx isa Int) + if (idx.portion == SciMLStructures.Tunable() || + idx.portion == SciMLStructures.Initials()) && !(idx.idx isa Int) stype = AbstractArray{<:stype} end validate_parameter_type( @@ -454,6 +495,7 @@ function SymbolicIndexingInterface.remake_buffer(indp, oldbuf::MTKParameters, id end function _remake_buffer(indp, oldbuf::MTKParameters, idxs, vals; validate = true) newbuf = @set oldbuf.tunable = similar(oldbuf.tunable, Any) + @set! newbuf.initials = similar(oldbuf.initials, Any) @set! newbuf.discrete = Tuple(similar(buf, Any) for buf in newbuf.discrete) @set! newbuf.constant = Tuple(similar(buf, Any) for buf in newbuf.constant) @set! newbuf.nonnumeric = Tuple(similar(buf, Any) for buf in newbuf.nonnumeric) @@ -529,6 +571,12 @@ function _remake_buffer(indp, oldbuf::MTKParameters, idxs, vals; validate = true T = promote_type(eltype(newbuf.tunable), Float64) @set! newbuf.tunable = T.(newbuf.tunable) end + @set! newbuf.initials = narrow_buffer_type_and_fallback_undefs( + oldbuf.initials, newbuf.initials) + if eltype(newbuf.initials) <: Integer + T = promote_type(eltype(newbuf.initials), Float64) + @set! newbuf.initials = T.(newbuf.initials) + end @set! newbuf.discrete = narrow_buffer_type_and_fallback_undefs.( oldbuf.discrete, newbuf.discrete) @set! newbuf.constant = narrow_buffer_type_and_fallback_undefs.( @@ -540,6 +588,7 @@ end function as_any_buffer(p::MTKParameters) @set! p.tunable = similar(p.tunable, Any) + @set! p.initials = similar(p.initials, Any) @set! p.discrete = Tuple(similar(buf, Any) for buf in p.discrete) @set! p.constant = Tuple(similar(buf, Any) for buf in p.constant) @set! p.nonnumeric = Tuple(similar(buf, Any) for buf in p.nonnumeric) @@ -615,11 +664,14 @@ end # getindex indexes the vectors, setindex! linearly indexes values # it's inconsistent, but we need it to be this way @generated function Base.getindex( - ps::MTKParameters{T, D, C, N, H}, idx::Int) where {T, D, C, N, H} + ps::MTKParameters{T, I, D, C, N, H}, idx::Int) where {T, I, D, C, N, H} paths = [] if !(T <: SizedVector{0, Float64}) push!(paths, :(ps.tunable)) end + if !(I <: SizedVector{0, Float64}) + push!(paths, :(ps.initials)) + end for i in 1:fieldcount(D) push!(paths, :(ps.discrete[$i])) end @@ -641,11 +693,15 @@ end return Expr(:block, expr, :(throw(BoundsError(ps, idx)))) end -@generated function Base.length(ps::MTKParameters{T, D, C, N, H}) where {T, D, C, N, H} +@generated function Base.length(ps::MTKParameters{ + T, I, D, C, N, H}) where {T, I, D, C, N, H} len = 0 if !(T <: SizedVector{0, Float64}) len += 1 end + if !(I <: SizedVector{0, Float64}) + len += 1 + end len += fieldcount(D) + fieldcount(C) + fieldcount(N) + fieldcount(H) return len end @@ -668,7 +724,7 @@ function Base.iterate(buf::MTKParameters, state = 1) end function Base.:(==)(a::MTKParameters, b::MTKParameters) - return a.tunable == b.tunable && a.discrete == b.discrete && + return a.tunable == b.tunable && a.initials == b.initials && a.discrete == b.discrete && a.constant == b.constant && a.nonnumeric == b.nonnumeric && all(Iterators.map(a.caches, b.caches) do acache, bcache eltype(acache) == eltype(bcache) && length(acache) == length(bcache) From e2251afbe1557e497e5a84a276701d80a645c70a Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 4 Mar 2025 14:17:53 +0530 Subject: [PATCH 1040/1337] refactor: remove dead code --- src/systems/parameter_buffer.jl | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/systems/parameter_buffer.jl b/src/systems/parameter_buffer.jl index b9c6e0d382..e132a5e596 100644 --- a/src/systems/parameter_buffer.jl +++ b/src/systems/parameter_buffer.jl @@ -760,12 +760,6 @@ function jacobian_wrt_vars(pf::F, p::MTKParameters, input_idxs, chunk::C) where ForwardDiff.jacobian(p_closure, p_small, cfg, Val(false)) end -function as_duals(p::MTKParameters, dualtype) - tunable = dualtype.(p.tunable) - discrete = dualtype.(p.discrete) - return MTKParameters{typeof(tunable), typeof(discrete)}(tunable, discrete) -end - const MISSING_PARAMETERS_MESSAGE = """ Some parameters are missing from the variable map. Please provide a value or default for the following variables: From 7f3b1e110774df31da108e469bca3d03951ced97 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 4 Mar 2025 14:18:17 +0530 Subject: [PATCH 1041/1337] fix: properly handle initial parameters in `complete` --- src/systems/abstractsystem.jl | 76 +++++++++++++++++++++-------------- 1 file changed, 46 insertions(+), 30 deletions(-) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index ddc70bb10a..8945d4022b 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -773,38 +773,21 @@ function complete(sys::AbstractSystem; split = true, flatten = true) if !isempty(all_ps) # reorder parameters by portions ps_split = reorder_parameters(sys, all_ps) + # if there are tunables, they will all be in `ps_split[1]` + # and the arrays will have been scalarized + ordered_ps = eltype(all_ps)[] # if there are no tunables, vcat them - if isempty(get_index_cache(sys).tunable_idx) - ordered_ps = reduce(vcat, ps_split) - else - # if there are tunables, they will all be in `ps_split[1]` - # and the arrays will have been scalarized - ordered_ps = eltype(all_ps)[] - i = 1 - # go through all the tunables - while i <= length(ps_split[1]) - sym = ps_split[1][i] - # if the sym is not a scalarized array symbolic OR it was already scalarized, - # just push it as-is - if !iscall(sym) || operation(sym) != getindex || - any(isequal(sym), all_ps) - push!(ordered_ps, sym) - i += 1 - continue - end - # the next `length(sym)` symbols should be scalarized versions of the same - # array symbolic - if !allequal(first(arguments(x)) - for x in view(ps_split[1], i:(i + length(sym) - 1))) - error("This should not be possible. Please open an issue in ModelingToolkit.jl with an MWE and stacktrace.") - end - arrsym = first(arguments(sym)) - push!(ordered_ps, arrsym) - i += length(arrsym) - end - ordered_ps = vcat( - ordered_ps, reduce(vcat, ps_split[2:end]; init = eltype(ordered_ps)[])) + if !isempty(get_index_cache(sys).tunable_idx) + unflatten_parameters!(ordered_ps, ps_split[1], all_ps) + ps_split = Base.tail(ps_split) end + # unflatten initial parameters + if !isempty(get_index_cache(sys).initials_idx) + unflatten_parameters!(ordered_ps, ps_split[1], all_ps) + ps_split = Base.tail(ps_split) + end + ordered_ps = vcat( + ordered_ps, reduce(vcat, ps_split; init = eltype(ordered_ps)[])) @set! sys.ps = ordered_ps end elseif has_index_cache(sys) @@ -816,6 +799,39 @@ function complete(sys::AbstractSystem; split = true, flatten = true) isdefined(sys, :complete) ? (@set! sys.complete = true) : sys end +""" + $(TYPEDSIGNATURES) + +Given a flattened array of parameters `params` and a collection of all (unscalarized) +parameters in the system `all_ps`, unscalarize the elements in `params` and append +to `buffer` in the same order as they are present in `params`. Effectively, if +`params = [p[1], p[2], p[3], q]` then this is equivalent to `push!(buffer, p, q)`. +""" +function unflatten_parameters!(buffer, params, all_ps) + i = 1 + # go through all the tunables + while i <= length(params) + sym = params[i] + # if the sym is not a scalarized array symbolic OR it was already scalarized, + # just push it as-is + if !iscall(sym) || operation(sym) != getindex || + any(isequal(sym), all_ps) + push!(buffer, sym) + i += 1 + continue + end + # the next `length(sym)` symbols should be scalarized versions of the same + # array symbolic + if !allequal(first(arguments(x)) + for x in view(params, i:(i + length(sym) - 1))) + error("This should not be possible. Please open an issue in ModelingToolkit.jl with an MWE and stacktrace.") + end + arrsym = first(arguments(sym)) + push!(buffer, arrsym) + i += length(arrsym) + end +end + for prop in [:eqs :tag :noiseeqs From 73fb137235a6c9d456fed5eab8f96eb42cebf328 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 4 Mar 2025 14:18:48 +0530 Subject: [PATCH 1042/1337] fix: handle initial parameters in symbolic indexing --- src/systems/index_cache.jl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/systems/index_cache.jl b/src/systems/index_cache.jl index 5e6867edb2..c3822fc868 100644 --- a/src/systems/index_cache.jl +++ b/src/systems/index_cache.jl @@ -428,6 +428,8 @@ function SymbolicIndexingInterface.parameter_index(ic::IndexCache, sym) Symbolics.shape(sym) !== Symbolics.Unknown() return if (idx = check_index_map(ic.tunable_idx, sym)) !== nothing ParameterIndex(SciMLStructures.Tunable(), idx, validate_size) + elseif (idx = check_index_map(ic.initials_idx, sym)) !== nothing + ParameterIndex(SciMLStructures.Initials(), idx, validate_size) elseif (idx = check_index_map(ic.discrete_idx, sym)) !== nothing ParameterIndex( SciMLStructures.Discrete(), (idx.buffer_idx, idx.idx_in_buffer), validate_size) From e56346607c42b118fff263c136eea20571afc868 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 4 Mar 2025 14:19:37 +0530 Subject: [PATCH 1043/1337] fix: handle initial parameters in codegen --- src/systems/index_cache.jl | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/src/systems/index_cache.jl b/src/systems/index_cache.jl index c3822fc868..47a784c00b 100644 --- a/src/systems/index_cache.jl +++ b/src/systems/index_cache.jl @@ -510,6 +510,12 @@ function reorder_parameters(ic::IndexCache, ps; drop_missing = false) (BasicSymbolic[unwrap(variable(:DEF)) for _ in 1:(ic.tunable_buffer_size.length)],) end + initials_buf = if ic.initials_buffer_size.length == 0 + () + else + (BasicSymbolic[unwrap(variable(:DEF)) + for _ in 1:(ic.initials_buffer_size.length)],) + end disc_buf = Tuple(BasicSymbolic[unwrap(variable(:DEF)) for _ in 1:(sum(x -> x.length, temp))] @@ -531,6 +537,13 @@ function reorder_parameters(ic::IndexCache, ps; drop_missing = false) else param_buf[1][i] = unwrap.(collect(p)) end + elseif haskey(ic.initials_idx, p) + i = ic.initials_idx[p] + if i isa Int + initials_buf[1][i] = unwrap(p) + else + initials_buf[1][i] = unwrap.(collect(p)) + end elseif haskey(ic.constant_idx, p) i, j = ic.constant_idx[p] const_buf[i][j] = p @@ -543,7 +556,8 @@ function reorder_parameters(ic::IndexCache, ps; drop_missing = false) end result = broadcast.( - unwrap, (param_buf..., disc_buf..., const_buf..., nonnumeric_buf...)) + unwrap, ( + param_buf..., initials_buf..., disc_buf..., const_buf..., nonnumeric_buf...)) if drop_missing result = map(result) do buf filter(buf) do sym @@ -566,6 +580,11 @@ function iterated_buffer_index(ic::IndexCache, ind::ParameterIndex) elseif ic.tunable_buffer_size.length > 0 idx += 1 end + if ind.portion isa SciMLStructures.Initials + return idx + 1 + elseif ic.initials_buffer_size.length > 0 + idx += 1 + end if ind.portion isa SciMLStructures.Discrete return idx + ind.idx[1] elseif !isempty(ic.discrete_buffer_sizes) @@ -587,6 +606,8 @@ function get_buffer_template(ic::IndexCache, pidx::ParameterIndex) if portion isa SciMLStructures.Tunable return ic.tunable_buffer_size + elseif portion isa SciMLStructures.Initials + return ic.initials_buffer_size elseif portion isa SciMLStructures.Discrete return ic.discrete_buffer_sizes[idx[1]][1] elseif portion isa SciMLStructures.Constants From 793abb5b81ea78a607beedcaba23c46405425714 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 4 Mar 2025 14:20:19 +0530 Subject: [PATCH 1044/1337] fix: fix calling `MTKParameters` functions with just tunables vector --- src/systems/codegen_utils.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/systems/codegen_utils.jl b/src/systems/codegen_utils.jl index 4dce62827d..1eeb7e026b 100644 --- a/src/systems/codegen_utils.jl +++ b/src/systems/codegen_utils.jl @@ -287,7 +287,7 @@ end # The user provided a single buffer/tuple for the parameter object, so wrap that # one in a tuple fargs = ntuple(Val(length(args))) do i - i == paramidx ? :((args[$i],)) : :(args[$i]) + i == paramidx ? :((args[$i], nothing)) : :(args[$i]) end return :($f($(fargs...))) end From ef5326b3ae25061164073f8bcea35a5cdf9f3ecb Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 4 Mar 2025 14:20:47 +0530 Subject: [PATCH 1045/1337] fix: promote `Initials` portion in `ReconstructInitializeprob` --- src/systems/nonlinear/initializesystem.jl | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/systems/nonlinear/initializesystem.jl b/src/systems/nonlinear/initializesystem.jl index ff203f12cb..13e5a61da1 100644 --- a/src/systems/nonlinear/initializesystem.jl +++ b/src/systems/nonlinear/initializesystem.jl @@ -338,7 +338,15 @@ function (rip::ReconstructInitializeprob)(srcvalp, dstvalp) end buf, repack, alias = SciMLStructures.canonicalize(SciMLStructures.Tunable(), newp) if eltype(buf) != T - newp = repack(T.(buf)) + newbuf = similar(buf, T) + copyto!(newbuf, buf) + newp = repack(newbuf) + end + buf, repack, alias = SciMLStructures.canonicalize(SciMLStructures.Initials(), newp) + if eltype(buf) != T + newbuf = similar(buf, T) + copyto!(newbuf, buf) + newp = repack(newbuf) end return u0, newp end From 0c9f032c8054a47d840225671113bda4f2d83d84 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 4 Mar 2025 14:21:03 +0530 Subject: [PATCH 1046/1337] fix: promote `Initials` portion in `late_binding_update_u0_p` --- src/systems/nonlinear/initializesystem.jl | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/systems/nonlinear/initializesystem.jl b/src/systems/nonlinear/initializesystem.jl index 13e5a61da1..0f4016be44 100644 --- a/src/systems/nonlinear/initializesystem.jl +++ b/src/systems/nonlinear/initializesystem.jl @@ -528,6 +528,9 @@ function SciMLBase.late_binding_update_u0_p( tunables, repack, alias = SciMLStructures.canonicalize(SciMLStructures.Tunable(), newp) tunables = DiffEqBase.promote_u0(tunables, newu0, t0) newp = repack(tunables) + initials, repack, alias = SciMLStructures.canonicalize(SciMLStructures.Initials(), newp) + initials = DiffEqBase.promote_u0(initials, newu0, t0) + newp = repack(initials) allsyms = all_symbols(sys) for (k, v) in u0 From d830ab26f58a4c2b137bd58f16df788dea150933 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 4 Mar 2025 14:21:19 +0530 Subject: [PATCH 1047/1337] fix: simplify `modelingtoolkitize` --- src/systems/diffeqs/modelingtoolkitize.jl | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/systems/diffeqs/modelingtoolkitize.jl b/src/systems/diffeqs/modelingtoolkitize.jl index e7321c4d2b..b2954f81e4 100644 --- a/src/systems/diffeqs/modelingtoolkitize.jl +++ b/src/systems/diffeqs/modelingtoolkitize.jl @@ -62,9 +62,9 @@ function modelingtoolkitize( fill!(rhs, 0) if prob.f isa ODEFunction && prob.f.f isa FunctionWrappersWrappers.FunctionWrappersWrapper - prob.f.f.fw[1].obj[](rhs, vars, p isa MTKParameters ? (params,) : params, t) + prob.f.f.fw[1].obj[](rhs, vars, params, t) else - prob.f(rhs, vars, p isa MTKParameters ? (params,) : params, t) + prob.f(rhs, vars, params, t) end else rhs = prob.f(vars, params, t) @@ -255,14 +255,14 @@ function modelingtoolkitize(prob::DiffEqBase.SDEProblem; kwargs...) if DiffEqBase.isinplace(prob) lhs = similar(vars, Any) - prob.f(lhs, vars, p isa MTKParameters ? (params,) : params, t) + prob.f(lhs, vars, params, t) if DiffEqBase.is_diagonal_noise(prob) neqs = similar(vars, Any) - prob.g(neqs, vars, p isa MTKParameters ? (params,) : params, t) + prob.g(neqs, vars, params, t) else neqs = similar(vars, Any, size(prob.noise_rate_prototype)) - prob.g(neqs, vars, p isa MTKParameters ? (params,) : params, t) + prob.g(neqs, vars, params, t) end else lhs = prob.f(vars, params, t) From d7da992122f1487875098983f2310e3c144e5d1e Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 4 Mar 2025 14:21:38 +0530 Subject: [PATCH 1048/1337] fix: return `GeneratedFunctionWrapper` from `generate_control_function` --- src/inputoutput.jl | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/inputoutput.jl b/src/inputoutput.jl index 2407ca6942..5f9420ff3a 100644 --- a/src/inputoutput.jl +++ b/src/inputoutput.jl @@ -250,6 +250,9 @@ function generate_control_function(sys::AbstractODESystem, inputs = unbound_inpu f = build_function_wrapper(sys, rhss, args...; p_start = 3 + implicit_dae, p_end = length(p) + 2 + implicit_dae) f = eval_or_rgf.(f; eval_expression, eval_module) + f = GeneratedFunctionWrapper{( + 3 + implicit_dae, length(args) - length(p) + 1, is_split(sys))}(f...) + f = f, f ps = setdiff(parameters(sys), inputs, disturbance_inputs) (; f, dvs, ps, io_sys = sys) end From 43c3ccb3670d6d4ace9941dd48b6491256c8eec8 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 4 Mar 2025 14:22:03 +0530 Subject: [PATCH 1049/1337] fix: support initials in adjoints --- ext/MTKChainRulesCoreExt.jl | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/ext/MTKChainRulesCoreExt.jl b/ext/MTKChainRulesCoreExt.jl index 9cf17d203d..681603392d 100644 --- a/ext/MTKChainRulesCoreExt.jl +++ b/ext/MTKChainRulesCoreExt.jl @@ -80,6 +80,8 @@ function ChainRulesCore.rrule( newbuf = MTK.remake_buffer(indp, oldbuf, idxs, vals) tunable_idxs = reduce( vcat, (idx.idx for idx in idxs if idx.portion isa MTK.SciMLStructures.Tunable)) + initials_idxs = reduce( + vcat, (idx.idx for idx in idxs if idx.portion isa MTK.SciMLStructures.Initials)) disc_idxs = subset_idxs(idxs, MTK.SciMLStructures.Discrete(), oldbuf.discrete) const_idxs = subset_idxs(idxs, MTK.SciMLStructures.Constants(), oldbuf.constant) nn_idxs = subset_idxs(idxs, MTK.NONNUMERIC_PORTION, oldbuf.nonnumeric) @@ -91,10 +93,12 @@ function ChainRulesCore.rrule( indp′ = NoTangent() tunable = selected_tangents(buf′.tunable, tunable_idxs) + initials = selected_tangents(buf′.initials, initials_idxs) discrete = selected_tangents(buf′.discrete, disc_idxs) constant = selected_tangents(buf′.constant, const_idxs) nonnumeric = selected_tangents(buf′.nonnumeric, nn_idxs) - oldbuf′ = Tangent{typeof(oldbuf)}(; tunable, discrete, constant, nonnumeric) + oldbuf′ = Tangent{typeof(oldbuf)}(; + tunable, initials, discrete, constant, nonnumeric) idxs′ = NoTangent() vals′ = map(i -> MTK._ducktyped_parameter_values(buf′, i), idxs) return f′, indp′, oldbuf′, idxs′, vals′ From 5647deeaf3204798824e7c8c812bbf0d4a31880a Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 4 Mar 2025 14:22:17 +0530 Subject: [PATCH 1050/1337] test: mark test as not broken --- test/structural_transformation/tearing.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/structural_transformation/tearing.jl b/test/structural_transformation/tearing.jl index a0ccc7355d..e9cd92ec94 100644 --- a/test/structural_transformation/tearing.jl +++ b/test/structural_transformation/tearing.jl @@ -159,7 +159,7 @@ du = [0.0, 0.0]; u = [1.0, -0.5π]; pr = 0.2; tt = 0.1; -@test_skip (@ballocated $(prob.f)($du, $u, $pr, $tt)) == 0 +@test (@ballocated $(prob.f)($du, $u, $pr, $tt)) == 0 prob.f(du, u, pr, tt) @test du≈[u[2], u[1] + sin(u[2]) - pr * tt] atol=1e-5 From d6b3f27b20683c063c6d003ef2ed2db68fb732dc Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 4 Mar 2025 14:25:32 +0530 Subject: [PATCH 1051/1337] test: remove unnecessary wrapping of parameter vector --- test/function_registration.jl | 8 ++++---- test/input_output_handling.jl | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/test/function_registration.jl b/test/function_registration.jl index a52eb3245a..a1d9041127 100644 --- a/test/function_registration.jl +++ b/test/function_registration.jl @@ -22,7 +22,7 @@ sys = complete(sys) fun = ODEFunction(sys) u0 = 5.0 -@test fun([0.5], [u0], 0.0) == [do_something(u0) * 2] +@test fun([0.5], u0, 0.0) == [do_something(u0) * 2] end # TEST: Function registration in a nested module. @@ -45,7 +45,7 @@ sys = complete(sys) fun = ODEFunction(sys) u0 = 3.0 -@test fun([0.5], [u0], 0.0) == [do_something_2(u0) * 2] +@test fun([0.5], u0, 0.0) == [do_something_2(u0) * 2] end end @@ -67,7 +67,7 @@ sys = complete(sys) fun = ODEFunction(sys) u0 = 7.0 -@test fun([0.5], [u0], 0.0) == [do_something_3(u0) * 2] +@test fun([0.5], u0, 0.0) == [do_something_3(u0) * 2] # TEST: Function registration works with derivatives. # --------------------------------------------------- @@ -106,7 +106,7 @@ end function run_test() fun = build_ode() u0 = 10.0 - @test fun([0.5], [u0], 0.0) == [do_something_4(u0) * 2] + @test fun([0.5], u0, 0.0) == [do_something_4(u0) * 2] end run_test() diff --git a/test/input_output_handling.jl b/test/input_output_handling.jl index 4191571ed8..84539f0b37 100644 --- a/test/input_output_handling.jl +++ b/test/input_output_handling.jl @@ -145,9 +145,9 @@ if VERSION >= v"1.8" # :opaque_closure not supported before inputs = [torque.tau.u]) x = randn(size(A, 1)) u = randn(size(B, 2)) - p = (getindex.( + p = getindex.( Ref(ModelingToolkit.defaults_and_guesses(ssys)), - parameters(ssys)),) + parameters(ssys)) y1 = obsf(x, u, p, 0) y2 = C * x + D * u @test y1[] ≈ y2[] From 5186afd3adece0ffd5f12bbaf2e981578a6ce42e Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 4 Mar 2025 14:25:42 +0530 Subject: [PATCH 1052/1337] test: test promotion of initials portion in `remake` --- test/initializationsystem.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/test/initializationsystem.jl b/test/initializationsystem.jl index 236f9e5ce3..4267f1d857 100644 --- a/test/initializationsystem.jl +++ b/test/initializationsystem.jl @@ -979,6 +979,7 @@ end ForwardDiff.Dual @test state_values(prob2.f.initialization_data.initializeprob) ≈ state_values(prob.f.initialization_data.initializeprob) + @test eltype(prob2.p.initials) <: ForwardDiff.Dual end end From 84f7309d6d573c79dfed22cc448674100b6ce49a Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 4 Mar 2025 14:26:16 +0530 Subject: [PATCH 1053/1337] test: remove splatting of parameter object only worked coincidentally --- test/odesystem.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/odesystem.jl b/test/odesystem.jl index b9a41e1c3d..87be5bbc54 100644 --- a/test/odesystem.jl +++ b/test/odesystem.jl @@ -1223,7 +1223,7 @@ end sys, [x + 1, x + P, x + t], return_inplace = true)[2] ps = ModelingToolkit.MTKParameters(sys, [P => 2.0]) buffer = zeros(3) - @test_nowarn obsfn(buffer, [1.0], ps..., 3.0) + @test_nowarn obsfn(buffer, [1.0], ps, 3.0) @test buffer ≈ [2.0, 3.0, 4.0] end From 25d52daba56d18e595763bbcba56d98292cdcfdd Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 4 Mar 2025 14:26:28 +0530 Subject: [PATCH 1054/1337] test: test indexing of `Initials` portion --- test/split_parameters.jl | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/test/split_parameters.jl b/test/split_parameters.jl index a38dd24c38..0a4f9fb25d 100644 --- a/test/split_parameters.jl +++ b/test/split_parameters.jl @@ -5,7 +5,7 @@ using DataInterpolations using BlockArrays: BlockedArray using ModelingToolkit: t_nounits as t, D_nounits as D using ModelingToolkit: MTKParameters, ParameterIndex, NONNUMERIC_PORTION -using SciMLStructures: Tunable, Discrete, Constants +using SciMLStructures: Tunable, Discrete, Constants, Initials using StaticArrays: SizedVector using SymbolicIndexingInterface: is_parameter, getp @@ -204,7 +204,7 @@ connections = [[state_feedback.input.u[i] ~ model_outputs[i] for i in 1:4] S = get_sensitivity(closed_loop, :u) @testset "Indexing MTKParameters with ParameterIndex" begin - ps = MTKParameters(collect(1.0:10.0), + ps = MTKParameters(collect(1.0:10.0), collect(11.0:20.0), (BlockedArray([true, false, false, true], [2, 2]), BlockedArray([[1 2; 3 4], [2 4; 6 8]], [1, 1])), # (BlockedArray([[true, false], [false, true]]), BlockedArray([[[1 2; 3 4]], [[2 4; 6 8]]])), @@ -213,6 +213,9 @@ S = get_sensitivity(closed_loop, :u) @test ps[ParameterIndex(Tunable(), 1)] == 1.0 @test ps[ParameterIndex(Tunable(), 2:4)] == collect(2.0:4.0) @test ps[ParameterIndex(Tunable(), reshape(4:7, 2, 2))] == reshape(4.0:7.0, 2, 2) + @test ps[ParameterIndex(Initials(), 1)] == 11.0 + @test ps[ParameterIndex(Initials(), 2:4)] == collect(12.0:14.0) + @test ps[ParameterIndex(Initials(), reshape(4:7, 2, 2))] == reshape(14.0:17.0, 2, 2) @test ps[ParameterIndex(Discrete(), (2, 1, 2, 2))] == 4 @test ps[ParameterIndex(Discrete(), (2, 2))] == [2 4; 6 8] @test ps[ParameterIndex(Constants(), (1, 1))] == 5 @@ -221,8 +224,12 @@ S = get_sensitivity(closed_loop, :u) ps[ParameterIndex(Tunable(), 1)] = 1.5 ps[ParameterIndex(Tunable(), 2:4)] = [2.5, 3.5, 4.5] ps[ParameterIndex(Tunable(), reshape(5:8, 2, 2))] = [5.5 7.5; 6.5 8.5] + ps[ParameterIndex(Initials(), 1)] = 11.5 + ps[ParameterIndex(Initials(), 2:4)] = [12.5, 13.5, 14.5] + ps[ParameterIndex(Initials(), reshape(5:8, 2, 2))] = [15.5 17.5; 16.5 18.5] ps[ParameterIndex(Discrete(), (2, 1, 2, 2))] = 5 @test ps[ParameterIndex(Tunable(), 1:8)] == collect(1.0:8.0) .+ 0.5 + @test ps[ParameterIndex(Initials(), 1:8)] == collect(11.0:18.0) .+ 0.5 @test ps[ParameterIndex(Discrete(), (2, 1, 2, 2))] == 5 end From ef75b3352d20aa5ee56f93edfd867c0ee9909474 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 4 Mar 2025 17:22:27 +0530 Subject: [PATCH 1055/1337] feat: allow `NonlinearSystem(::ODESystem)` and `NonlinearProblem(::ODESystem)` --- src/systems/nonlinear/nonlinearsystem.jl | 30 ++++++++++++++ test/nonlinearsystem.jl | 50 ++++++++++++++++++++++++ 2 files changed, 80 insertions(+) diff --git a/src/systems/nonlinear/nonlinearsystem.jl b/src/systems/nonlinear/nonlinearsystem.jl index f93e8a5838..523fee28fc 100644 --- a/src/systems/nonlinear/nonlinearsystem.jl +++ b/src/systems/nonlinear/nonlinearsystem.jl @@ -232,6 +232,32 @@ function NonlinearSystem(eqs; kwargs...) return NonlinearSystem(eqs, collect(allunknowns), collect(new_ps); kwargs...) end +""" + $(TYPEDSIGNATURES) + +Convert an `ODESystem` to a `NonlinearSystem` solving for its steady state (where derivatives are zero). +Any differential variable `D(x) ~ f(...)` will be turned into `0 ~ f(...)`. The returned system is not +simplified. If the input system is `complete`d, then so will the returned system. +""" +function NonlinearSystem(sys::ODESystem) + eqs = equations(sys) + obs = observed(sys) + subrules = Dict(D(x) => 0.0 for x in unknowns(sys)) + eqs = map(eqs) do eq + fast_substitute(eq, subrules) + end + + nsys = NonlinearSystem(eqs, unknowns(sys), [parameters(sys); get_iv(sys)]; + parameter_dependencies = parameter_dependencies(sys), + defaults = merge(defaults(sys), Dict(get_iv(sys) => Inf)), guesses = guesses(sys), + initialization_eqs = initialization_equations(sys), name = nameof(sys), + observed = obs) + if iscomplete(sys) + nsys = complete(nsys; split = is_split(sys)) + end + return nsys +end + function calculate_jacobian(sys::NonlinearSystem; sparse = false, simplify = false) cache = get_jac(sys)[] if cache isa Tuple && cache[2] == (sparse, simplify) @@ -529,6 +555,10 @@ function DiffEqBase.NonlinearProblem{iip}(sys::NonlinearSystem, u0map, return remake(NonlinearProblem{iip}(f, u0, p, pt; filter_kwargs(kwargs)...)) end +function DiffEqBase.NonlinearProblem(sys::ODESystem, args...; kwargs...) + NonlinearProblem(NonlinearSystem(sys), args...; kwargs...) +end + """ ```julia DiffEqBase.NonlinearLeastSquaresProblem{iip}(sys::NonlinearSystem, u0map, diff --git a/test/nonlinearsystem.jl b/test/nonlinearsystem.jl index c8dda530e2..cde0be7373 100644 --- a/test/nonlinearsystem.jl +++ b/test/nonlinearsystem.jl @@ -390,3 +390,53 @@ end @test !any(isequal(p[1]), parameters(sys)) @test is_parameter(sys, p) end + +@testset "Can convert from `ODESystem`" begin + @variables x(t) y(t) + @parameters p q r + @named sys = ODESystem([D(x) ~ p * x^3 + q, 0 ~ -y + q * x - r], t; + defaults = [x => 1.0, p => missing], guesses = [p => 1.0], + initialization_eqs = [p^3 + q^3 ~ 4r], parameter_dependencies = [r ~ 3p]) + nlsys = NonlinearSystem(sys) + defs = defaults(nlsys) + @test length(defs) == 3 + @test defs[x] == 1.0 + @test defs[p] === missing + @test isinf(defs[t]) + @test length(guesses(nlsys)) == 1 + @test guesses(nlsys)[p] == 1.0 + @test length(initialization_equations(nlsys)) == 1 + @test length(parameter_dependencies(nlsys)) == 1 + @test length(equations(nlsys)) == 2 + @test all(iszero, [eq.lhs for eq in equations(nlsys)]) + @test nameof(nlsys) == nameof(sys) + @test !ModelingToolkit.iscomplete(nlsys) + + sys1 = complete(sys; split = false) + nlsys = NonlinearSystem(sys1) + @test ModelingToolkit.iscomplete(nlsys) + @test !ModelingToolkit.is_split(nlsys) + + sys2 = complete(sys) + nlsys = NonlinearSystem(sys2) + @test ModelingToolkit.iscomplete(nlsys) + @test ModelingToolkit.is_split(nlsys) + + sys3 = structural_simplify(sys) + nlsys = NonlinearSystem(sys3) + @test length(equations(nlsys)) == length(observed(nlsys)) == 1 + + prob = NonlinearProblem(sys3, [q => 2.0]) + @test prob.f.initialization_data.initializeprobmap === nothing + sol = solve(prob) + @test SciMLBase.successful_retcode(sol) + @test sol.ps[p^3 + q^3]≈sol.ps[4r] atol=1e-10 + + @testset "Differential inside expression also substituted" begin + @named sys = ODESystem([0 ~ y * D(x) + x^2 - p, 0 ~ x * D(y) + y * p], t) + nlsys = NonlinearSystem(sys) + vs = ModelingToolkit.vars(equations(nlsys)) + @test !in(D(x), vs) + @test !in(D(y), vs) + end +end From ee84d23969f1eea39add5bed3feba6e9d74faec8 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 4 Mar 2025 18:11:03 +0530 Subject: [PATCH 1056/1337] fix: dispatch on `AbstractODESystem` to avoid use-before-define problems --- src/systems/nonlinear/nonlinearsystem.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/systems/nonlinear/nonlinearsystem.jl b/src/systems/nonlinear/nonlinearsystem.jl index 523fee28fc..ea3730fdb6 100644 --- a/src/systems/nonlinear/nonlinearsystem.jl +++ b/src/systems/nonlinear/nonlinearsystem.jl @@ -239,7 +239,7 @@ Convert an `ODESystem` to a `NonlinearSystem` solving for its steady state (wher Any differential variable `D(x) ~ f(...)` will be turned into `0 ~ f(...)`. The returned system is not simplified. If the input system is `complete`d, then so will the returned system. """ -function NonlinearSystem(sys::ODESystem) +function NonlinearSystem(sys::AbstractODESystem) eqs = equations(sys) obs = observed(sys) subrules = Dict(D(x) => 0.0 for x in unknowns(sys)) @@ -555,7 +555,7 @@ function DiffEqBase.NonlinearProblem{iip}(sys::NonlinearSystem, u0map, return remake(NonlinearProblem{iip}(f, u0, p, pt; filter_kwargs(kwargs)...)) end -function DiffEqBase.NonlinearProblem(sys::ODESystem, args...; kwargs...) +function DiffEqBase.NonlinearProblem(sys::AbstractODESystem, args...; kwargs...) NonlinearProblem(NonlinearSystem(sys), args...; kwargs...) end From 82b9b40371aedb6165fb7d88de080a6631f7b181 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 5 Mar 2025 11:29:25 +0530 Subject: [PATCH 1057/1337] fix: disambiguate `observed` --- test/nonlinearsystem.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/nonlinearsystem.jl b/test/nonlinearsystem.jl index cde0be7373..420bdc088e 100644 --- a/test/nonlinearsystem.jl +++ b/test/nonlinearsystem.jl @@ -424,7 +424,7 @@ end sys3 = structural_simplify(sys) nlsys = NonlinearSystem(sys3) - @test length(equations(nlsys)) == length(observed(nlsys)) == 1 + @test length(equations(nlsys)) == length(ModelingToolkit.observed(nlsys)) == 1 prob = NonlinearProblem(sys3, [q => 2.0]) @test prob.f.initialization_data.initializeprobmap === nothing From 6686524573989d1b5fe4fc6ed43668f444337aad Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 5 Mar 2025 11:33:48 +0530 Subject: [PATCH 1058/1337] test: update mtkparameters tests with new portion --- test/mtkparameters.jl | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/test/mtkparameters.jl b/test/mtkparameters.jl index 3ac8f7569d..f9d71f00bc 100644 --- a/test/mtkparameters.jl +++ b/test/mtkparameters.jl @@ -40,7 +40,7 @@ setp(sys, a)(ps, 1.0) @test getp(sys, a)(ps) == getp(sys, b)(ps) / 2 == getp(sys, c)(ps) / 3 == 1.0 -for (portion, values) in [(Tunable(), [1.0, 2.0, 5.0, 6.0, 7.0]) +for (portion, values) in [(Tunable(), [1.0, 5.0, 6.0, 7.0]) (Discrete(), [3.0]) (Constants(), vcat([0.1, 0.2, 0.3], ones(9), [4.0]))] buffer, repack, alias = canonicalize(portion, ps) @@ -109,7 +109,7 @@ eq = D(X) ~ p[1] - p[2] * X u0 = [X => 1.0] ps = [p => [2.0, 0.1]] p = MTKParameters(osys, ps, u0) -@test p.tunable == [2.0, 0.1, 1.0] +@test p.tunable == [2.0, 0.1] # Ensure partial update promotes the buffer @parameters p q r @@ -298,7 +298,7 @@ end end # Parameter timeseries -ps = MTKParameters(([1.0, 1.0],), (BlockedArray(zeros(4), [2, 2]),), +ps = MTKParameters([1.0, 1.0], (), (BlockedArray(zeros(4), [2, 2]),), (), (), ()) ps2 = SciMLStructures.replace(Discrete(), ps, ones(4)) @test typeof(ps2.discrete) == typeof(ps.discrete) @@ -314,7 +314,8 @@ with_updated_parameter_timeseries_values( # With multiple types and clocks ps = MTKParameters( - (), (BlockedArray([1.0, 2.0, 3.0, 4.0, 5.0, 6.0], [3, 3]), + (), (), + (BlockedArray([1.0, 2.0, 3.0, 4.0, 5.0, 6.0], [3, 3]), BlockedArray(falses(1), [1, 0])), (), (), ()) @test SciMLBase.get_saveable_values(sys, ps, 1).x isa Tuple{Vector{Float64}, Vector{Bool}} From 66ec1138ed0b8acb4c8ea8655a47c84f3d0501e8 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 5 Mar 2025 11:35:47 +0530 Subject: [PATCH 1059/1337] fix: handle empty `reduce` in rrule --- ext/MTKChainRulesCoreExt.jl | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/ext/MTKChainRulesCoreExt.jl b/ext/MTKChainRulesCoreExt.jl index 681603392d..a2974ea2dd 100644 --- a/ext/MTKChainRulesCoreExt.jl +++ b/ext/MTKChainRulesCoreExt.jl @@ -79,9 +79,11 @@ function ChainRulesCore.rrule( end newbuf = MTK.remake_buffer(indp, oldbuf, idxs, vals) tunable_idxs = reduce( - vcat, (idx.idx for idx in idxs if idx.portion isa MTK.SciMLStructures.Tunable)) + vcat, (idx.idx for idx in idxs if idx.portion isa MTK.SciMLStructures.Tunable); + init = Union{Int, AbstractVector{Int}}[]) initials_idxs = reduce( - vcat, (idx.idx for idx in idxs if idx.portion isa MTK.SciMLStructures.Initials)) + vcat, (idx.idx for idx in idxs if idx.portion isa MTK.SciMLStructures.Initials); + init = Union{Int, AbstractVector{Int}}[]) disc_idxs = subset_idxs(idxs, MTK.SciMLStructures.Discrete(), oldbuf.discrete) const_idxs = subset_idxs(idxs, MTK.SciMLStructures.Constants(), oldbuf.constant) nn_idxs = subset_idxs(idxs, MTK.NONNUMERIC_PORTION, oldbuf.nonnumeric) From bde633c2d16169ccd6b0b28e28beda4b5466a401 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 5 Mar 2025 11:44:38 +0530 Subject: [PATCH 1060/1337] build: bump SciMLStructures compat Co-authored-by: Christopher Rackauckas --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 6941a5f4d8..22ec0f80eb 100644 --- a/Project.toml +++ b/Project.toml @@ -136,7 +136,7 @@ Reexport = "0.2, 1" RuntimeGeneratedFunctions = "0.5.9" SCCNonlinearSolve = "1.0.0" SciMLBase = "2.75" -SciMLStructures = "1.0" +SciMLStructures = "1.7" Serialization = "1" Setfield = "0.7, 0.8, 1" SimpleNonlinearSolve = "0.1.0, 1, 2" From a553fd16cbe236b5f4c49fe8fb559d11bd10afa8 Mon Sep 17 00:00:00 2001 From: Christopher Rackauckas Date: Wed, 5 Mar 2025 08:20:37 -0500 Subject: [PATCH 1061/1337] Update Project.toml --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 22ec0f80eb..ad5b7f9d28 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "ModelingToolkit" uuid = "961ee093-0014-501f-94e3-6117800e7a78" authors = ["Yingbo Ma ", "Chris Rackauckas and contributors"] -version = "9.64.3" +version = "9.65.0" [deps] AbstractTrees = "1520ce14-60c1-5f80-bbc7-55ef81b5835c" From acec3cb7e2ae3037d239a4e09ef57d73e6a72559 Mon Sep 17 00:00:00 2001 From: Herman Sletmoen Date: Sun, 23 Feb 2025 17:35:10 +0100 Subject: [PATCH 1062/1337] Change independent variable of simple 1st order system --- src/systems/diffeqs/basic_transformations.jl | 31 ++++++++++++++++++++ test/basic_transformations.jl | 25 ++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/src/systems/diffeqs/basic_transformations.jl b/src/systems/diffeqs/basic_transformations.jl index e2be889c5e..7bf348911b 100644 --- a/src/systems/diffeqs/basic_transformations.jl +++ b/src/systems/diffeqs/basic_transformations.jl @@ -55,3 +55,34 @@ function liouville_transform(sys::AbstractODESystem) vars = [unknowns(sys); trJ] ODESystem(neweqs, t, vars, parameters(sys), checks = false) end + +# TODO: handle case when new iv is a variable already in the system +function change_independent_variable(sys::AbstractODESystem, iv, iv1_of_iv2; kwargs...) + iv1 = ModelingToolkit.get_iv(sys) # old independent variable + iv2 = iv # new independent variable + + name2 = nameof(iv2) + iv2func, = @variables $name2(iv1) + + eqs = ModelingToolkit.get_eqs(sys) |> copy # don't modify original system + vars = [] + for (i, eq) in enumerate(eqs) + vars = Symbolics.get_variables(eq) + for var1 in vars + if Symbolics.iscall(var1) # skip e.g. constants + name = nameof(operation(var1)) + var2, = @variables $name(iv2func) + eq = substitute(eq, var1 => var2; fold = false) + end + end + eq = expand_derivatives(eq) # expand out with chain rule to get d(iv2)/d(iv1) + div1_div2 = Differential(iv2)(iv1_of_iv2) |> expand_derivatives + div2_div1 = 1 / div1_div2 + eq = substitute(eq, Differential(iv1)(iv2func) => div2_div1) # substitute in d(iv1)/d(iv2) + eq = substitute(eq, iv2func => iv2) # make iv2 independent + eqs[i] = eq + end + + sys2 = typeof(sys)(eqs, iv2; name = nameof(sys), description = description(sys), kwargs...) + return sys2 +end diff --git a/test/basic_transformations.jl b/test/basic_transformations.jl index d9b59408e6..512510011c 100644 --- a/test/basic_transformations.jl +++ b/test/basic_transformations.jl @@ -32,3 +32,28 @@ u0 = [x => 1.0, prob = ODEProblem(sys2, u0, tspan, p, jac = true) sol = solve(prob, Tsit5()) @test sol[end, end] ≈ 1.0742818931017244 + +@testset "Change independent variable" begin + # Autonomous 1st order (t doesn't appear in the equations) + @independent_variables t + @variables x(t) y(t) z(t) + eqs = [ + D(x) ~ y + D(y) ~ 2*D(x) + z ~ x + D(y) + ] + @named sys1 = ODESystem(eqs, t) + + @independent_variables s + @named sys2 = ModelingToolkit.change_independent_variable(sys1, s, s^2) + + sys1 = structural_simplify(sys1) + sys2 = structural_simplify(sys2) + prob1 = ODEProblem(sys1, unknowns(sys1) .=> 1.0, (0.0, 1.0)) + prob2 = ODEProblem(sys2, unknowns(sys2) .=> 1.0, (0.0, 1.0)) + sol1 = solve(prob1, Tsit5(); reltol = 1e-10, abstol = 1e-10) + sol2 = solve(prob2, Tsit5(); reltol = 1e-10, abstol = 1e-10) + ts = range(0.0, 1.0, length = 50) + ss = .√(ts) + @test isapprox(sol1(ts), sol2(ss); atol = 1e-8) +end From 3600b0de65323f32ee6f47b422f810fc15ed1dc8 Mon Sep 17 00:00:00 2001 From: Herman Sletmoen Date: Sun, 23 Feb 2025 18:00:12 +0100 Subject: [PATCH 1063/1337] Change independent variable of 2nd-order systems --- src/systems/diffeqs/basic_transformations.jl | 8 ++++---- test/basic_transformations.jl | 11 ++++++----- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/systems/diffeqs/basic_transformations.jl b/src/systems/diffeqs/basic_transformations.jl index 7bf348911b..60eb46483e 100644 --- a/src/systems/diffeqs/basic_transformations.jl +++ b/src/systems/diffeqs/basic_transformations.jl @@ -57,7 +57,7 @@ function liouville_transform(sys::AbstractODESystem) end # TODO: handle case when new iv is a variable already in the system -function change_independent_variable(sys::AbstractODESystem, iv, iv1_of_iv2; kwargs...) +function change_independent_variable(sys::AbstractODESystem, iv, iv1_of_iv2, iv2_of_iv1; kwargs...) iv1 = ModelingToolkit.get_iv(sys) # old independent variable iv2 = iv # new independent variable @@ -76,10 +76,10 @@ function change_independent_variable(sys::AbstractODESystem, iv, iv1_of_iv2; kwa end end eq = expand_derivatives(eq) # expand out with chain rule to get d(iv2)/d(iv1) - div1_div2 = Differential(iv2)(iv1_of_iv2) |> expand_derivatives - div2_div1 = 1 / div1_div2 - eq = substitute(eq, Differential(iv1)(iv2func) => div2_div1) # substitute in d(iv1)/d(iv2) + eq = substitute(eq, Differential(iv1)(iv2func) => Differential(iv1)(iv2_of_iv1)) # substitute in d(iv2)/d(iv1) + eq = expand_derivatives(eq) eq = substitute(eq, iv2func => iv2) # make iv2 independent + eq = substitute(eq, iv1 => iv1_of_iv2) # substitute any remaining old ivars eqs[i] = eq end diff --git a/test/basic_transformations.jl b/test/basic_transformations.jl index 512510011c..efa799a0ee 100644 --- a/test/basic_transformations.jl +++ b/test/basic_transformations.jl @@ -39,21 +39,22 @@ sol = solve(prob, Tsit5()) @variables x(t) y(t) z(t) eqs = [ D(x) ~ y - D(y) ~ 2*D(x) + D(D(y)) ~ 2 * x * D(y) z ~ x + D(y) ] @named sys1 = ODESystem(eqs, t) @independent_variables s - @named sys2 = ModelingToolkit.change_independent_variable(sys1, s, s^2) + @named sys2 = ModelingToolkit.change_independent_variable(sys1, s, s^2, √(t)) sys1 = structural_simplify(sys1) sys2 = structural_simplify(sys2) - prob1 = ODEProblem(sys1, unknowns(sys1) .=> 1.0, (0.0, 1.0)) - prob2 = ODEProblem(sys2, unknowns(sys2) .=> 1.0, (0.0, 1.0)) + prob1 = ODEProblem(sys1, [sys1.x => 1.0, sys1.y => 1.0, Differential(t)(sys1.y) => 0.0], (1.0, 4.0)) + prob2 = ODEProblem(sys2, [sys2.x => 1.0, sys2.y => 1.0, Differential(s)(sys2.y) => 0.0], (1.0, 2.0)) sol1 = solve(prob1, Tsit5(); reltol = 1e-10, abstol = 1e-10) sol2 = solve(prob2, Tsit5(); reltol = 1e-10, abstol = 1e-10) ts = range(0.0, 1.0, length = 50) ss = .√(ts) - @test isapprox(sol1(ts), sol2(ss); atol = 1e-8) + @test all(isapprox.(sol1(ts, idxs=sys1.x), sol2(ss, idxs=sys2.x); atol = 1e-7)) && + all(isapprox.(sol1(ts, idxs=sys1.y), sol2(ss, idxs=sys2.y); atol = 1e-7)) end From acd0fabc7aea69e7702e98623f944a7171b85138 Mon Sep 17 00:00:00 2001 From: Herman Sletmoen Date: Sat, 1 Mar 2025 13:28:27 +0100 Subject: [PATCH 1064/1337] Change independent variable to a dependent one --- src/systems/diffeqs/basic_transformations.jl | 38 ++++++++++++++++++++ test/basic_transformations.jl | 21 +++++++++++ 2 files changed, 59 insertions(+) diff --git a/src/systems/diffeqs/basic_transformations.jl b/src/systems/diffeqs/basic_transformations.jl index 60eb46483e..01a103adab 100644 --- a/src/systems/diffeqs/basic_transformations.jl +++ b/src/systems/diffeqs/basic_transformations.jl @@ -86,3 +86,41 @@ function change_independent_variable(sys::AbstractODESystem, iv, iv1_of_iv2, iv2 sys2 = typeof(sys)(eqs, iv2; name = nameof(sys), description = description(sys), kwargs...) return sys2 end + +function change_independent_variable(sys::AbstractODESystem, iv; kwargs...) + iv1 = get_iv(sys) # e.g. t + iv2func = iv # e.g. a(t) + iv2name = nameof(operation(unwrap(iv))) # TODO: handle namespacing? + iv2, = @independent_variables $iv2name + + # TODO: find iv2func in system and replace it with some dummy variable + # TODO: not just to 1st, but to all orders + + div2name = Symbol(iv2name, :_t) + div2, = @variables $div2name(iv2) + + eqs = ModelingToolkit.get_eqs(sys) |> copy # don't modify original system + vars = [] + for (i, eq) in enumerate(eqs) + vars = Symbolics.get_variables(eq) + for var1 in vars + if Symbolics.iscall(var1) && !isequal(var1, iv2func) # && isequal(only(arguments(var1)), iv1) # skip e.g. constants + name = nameof(operation(var1)) + var2, = @variables $name(iv2func) + eq = substitute(eq, var1 => var2; fold = false) + end + end + eq = expand_derivatives(eq) # expand out with chain rule to get d(iv2)/d(iv1) + #eq = substitute(eq, Differential(iv1)(iv2func) => Differential(iv1)(iv2_of_iv1)) # substitute in d(iv2)/d(iv1) + #eq = expand_derivatives(eq) + eq = substitute(eq, Differential(iv1)(Differential(iv1)(iv2func)) => div2) # e.g. D(a(t)) => a_t(t) # TODO: more orders + eq = substitute(eq, Differential(iv1)(iv2func) => div2) # e.g. D(a(t)) => a_t(t) # TODO: more orders + eq = substitute(eq, iv2func => iv2) # make iv2 independent + #eq = substitute(eq, iv1 => iv1_of_iv2) # substitute any remaining old ivars + eqs[i] = eq + println(eq) + end + + sys2 = typeof(sys)(eqs, iv2; name = nameof(sys), description = description(sys), kwargs...) + return sys2 +end diff --git a/test/basic_transformations.jl b/test/basic_transformations.jl index efa799a0ee..3c7a4e6755 100644 --- a/test/basic_transformations.jl +++ b/test/basic_transformations.jl @@ -58,3 +58,24 @@ sol = solve(prob, Tsit5()) @test all(isapprox.(sol1(ts, idxs=sys1.x), sol2(ss, idxs=sys2.x); atol = 1e-7)) && all(isapprox.(sol1(ts, idxs=sys1.y), sol2(ss, idxs=sys2.y); atol = 1e-7)) end + +#@testset "Change independent variable (Friedmann equation)" begin + @independent_variables t + D = Differential(t) + @variables a(t) ρr(t) ρm(t) ρΛ(t) ρ(t) ϕ(t) + @parameters Ωr0 Ωm0 ΩΛ0 + eqs = [ + ρr ~ 3/(8*Num(π)) * Ωr0 / a^4 + ρm ~ 3/(8*Num(π)) * Ωm0 / a^3 + ρΛ ~ 3/(8*Num(π)) * ΩΛ0 + ρ ~ ρr + ρm + ρΛ + D(a) ~ √(8*Num(π)/3*ρ*a^4) + D(D(ϕ)) ~ -3*D(a)/a*D(ϕ) + ] + @named M1 = ODESystem(eqs, t) + M1 = complete(M1) + + M2 = ModelingToolkit.change_independent_variable(M1, M1.a) + + M2 = structural_simplify(M2; allow_symbolic = true) +#end From 865ef02623a9fcdb81b6227fa64a7aa4800a3744 Mon Sep 17 00:00:00 2001 From: Herman Sletmoen Date: Sat, 1 Mar 2025 23:45:22 +0100 Subject: [PATCH 1065/1337] Merge functions for changing independent variable into one --- src/systems/diffeqs/basic_transformations.jl | 83 +++++++++----------- test/basic_transformations.jl | 24 +++--- 2 files changed, 50 insertions(+), 57 deletions(-) diff --git a/src/systems/diffeqs/basic_transformations.jl b/src/systems/diffeqs/basic_transformations.jl index 01a103adab..841c0bda6d 100644 --- a/src/systems/diffeqs/basic_transformations.jl +++ b/src/systems/diffeqs/basic_transformations.jl @@ -56,52 +56,28 @@ function liouville_transform(sys::AbstractODESystem) ODESystem(neweqs, t, vars, parameters(sys), checks = false) end -# TODO: handle case when new iv is a variable already in the system -function change_independent_variable(sys::AbstractODESystem, iv, iv1_of_iv2, iv2_of_iv1; kwargs...) - iv1 = ModelingToolkit.get_iv(sys) # old independent variable - iv2 = iv # new independent variable - - name2 = nameof(iv2) - iv2func, = @variables $name2(iv1) - - eqs = ModelingToolkit.get_eqs(sys) |> copy # don't modify original system - vars = [] - for (i, eq) in enumerate(eqs) - vars = Symbolics.get_variables(eq) - for var1 in vars - if Symbolics.iscall(var1) # skip e.g. constants - name = nameof(operation(var1)) - var2, = @variables $name(iv2func) - eq = substitute(eq, var1 => var2; fold = false) - end - end - eq = expand_derivatives(eq) # expand out with chain rule to get d(iv2)/d(iv1) - eq = substitute(eq, Differential(iv1)(iv2func) => Differential(iv1)(iv2_of_iv1)) # substitute in d(iv2)/d(iv1) - eq = expand_derivatives(eq) - eq = substitute(eq, iv2func => iv2) # make iv2 independent - eq = substitute(eq, iv1 => iv1_of_iv2) # substitute any remaining old ivars - eqs[i] = eq - end - - sys2 = typeof(sys)(eqs, iv2; name = nameof(sys), description = description(sys), kwargs...) - return sys2 -end - -function change_independent_variable(sys::AbstractODESystem, iv; kwargs...) +function change_independent_variable(sys::AbstractODESystem, iv, eq = nothing; verbose = false, kwargs...) iv1 = get_iv(sys) # e.g. t - iv2func = iv # e.g. a(t) - iv2name = nameof(operation(unwrap(iv))) # TODO: handle namespacing? - iv2, = @independent_variables $iv2name - - # TODO: find iv2func in system and replace it with some dummy variable - # TODO: not just to 1st, but to all orders + iv2name = nameof(iv) # TODO: handle namespacing? + iv2, = @independent_variables $iv2name # e.g. a + iv2func, = @variables $iv2name(iv1) # e.g. a(t) + D1 = Differential(iv1) + D2 = Differential(iv2) div2name = Symbol(iv2name, :_t) - div2, = @variables $div2name(iv2) + div2, = @variables $div2name(iv2) # e.g. a_t(a) + + ddiv2name = Symbol(iv2name, :_tt) + ddiv2, = @variables $ddiv2name(iv2) # e.g. a_tt(a) eqs = ModelingToolkit.get_eqs(sys) |> copy # don't modify original system + !isnothing(eq) && push!(eqs, eq) vars = [] + div2_div1 = nothing for (i, eq) in enumerate(eqs) + verbose && println("1. ", eq) + + # 1) Substitute f(t₁) => f(t₂(t₁)) in all variables vars = Symbolics.get_variables(eq) for var1 in vars if Symbolics.iscall(var1) && !isequal(var1, iv2func) # && isequal(only(arguments(var1)), iv1) # skip e.g. constants @@ -110,17 +86,32 @@ function change_independent_variable(sys::AbstractODESystem, iv; kwargs...) eq = substitute(eq, var1 => var2; fold = false) end end + verbose && println("2. ", eq) + + # 2) Substitute in dummy variables for dⁿt₂/dt₁ⁿ eq = expand_derivatives(eq) # expand out with chain rule to get d(iv2)/d(iv1) - #eq = substitute(eq, Differential(iv1)(iv2func) => Differential(iv1)(iv2_of_iv1)) # substitute in d(iv2)/d(iv1) - #eq = expand_derivatives(eq) - eq = substitute(eq, Differential(iv1)(Differential(iv1)(iv2func)) => div2) # e.g. D(a(t)) => a_t(t) # TODO: more orders - eq = substitute(eq, Differential(iv1)(iv2func) => div2) # e.g. D(a(t)) => a_t(t) # TODO: more orders - eq = substitute(eq, iv2func => iv2) # make iv2 independent - #eq = substitute(eq, iv1 => iv1_of_iv2) # substitute any remaining old ivars + verbose && println("3. ", eq) + eq = substitute(eq, D1(D1(iv2func)) => ddiv2) # order 2 # TODO: more orders + eq = substitute(eq, D1(iv2func) => div2) # order 1; e.g. D(a(t)) => a_t(t) + eq = substitute(eq, iv2func => iv2) # order 0; make iv2 independent + verbose && println("4. ", eq) + verbose && println() + eqs[i] = eq - println(eq) + + if isequal(eq.lhs, div2) + div2_div1 = eq.rhs + end end + isnothing(div2_div1) && error("No equation for $D1($iv2func) was specified.") + verbose && println("Found $div2 = $div2_div1") + + # 3) Add equations for dummy variables + div1_div2 = 1 / div2_div1 + push!(eqs, ddiv2 ~ expand_derivatives(-D2(div1_div2) / div1_div2^3)) # e.g. https://math.stackexchange.com/questions/249253/second-derivative-of-the-inverse-function # TODO: higher orders + + # 4) Recreate system with new equations sys2 = typeof(sys)(eqs, iv2; name = nameof(sys), description = description(sys), kwargs...) return sys2 end diff --git a/test/basic_transformations.jl b/test/basic_transformations.jl index 3c7a4e6755..34f350222c 100644 --- a/test/basic_transformations.jl +++ b/test/basic_transformations.jl @@ -34,22 +34,23 @@ sol = solve(prob, Tsit5()) @test sol[end, end] ≈ 1.0742818931017244 @testset "Change independent variable" begin - # Autonomous 1st order (t doesn't appear in the equations) @independent_variables t - @variables x(t) y(t) z(t) + @variables x(t) y(t) z(t) s(t) eqs = [ D(x) ~ y D(D(y)) ~ 2 * x * D(y) z ~ x + D(y) + D(s) ~ 1 / (2*s) ] @named sys1 = ODESystem(eqs, t) + sys1 = complete(sys1) @independent_variables s - @named sys2 = ModelingToolkit.change_independent_variable(sys1, s, s^2, √(t)) + sys2 = ModelingToolkit.change_independent_variable(sys1, s) - sys1 = structural_simplify(sys1) - sys2 = structural_simplify(sys2) - prob1 = ODEProblem(sys1, [sys1.x => 1.0, sys1.y => 1.0, Differential(t)(sys1.y) => 0.0], (1.0, 4.0)) + sys1 = structural_simplify(sys1; allow_symbolic = true) + sys2 = structural_simplify(sys2; allow_symbolic = true) + prob1 = ODEProblem(sys1, [sys1.x => 1.0, sys1.y => 1.0, Differential(t)(sys1.y) => 0.0, sys1.s => 1.0], (1.0, 4.0)) prob2 = ODEProblem(sys2, [sys2.x => 1.0, sys2.y => 1.0, Differential(s)(sys2.y) => 0.0], (1.0, 2.0)) sol1 = solve(prob1, Tsit5(); reltol = 1e-10, abstol = 1e-10) sol2 = solve(prob2, Tsit5(); reltol = 1e-10, abstol = 1e-10) @@ -59,10 +60,10 @@ sol = solve(prob, Tsit5()) all(isapprox.(sol1(ts, idxs=sys1.y), sol2(ss, idxs=sys2.y); atol = 1e-7)) end -#@testset "Change independent variable (Friedmann equation)" begin +@testset "Change independent variable (Friedmann equation)" begin @independent_variables t D = Differential(t) - @variables a(t) ρr(t) ρm(t) ρΛ(t) ρ(t) ϕ(t) + @variables a(t) ρr(t) ρm(t) ρΛ(t) ρ(t) P(t) ϕ(t) @parameters Ωr0 Ωm0 ΩΛ0 eqs = [ ρr ~ 3/(8*Num(π)) * Ωr0 / a^4 @@ -75,7 +76,8 @@ end @named M1 = ODESystem(eqs, t) M1 = complete(M1) - M2 = ModelingToolkit.change_independent_variable(M1, M1.a) - + @independent_variables a + M2 = ModelingToolkit.change_independent_variable(M1, a) M2 = structural_simplify(M2; allow_symbolic = true) -#end + @test length(unknowns(M2)) == 2 +end From af3ae69d0a307822a2212462eccf63a843451ffb Mon Sep 17 00:00:00 2001 From: Herman Sletmoen Date: Sun, 2 Mar 2025 12:03:16 +0100 Subject: [PATCH 1066/1337] Fix broken Liouville transform test/documentation --- src/systems/diffeqs/basic_transformations.jl | 24 ++++------- test/basic_transformations.jl | 44 ++++++++------------ 2 files changed, 27 insertions(+), 41 deletions(-) diff --git a/src/systems/diffeqs/basic_transformations.jl b/src/systems/diffeqs/basic_transformations.jl index 841c0bda6d..4640aa9b5f 100644 --- a/src/systems/diffeqs/basic_transformations.jl +++ b/src/systems/diffeqs/basic_transformations.jl @@ -14,26 +14,20 @@ the final value of `trJ` is the probability of ``u(t)``. Example: ```julia -using ModelingToolkit, OrdinaryDiffEq, Test +using ModelingToolkit, OrdinaryDiffEq @independent_variables t @parameters α β γ δ @variables x(t) y(t) D = Differential(t) +eqs = [D(x) ~ α*x - β*x*y, D(y) ~ -δ*y + γ*x*y] +@named sys = ODESystem(eqs, t) -eqs = [D(x) ~ α*x - β*x*y, - D(y) ~ -δ*y + γ*x*y] - -sys = ODESystem(eqs) sys2 = liouville_transform(sys) -@variables trJ - -u0 = [x => 1.0, - y => 1.0, - trJ => 1.0] - -prob = ODEProblem(complete(sys2),u0,tspan,p) -sol = solve(prob,Tsit5()) +sys2 = complete(sys2) +u0 = [x => 1.0, y => 1.0, sys2.trJ => 1.0] +prob = ODEProblem(sys2, u0, tspan, p) +sol = solve(prob, Tsit5()) ``` Where `sol[3,:]` is the evolution of `trJ` over time. @@ -46,14 +40,14 @@ Optimal Transport Approach Abhishek Halder, Kooktae Lee, and Raktim Bhattacharya https://abhishekhalder.bitbucket.io/F16ACC2013Final.pdf """ -function liouville_transform(sys::AbstractODESystem) +function liouville_transform(sys::AbstractODESystem; kwargs...) t = get_iv(sys) @variables trJ D = ModelingToolkit.Differential(t) neweq = D(trJ) ~ trJ * -tr(calculate_jacobian(sys)) neweqs = [equations(sys); neweq] vars = [unknowns(sys); trJ] - ODESystem(neweqs, t, vars, parameters(sys), checks = false) + ODESystem(neweqs, t, vars, parameters(sys); checks = false, name = nameof(sys), kwargs...) end function change_independent_variable(sys::AbstractODESystem, iv, eq = nothing; verbose = false, kwargs...) diff --git a/test/basic_transformations.jl b/test/basic_transformations.jl index 34f350222c..2436dc7fc4 100644 --- a/test/basic_transformations.jl +++ b/test/basic_transformations.jl @@ -1,37 +1,29 @@ using ModelingToolkit, OrdinaryDiffEq, Test @independent_variables t -@parameters α β γ δ -@variables x(t) y(t) D = Differential(t) -eqs = [D(x) ~ α * x - β * x * y, - D(y) ~ -δ * y + γ * x * y] +@testset "Liouville transform" begin + @parameters α β γ δ + @variables x(t) y(t) + eqs = [D(x) ~ α * x - β * x * y, D(y) ~ -δ * y + γ * x * y] + @named sys = ODESystem(eqs, t) + sys = complete(sys) -sys = ODESystem(eqs) + u0 = [x => 1.0, y => 1.0] + p = [α => 1.5, β => 1.0, δ => 3.0, γ => 1.0] + tspan = (0.0, 10.0) + prob = ODEProblem(sys, u0, tspan, p) + sol = solve(prob, Tsit5()) -u0 = [x => 1.0, - y => 1.0] + sys2 = liouville_transform(sys) + sys2 = complete(sys2) -p = [α => 1.5, - β => 1.0, - δ => 3.0, - γ => 1.0] - -tspan = (0.0, 10.0) -prob = ODEProblem(sys, u0, tspan, p) -sol = solve(prob, Tsit5()) - -sys2 = liouville_transform(sys) -@variables trJ - -u0 = [x => 1.0, - y => 1.0, - trJ => 1.0] - -prob = ODEProblem(sys2, u0, tspan, p, jac = true) -sol = solve(prob, Tsit5()) -@test sol[end, end] ≈ 1.0742818931017244 + u0 = [x => 1.0, y => 1.0, sys2.trJ => 1.0] + prob = ODEProblem(sys2, u0, tspan, p, jac = true) + sol = solve(prob, Tsit5()) + @test sol[end, end] ≈ 1.0742818931017244 +end @testset "Change independent variable" begin @independent_variables t From cbceec481a050189015189edc56d4f879a112879 Mon Sep 17 00:00:00 2001 From: Herman Sletmoen Date: Sun, 2 Mar 2025 12:12:47 +0100 Subject: [PATCH 1067/1337] Specify new independent variable as a dependent variable in the old system --- src/systems/diffeqs/basic_transformations.jl | 2 +- test/basic_transformations.jl | 24 ++++++++------------ 2 files changed, 10 insertions(+), 16 deletions(-) diff --git a/src/systems/diffeqs/basic_transformations.jl b/src/systems/diffeqs/basic_transformations.jl index 4640aa9b5f..19c997e787 100644 --- a/src/systems/diffeqs/basic_transformations.jl +++ b/src/systems/diffeqs/basic_transformations.jl @@ -52,7 +52,7 @@ end function change_independent_variable(sys::AbstractODESystem, iv, eq = nothing; verbose = false, kwargs...) iv1 = get_iv(sys) # e.g. t - iv2name = nameof(iv) # TODO: handle namespacing? + iv2name = nameof(operation(unwrap(iv))) # TODO: handle namespacing? iv2, = @independent_variables $iv2name # e.g. a iv2func, = @variables $iv2name(iv1) # e.g. a(t) D1 = Differential(iv1) diff --git a/test/basic_transformations.jl b/test/basic_transformations.jl index 2436dc7fc4..55f1612fd3 100644 --- a/test/basic_transformations.jl +++ b/test/basic_transformations.jl @@ -26,7 +26,6 @@ D = Differential(t) end @testset "Change independent variable" begin - @independent_variables t @variables x(t) y(t) z(t) s(t) eqs = [ D(x) ~ y @@ -34,26 +33,22 @@ end z ~ x + D(y) D(s) ~ 1 / (2*s) ] - @named sys1 = ODESystem(eqs, t) - sys1 = complete(sys1) + M1 = ODESystem(eqs, t; name = :M) |> complete + M2 = ModelingToolkit.change_independent_variable(M1, M1.s) - @independent_variables s - sys2 = ModelingToolkit.change_independent_variable(sys1, s) - - sys1 = structural_simplify(sys1; allow_symbolic = true) - sys2 = structural_simplify(sys2; allow_symbolic = true) - prob1 = ODEProblem(sys1, [sys1.x => 1.0, sys1.y => 1.0, Differential(t)(sys1.y) => 0.0, sys1.s => 1.0], (1.0, 4.0)) - prob2 = ODEProblem(sys2, [sys2.x => 1.0, sys2.y => 1.0, Differential(s)(sys2.y) => 0.0], (1.0, 2.0)) + M1 = structural_simplify(M1; allow_symbolic = true) + M2 = structural_simplify(M2; allow_symbolic = true) + prob1 = ODEProblem(M1, [M1.x => 1.0, M1.y => 1.0, Differential(M1.t)(M1.y) => 0.0, M1.s => 1.0], (1.0, 4.0)) + prob2 = ODEProblem(M2, [M2.x => 1.0, M2.y => 1.0, Differential(M2.s)(M2.y) => 0.0], (1.0, 2.0)) sol1 = solve(prob1, Tsit5(); reltol = 1e-10, abstol = 1e-10) sol2 = solve(prob2, Tsit5(); reltol = 1e-10, abstol = 1e-10) ts = range(0.0, 1.0, length = 50) ss = .√(ts) - @test all(isapprox.(sol1(ts, idxs=sys1.x), sol2(ss, idxs=sys2.x); atol = 1e-7)) && - all(isapprox.(sol1(ts, idxs=sys1.y), sol2(ss, idxs=sys2.y); atol = 1e-7)) + @test all(isapprox.(sol1(ts, idxs=M1.x), sol2(ss, idxs=M2.x); atol = 1e-7)) && + all(isapprox.(sol1(ts, idxs=M1.y), sol2(ss, idxs=M2.y); atol = 1e-7)) end @testset "Change independent variable (Friedmann equation)" begin - @independent_variables t D = Differential(t) @variables a(t) ρr(t) ρm(t) ρΛ(t) ρ(t) P(t) ϕ(t) @parameters Ωr0 Ωm0 ΩΛ0 @@ -68,8 +63,7 @@ end @named M1 = ODESystem(eqs, t) M1 = complete(M1) - @independent_variables a - M2 = ModelingToolkit.change_independent_variable(M1, a) + M2 = ModelingToolkit.change_independent_variable(M1, M1.a) M2 = structural_simplify(M2; allow_symbolic = true) @test length(unknowns(M2)) == 2 end From 75fe0f062dce5a3d89c71b8feffd3fd4ed1ff751 Mon Sep 17 00:00:00 2001 From: Herman Sletmoen Date: Sun, 2 Mar 2025 14:51:59 +0100 Subject: [PATCH 1068/1337] Optionally simplify dummy derivative expressions when changing independent variable --- src/systems/diffeqs/basic_transformations.jl | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/systems/diffeqs/basic_transformations.jl b/src/systems/diffeqs/basic_transformations.jl index 19c997e787..a771f0e5f0 100644 --- a/src/systems/diffeqs/basic_transformations.jl +++ b/src/systems/diffeqs/basic_transformations.jl @@ -50,7 +50,7 @@ function liouville_transform(sys::AbstractODESystem; kwargs...) ODESystem(neweqs, t, vars, parameters(sys); checks = false, name = nameof(sys), kwargs...) end -function change_independent_variable(sys::AbstractODESystem, iv, eq = nothing; verbose = false, kwargs...) +function change_independent_variable(sys::AbstractODESystem, iv, eq = nothing; verbose = false, simplify = true, kwargs...) iv1 = get_iv(sys) # e.g. t iv2name = nameof(operation(unwrap(iv))) # TODO: handle namespacing? iv2, = @independent_variables $iv2name # e.g. a @@ -103,7 +103,11 @@ function change_independent_variable(sys::AbstractODESystem, iv, eq = nothing; v # 3) Add equations for dummy variables div1_div2 = 1 / div2_div1 - push!(eqs, ddiv2 ~ expand_derivatives(-D2(div1_div2) / div1_div2^3)) # e.g. https://math.stackexchange.com/questions/249253/second-derivative-of-the-inverse-function # TODO: higher orders + ddiv1_ddiv2 = expand_derivatives(-D2(div1_div2) / div1_div2^3) + if simplify + ddiv1_ddiv2 = Symbolics.simplify(ddiv1_ddiv2) + end + push!(eqs, ddiv2 ~ ddiv1_ddiv2) # e.g. https://math.stackexchange.com/questions/249253/second-derivative-of-the-inverse-function # TODO: higher orders # 4) Recreate system with new equations sys2 = typeof(sys)(eqs, iv2; name = nameof(sys), description = description(sys), kwargs...) From b8c9839bbfc071b7b32c7a1a0386b0a69a86f135 Mon Sep 17 00:00:00 2001 From: Herman Sletmoen Date: Sun, 2 Mar 2025 15:34:25 +0100 Subject: [PATCH 1069/1337] Improve change of independent variable tests --- test/basic_transformations.jl | 33 ++++++++++++++++++++++++++++----- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/test/basic_transformations.jl b/test/basic_transformations.jl index 55f1612fd3..5f17abdd52 100644 --- a/test/basic_transformations.jl +++ b/test/basic_transformations.jl @@ -50,20 +50,43 @@ end @testset "Change independent variable (Friedmann equation)" begin D = Differential(t) - @variables a(t) ρr(t) ρm(t) ρΛ(t) ρ(t) P(t) ϕ(t) + @variables a(t) ȧ(t) ρr(t) ρm(t) ρΛ(t) ρ(t) P(t) ϕ(t) @parameters Ωr0 Ωm0 ΩΛ0 eqs = [ ρr ~ 3/(8*Num(π)) * Ωr0 / a^4 ρm ~ 3/(8*Num(π)) * Ωm0 / a^3 ρΛ ~ 3/(8*Num(π)) * ΩΛ0 ρ ~ ρr + ρm + ρΛ - D(a) ~ √(8*Num(π)/3*ρ*a^4) + ȧ ~ √(8*Num(π)/3*ρ*a^4) + D(a) ~ ȧ D(D(ϕ)) ~ -3*D(a)/a*D(ϕ) ] - @named M1 = ODESystem(eqs, t) - M1 = complete(M1) + M1 = ODESystem(eqs, t; name = :M) |> complete - M2 = ModelingToolkit.change_independent_variable(M1, M1.a) + # Apply in two steps, where derivatives are defined at each step: first t -> a, then a -> b + M2 = ModelingToolkit.change_independent_variable(M1, M1.a) |> complete #, D(b) ~ D(a)/a; verbose = true) + @variables b(M2.a) + M3 = ModelingToolkit.change_independent_variable(M2, b, Differential(M2.a)(b) ~ exp(-b)) M2 = structural_simplify(M2; allow_symbolic = true) + M3 = structural_simplify(M3; allow_symbolic = true) @test length(unknowns(M2)) == 2 end + +@testset "Change independent variable (simple)" begin + @variables x(t) + Mt = ODESystem([D(x) ~ 2*x], t; name = :M) |> complete + Mx = ModelingToolkit.change_independent_variable(Mt, Mt.x) + @test (@variables x x_t(x) x_tt(x); Set(equations(Mx)) == Set([x_t ~ 2x, x_tt ~ 4x])) +end + +@testset "Change independent variable (free fall)" begin + @variables x(t) y(t) + @parameters g v # gravitational acceleration and constant horizontal velocity + Mt = ODESystem([D(D(y)) ~ -g, D(x) ~ v], t; name = :M) |> complete # gives (x, y) as function of t, ... + Mx = ModelingToolkit.change_independent_variable(Mt, Mt.x) # ... but we want y as a function of x + Mx = structural_simplify(Mx; allow_symbolic = true) + Dx = Differential(Mx.x) + prob = ODEProblem(Mx, [Mx.y => 0.0, Dx(Mx.y) => 1.0], (0.0, 20.0), [g => 9.81, v => 10.0]) # 1 = dy/dx = (dy/dt)/(dx/dt) means equal initial horizontal and vertical velocities + sol = solve(prob, Tsit5(); reltol = 1e-5) + @test all(isapprox.(sol[Mx.y], sol[Mx.x - g*(Mx.x/v)^2/2]; atol = 1e-10)) # eliminate t from anal solution x(t) = v*t, y(t) = v*t - g*t^2/2 +end From dfc52694717c65710585e5aa96b24cc0ce0158f7 Mon Sep 17 00:00:00 2001 From: Herman Sletmoen Date: Sun, 2 Mar 2025 15:58:03 +0100 Subject: [PATCH 1070/1337] Explicitly insert dummy equations into system, if requested --- src/systems/diffeqs/basic_transformations.jl | 12 ++++++++++-- test/basic_transformations.jl | 15 ++++++++++++--- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/src/systems/diffeqs/basic_transformations.jl b/src/systems/diffeqs/basic_transformations.jl index a771f0e5f0..f0c8702e92 100644 --- a/src/systems/diffeqs/basic_transformations.jl +++ b/src/systems/diffeqs/basic_transformations.jl @@ -50,7 +50,7 @@ function liouville_transform(sys::AbstractODESystem; kwargs...) ODESystem(neweqs, t, vars, parameters(sys); checks = false, name = nameof(sys), kwargs...) end -function change_independent_variable(sys::AbstractODESystem, iv, eq = nothing; verbose = false, simplify = true, kwargs...) +function change_independent_variable(sys::AbstractODESystem, iv, eq = nothing; verbose = false, simplify = true, dummies = false, kwargs...) iv1 = get_iv(sys) # e.g. t iv2name = nameof(operation(unwrap(iv))) # TODO: handle namespacing? iv2, = @independent_variables $iv2name # e.g. a @@ -109,7 +109,15 @@ function change_independent_variable(sys::AbstractODESystem, iv, eq = nothing; v end push!(eqs, ddiv2 ~ ddiv1_ddiv2) # e.g. https://math.stackexchange.com/questions/249253/second-derivative-of-the-inverse-function # TODO: higher orders - # 4) Recreate system with new equations + # 4) If requested, instead remove and insert dummy equations + if !dummies + dummyidxs = findall(eq -> isequal(eq.lhs, div2) || isequal(eq.lhs, ddiv2), eqs) + dummyeqs = splice!(eqs, dummyidxs) # return and remove dummy equations + dummysubs = Dict(eq.lhs => eq.rhs for eq in dummyeqs) + eqs = substitute.(eqs, Ref(dummysubs)) # don't iterate over dummysubs + end + + # 5) Recreate system with new equations sys2 = typeof(sys)(eqs, iv2; name = nameof(sys), description = description(sys), kwargs...) return sys2 end diff --git a/test/basic_transformations.jl b/test/basic_transformations.jl index 5f17abdd52..add54a62c8 100644 --- a/test/basic_transformations.jl +++ b/test/basic_transformations.jl @@ -25,6 +25,15 @@ D = Differential(t) @test sol[end, end] ≈ 1.0742818931017244 end +@testset "Change independent variable (trivial)" begin + @variables x(t) y(t) + eqs1 = [D(D(x)) ~ D(x) + x, D(y) ~ 1] + M1 = ODESystem(eqs1, t; name = :M) |> complete + M2 = ModelingToolkit.change_independent_variable(M1, M1.y) + eqs2 = substitute(equations(M2), M2.y => M1.t) # system should be equivalent when parametrized with y (since D(y) ~ 1), so substitute back ... + @test eqs1[1] == only(eqs2) # ... and check that the equations are unmodified +end + @testset "Change independent variable" begin @variables x(t) y(t) z(t) s(t) eqs = [ @@ -75,7 +84,7 @@ end @testset "Change independent variable (simple)" begin @variables x(t) Mt = ODESystem([D(x) ~ 2*x], t; name = :M) |> complete - Mx = ModelingToolkit.change_independent_variable(Mt, Mt.x) + Mx = ModelingToolkit.change_independent_variable(Mt, Mt.x; dummies = true) @test (@variables x x_t(x) x_tt(x); Set(equations(Mx)) == Set([x_t ~ 2x, x_tt ~ 4x])) end @@ -83,10 +92,10 @@ end @variables x(t) y(t) @parameters g v # gravitational acceleration and constant horizontal velocity Mt = ODESystem([D(D(y)) ~ -g, D(x) ~ v], t; name = :M) |> complete # gives (x, y) as function of t, ... - Mx = ModelingToolkit.change_independent_variable(Mt, Mt.x) # ... but we want y as a function of x + Mx = ModelingToolkit.change_independent_variable(Mt, Mt.x; dummies = false) # ... but we want y as a function of x Mx = structural_simplify(Mx; allow_symbolic = true) Dx = Differential(Mx.x) prob = ODEProblem(Mx, [Mx.y => 0.0, Dx(Mx.y) => 1.0], (0.0, 20.0), [g => 9.81, v => 10.0]) # 1 = dy/dx = (dy/dt)/(dx/dt) means equal initial horizontal and vertical velocities sol = solve(prob, Tsit5(); reltol = 1e-5) - @test all(isapprox.(sol[Mx.y], sol[Mx.x - g*(Mx.x/v)^2/2]; atol = 1e-10)) # eliminate t from anal solution x(t) = v*t, y(t) = v*t - g*t^2/2 + @test all(isapprox.(sol[Mx.y], sol[Mx.x - g*(Mx.x/v)^2/2]; atol = 1e-10)) # compare to analytical solution (x(t) = v*t, y(t) = v*t - g*t^2/2) end From ae9c174b06e4a5eb959767861576f81af08b02cb Mon Sep 17 00:00:00 2001 From: Herman Sletmoen Date: Sun, 2 Mar 2025 18:09:03 +0100 Subject: [PATCH 1071/1337] Add and test errors when changing independent variable --- src/systems/diffeqs/basic_transformations.jl | 24 +++++++++++++++++--- test/basic_transformations.jl | 15 ++++++++++++ 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/src/systems/diffeqs/basic_transformations.jl b/src/systems/diffeqs/basic_transformations.jl index f0c8702e92..4991ef73f1 100644 --- a/src/systems/diffeqs/basic_transformations.jl +++ b/src/systems/diffeqs/basic_transformations.jl @@ -51,10 +51,24 @@ function liouville_transform(sys::AbstractODESystem; kwargs...) end function change_independent_variable(sys::AbstractODESystem, iv, eq = nothing; verbose = false, simplify = true, dummies = false, kwargs...) + if !iscomplete(sys) + error("Cannot change independent variable of incomplete system $(nameof(sys))") + elseif isscheduled(sys) + error("Cannot change independent variable of structurally simplified system $(nameof(sys))") + elseif !isempty(get_systems(sys)) + error("Cannot change independent variable of hierarchical system $(nameof(sys)). Flatten it first.") # TODO: implement + end + + iv = unwrap(iv) iv1 = get_iv(sys) # e.g. t - iv2name = nameof(operation(unwrap(iv))) # TODO: handle namespacing? + + if !iscall(iv) || !isequal(only(arguments(iv)), iv1) + error("New independent variable $iv is not a function of the independent variable $iv1 of the system $(nameof(sys))") + end + + iv2func = iv # e.g. a(t) + iv2name = nameof(operation(iv)) iv2, = @independent_variables $iv2name # e.g. a - iv2func, = @variables $iv2name(iv1) # e.g. a(t) D1 = Differential(iv1) D2 = Differential(iv2) @@ -98,8 +112,12 @@ function change_independent_variable(sys::AbstractODESystem, iv, eq = nothing; v end end - isnothing(div2_div1) && error("No equation for $D1($iv2func) was specified.") verbose && println("Found $div2 = $div2_div1") + if isnothing(div2_div1) + error("No equation for $D1($iv2func) was specified.") + elseif isequal(div2_div1, 0) + error("Cannot change independent variable from $iv1 to $iv2 with singular transformation $(div2 ~ div2_div1).") + end # 3) Add equations for dummy variables div1_div2 = 1 / div2_div1 diff --git a/test/basic_transformations.jl b/test/basic_transformations.jl index add54a62c8..992bbfcc48 100644 --- a/test/basic_transformations.jl +++ b/test/basic_transformations.jl @@ -99,3 +99,18 @@ end sol = solve(prob, Tsit5(); reltol = 1e-5) @test all(isapprox.(sol[Mx.y], sol[Mx.x - g*(Mx.x/v)^2/2]; atol = 1e-10)) # compare to analytical solution (x(t) = v*t, y(t) = v*t - g*t^2/2) end + +@testset "Change independent variable (errors)" begin + @variables x(t) y z(y) w(t) v(t) + M = ODESystem([D(x) ~ 0, v ~ x], t; name = :M) + @test_throws "incomplete" ModelingToolkit.change_independent_variable(M, M.x) + M = complete(M) + @test_throws "singular" ModelingToolkit.change_independent_variable(M, M.x) + @test_throws "structurally simplified" ModelingToolkit.change_independent_variable(structural_simplify(M), y) + @test_throws "No equation" ModelingToolkit.change_independent_variable(M, w) + @test_throws "No equation" ModelingToolkit.change_independent_variable(M, v) + @test_throws "not a function of the independent variable" ModelingToolkit.change_independent_variable(M, y) + @test_throws "not a function of the independent variable" ModelingToolkit.change_independent_variable(M, z) + M = compose(M, M) + @test_throws "hierarchical" ModelingToolkit.change_independent_variable(M, M.x) +end From 00b27111ddd3ec9b00ebd620cab0c3e5b6fd8116 Mon Sep 17 00:00:00 2001 From: Herman Sletmoen Date: Sun, 2 Mar 2025 19:45:22 +0100 Subject: [PATCH 1072/1337] Export and document change_independent_variable --- docs/src/systems/ODESystem.md | 1 + src/ModelingToolkit.jl | 2 +- src/systems/diffeqs/basic_transformations.jl | 41 +++++++++++++++++++- test/basic_transformations.jl | 36 ++++++++--------- 4 files changed, 60 insertions(+), 20 deletions(-) diff --git a/docs/src/systems/ODESystem.md b/docs/src/systems/ODESystem.md index 6cc34725c4..4be0f6e263 100644 --- a/docs/src/systems/ODESystem.md +++ b/docs/src/systems/ODESystem.md @@ -30,6 +30,7 @@ ODESystem structural_simplify ode_order_lowering dae_index_lowering +change_independent_variable liouville_transform alias_elimination tearing diff --git a/src/ModelingToolkit.jl b/src/ModelingToolkit.jl index e6b8130af3..51156ba993 100644 --- a/src/ModelingToolkit.jl +++ b/src/ModelingToolkit.jl @@ -255,7 +255,7 @@ export isinput, isoutput, getbounds, hasbounds, getguess, hasguess, isdisturbanc tunable_parameters, isirreducible, getdescription, hasdescription, hasunit, getunit, hasconnect, getconnect, hasmisc, getmisc, state_priority -export ode_order_lowering, dae_order_lowering, liouville_transform +export ode_order_lowering, dae_order_lowering, liouville_transform, change_independent_variable export PDESystem export Differential, expand_derivatives, @derivatives export Equation, ConstrainedEquation diff --git a/src/systems/diffeqs/basic_transformations.jl b/src/systems/diffeqs/basic_transformations.jl index 4991ef73f1..665f150407 100644 --- a/src/systems/diffeqs/basic_transformations.jl +++ b/src/systems/diffeqs/basic_transformations.jl @@ -50,7 +50,46 @@ function liouville_transform(sys::AbstractODESystem; kwargs...) ODESystem(neweqs, t, vars, parameters(sys); checks = false, name = nameof(sys), kwargs...) end -function change_independent_variable(sys::AbstractODESystem, iv, eq = nothing; verbose = false, simplify = true, dummies = false, kwargs...) +""" + function change_independent_variable(sys::AbstractODESystem, iv, eq = nothing; dummies = false, simplify = true, verbose = false, kwargs...) + +Transform the independent variable (e.g. ``t``) of the ODE system `sys` to a dependent variable `iv` (e.g. ``f(t)``). +An equation in `sys` must define the rate of change of the new independent variable (e.g. ``df(t)/dt``). +Alternatively, `eq` can specify such an equation. + +The transformation is well-defined when the mapping between the new and old independent variables are one-to-one. +This is satisfied if one is a strictly increasing function of the other (e.g. ``df(t)/dt > 0`` or ``df(t)/dt < 0``). + +Keyword arguments +================= +If `dummies`, derivatives of the new independent variable are expressed through dummy equations; otherwise they are explicitly inserted into the equations. +If `simplify`, these dummy expressions are simplified and often give a tidier transformation. +If `verbose`, the function prints intermediate transformations of equations to aid debugging. +Any additional keyword arguments `kwargs...` are forwarded to the constructor that rebuilds the system. + +Usage before structural simplification +====================================== +The variable change must take place before structural simplification. +Subsequently, consider passing `allow_symbolic = true` to `structural_simplify(sys)` to reduce the number of unknowns, with the understanding that the transformation is well-defined. + +Example +======= +Consider a free fall with constant horizontal velocity. +The laws of physics naturally describes position as a function of time. +By changing the independent variable, it can be reformulated for vertical position as a function of horizontal distance: +```julia +julia> @variables x(t) y(t); + +julia> @named M = ODESystem([D(D(y)) ~ -9.81, D(x) ~ 10.0], t); + +julia> M′ = change_independent_variable(complete(M), x); + +julia> unknowns(M′) +1-element Vector{SymbolicUtils.BasicSymbolic{Real}}: + y(x) +``` +""" +function change_independent_variable(sys::AbstractODESystem, iv, eq = nothing; dummies = false, simplify = true, verbose = false, kwargs...) if !iscomplete(sys) error("Cannot change independent variable of incomplete system $(nameof(sys))") elseif isscheduled(sys) diff --git a/test/basic_transformations.jl b/test/basic_transformations.jl index 992bbfcc48..2e7f0ce54b 100644 --- a/test/basic_transformations.jl +++ b/test/basic_transformations.jl @@ -29,7 +29,7 @@ end @variables x(t) y(t) eqs1 = [D(D(x)) ~ D(x) + x, D(y) ~ 1] M1 = ODESystem(eqs1, t; name = :M) |> complete - M2 = ModelingToolkit.change_independent_variable(M1, M1.y) + M2 = change_independent_variable(M1, M1.y) eqs2 = substitute(equations(M2), M2.y => M1.t) # system should be equivalent when parametrized with y (since D(y) ~ 1), so substitute back ... @test eqs1[1] == only(eqs2) # ... and check that the equations are unmodified end @@ -43,10 +43,10 @@ end D(s) ~ 1 / (2*s) ] M1 = ODESystem(eqs, t; name = :M) |> complete - M2 = ModelingToolkit.change_independent_variable(M1, M1.s) + M2 = change_independent_variable(M1, M1.s) - M1 = structural_simplify(M1; allow_symbolic = true) - M2 = structural_simplify(M2; allow_symbolic = true) + M1 = structural_simplify(M1) + M2 = structural_simplify(M2) prob1 = ODEProblem(M1, [M1.x => 1.0, M1.y => 1.0, Differential(M1.t)(M1.y) => 0.0, M1.s => 1.0], (1.0, 4.0)) prob2 = ODEProblem(M2, [M2.x => 1.0, M2.y => 1.0, Differential(M2.s)(M2.y) => 0.0], (1.0, 2.0)) sol1 = solve(prob1, Tsit5(); reltol = 1e-10, abstol = 1e-10) @@ -73,18 +73,18 @@ end M1 = ODESystem(eqs, t; name = :M) |> complete # Apply in two steps, where derivatives are defined at each step: first t -> a, then a -> b - M2 = ModelingToolkit.change_independent_variable(M1, M1.a) |> complete #, D(b) ~ D(a)/a; verbose = true) + M2 = change_independent_variable(M1, M1.a) |> complete #, D(b) ~ D(a)/a; verbose = true) @variables b(M2.a) - M3 = ModelingToolkit.change_independent_variable(M2, b, Differential(M2.a)(b) ~ exp(-b)) + M3 = change_independent_variable(M2, b, Differential(M2.a)(b) ~ exp(-b)) M2 = structural_simplify(M2; allow_symbolic = true) M3 = structural_simplify(M3; allow_symbolic = true) - @test length(unknowns(M2)) == 2 + @test length(unknowns(M2)) == 2 && length(unknowns(M3)) == 2 end @testset "Change independent variable (simple)" begin @variables x(t) - Mt = ODESystem([D(x) ~ 2*x], t; name = :M) |> complete - Mx = ModelingToolkit.change_independent_variable(Mt, Mt.x; dummies = true) + Mt = ODESystem([D(x) ~ 2*x], t; name = :M) |> complete # TODO: avoid complete. can avoid it if passing defined $variable directly to change_independent_variable + Mx = change_independent_variable(Mt, Mt.x; dummies = true) @test (@variables x x_t(x) x_tt(x); Set(equations(Mx)) == Set([x_t ~ 2x, x_tt ~ 4x])) end @@ -92,7 +92,7 @@ end @variables x(t) y(t) @parameters g v # gravitational acceleration and constant horizontal velocity Mt = ODESystem([D(D(y)) ~ -g, D(x) ~ v], t; name = :M) |> complete # gives (x, y) as function of t, ... - Mx = ModelingToolkit.change_independent_variable(Mt, Mt.x; dummies = false) # ... but we want y as a function of x + Mx = change_independent_variable(Mt, Mt.x; dummies = false) # ... but we want y as a function of x Mx = structural_simplify(Mx; allow_symbolic = true) Dx = Differential(Mx.x) prob = ODEProblem(Mx, [Mx.y => 0.0, Dx(Mx.y) => 1.0], (0.0, 20.0), [g => 9.81, v => 10.0]) # 1 = dy/dx = (dy/dt)/(dx/dt) means equal initial horizontal and vertical velocities @@ -103,14 +103,14 @@ end @testset "Change independent variable (errors)" begin @variables x(t) y z(y) w(t) v(t) M = ODESystem([D(x) ~ 0, v ~ x], t; name = :M) - @test_throws "incomplete" ModelingToolkit.change_independent_variable(M, M.x) + @test_throws "incomplete" change_independent_variable(M, M.x) M = complete(M) - @test_throws "singular" ModelingToolkit.change_independent_variable(M, M.x) - @test_throws "structurally simplified" ModelingToolkit.change_independent_variable(structural_simplify(M), y) - @test_throws "No equation" ModelingToolkit.change_independent_variable(M, w) - @test_throws "No equation" ModelingToolkit.change_independent_variable(M, v) - @test_throws "not a function of the independent variable" ModelingToolkit.change_independent_variable(M, y) - @test_throws "not a function of the independent variable" ModelingToolkit.change_independent_variable(M, z) + @test_throws "singular" change_independent_variable(M, M.x) + @test_throws "structurally simplified" change_independent_variable(structural_simplify(M), y) + @test_throws "No equation" change_independent_variable(M, w) + @test_throws "No equation" change_independent_variable(M, v) + @test_throws "not a function of the independent variable" change_independent_variable(M, y) + @test_throws "not a function of the independent variable" change_independent_variable(M, z) M = compose(M, M) - @test_throws "hierarchical" ModelingToolkit.change_independent_variable(M, M.x) + @test_throws "hierarchical" change_independent_variable(M, M.x) end From ee212232247ef248c969ae6dcc84be27dbebfb02 Mon Sep 17 00:00:00 2001 From: Herman Sletmoen Date: Mon, 3 Mar 2025 16:45:27 +0100 Subject: [PATCH 1073/1337] Handle autonomous systems and more fields --- src/systems/diffeqs/basic_transformations.jl | 66 +++++++++++--------- test/basic_transformations.jl | 21 ++++--- 2 files changed, 49 insertions(+), 38 deletions(-) diff --git a/src/systems/diffeqs/basic_transformations.jl b/src/systems/diffeqs/basic_transformations.jl index 665f150407..56d3ada089 100644 --- a/src/systems/diffeqs/basic_transformations.jl +++ b/src/systems/diffeqs/basic_transformations.jl @@ -51,7 +51,7 @@ function liouville_transform(sys::AbstractODESystem; kwargs...) end """ - function change_independent_variable(sys::AbstractODESystem, iv, eq = nothing; dummies = false, simplify = true, verbose = false, kwargs...) + function change_independent_variable(sys::AbstractODESystem, iv, eqs = []; dummies = false, simplify = true, verbose = false, kwargs...) Transform the independent variable (e.g. ``t``) of the ODE system `sys` to a dependent variable `iv` (e.g. ``f(t)``). An equation in `sys` must define the rate of change of the new independent variable (e.g. ``df(t)/dt``). @@ -89,7 +89,7 @@ julia> unknowns(M′) y(x) ``` """ -function change_independent_variable(sys::AbstractODESystem, iv, eq = nothing; dummies = false, simplify = true, verbose = false, kwargs...) +function change_independent_variable(sys::AbstractODESystem, iv, eqs = []; dummies = false, simplify = true, verbose = false, kwargs...) if !iscomplete(sys) error("Cannot change independent variable of incomplete system $(nameof(sys))") elseif isscheduled(sys) @@ -100,9 +100,10 @@ function change_independent_variable(sys::AbstractODESystem, iv, eq = nothing; d iv = unwrap(iv) iv1 = get_iv(sys) # e.g. t - if !iscall(iv) || !isequal(only(arguments(iv)), iv1) error("New independent variable $iv is not a function of the independent variable $iv1 of the system $(nameof(sys))") + elseif !isautonomous(sys) && isempty(findall(eq -> isequal(eq.lhs, iv1), eqs)) + error("System $(nameof(sys)) is autonomous in $iv1. An equation of the form $iv1 ~ F($iv) must be provided.") end iv2func = iv # e.g. a(t) @@ -111,50 +112,53 @@ function change_independent_variable(sys::AbstractODESystem, iv, eq = nothing; d D1 = Differential(iv1) D2 = Differential(iv2) + iv1name = nameof(iv1) # e.g. t + iv1func, = @variables $iv1name(iv2) # e.g. t(a) + div2name = Symbol(iv2name, :_t) div2, = @variables $div2name(iv2) # e.g. a_t(a) ddiv2name = Symbol(iv2name, :_tt) ddiv2, = @variables $ddiv2name(iv2) # e.g. a_tt(a) - eqs = ModelingToolkit.get_eqs(sys) |> copy # don't modify original system - !isnothing(eq) && push!(eqs, eq) - vars = [] - div2_div1 = nothing - for (i, eq) in enumerate(eqs) - verbose && println("1. ", eq) - + function transform(ex) # 1) Substitute f(t₁) => f(t₂(t₁)) in all variables - vars = Symbolics.get_variables(eq) + verbose && println("1. ", ex) + vars = Symbolics.get_variables(ex) for var1 in vars if Symbolics.iscall(var1) && !isequal(var1, iv2func) # && isequal(only(arguments(var1)), iv1) # skip e.g. constants name = nameof(operation(var1)) var2, = @variables $name(iv2func) - eq = substitute(eq, var1 => var2; fold = false) + ex = substitute(ex, var1 => var2; fold = false) end end - verbose && println("2. ", eq) # 2) Substitute in dummy variables for dⁿt₂/dt₁ⁿ - eq = expand_derivatives(eq) # expand out with chain rule to get d(iv2)/d(iv1) - verbose && println("3. ", eq) - eq = substitute(eq, D1(D1(iv2func)) => ddiv2) # order 2 # TODO: more orders - eq = substitute(eq, D1(iv2func) => div2) # order 1; e.g. D(a(t)) => a_t(t) - eq = substitute(eq, iv2func => iv2) # order 0; make iv2 independent - verbose && println("4. ", eq) - verbose && println() - - eqs[i] = eq - - if isequal(eq.lhs, div2) - div2_div1 = eq.rhs - end + verbose && println("2. ", ex) + ex = expand_derivatives(ex) # expand out with chain rule to get d(iv2)/d(iv1) + verbose && println("3. ", ex) + ex = substitute(ex, D1(D1(iv2func)) => ddiv2) # order 2 # TODO: more orders + ex = substitute(ex, D1(iv2func) => div2) # order 1; e.g. D(a(t)) => a_t(t) + ex = substitute(ex, iv2func => iv2) # order 0; make iv2 independent + verbose && println("4. ", ex) + ex = substitute(ex, iv1 => iv1func) # if autonomous + verbose && println("5. ", ex) + return ex end - verbose && println("Found $div2 = $div2_div1") - if isnothing(div2_div1) + eqs = [get_eqs(sys); eqs] + eqs = map(transform, eqs) + + initialization_eqs = map(transform, get_initialization_eqs(sys)) + + div2_div1_idxs = findall(eq -> isequal(eq.lhs, div2), eqs) + if length(div2_div1_idxs) == 0 error("No equation for $D1($iv2func) was specified.") - elseif isequal(div2_div1, 0) + elseif length(div2_div1_idxs) >= 2 + error("Multiple equations for $D1($iv2func) were specified.") + end + div2_div1 = eqs[only(div2_div1_idxs)].rhs + if isequal(div2_div1, 0) error("Cannot change independent variable from $iv1 to $iv2 with singular transformation $(div2 ~ div2_div1).") end @@ -167,7 +171,7 @@ function change_independent_variable(sys::AbstractODESystem, iv, eq = nothing; d push!(eqs, ddiv2 ~ ddiv1_ddiv2) # e.g. https://math.stackexchange.com/questions/249253/second-derivative-of-the-inverse-function # TODO: higher orders # 4) If requested, instead remove and insert dummy equations - if !dummies + if !dummies # TODO: make dummies = false work with more fields! dummyidxs = findall(eq -> isequal(eq.lhs, div2) || isequal(eq.lhs, ddiv2), eqs) dummyeqs = splice!(eqs, dummyidxs) # return and remove dummy equations dummysubs = Dict(eq.lhs => eq.rhs for eq in dummyeqs) @@ -175,6 +179,6 @@ function change_independent_variable(sys::AbstractODESystem, iv, eq = nothing; d end # 5) Recreate system with new equations - sys2 = typeof(sys)(eqs, iv2; name = nameof(sys), description = description(sys), kwargs...) + sys2 = typeof(sys)(eqs, iv2; initialization_eqs, name = nameof(sys), description = description(sys), kwargs...) return sys2 end diff --git a/test/basic_transformations.jl b/test/basic_transformations.jl index 2e7f0ce54b..dc9fa21981 100644 --- a/test/basic_transformations.jl +++ b/test/basic_transformations.jl @@ -42,13 +42,14 @@ end z ~ x + D(y) D(s) ~ 1 / (2*s) ] - M1 = ODESystem(eqs, t; name = :M) |> complete - M2 = change_independent_variable(M1, M1.s) + initialization_eqs = [x ~ 1.0, y ~ 1.0, D(y) ~ 0.0] + M1 = ODESystem(eqs, t; initialization_eqs, name = :M) |> complete + M2 = change_independent_variable(M1, M1.s; dummies = true) - M1 = structural_simplify(M1) - M2 = structural_simplify(M2) - prob1 = ODEProblem(M1, [M1.x => 1.0, M1.y => 1.0, Differential(M1.t)(M1.y) => 0.0, M1.s => 1.0], (1.0, 4.0)) - prob2 = ODEProblem(M2, [M2.x => 1.0, M2.y => 1.0, Differential(M2.s)(M2.y) => 0.0], (1.0, 2.0)) + M1 = structural_simplify(M1; allow_symbolic = true) + M2 = structural_simplify(M2; allow_symbolic = true) + prob1 = ODEProblem(M1, [M1.s => 1.0], (1.0, 4.0), []) + prob2 = ODEProblem(M2, [], (1.0, 2.0), []) sol1 = solve(prob1, Tsit5(); reltol = 1e-10, abstol = 1e-10) sol2 = solve(prob2, Tsit5(); reltol = 1e-10, abstol = 1e-10) ts = range(0.0, 1.0, length = 50) @@ -75,7 +76,7 @@ end # Apply in two steps, where derivatives are defined at each step: first t -> a, then a -> b M2 = change_independent_variable(M1, M1.a) |> complete #, D(b) ~ D(a)/a; verbose = true) @variables b(M2.a) - M3 = change_independent_variable(M2, b, Differential(M2.a)(b) ~ exp(-b)) + M3 = change_independent_variable(M2, b, [Differential(M2.a)(b) ~ exp(-b), M2.a ~ exp(b)]) M2 = structural_simplify(M2; allow_symbolic = true) M3 = structural_simplify(M3; allow_symbolic = true) @test length(unknowns(M2)) == 2 && length(unknowns(M3)) == 2 @@ -100,6 +101,12 @@ end @test all(isapprox.(sol[Mx.y], sol[Mx.x - g*(Mx.x/v)^2/2]; atol = 1e-10)) # compare to analytical solution (x(t) = v*t, y(t) = v*t - g*t^2/2) end +@testset "Change independent variable (autonomous system)" begin + M = ODESystem([D(x) ~ t], t; name = :M) |> complete # non-autonomous + @test_throws "t ~ F(x(t)) must be provided" change_independent_variable(M, M.x) + @test_nowarn change_independent_variable(M, M.x, [t ~ 2*x]) +end + @testset "Change independent variable (errors)" begin @variables x(t) y z(y) w(t) v(t) M = ODESystem([D(x) ~ 0, v ~ x], t; name = :M) From 9444d9395ac795042023687822f7a7a878db8be0 Mon Sep 17 00:00:00 2001 From: Herman Sletmoen Date: Mon, 3 Mar 2025 16:48:18 +0100 Subject: [PATCH 1074/1337] Add tutorial for changing independent variable --- docs/pages.jl | 1 + .../tutorials/change_independent_variable.md | 108 ++++++++++++++++++ 2 files changed, 109 insertions(+) create mode 100644 docs/src/tutorials/change_independent_variable.md diff --git a/docs/pages.jl b/docs/pages.jl index f6c49a0de3..829c0d1c08 100644 --- a/docs/pages.jl +++ b/docs/pages.jl @@ -10,6 +10,7 @@ pages = [ "tutorials/stochastic_diffeq.md", "tutorials/discrete_system.md", "tutorials/parameter_identifiability.md", + "tutorials/change_independent_variable.md", "tutorials/bifurcation_diagram_computation.md", "tutorials/SampledData.md", "tutorials/domain_connections.md", diff --git a/docs/src/tutorials/change_independent_variable.md b/docs/src/tutorials/change_independent_variable.md new file mode 100644 index 0000000000..97ca77fef8 --- /dev/null +++ b/docs/src/tutorials/change_independent_variable.md @@ -0,0 +1,108 @@ +# Changing the independent variable of ODEs + +Ordinary differential equations describe the rate of change of some dependent variables with respect to one independent variable. +For the modeler it is often most natural to write down the equations with a particular independent variable, say time $t$. +In many cases there are good reasons for reparametrizing ODEs in terms of a different independent variable: + +1. One may want $y(x)$ as a function of $x$ instead of $(x(t), y(t))$ as a function of $t$ +2. Some differential equations vary more nicely (e.g. less stiff or better behaved) with respect to one independent variable than another. +3. It can reduce the number of equations that must be solved (e.g. $y(x)$ is one equation, while $(x(t), y(t))$ are two). + +To manually change the independent variable of an ODE, one must rewrite all equations in terms of a new variable and transform differentials with the chain rule. +This is mechanical and error-prone. +ModelingToolkit provides the utility function [`change_independent_variable`](@ref) that automates this process. + +## 1. Get one dependent variable as a function of another + +Consider a projectile shot with some initial velocity in a gravitational field. +```@example changeivar +using ModelingToolkit +@independent_variables t +D = Differential(t) +@variables x(t) y(t) +@parameters g = 9.81 v # gravitational acceleration and constant horizontal velocity +M1 = ODESystem([ + D(D(y)) ~ -g, D(x) ~ v # constant horizontal velocity +], t; initialization_eqs = [ + #x ~ 0.0, # TODO: handle? + y ~ 0.0, D(x) ~ D(y) # equal initial horizontal and vertical velocity (45 °) +], name = :M) |> complete +M1s = structural_simplify(M1) +``` +This is the standard parametrization that arises naturally from kinematics and Newton's laws. +It expresses the position $(x(t), y(t))$ as a function of time $t$. +But suppose we want to determine whether the projectile hits a target 10 meters away. +There are at least three ways of answering this: +* Solve the ODE for $(x(t), y(t))$ and use a callback to terminate when $x$ reaches 10 meters, and evaluate $y$ at the final time. +* Solve the ODE for $(x(t), y(t))$ and use root finding to find the time when $x$ reaches 10 meters, and evaluate $y$ at that time. +* Solve the ODE for $y(x)$ and evaluate it at 10 meters. + +We will demonstrate the last method by changing the independent variable from $t$ to $x$. +This transformation is well-defined for any non-zero horizontal velocity $v$. +```@example changeivar +M2 = change_independent_variable(M1, M1.x; dummies = true) |> complete +@assert M2.x isa Num # hide +M2s = structural_simplify(M2; allow_symbolic = true) +``` +The derivatives are now with respect to the new independent variable $x$, which can be accessed with `M2.x`. +Notice how the number of equations has also decreased from three to two, as $\mathrm{d}x/\mathrm{d}t$ has been turned into an observed equation. +It is straightforward to evolve the ODE for 10 meters and plot the resulting trajectory $y(x)$: +```@example changeivar +using OrdinaryDiffEq, Plots +prob = ODEProblem(M2s, [], [0.0, 10.0], [v => 8.0]) # throw 10 meters with x-velocity 8 m/s +sol = solve(prob, Tsit5()) +plot(sol; idxs = M2.y) # must index by M2.y = y(x); not M1.y = y(t)! +``` + +## 2. Reduce stiffness by changing to a logarithmic time axis + +In cosmology, the [Friedmann equations](https://en.wikipedia.org/wiki/Friedmann_equations) describe the expansion of the universe. +In terms of conformal time $t$, they can be written +```@example changeivar +@variables a(t) Ω(t) Ωr(t) Ωm(t) ΩΛ(t) +M1 = ODESystem([ + D(a) ~ √(Ω) * a^2 + Ω ~ Ωr + Ωm + ΩΛ + D(Ωm) ~ -3 * D(a)/a * Ωm + D(Ωr) ~ -4 * D(a)/a * Ωr + D(ΩΛ) ~ 0 +], t; initialization_eqs = [ + ΩΛ + Ωr + Ωm ~ 1 +], name = :M) |> complete +M1s = structural_simplify(M1) +``` +Of course, we can solve this ODE as it is: +```@example changeivar +prob = ODEProblem(M1s, [M1.a => 1.0, M1.Ωr => 5e-5, M1.Ωm => 0.3], (0.0, -3.3), []) +sol = solve(prob, Tsit5(); reltol = 1e-5) +@assert Symbol(sol.retcode) == :Unstable # surrounding text assumes this was unstable # hide +plot(sol, idxs = [M1.a, M1.Ωr/M1.Ω, M1.Ωm/M1.Ω, M1.ΩΛ/M1.Ω]) +``` +The solver becomes unstable due to stiffness. +Also notice the interesting dynamics taking place towards the end of the integration (in the early universe), which gets compressed into a very small time interval. +These ODEs would benefit from being defined with respect to a logarithmic "time" that better captures the evolution of the universe through *orders of magnitude* of time. + +It is therefore common to write these ODEs in terms of $b = \ln a$. +To do this, we will change the independent variable in two stages; from $t$ to $a$ to $b$. +Notice that $\mathrm{d}a/\mathrm{d}t > 0$ provided that $\Omega > 0$, and $\mathrm{d}b/\mathrm{d}a > 0$, so the transformation is well-defined. +First, we transform from $t$ to $a$: +```@example changeivar +M2 = change_independent_variable(M1, M1.a; dummies = true) |> complete +``` +Unlike the original, notice that this system is *non-autonomous* because the independent variable $a$ appears explicitly in the equations! +This means that to change the independent variable from $a$ to $b$, we must provide not only the rate of change relation $db(a)/da = \exp(-b)$, but *also* the equation $a(b) = \exp(b)$ so $a$ can be eliminated in favor of $b$: +```@example changeivar +a = M2.a +Da = Differential(a) +@variables b(a) +M3 = change_independent_variable(M2, b, [Da(b) ~ exp(-b), a ~ exp(b)]) |> complete +``` +We can now solve and plot the ODE in terms of $b$: +```@example changeivar +M3s = structural_simplify(M3; allow_symbolic = true) +prob = ODEProblem(M3s, [M3.Ωr => 5e-5, M3.Ωm => 0.3], (0, -15), []) +sol = solve(prob, Tsit5()) +@assert Symbol(sol.retcode) == :Success # surrounding text assumes the solution was successful # hide +plot(sol, idxs = [M3.Ωr/M3.Ω, M3.Ωm/M3.Ω, M3.ΩΛ/M3.Ω]) +``` +Notice that the variables vary "more nicely" with respect to $b$ than $t$, making the solver happier and avoiding numerical problems. From 98318864dbfa9fc0ac0a4721ebb25433bd3524b8 Mon Sep 17 00:00:00 2001 From: Herman Sletmoen Date: Mon, 3 Mar 2025 21:31:23 +0100 Subject: [PATCH 1075/1337] Reorder things and make universal function that transforms all ODESystem fields --- .../tutorials/change_independent_variable.md | 6 +- src/systems/diffeqs/basic_transformations.jl | 120 +++++++++++------- test/basic_transformations.jl | 4 +- 3 files changed, 77 insertions(+), 53 deletions(-) diff --git a/docs/src/tutorials/change_independent_variable.md b/docs/src/tutorials/change_independent_variable.md index 97ca77fef8..6274465264 100644 --- a/docs/src/tutorials/change_independent_variable.md +++ b/docs/src/tutorials/change_independent_variable.md @@ -23,9 +23,11 @@ D = Differential(t) @parameters g = 9.81 v # gravitational acceleration and constant horizontal velocity M1 = ODESystem([ D(D(y)) ~ -g, D(x) ~ v # constant horizontal velocity -], t; initialization_eqs = [ +], t; defaults = [ + y => 0.0 +], initialization_eqs = [ #x ~ 0.0, # TODO: handle? - y ~ 0.0, D(x) ~ D(y) # equal initial horizontal and vertical velocity (45 °) + D(x) ~ D(y) # equal initial horizontal and vertical velocity (45 °) ], name = :M) |> complete M1s = structural_simplify(M1) ``` diff --git a/src/systems/diffeqs/basic_transformations.jl b/src/systems/diffeqs/basic_transformations.jl index 56d3ada089..396dbd51f5 100644 --- a/src/systems/diffeqs/basic_transformations.jl +++ b/src/systems/diffeqs/basic_transformations.jl @@ -110,20 +110,59 @@ function change_independent_variable(sys::AbstractODESystem, iv, eqs = []; dummi iv2name = nameof(operation(iv)) iv2, = @independent_variables $iv2name # e.g. a D1 = Differential(iv1) - D2 = Differential(iv2) iv1name = nameof(iv1) # e.g. t iv1func, = @variables $iv1name(iv2) # e.g. t(a) - div2name = Symbol(iv2name, :_t) - div2, = @variables $div2name(iv2) # e.g. a_t(a) + eqs = [get_eqs(sys); eqs] # copies system equations to avoid modifying original system - ddiv2name = Symbol(iv2name, :_tt) - ddiv2, = @variables $ddiv2name(iv2) # e.g. a_tt(a) + # 1) Find and compute all necessary expressions for e.g. df/dt, d²f/dt², ... + # 1.1) Find the 1st order derivative of the new independent variable (e.g. da(t)/dt = ...), ... + div2_div1_idxs = findall(eq -> isequal(eq.lhs, D1(iv2func)), eqs) # index of e.g. da/dt = ... + if length(div2_div1_idxs) != 1 + error("Exactly one equation for $D1($iv2func) was not specified.") + end + div2_div1_eq = popat!(eqs, only(div2_div1_idxs)) # get and remove e.g. df/dt = ... (may be added back later) + div2_div1 = div2_div1_eq.rhs + if isequal(div2_div1, 0) + error("Cannot change independent variable from $iv1 to $iv2 with singular transformation $div2_div1_eq.") + end + # 1.2) ... then compute the 2nd order derivative of the new independent variable + div1_div2 = 1 / div2_div1 # TODO: URL reference for clarity + ddiv2_ddiv1 = expand_derivatives(-Differential(iv2func)(div1_div2) / div1_div2^3, simplify) # e.g. https://math.stackexchange.com/questions/249253/second-derivative-of-the-inverse-function # TODO: higher orders # TODO: pass simplify here + # 1.3) # TODO: handle higher orders (3+) derivatives ... + + # 2) If requested, insert extra dummy equations for e.g. df/dt, d²f/dt², ... + # Otherwise, replace all these derivatives by their explicit expressions + if dummies + div2name = Symbol(iv2name, :_t) # TODO: not always t + div2, = @variables $div2name(iv2) # e.g. a_t(a) + ddiv2name = Symbol(iv2name, :_tt) # TODO: not always t + ddiv2, = @variables $ddiv2name(iv2) # e.g. a_tt(a) + eqs = [eqs; [div2 ~ div2_div1, ddiv2 ~ ddiv2_ddiv1]] # add dummy equations + derivsubs = [D1(D1(iv2func)) => ddiv2, D1(iv2func) => div2] # order is crucial! + else + derivsubs = [D1(D1(iv2func)) => ddiv2_ddiv1, D1(iv2func) => div2_div1] # order is crucial! + end + derivsubs = [derivsubs; [iv2func => iv2, iv1 => iv1func]] + + if verbose + # Explain what we just did + println("Order 1 (found): $div2_div1_eq") + println("Order 2 (computed): $(D1(div2_div1_eq.lhs) ~ ddiv2_ddiv1)") + println() + println("Substitutions will be made in this order:") + for (n, sub) in enumerate(derivsubs) + println("$n: $(sub[1]) => $(sub[2])") + end + println() + end + # 3) Define a transformation function that performs the change of variable on any expression/equation function transform(ex) - # 1) Substitute f(t₁) => f(t₂(t₁)) in all variables - verbose && println("1. ", ex) + verbose && println("Step 0: ", ex) + + # Step 1: substitute f(t₁) => f(t₂(t₁)) in all variables in the expression vars = Symbolics.get_variables(ex) for var1 in vars if Symbolics.iscall(var1) && !isequal(var1, iv2func) # && isequal(only(arguments(var1)), iv1) # skip e.g. constants @@ -132,53 +171,36 @@ function change_independent_variable(sys::AbstractODESystem, iv, eqs = []; dummi ex = substitute(ex, var1 => var2; fold = false) end end + verbose && println("Step 1: ", ex) - # 2) Substitute in dummy variables for dⁿt₂/dt₁ⁿ - verbose && println("2. ", ex) + # Step 2: expand out all chain rule derivatives ex = expand_derivatives(ex) # expand out with chain rule to get d(iv2)/d(iv1) - verbose && println("3. ", ex) - ex = substitute(ex, D1(D1(iv2func)) => ddiv2) # order 2 # TODO: more orders - ex = substitute(ex, D1(iv2func) => div2) # order 1; e.g. D(a(t)) => a_t(t) - ex = substitute(ex, iv2func => iv2) # order 0; make iv2 independent - verbose && println("4. ", ex) - ex = substitute(ex, iv1 => iv1func) # if autonomous - verbose && println("5. ", ex) + verbose && println("Step 2: ", ex) + + # Step 3: substitute d²f/dt², df/dt, ... (to dummy variables or explicit expressions, depending on dummies) + for sub in derivsubs + ex = substitute(ex, sub) + end + verbose && println("Step 3: ", ex) + verbose && println() + return ex end - eqs = [get_eqs(sys); eqs] + # 4) Transform all fields eqs = map(transform, eqs) - + observed = map(transform, get_observed(sys)) initialization_eqs = map(transform, get_initialization_eqs(sys)) - - div2_div1_idxs = findall(eq -> isequal(eq.lhs, div2), eqs) - if length(div2_div1_idxs) == 0 - error("No equation for $D1($iv2func) was specified.") - elseif length(div2_div1_idxs) >= 2 - error("Multiple equations for $D1($iv2func) were specified.") - end - div2_div1 = eqs[only(div2_div1_idxs)].rhs - if isequal(div2_div1, 0) - error("Cannot change independent variable from $iv1 to $iv2 with singular transformation $(div2 ~ div2_div1).") - end - - # 3) Add equations for dummy variables - div1_div2 = 1 / div2_div1 - ddiv1_ddiv2 = expand_derivatives(-D2(div1_div2) / div1_div2^3) - if simplify - ddiv1_ddiv2 = Symbolics.simplify(ddiv1_ddiv2) - end - push!(eqs, ddiv2 ~ ddiv1_ddiv2) # e.g. https://math.stackexchange.com/questions/249253/second-derivative-of-the-inverse-function # TODO: higher orders - - # 4) If requested, instead remove and insert dummy equations - if !dummies # TODO: make dummies = false work with more fields! - dummyidxs = findall(eq -> isequal(eq.lhs, div2) || isequal(eq.lhs, ddiv2), eqs) - dummyeqs = splice!(eqs, dummyidxs) # return and remove dummy equations - dummysubs = Dict(eq.lhs => eq.rhs for eq in dummyeqs) - eqs = substitute.(eqs, Ref(dummysubs)) # don't iterate over dummysubs - end - - # 5) Recreate system with new equations - sys2 = typeof(sys)(eqs, iv2; initialization_eqs, name = nameof(sys), description = description(sys), kwargs...) - return sys2 + parameter_dependencies = map(transform, get_parameter_dependencies(sys)) + defaults = Dict(transform(var) => transform(val) for (var, val) in get_defaults(sys)) + guesses = Dict(transform(var) => transform(val) for (var, val) in get_guesses(sys)) + assertions = Dict(transform(condition) => msg for (condition, msg) in get_assertions(sys)) + # TODO: handle subsystems + + # 5) Recreate system with transformed fields + return typeof(sys)( + eqs, iv2; + observed, initialization_eqs, parameter_dependencies, defaults, guesses, assertions, + name = nameof(sys), description = description(sys), kwargs... + ) |> complete # original system had to be complete end diff --git a/test/basic_transformations.jl b/test/basic_transformations.jl index dc9fa21981..99a252b5b8 100644 --- a/test/basic_transformations.jl +++ b/test/basic_transformations.jl @@ -114,8 +114,8 @@ end M = complete(M) @test_throws "singular" change_independent_variable(M, M.x) @test_throws "structurally simplified" change_independent_variable(structural_simplify(M), y) - @test_throws "No equation" change_independent_variable(M, w) - @test_throws "No equation" change_independent_variable(M, v) + @test_throws "not specified" change_independent_variable(M, w) + @test_throws "not specified" change_independent_variable(M, v) @test_throws "not a function of the independent variable" change_independent_variable(M, y) @test_throws "not a function of the independent variable" change_independent_variable(M, z) M = compose(M, M) From 1ac7c7d3c221b115e58ce4ed52d010926da22b90 Mon Sep 17 00:00:00 2001 From: Herman Sletmoen Date: Tue, 4 Mar 2025 22:37:37 +0100 Subject: [PATCH 1076/1337] Clean up independent variable change implementation --- .../tutorials/change_independent_variable.md | 2 +- src/systems/diffeqs/basic_transformations.jl | 167 ++++++++---------- test/basic_transformations.jl | 55 ++++-- 3 files changed, 115 insertions(+), 109 deletions(-) diff --git a/docs/src/tutorials/change_independent_variable.md b/docs/src/tutorials/change_independent_variable.md index 6274465264..0d4d8bcac8 100644 --- a/docs/src/tutorials/change_independent_variable.md +++ b/docs/src/tutorials/change_independent_variable.md @@ -26,7 +26,7 @@ M1 = ODESystem([ ], t; defaults = [ y => 0.0 ], initialization_eqs = [ - #x ~ 0.0, # TODO: handle? + #x ~ 0.0, # TODO: handle? # hide D(x) ~ D(y) # equal initial horizontal and vertical velocity (45 °) ], name = :M) |> complete M1s = structural_simplify(M1) diff --git a/src/systems/diffeqs/basic_transformations.jl b/src/systems/diffeqs/basic_transformations.jl index 396dbd51f5..11a5514f42 100644 --- a/src/systems/diffeqs/basic_transformations.jl +++ b/src/systems/diffeqs/basic_transformations.jl @@ -51,32 +51,37 @@ function liouville_transform(sys::AbstractODESystem; kwargs...) end """ - function change_independent_variable(sys::AbstractODESystem, iv, eqs = []; dummies = false, simplify = true, verbose = false, kwargs...) + change_independent_variable(sys::AbstractODESystem, iv, eqs = []; dummies = false, simplify = true, fold = false, kwargs...) -Transform the independent variable (e.g. ``t``) of the ODE system `sys` to a dependent variable `iv` (e.g. ``f(t)``). -An equation in `sys` must define the rate of change of the new independent variable (e.g. ``df(t)/dt``). -Alternatively, `eq` can specify such an equation. +Transform the independent variable (e.g. ``t``) of the ODE system `sys` to a dependent variable `iv` (e.g. ``u(t)``). +An equation in `sys` must define the rate of change of the new independent variable (e.g. ``du(t)/dt``). +This or other additional equations can also be specified through `eqs`. The transformation is well-defined when the mapping between the new and old independent variables are one-to-one. -This is satisfied if one is a strictly increasing function of the other (e.g. ``df(t)/dt > 0`` or ``df(t)/dt < 0``). +This is satisfied if one is a strictly increasing function of the other (e.g. ``du(t)/dt > 0`` or ``du(t)/dt < 0``). Keyword arguments ================= -If `dummies`, derivatives of the new independent variable are expressed through dummy equations; otherwise they are explicitly inserted into the equations. +If `dummies`, derivatives of the new independent variable with respect to the old one are expressed through dummy equations; otherwise they are explicitly inserted into the equations. If `simplify`, these dummy expressions are simplified and often give a tidier transformation. -If `verbose`, the function prints intermediate transformations of equations to aid debugging. -Any additional keyword arguments `kwargs...` are forwarded to the constructor that rebuilds the system. +If `fold`, internal substitutions will evaluate numerical expressions. +Additional keyword arguments `kwargs...` are forwarded to the constructor that rebuilds `sys`. Usage before structural simplification ====================================== The variable change must take place before structural simplification. Subsequently, consider passing `allow_symbolic = true` to `structural_simplify(sys)` to reduce the number of unknowns, with the understanding that the transformation is well-defined. +Usage with non-autonomous systems +================================= +If `sys` is autonomous (i.e. ``t`` appears explicitly in its equations), it is often desirable to also pass an algebraic equation relating the new and old independent variables (e.g. ``t = f(u(t))``). +Otherwise the transformed system will be underdetermined and cannot be structurally simplified without additional changes. + Example ======= Consider a free fall with constant horizontal velocity. -The laws of physics naturally describes position as a function of time. -By changing the independent variable, it can be reformulated for vertical position as a function of horizontal distance: +Physics naturally describes position as a function of time. +By changing the independent variable, it can be reformulated for vertical position as a function of horizontal position: ```julia julia> @variables x(t) y(t); @@ -89,118 +94,94 @@ julia> unknowns(M′) y(x) ``` """ -function change_independent_variable(sys::AbstractODESystem, iv, eqs = []; dummies = false, simplify = true, verbose = false, kwargs...) +function change_independent_variable(sys::AbstractODESystem, iv, eqs = []; dummies = false, simplify = true, fold = false, kwargs...) + iv2_of_iv1 = unwrap(iv) # e.g. u(t) + iv1 = get_iv(sys) # e.g. t + if !iscomplete(sys) - error("Cannot change independent variable of incomplete system $(nameof(sys))") + error("System $(nameof(sys)) is incomplete. Complete it first!") elseif isscheduled(sys) - error("Cannot change independent variable of structurally simplified system $(nameof(sys))") + error("System $(nameof(sys)) is structurally simplified. Change independent variable before structural simplification!") elseif !isempty(get_systems(sys)) - error("Cannot change independent variable of hierarchical system $(nameof(sys)). Flatten it first.") # TODO: implement + error("System $(nameof(sys)) is hierarchical. Flatten it first!") # TODO: implement and allow? + elseif !iscall(iv2_of_iv1) || !isequal(only(arguments(iv2_of_iv1)), iv1) + error("Variable $iv is not a function of the independent variable $iv1 of the system $(nameof(sys))!") end - iv = unwrap(iv) - iv1 = get_iv(sys) # e.g. t - if !iscall(iv) || !isequal(only(arguments(iv)), iv1) - error("New independent variable $iv is not a function of the independent variable $iv1 of the system $(nameof(sys))") - elseif !isautonomous(sys) && isempty(findall(eq -> isequal(eq.lhs, iv1), eqs)) - error("System $(nameof(sys)) is autonomous in $iv1. An equation of the form $iv1 ~ F($iv) must be provided.") + iv1name = nameof(iv1) # e.g. :t + iv2name = nameof(operation(iv2_of_iv1)) # e.g. :u + iv2, = @independent_variables $iv2name # e.g. u + iv1_of_iv2, = @variables $iv1name(iv2) # inverse in case sys is autonomous; e.g. t(u) + D1 = Differential(iv1) # e.g. d/d(t) + D2 = Differential(iv2_of_iv1) # e.g. d/d(u(t)) + + # 1) Utility that performs the chain rule on an expression, e.g. (d/dt)(f(t)) -> (d/dt)(f(u(t))) -> df(u(t))/du(t) * du(t)/dt + function chain_rule(ex) + vars = get_variables(ex) + for var_of_iv1 in vars # loop through e.g. f(t) + if iscall(var_of_iv1) && !isequal(var_of_iv1, iv2_of_iv1) # handle e.g. f(t) -> f(u(t)), but not u(t) -> u(u(t)) + varname = nameof(operation(var_of_iv1)) # e.g. :f + var_of_iv2, = @variables $varname(iv2_of_iv1) # e.g. f(u(t)) + ex = substitute(ex, var_of_iv1 => var_of_iv2; fold) # e.g. f(t) -> f(u(t)) + end + end + ex = expand_derivatives(ex, simplify) # expand chain rule, e.g. (d/dt)(f(u(t)))) -> df(u(t))/du(t) * du(t)/dt + return ex end - iv2func = iv # e.g. a(t) - iv2name = nameof(operation(iv)) - iv2, = @independent_variables $iv2name # e.g. a - D1 = Differential(iv1) - - iv1name = nameof(iv1) # e.g. t - iv1func, = @variables $iv1name(iv2) # e.g. t(a) - - eqs = [get_eqs(sys); eqs] # copies system equations to avoid modifying original system - - # 1) Find and compute all necessary expressions for e.g. df/dt, d²f/dt², ... - # 1.1) Find the 1st order derivative of the new independent variable (e.g. da(t)/dt = ...), ... - div2_div1_idxs = findall(eq -> isequal(eq.lhs, D1(iv2func)), eqs) # index of e.g. da/dt = ... - if length(div2_div1_idxs) != 1 - error("Exactly one equation for $D1($iv2func) was not specified.") + # 2) Find e.g. du/dt in equations, then calculate e.g. d²u/dt², ... + eqs = [get_eqs(sys); eqs] # all equations (system-defined + user-provided) we may use + idxs = findall(eq -> isequal(eq.lhs, D1(iv2_of_iv1)), eqs) + if length(idxs) != 1 + error("Exactly one equation for $D1($iv2_of_iv1) was not specified!") end - div2_div1_eq = popat!(eqs, only(div2_div1_idxs)) # get and remove e.g. df/dt = ... (may be added back later) - div2_div1 = div2_div1_eq.rhs - if isequal(div2_div1, 0) - error("Cannot change independent variable from $iv1 to $iv2 with singular transformation $div2_div1_eq.") + div2_of_iv1_eq = popat!(eqs, only(idxs)) # get and remove e.g. du/dt = ... (may be added back later as a dummy) + div2_of_iv1 = chain_rule(div2_of_iv1_eq.rhs) + if isequal(div2_of_iv1, 0) # e.g. du/dt ~ 0 + error("Independent variable transformation $(div2_of_iv1_eq) is singular!") end - # 1.2) ... then compute the 2nd order derivative of the new independent variable - div1_div2 = 1 / div2_div1 # TODO: URL reference for clarity - ddiv2_ddiv1 = expand_derivatives(-Differential(iv2func)(div1_div2) / div1_div2^3, simplify) # e.g. https://math.stackexchange.com/questions/249253/second-derivative-of-the-inverse-function # TODO: higher orders # TODO: pass simplify here - # 1.3) # TODO: handle higher orders (3+) derivatives ... + ddiv2_of_iv1 = chain_rule(D1(div2_of_iv1)) # TODO: implement higher orders (order >= 3) derivatives with a loop - # 2) If requested, insert extra dummy equations for e.g. df/dt, d²f/dt², ... + # 3) If requested, insert extra dummy equations for e.g. du/dt, d²u/dt², ... # Otherwise, replace all these derivatives by their explicit expressions if dummies - div2name = Symbol(iv2name, :_t) # TODO: not always t - div2, = @variables $div2name(iv2) # e.g. a_t(a) - ddiv2name = Symbol(iv2name, :_tt) # TODO: not always t - ddiv2, = @variables $ddiv2name(iv2) # e.g. a_tt(a) - eqs = [eqs; [div2 ~ div2_div1, ddiv2 ~ ddiv2_ddiv1]] # add dummy equations - derivsubs = [D1(D1(iv2func)) => ddiv2, D1(iv2func) => div2] # order is crucial! + div2name = Symbol(iv2name, :_, iv1name) # e.g. :u_t # TODO: customize + ddiv2name = Symbol(div2name, iv1name) # e.g. :u_tt # TODO: customize + div2, ddiv2 = @variables $div2name(iv2) $ddiv2name(iv2) # e.g. u_t(u), u_tt(u) + eqs = [eqs; [div2 ~ div2_of_iv1, ddiv2 ~ ddiv2_of_iv1]] # add dummy equations else - derivsubs = [D1(D1(iv2func)) => ddiv2_ddiv1, D1(iv2func) => div2_div1] # order is crucial! - end - derivsubs = [derivsubs; [iv2func => iv2, iv1 => iv1func]] - - if verbose - # Explain what we just did - println("Order 1 (found): $div2_div1_eq") - println("Order 2 (computed): $(D1(div2_div1_eq.lhs) ~ ddiv2_ddiv1)") - println() - println("Substitutions will be made in this order:") - for (n, sub) in enumerate(derivsubs) - println("$n: $(sub[1]) => $(sub[2])") - end - println() + div2 = div2_of_iv1 + ddiv2 = ddiv2_of_iv1 end - # 3) Define a transformation function that performs the change of variable on any expression/equation + # 4) Transform everything from old to new independent variable, e.g. t -> u. + # Substitution order matters! Must begin with highest order to get D(D(u(t))) -> u_tt(u). + # If we had started with the lowest order, we would get D(D(u(t))) -> D(u_t(u)) -> 0! + iv1_to_iv2_subs = [ # a vector ensures substitution order + D1(D1(iv2_of_iv1)) => ddiv2 # order 2, e.g. D(D(u(t))) -> u_tt(u) + D1(iv2_of_iv1) => div2 # order 1, e.g. D(u(t)) -> u_t(u) + iv2_of_iv1 => iv2 # order 0, e.g. u(t) -> u + iv1 => iv1_of_iv2 # in case sys was autonomous, e.g. t -> t(u) + ] function transform(ex) - verbose && println("Step 0: ", ex) - - # Step 1: substitute f(t₁) => f(t₂(t₁)) in all variables in the expression - vars = Symbolics.get_variables(ex) - for var1 in vars - if Symbolics.iscall(var1) && !isequal(var1, iv2func) # && isequal(only(arguments(var1)), iv1) # skip e.g. constants - name = nameof(operation(var1)) - var2, = @variables $name(iv2func) - ex = substitute(ex, var1 => var2; fold = false) - end + ex = chain_rule(ex) + for sub in iv1_to_iv2_subs + ex = substitute(ex, sub; fold) end - verbose && println("Step 1: ", ex) - - # Step 2: expand out all chain rule derivatives - ex = expand_derivatives(ex) # expand out with chain rule to get d(iv2)/d(iv1) - verbose && println("Step 2: ", ex) - - # Step 3: substitute d²f/dt², df/dt, ... (to dummy variables or explicit expressions, depending on dummies) - for sub in derivsubs - ex = substitute(ex, sub) - end - verbose && println("Step 3: ", ex) - verbose && println() - return ex end - - # 4) Transform all fields - eqs = map(transform, eqs) + eqs = map(transform, eqs) # we derived and added equations to eqs; they are not in get_eqs(sys)! observed = map(transform, get_observed(sys)) initialization_eqs = map(transform, get_initialization_eqs(sys)) parameter_dependencies = map(transform, get_parameter_dependencies(sys)) defaults = Dict(transform(var) => transform(val) for (var, val) in get_defaults(sys)) guesses = Dict(transform(var) => transform(val) for (var, val) in get_guesses(sys)) assertions = Dict(transform(condition) => msg for (condition, msg) in get_assertions(sys)) - # TODO: handle subsystems # 5) Recreate system with transformed fields return typeof(sys)( eqs, iv2; observed, initialization_eqs, parameter_dependencies, defaults, guesses, assertions, name = nameof(sys), description = description(sys), kwargs... - ) |> complete # original system had to be complete + ) |> complete # input system must be complete, so complete the output system end diff --git a/test/basic_transformations.jl b/test/basic_transformations.jl index 99a252b5b8..cfb3bf5db6 100644 --- a/test/basic_transformations.jl +++ b/test/basic_transformations.jl @@ -59,34 +59,44 @@ end end @testset "Change independent variable (Friedmann equation)" begin + @independent_variables t D = Differential(t) - @variables a(t) ȧ(t) ρr(t) ρm(t) ρΛ(t) ρ(t) P(t) ϕ(t) - @parameters Ωr0 Ωm0 ΩΛ0 + @variables a(t) ȧ(t) Ω(t) ϕ(t) eqs = [ - ρr ~ 3/(8*Num(π)) * Ωr0 / a^4 - ρm ~ 3/(8*Num(π)) * Ωm0 / a^3 - ρΛ ~ 3/(8*Num(π)) * ΩΛ0 - ρ ~ ρr + ρm + ρΛ - ȧ ~ √(8*Num(π)/3*ρ*a^4) + Ω ~ 123 D(a) ~ ȧ + ȧ ~ √(Ω) * a^2 D(D(ϕ)) ~ -3*D(a)/a*D(ϕ) ] M1 = ODESystem(eqs, t; name = :M) |> complete # Apply in two steps, where derivatives are defined at each step: first t -> a, then a -> b - M2 = change_independent_variable(M1, M1.a) |> complete #, D(b) ~ D(a)/a; verbose = true) + M2 = change_independent_variable(M1, M1.a; dummies = true) + @independent_variables a + @variables ȧ(a) Ω(a) ϕ(a) a_t(a) a_tt(a) + Da = Differential(a) + @test Set(equations(M2)) == Set([ + a_tt*Da(ϕ) + a_t^2*(Da^2)(ϕ) ~ -3*a_t^2/a*Da(ϕ) + ȧ ~ √(Ω) * a^2 + Ω ~ 123 + a_t ~ ȧ # 1st order dummy equation + a_tt ~ Da(ȧ) * a_t # 2nd order dummy equation + ]) + @variables b(M2.a) M3 = change_independent_variable(M2, b, [Differential(M2.a)(b) ~ exp(-b), M2.a ~ exp(b)]) + + M1 = structural_simplify(M1) M2 = structural_simplify(M2; allow_symbolic = true) M3 = structural_simplify(M3; allow_symbolic = true) - @test length(unknowns(M2)) == 2 && length(unknowns(M3)) == 2 + @test length(unknowns(M3)) == length(unknowns(M2)) == length(unknowns(M1)) - 1 end @testset "Change independent variable (simple)" begin @variables x(t) - Mt = ODESystem([D(x) ~ 2*x], t; name = :M) |> complete # TODO: avoid complete. can avoid it if passing defined $variable directly to change_independent_variable + Mt = ODESystem([D(x) ~ 2*x], t; name = :M) |> complete Mx = change_independent_variable(Mt, Mt.x; dummies = true) - @test (@variables x x_t(x) x_tt(x); Set(equations(Mx)) == Set([x_t ~ 2x, x_tt ~ 4x])) + @test (@variables x x_t(x) x_tt(x); Set(equations(Mx)) == Set([x_t ~ 2*x, x_tt ~ 2*x_t])) end @testset "Change independent variable (free fall)" begin @@ -101,10 +111,25 @@ end @test all(isapprox.(sol[Mx.y], sol[Mx.x - g*(Mx.x/v)^2/2]; atol = 1e-10)) # compare to analytical solution (x(t) = v*t, y(t) = v*t - g*t^2/2) end -@testset "Change independent variable (autonomous system)" begin - M = ODESystem([D(x) ~ t], t; name = :M) |> complete # non-autonomous - @test_throws "t ~ F(x(t)) must be provided" change_independent_variable(M, M.x) - @test_nowarn change_independent_variable(M, M.x, [t ~ 2*x]) +@testset "Change independent variable (crazy analytical example)" begin + @independent_variables t + D = Differential(t) + @variables x(t) y(t) + M1 = ODESystem([ # crazy non-autonomous non-linear 2nd order ODE + D(D(y)) ~ D(x)^2 + D(y^3) |> expand_derivatives # expand D(y^3) # TODO: make this test 3rd order + D(x) ~ x^4 + y^5 + t^6 + ], t; name = :M) |> complete + M2 = change_independent_variable(M1, M1.x; dummies = true) + + # Compare to pen-and-paper result + @independent_variables x + Dx = Differential(x) + @variables x_t(x) x_tt(x) y(x) t(x) + @test Set(equations(M2)) == Set([ + x_t^2*(Dx^2)(y) + x_tt*Dx(y) ~ x_t^2 + 3*y^2*Dx(y)*x_t # from D(D(y)) + x_t ~ x^4 + y^5 + t^6 # 1st order dummy equation + x_tt ~ 4*x^3*x_t + 5*y^4*Dx(y)*x_t + 6*t^5 # 2nd order dummy equation + ]) end @testset "Change independent variable (errors)" begin From 34a6a4ee655c68b93904b7cf04ca3e9a3bab28b7 Mon Sep 17 00:00:00 2001 From: Herman Sletmoen Date: Wed, 5 Mar 2025 12:25:06 +0100 Subject: [PATCH 1077/1337] Actually run basic transformations test --- test/runtests.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/test/runtests.jl b/test/runtests.jl index 0ae66d3755..8f1f2a0be4 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -46,6 +46,7 @@ end @safetestset "Model Parsing Test" include("model_parsing.jl") @safetestset "Error Handling" include("error_handling.jl") @safetestset "StructuralTransformations" include("structural_transformation/runtests.jl") + @safetestset "Basic transformations" include("basic_transformations.jl") @safetestset "State Selection Test" include("state_selection.jl") @safetestset "Symbolic Event Test" include("symbolic_events.jl") @safetestset "Stream Connect Test" include("stream_connectors.jl") From 636ad045cebbf88f6755c9bd4b481ed9ee5c748b Mon Sep 17 00:00:00 2001 From: Herman Sletmoen Date: Wed, 5 Mar 2025 21:41:05 +0100 Subject: [PATCH 1078/1337] Change independent variable of hierarchical systems --- .../tutorials/change_independent_variable.md | 43 ++++++++----- src/systems/diffeqs/basic_transformations.jl | 63 ++++++++++++------- test/basic_transformations.jl | 25 +++++--- 3 files changed, 83 insertions(+), 48 deletions(-) diff --git a/docs/src/tutorials/change_independent_variable.md b/docs/src/tutorials/change_independent_variable.md index 0d4d8bcac8..e49ededacd 100644 --- a/docs/src/tutorials/change_independent_variable.md +++ b/docs/src/tutorials/change_independent_variable.md @@ -42,7 +42,7 @@ There are at least three ways of answering this: We will demonstrate the last method by changing the independent variable from $t$ to $x$. This transformation is well-defined for any non-zero horizontal velocity $v$. ```@example changeivar -M2 = change_independent_variable(M1, M1.x; dummies = true) |> complete +M2 = change_independent_variable(M1, M1.x; dummies = true) @assert M2.x isa Num # hide M2s = structural_simplify(M2; allow_symbolic = true) ``` @@ -56,29 +56,38 @@ sol = solve(prob, Tsit5()) plot(sol; idxs = M2.y) # must index by M2.y = y(x); not M1.y = y(t)! ``` -## 2. Reduce stiffness by changing to a logarithmic time axis +## 2. Alleviating stiffness by changing to logarithmic time In cosmology, the [Friedmann equations](https://en.wikipedia.org/wiki/Friedmann_equations) describe the expansion of the universe. In terms of conformal time $t$, they can be written ```@example changeivar -@variables a(t) Ω(t) Ωr(t) Ωm(t) ΩΛ(t) -M1 = ODESystem([ +@variables a(t) Ω(t) +a = GlobalScope(a) # global var needed by all species +function species(w; kw...) + eqs = [D(Ω) ~ -3(1 + w) * D(a)/a * Ω] + return ODESystem(eqs, t, [Ω], []; kw...) +end +@named r = species(1//3) # radiation +@named m = species(0) # matter +@named Λ = species(-1) # dark energy / cosmological constant +eqs = [ + Ω ~ r.Ω + m.Ω + Λ.Ω # total energy density D(a) ~ √(Ω) * a^2 - Ω ~ Ωr + Ωm + ΩΛ - D(Ωm) ~ -3 * D(a)/a * Ωm - D(Ωr) ~ -4 * D(a)/a * Ωr - D(ΩΛ) ~ 0 -], t; initialization_eqs = [ - ΩΛ + Ωr + Ωm ~ 1 -], name = :M) |> complete +] +initialization_eqs = [ + Λ.Ω + r.Ω + m.Ω ~ 1 +] +M1 = ODESystem(eqs, t, [Ω, a], []; initialization_eqs, name = :M) +M1 = compose(M1, r, m, Λ) +M1 = complete(M1; flatten = false) M1s = structural_simplify(M1) ``` Of course, we can solve this ODE as it is: ```@example changeivar -prob = ODEProblem(M1s, [M1.a => 1.0, M1.Ωr => 5e-5, M1.Ωm => 0.3], (0.0, -3.3), []) +prob = ODEProblem(M1s, [M1.a => 1.0, M1.r.Ω => 5e-5, M1.m.Ω => 0.3], (0.0, -3.3), []) sol = solve(prob, Tsit5(); reltol = 1e-5) @assert Symbol(sol.retcode) == :Unstable # surrounding text assumes this was unstable # hide -plot(sol, idxs = [M1.a, M1.Ωr/M1.Ω, M1.Ωm/M1.Ω, M1.ΩΛ/M1.Ω]) +plot(sol, idxs = [M1.a, M1.r.Ω/M1.Ω, M1.m.Ω/M1.Ω, M1.Λ.Ω/M1.Ω]) ``` The solver becomes unstable due to stiffness. Also notice the interesting dynamics taking place towards the end of the integration (in the early universe), which gets compressed into a very small time interval. @@ -89,7 +98,7 @@ To do this, we will change the independent variable in two stages; from $t$ to $ Notice that $\mathrm{d}a/\mathrm{d}t > 0$ provided that $\Omega > 0$, and $\mathrm{d}b/\mathrm{d}a > 0$, so the transformation is well-defined. First, we transform from $t$ to $a$: ```@example changeivar -M2 = change_independent_variable(M1, M1.a; dummies = true) |> complete +M2 = change_independent_variable(M1, M1.a; dummies = true) ``` Unlike the original, notice that this system is *non-autonomous* because the independent variable $a$ appears explicitly in the equations! This means that to change the independent variable from $a$ to $b$, we must provide not only the rate of change relation $db(a)/da = \exp(-b)$, but *also* the equation $a(b) = \exp(b)$ so $a$ can be eliminated in favor of $b$: @@ -97,14 +106,14 @@ This means that to change the independent variable from $a$ to $b$, we must prov a = M2.a Da = Differential(a) @variables b(a) -M3 = change_independent_variable(M2, b, [Da(b) ~ exp(-b), a ~ exp(b)]) |> complete +M3 = change_independent_variable(M2, b, [Da(b) ~ exp(-b), a ~ exp(b)]) ``` We can now solve and plot the ODE in terms of $b$: ```@example changeivar M3s = structural_simplify(M3; allow_symbolic = true) -prob = ODEProblem(M3s, [M3.Ωr => 5e-5, M3.Ωm => 0.3], (0, -15), []) +prob = ODEProblem(M3s, [M3.r.Ω => 5e-5, M3.m.Ω => 0.3], (0, -15), []) sol = solve(prob, Tsit5()) @assert Symbol(sol.retcode) == :Success # surrounding text assumes the solution was successful # hide -plot(sol, idxs = [M3.Ωr/M3.Ω, M3.Ωm/M3.Ω, M3.ΩΛ/M3.Ω]) +plot(sol, idxs = [M3.r.Ω/M3.Ω, M3.m.Ω/M3.Ω, M3.Λ.Ω/M3.Ω]) ``` Notice that the variables vary "more nicely" with respect to $b$ than $t$, making the solver happier and avoiding numerical problems. diff --git a/src/systems/diffeqs/basic_transformations.jl b/src/systems/diffeqs/basic_transformations.jl index 11a5514f42..a8f3db11e4 100644 --- a/src/systems/diffeqs/basic_transformations.jl +++ b/src/systems/diffeqs/basic_transformations.jl @@ -77,6 +77,11 @@ Usage with non-autonomous systems If `sys` is autonomous (i.e. ``t`` appears explicitly in its equations), it is often desirable to also pass an algebraic equation relating the new and old independent variables (e.g. ``t = f(u(t))``). Otherwise the transformed system will be underdetermined and cannot be structurally simplified without additional changes. +Usage with hierarchical systems +=============================== +It is recommended that `iv` is a non-namespaced variable in `sys`. +This means it can belong to the top-level system or be a variable in a subsystem declared with `GlobalScope`. + Example ======= Consider a free fall with constant horizontal velocity. @@ -102,8 +107,6 @@ function change_independent_variable(sys::AbstractODESystem, iv, eqs = []; dummi error("System $(nameof(sys)) is incomplete. Complete it first!") elseif isscheduled(sys) error("System $(nameof(sys)) is structurally simplified. Change independent variable before structural simplification!") - elseif !isempty(get_systems(sys)) - error("System $(nameof(sys)) is hierarchical. Flatten it first!") # TODO: implement and allow? elseif !iscall(iv2_of_iv1) || !isequal(only(arguments(iv2_of_iv1)), iv1) error("Variable $iv is not a function of the independent variable $iv1 of the system $(nameof(sys))!") end @@ -112,6 +115,7 @@ function change_independent_variable(sys::AbstractODESystem, iv, eqs = []; dummi iv2name = nameof(operation(iv2_of_iv1)) # e.g. :u iv2, = @independent_variables $iv2name # e.g. u iv1_of_iv2, = @variables $iv1name(iv2) # inverse in case sys is autonomous; e.g. t(u) + iv1_of_iv2 = GlobalScope(iv1_of_iv2) # do not namespace old independent variable as new dependent variable D1 = Differential(iv1) # e.g. d/d(t) D2 = Differential(iv2_of_iv1) # e.g. d/d(u(t)) @@ -119,10 +123,9 @@ function change_independent_variable(sys::AbstractODESystem, iv, eqs = []; dummi function chain_rule(ex) vars = get_variables(ex) for var_of_iv1 in vars # loop through e.g. f(t) - if iscall(var_of_iv1) && !isequal(var_of_iv1, iv2_of_iv1) # handle e.g. f(t) -> f(u(t)), but not u(t) -> u(u(t)) - varname = nameof(operation(var_of_iv1)) # e.g. :f - var_of_iv2, = @variables $varname(iv2_of_iv1) # e.g. f(u(t)) - ex = substitute(ex, var_of_iv1 => var_of_iv2; fold) # e.g. f(t) -> f(u(t)) + if iscall(var_of_iv1) && isequal(only(arguments(var_of_iv1)), iv1) && !isequal(var_of_iv1, iv2_of_iv1) # handle e.g. f(t) -> f(u(t)), but not u(t) -> u(u(t)) + var_of_iv2 = substitute(var_of_iv1, iv1 => iv2_of_iv1) # e.g. f(t) -> f(u(t)) + ex = substitute(ex, var_of_iv1 => var_of_iv2) end end ex = expand_derivatives(ex, simplify) # expand chain rule, e.g. (d/dt)(f(u(t)))) -> df(u(t))/du(t) * du(t)/dt @@ -130,7 +133,7 @@ function change_independent_variable(sys::AbstractODESystem, iv, eqs = []; dummi end # 2) Find e.g. du/dt in equations, then calculate e.g. d²u/dt², ... - eqs = [get_eqs(sys); eqs] # all equations (system-defined + user-provided) we may use + eqs = [eqs; get_eqs(sys)] # all equations (system-defined + user-provided) we may use idxs = findall(eq -> isequal(eq.lhs, D1(iv2_of_iv1)), eqs) if length(idxs) != 1 error("Exactly one equation for $D1($iv2_of_iv1) was not specified!") @@ -148,11 +151,14 @@ function change_independent_variable(sys::AbstractODESystem, iv, eqs = []; dummi div2name = Symbol(iv2name, :_, iv1name) # e.g. :u_t # TODO: customize ddiv2name = Symbol(div2name, iv1name) # e.g. :u_tt # TODO: customize div2, ddiv2 = @variables $div2name(iv2) $ddiv2name(iv2) # e.g. u_t(u), u_tt(u) - eqs = [eqs; [div2 ~ div2_of_iv1, ddiv2 ~ ddiv2_of_iv1]] # add dummy equations + div2, ddiv2 = GlobalScope.([div2, ddiv2]) # do not namespace dummies in new system + eqs = [[div2 ~ div2_of_iv1, ddiv2 ~ ddiv2_of_iv1]; eqs] # add dummy equations + @set! sys.unknowns = [get_unknowns(sys); [div2, ddiv2]] # add dummy variables else div2 = div2_of_iv1 ddiv2 = ddiv2_of_iv1 end + @set! sys.eqs = eqs # add extra equations we derived before starting transformation process # 4) Transform everything from old to new independent variable, e.g. t -> u. # Substitution order matters! Must begin with highest order to get D(D(u(t))) -> u_tt(u). @@ -170,18 +176,31 @@ function change_independent_variable(sys::AbstractODESystem, iv, eqs = []; dummi end return ex end - eqs = map(transform, eqs) # we derived and added equations to eqs; they are not in get_eqs(sys)! - observed = map(transform, get_observed(sys)) - initialization_eqs = map(transform, get_initialization_eqs(sys)) - parameter_dependencies = map(transform, get_parameter_dependencies(sys)) - defaults = Dict(transform(var) => transform(val) for (var, val) in get_defaults(sys)) - guesses = Dict(transform(var) => transform(val) for (var, val) in get_guesses(sys)) - assertions = Dict(transform(condition) => msg for (condition, msg) in get_assertions(sys)) - - # 5) Recreate system with transformed fields - return typeof(sys)( - eqs, iv2; - observed, initialization_eqs, parameter_dependencies, defaults, guesses, assertions, - name = nameof(sys), description = description(sys), kwargs... - ) |> complete # input system must be complete, so complete the output system + function transform(sys::AbstractODESystem) + eqs = map(transform, get_eqs(sys)) + unknowns = map(transform, get_unknowns(sys)) + unknowns = filter(var -> !isequal(var, iv2), unknowns) # remove e.g. u + ps = map(transform, get_ps(sys)) + ps = filter(!isinitial, ps) # remove Initial(...) # # TODO: shouldn't have to touch this + observed = map(transform, get_observed(sys)) + initialization_eqs = map(transform, get_initialization_eqs(sys)) + parameter_dependencies = map(transform, get_parameter_dependencies(sys)) + defaults = Dict(transform(var) => transform(val) for (var, val) in get_defaults(sys)) + guesses = Dict(transform(var) => transform(val) for (var, val) in get_guesses(sys)) + assertions = Dict(transform(condition) => msg for (condition, msg) in get_assertions(sys)) + systems = get_systems(sys) # save before reconstructing system + wascomplete = iscomplete(sys) # save before reconstructing system + sys = typeof(sys)( # recreate system with transformed fields + eqs, iv2, unknowns, ps; observed, initialization_eqs, parameter_dependencies, defaults, guesses, + assertions, name = nameof(sys), description = description(sys), kwargs... + ) + systems = map(transform, systems) # recurse through subsystems + sys = compose(sys, systems) # rebuild hierarchical system + if wascomplete + wasflat = isempty(systems) + sys = complete(sys; flatten = wasflat) # complete output if input was complete + end + return sys + end + return transform(sys) end diff --git a/test/basic_transformations.jl b/test/basic_transformations.jl index cfb3bf5db6..4fed453390 100644 --- a/test/basic_transformations.jl +++ b/test/basic_transformations.jl @@ -62,25 +62,34 @@ end @independent_variables t D = Differential(t) @variables a(t) ȧ(t) Ω(t) ϕ(t) + a, ȧ = GlobalScope.([a, ȧ]) + species(w; kw...) = ODESystem([D(Ω) ~ -3(1 + w) * D(a)/a * Ω], t, [Ω], []; kw...) + @named r = species(1//3) + @named m = species(0) + @named Λ = species(-1) eqs = [ - Ω ~ 123 + Ω ~ r.Ω + m.Ω + Λ.Ω D(a) ~ ȧ ȧ ~ √(Ω) * a^2 D(D(ϕ)) ~ -3*D(a)/a*D(ϕ) ] - M1 = ODESystem(eqs, t; name = :M) |> complete + M1 = ODESystem(eqs, t, [Ω, a, ȧ, ϕ], []; name = :M) + M1 = compose(M1, r, m, Λ) + M1 = complete(M1; flatten = false) # Apply in two steps, where derivatives are defined at each step: first t -> a, then a -> b M2 = change_independent_variable(M1, M1.a; dummies = true) - @independent_variables a - @variables ȧ(a) Ω(a) ϕ(a) a_t(a) a_tt(a) + a, ȧ, Ω, Ωr, Ωm, ΩΛ, ϕ, a_t, a_tt = M2.a, M2.ȧ, M2.Ω, M2.r.Ω, M2.m.Ω, M2.Λ.Ω, M2.ϕ, M2.a_t, M2.a_tt Da = Differential(a) @test Set(equations(M2)) == Set([ - a_tt*Da(ϕ) + a_t^2*(Da^2)(ϕ) ~ -3*a_t^2/a*Da(ϕ) - ȧ ~ √(Ω) * a^2 - Ω ~ 123 a_t ~ ȧ # 1st order dummy equation a_tt ~ Da(ȧ) * a_t # 2nd order dummy equation + Ω ~ Ωr + Ωm + ΩΛ + ȧ ~ √(Ω) * a^2 + a_tt*Da(ϕ) + a_t^2*(Da^2)(ϕ) ~ -3*a_t^2/a*Da(ϕ) + a_t*Da(Ωr) ~ -4*Ωr*a_t/a + a_t*Da(Ωm) ~ -3*Ωm*a_t/a + a_t*Da(ΩΛ) ~ 0 ]) @variables b(M2.a) @@ -143,6 +152,4 @@ end @test_throws "not specified" change_independent_variable(M, v) @test_throws "not a function of the independent variable" change_independent_variable(M, y) @test_throws "not a function of the independent variable" change_independent_variable(M, z) - M = compose(M, M) - @test_throws "hierarchical" change_independent_variable(M, M.x) end From 2afacb5c696c2e760fd62597b427fae21e19a560 Mon Sep 17 00:00:00 2001 From: Herman Sletmoen Date: Thu, 6 Mar 2025 18:34:25 +0100 Subject: [PATCH 1079/1337] Forbid DDEs for now --- src/systems/diffeqs/basic_transformations.jl | 2 ++ test/basic_transformations.jl | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/src/systems/diffeqs/basic_transformations.jl b/src/systems/diffeqs/basic_transformations.jl index a8f3db11e4..0df7a5d51d 100644 --- a/src/systems/diffeqs/basic_transformations.jl +++ b/src/systems/diffeqs/basic_transformations.jl @@ -105,6 +105,8 @@ function change_independent_variable(sys::AbstractODESystem, iv, eqs = []; dummi if !iscomplete(sys) error("System $(nameof(sys)) is incomplete. Complete it first!") + elseif is_dde(sys) + error("System $(nameof(sys)) contains delay differential equations (DDEs). This is currently not supported!") elseif isscheduled(sys) error("System $(nameof(sys)) is structurally simplified. Change independent variable before structural simplification!") elseif !iscall(iv2_of_iv1) || !isequal(only(arguments(iv2_of_iv1)), iv1) diff --git a/test/basic_transformations.jl b/test/basic_transformations.jl index 4fed453390..798548ff44 100644 --- a/test/basic_transformations.jl +++ b/test/basic_transformations.jl @@ -152,4 +152,8 @@ end @test_throws "not specified" change_independent_variable(M, v) @test_throws "not a function of the independent variable" change_independent_variable(M, y) @test_throws "not a function of the independent variable" change_independent_variable(M, z) + M = ODESystem([D(x) ~ 0, v ~ x], t; name = :M) + @variables x(..) # require explicit argument + M = ODESystem([D(x(t)) ~ x(t-1)], t; name = :M) |> complete + @test_throws "DDE" change_independent_variable(M, M.x) end From 2d5aa12109c8b494e5f4af975e09a63630e14fce Mon Sep 17 00:00:00 2001 From: Herman Sletmoen Date: Thu, 6 Mar 2025 18:48:29 +0100 Subject: [PATCH 1080/1337] Use vars(ex; op = Nothing) instead of get_variables(ex) --- src/systems/diffeqs/basic_transformations.jl | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/systems/diffeqs/basic_transformations.jl b/src/systems/diffeqs/basic_transformations.jl index 0df7a5d51d..98c50758c2 100644 --- a/src/systems/diffeqs/basic_transformations.jl +++ b/src/systems/diffeqs/basic_transformations.jl @@ -123,8 +123,7 @@ function change_independent_variable(sys::AbstractODESystem, iv, eqs = []; dummi # 1) Utility that performs the chain rule on an expression, e.g. (d/dt)(f(t)) -> (d/dt)(f(u(t))) -> df(u(t))/du(t) * du(t)/dt function chain_rule(ex) - vars = get_variables(ex) - for var_of_iv1 in vars # loop through e.g. f(t) + for var_of_iv1 in vars(ex; op = Nothing) # loop over all variables in expression, e.g. f(t) (op = Nothing so "D(f(t))" yields only "f(t)" and not "D(f(t))"; we want to replace *inside* derivative signs) if iscall(var_of_iv1) && isequal(only(arguments(var_of_iv1)), iv1) && !isequal(var_of_iv1, iv2_of_iv1) # handle e.g. f(t) -> f(u(t)), but not u(t) -> u(u(t)) var_of_iv2 = substitute(var_of_iv1, iv1 => iv2_of_iv1) # e.g. f(t) -> f(u(t)) ex = substitute(ex, var_of_iv1 => var_of_iv2) From 5780d91fd26f5fdcdc48980bc22b0a318284a2f1 Mon Sep 17 00:00:00 2001 From: Herman Sletmoen Date: Thu, 6 Mar 2025 19:04:34 +0100 Subject: [PATCH 1081/1337] Print detected transformation equations when there is not exactly one --- src/systems/diffeqs/basic_transformations.jl | 2 +- test/basic_transformations.jl | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/systems/diffeqs/basic_transformations.jl b/src/systems/diffeqs/basic_transformations.jl index 98c50758c2..3d9d92b13f 100644 --- a/src/systems/diffeqs/basic_transformations.jl +++ b/src/systems/diffeqs/basic_transformations.jl @@ -137,7 +137,7 @@ function change_independent_variable(sys::AbstractODESystem, iv, eqs = []; dummi eqs = [eqs; get_eqs(sys)] # all equations (system-defined + user-provided) we may use idxs = findall(eq -> isequal(eq.lhs, D1(iv2_of_iv1)), eqs) if length(idxs) != 1 - error("Exactly one equation for $D1($iv2_of_iv1) was not specified!") + error("Exactly one equation for $D1($iv2_of_iv1) was not specified! Got $(length(idxs)) equations:\n", join(eqs[idxs], '\n')) end div2_of_iv1_eq = popat!(eqs, only(idxs)) # get and remove e.g. du/dt = ... (may be added back later as a dummy) div2_of_iv1 = chain_rule(div2_of_iv1_eq.rhs) diff --git a/test/basic_transformations.jl b/test/basic_transformations.jl index 798548ff44..b1d66fc9ee 100644 --- a/test/basic_transformations.jl +++ b/test/basic_transformations.jl @@ -148,8 +148,10 @@ end M = complete(M) @test_throws "singular" change_independent_variable(M, M.x) @test_throws "structurally simplified" change_independent_variable(structural_simplify(M), y) - @test_throws "not specified" change_independent_variable(M, w) - @test_throws "not specified" change_independent_variable(M, v) + @test_throws "Got 0 equations:" change_independent_variable(M, w) + @test_throws "Got 0 equations:" change_independent_variable(M, v) + M = ODESystem([D(x) ~ 1, v ~ 1], t; name = :M) |> complete + @test_throws "Got 2 equations:" change_independent_variable(M, M.x, [D(x) ~ 2]) @test_throws "not a function of the independent variable" change_independent_variable(M, y) @test_throws "not a function of the independent variable" change_independent_variable(M, z) M = ODESystem([D(x) ~ 0, v ~ x], t; name = :M) From 5ca058d64b1918c40ec910acdf652e8263f537d6 Mon Sep 17 00:00:00 2001 From: Herman Sletmoen Date: Thu, 6 Mar 2025 19:38:33 +0100 Subject: [PATCH 1082/1337] Use default_toterm (with special underscore) for dummies --- src/systems/diffeqs/basic_transformations.jl | 5 ++-- test/basic_transformations.jl | 24 ++++++++++---------- 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/src/systems/diffeqs/basic_transformations.jl b/src/systems/diffeqs/basic_transformations.jl index 3d9d92b13f..7ed3c16049 100644 --- a/src/systems/diffeqs/basic_transformations.jl +++ b/src/systems/diffeqs/basic_transformations.jl @@ -149,9 +149,8 @@ function change_independent_variable(sys::AbstractODESystem, iv, eqs = []; dummi # 3) If requested, insert extra dummy equations for e.g. du/dt, d²u/dt², ... # Otherwise, replace all these derivatives by their explicit expressions if dummies - div2name = Symbol(iv2name, :_, iv1name) # e.g. :u_t # TODO: customize - ddiv2name = Symbol(div2name, iv1name) # e.g. :u_tt # TODO: customize - div2, ddiv2 = @variables $div2name(iv2) $ddiv2name(iv2) # e.g. u_t(u), u_tt(u) + div2 = substitute(default_toterm(D1(iv2_of_iv1)), iv1 => iv2) # e.g. uˍt(u) + ddiv2 = substitute(default_toterm(D1(D1(iv2_of_iv1))), iv1 => iv2) # e.g. uˍtt(u) div2, ddiv2 = GlobalScope.([div2, ddiv2]) # do not namespace dummies in new system eqs = [[div2 ~ div2_of_iv1, ddiv2 ~ ddiv2_of_iv1]; eqs] # add dummy equations @set! sys.unknowns = [get_unknowns(sys); [div2, ddiv2]] # add dummy variables diff --git a/test/basic_transformations.jl b/test/basic_transformations.jl index b1d66fc9ee..7a8c063283 100644 --- a/test/basic_transformations.jl +++ b/test/basic_transformations.jl @@ -79,17 +79,17 @@ end # Apply in two steps, where derivatives are defined at each step: first t -> a, then a -> b M2 = change_independent_variable(M1, M1.a; dummies = true) - a, ȧ, Ω, Ωr, Ωm, ΩΛ, ϕ, a_t, a_tt = M2.a, M2.ȧ, M2.Ω, M2.r.Ω, M2.m.Ω, M2.Λ.Ω, M2.ϕ, M2.a_t, M2.a_tt + a, ȧ, Ω, Ωr, Ωm, ΩΛ, ϕ, aˍt, aˍtt = M2.a, M2.ȧ, M2.Ω, M2.r.Ω, M2.m.Ω, M2.Λ.Ω, M2.ϕ, M2.aˍt, M2.aˍtt Da = Differential(a) @test Set(equations(M2)) == Set([ - a_t ~ ȧ # 1st order dummy equation - a_tt ~ Da(ȧ) * a_t # 2nd order dummy equation + aˍt ~ ȧ # 1st order dummy equation + aˍtt ~ Da(ȧ) * aˍt # 2nd order dummy equation Ω ~ Ωr + Ωm + ΩΛ ȧ ~ √(Ω) * a^2 - a_tt*Da(ϕ) + a_t^2*(Da^2)(ϕ) ~ -3*a_t^2/a*Da(ϕ) - a_t*Da(Ωr) ~ -4*Ωr*a_t/a - a_t*Da(Ωm) ~ -3*Ωm*a_t/a - a_t*Da(ΩΛ) ~ 0 + aˍtt*Da(ϕ) + aˍt^2*(Da^2)(ϕ) ~ -3*aˍt^2/a*Da(ϕ) + aˍt*Da(Ωr) ~ -4*Ωr*aˍt/a + aˍt*Da(Ωm) ~ -3*Ωm*aˍt/a + aˍt*Da(ΩΛ) ~ 0 ]) @variables b(M2.a) @@ -105,7 +105,7 @@ end @variables x(t) Mt = ODESystem([D(x) ~ 2*x], t; name = :M) |> complete Mx = change_independent_variable(Mt, Mt.x; dummies = true) - @test (@variables x x_t(x) x_tt(x); Set(equations(Mx)) == Set([x_t ~ 2*x, x_tt ~ 2*x_t])) + @test (@variables x xˍt(x) xˍtt(x); Set(equations(Mx)) == Set([xˍt ~ 2*x, xˍtt ~ 2*xˍt])) end @testset "Change independent variable (free fall)" begin @@ -133,11 +133,11 @@ end # Compare to pen-and-paper result @independent_variables x Dx = Differential(x) - @variables x_t(x) x_tt(x) y(x) t(x) + @variables xˍt(x) xˍtt(x) y(x) t(x) @test Set(equations(M2)) == Set([ - x_t^2*(Dx^2)(y) + x_tt*Dx(y) ~ x_t^2 + 3*y^2*Dx(y)*x_t # from D(D(y)) - x_t ~ x^4 + y^5 + t^6 # 1st order dummy equation - x_tt ~ 4*x^3*x_t + 5*y^4*Dx(y)*x_t + 6*t^5 # 2nd order dummy equation + xˍt^2*(Dx^2)(y) + xˍtt*Dx(y) ~ xˍt^2 + 3*y^2*Dx(y)*xˍt # from D(D(y)) + xˍt ~ x^4 + y^5 + t^6 # 1st order dummy equation + xˍtt ~ 4*x^3*xˍt + 5*y^4*Dx(y)*xˍt + 6*t^5 # 2nd order dummy equation ]) end From 2397d9a22cda11008f0288d50e1b23a4d24b80b5 Mon Sep 17 00:00:00 2001 From: Herman Sletmoen Date: Thu, 6 Mar 2025 20:18:27 +0100 Subject: [PATCH 1083/1337] Change independent variable of incomplete systems --- .../tutorials/change_independent_variable.md | 11 +++--- src/systems/diffeqs/basic_transformations.jl | 4 +-- test/basic_transformations.jl | 36 +++++++++---------- 3 files changed, 23 insertions(+), 28 deletions(-) diff --git a/docs/src/tutorials/change_independent_variable.md b/docs/src/tutorials/change_independent_variable.md index e49ededacd..f196c3cb64 100644 --- a/docs/src/tutorials/change_independent_variable.md +++ b/docs/src/tutorials/change_independent_variable.md @@ -24,11 +24,11 @@ D = Differential(t) M1 = ODESystem([ D(D(y)) ~ -g, D(x) ~ v # constant horizontal velocity ], t; defaults = [ - y => 0.0 + y => 0.0 # start on the ground ], initialization_eqs = [ #x ~ 0.0, # TODO: handle? # hide D(x) ~ D(y) # equal initial horizontal and vertical velocity (45 °) -], name = :M) |> complete +], name = :M) M1s = structural_simplify(M1) ``` This is the standard parametrization that arises naturally from kinematics and Newton's laws. @@ -42,7 +42,7 @@ There are at least three ways of answering this: We will demonstrate the last method by changing the independent variable from $t$ to $x$. This transformation is well-defined for any non-zero horizontal velocity $v$. ```@example changeivar -M2 = change_independent_variable(M1, M1.x; dummies = true) +M2 = change_independent_variable(M1, x; dummies = true) @assert M2.x isa Num # hide M2s = structural_simplify(M2; allow_symbolic = true) ``` @@ -79,12 +79,11 @@ initialization_eqs = [ ] M1 = ODESystem(eqs, t, [Ω, a], []; initialization_eqs, name = :M) M1 = compose(M1, r, m, Λ) -M1 = complete(M1; flatten = false) M1s = structural_simplify(M1) ``` Of course, we can solve this ODE as it is: ```@example changeivar -prob = ODEProblem(M1s, [M1.a => 1.0, M1.r.Ω => 5e-5, M1.m.Ω => 0.3], (0.0, -3.3), []) +prob = ODEProblem(M1s, [M1s.a => 1.0, M1s.r.Ω => 5e-5, M1s.m.Ω => 0.3], (0.0, -3.3), []) sol = solve(prob, Tsit5(); reltol = 1e-5) @assert Symbol(sol.retcode) == :Unstable # surrounding text assumes this was unstable # hide plot(sol, idxs = [M1.a, M1.r.Ω/M1.Ω, M1.m.Ω/M1.Ω, M1.Λ.Ω/M1.Ω]) @@ -111,7 +110,7 @@ M3 = change_independent_variable(M2, b, [Da(b) ~ exp(-b), a ~ exp(b)]) We can now solve and plot the ODE in terms of $b$: ```@example changeivar M3s = structural_simplify(M3; allow_symbolic = true) -prob = ODEProblem(M3s, [M3.r.Ω => 5e-5, M3.m.Ω => 0.3], (0, -15), []) +prob = ODEProblem(M3s, [M3s.r.Ω => 5e-5, M3s.m.Ω => 0.3], (0, -15), []) sol = solve(prob, Tsit5()) @assert Symbol(sol.retcode) == :Success # surrounding text assumes the solution was successful # hide plot(sol, idxs = [M3.r.Ω/M3.Ω, M3.m.Ω/M3.Ω, M3.Λ.Ω/M3.Ω]) diff --git a/src/systems/diffeqs/basic_transformations.jl b/src/systems/diffeqs/basic_transformations.jl index 7ed3c16049..a6ce1fcc14 100644 --- a/src/systems/diffeqs/basic_transformations.jl +++ b/src/systems/diffeqs/basic_transformations.jl @@ -103,9 +103,7 @@ function change_independent_variable(sys::AbstractODESystem, iv, eqs = []; dummi iv2_of_iv1 = unwrap(iv) # e.g. u(t) iv1 = get_iv(sys) # e.g. t - if !iscomplete(sys) - error("System $(nameof(sys)) is incomplete. Complete it first!") - elseif is_dde(sys) + if is_dde(sys) error("System $(nameof(sys)) contains delay differential equations (DDEs). This is currently not supported!") elseif isscheduled(sys) error("System $(nameof(sys)) is structurally simplified. Change independent variable before structural simplification!") diff --git a/test/basic_transformations.jl b/test/basic_transformations.jl index 7a8c063283..0aa679a817 100644 --- a/test/basic_transformations.jl +++ b/test/basic_transformations.jl @@ -28,8 +28,8 @@ end @testset "Change independent variable (trivial)" begin @variables x(t) y(t) eqs1 = [D(D(x)) ~ D(x) + x, D(y) ~ 1] - M1 = ODESystem(eqs1, t; name = :M) |> complete - M2 = change_independent_variable(M1, M1.y) + M1 = ODESystem(eqs1, t; name = :M) + M2 = change_independent_variable(M1, y) eqs2 = substitute(equations(M2), M2.y => M1.t) # system should be equivalent when parametrized with y (since D(y) ~ 1), so substitute back ... @test eqs1[1] == only(eqs2) # ... and check that the equations are unmodified end @@ -43,8 +43,8 @@ end D(s) ~ 1 / (2*s) ] initialization_eqs = [x ~ 1.0, y ~ 1.0, D(y) ~ 0.0] - M1 = ODESystem(eqs, t; initialization_eqs, name = :M) |> complete - M2 = change_independent_variable(M1, M1.s; dummies = true) + M1 = ODESystem(eqs, t; initialization_eqs, name = :M) + M2 = change_independent_variable(M1, s; dummies = true) M1 = structural_simplify(M1; allow_symbolic = true) M2 = structural_simplify(M2; allow_symbolic = true) @@ -75,11 +75,11 @@ end ] M1 = ODESystem(eqs, t, [Ω, a, ȧ, ϕ], []; name = :M) M1 = compose(M1, r, m, Λ) - M1 = complete(M1; flatten = false) # Apply in two steps, where derivatives are defined at each step: first t -> a, then a -> b M2 = change_independent_variable(M1, M1.a; dummies = true) - a, ȧ, Ω, Ωr, Ωm, ΩΛ, ϕ, aˍt, aˍtt = M2.a, M2.ȧ, M2.Ω, M2.r.Ω, M2.m.Ω, M2.Λ.Ω, M2.ϕ, M2.aˍt, M2.aˍtt + M2c = complete(M2) # just for the following equation comparison (without namespacing) + a, ȧ, Ω, Ωr, Ωm, ΩΛ, ϕ, aˍt, aˍtt = M2c.a, M2c.ȧ, M2c.Ω, M2c.r.Ω, M2c.m.Ω, M2c.Λ.Ω, M2c.ϕ, M2c.aˍt, M2c.aˍtt Da = Differential(a) @test Set(equations(M2)) == Set([ aˍt ~ ȧ # 1st order dummy equation @@ -103,16 +103,16 @@ end @testset "Change independent variable (simple)" begin @variables x(t) - Mt = ODESystem([D(x) ~ 2*x], t; name = :M) |> complete - Mx = change_independent_variable(Mt, Mt.x; dummies = true) + Mt = ODESystem([D(x) ~ 2*x], t; name = :M) + Mx = change_independent_variable(Mt, x; dummies = true) @test (@variables x xˍt(x) xˍtt(x); Set(equations(Mx)) == Set([xˍt ~ 2*x, xˍtt ~ 2*xˍt])) end @testset "Change independent variable (free fall)" begin @variables x(t) y(t) @parameters g v # gravitational acceleration and constant horizontal velocity - Mt = ODESystem([D(D(y)) ~ -g, D(x) ~ v], t; name = :M) |> complete # gives (x, y) as function of t, ... - Mx = change_independent_variable(Mt, Mt.x; dummies = false) # ... but we want y as a function of x + Mt = ODESystem([D(D(y)) ~ -g, D(x) ~ v], t; name = :M) # gives (x, y) as function of t, ... + Mx = change_independent_variable(Mt, x; dummies = false) # ... but we want y as a function of x Mx = structural_simplify(Mx; allow_symbolic = true) Dx = Differential(Mx.x) prob = ODEProblem(Mx, [Mx.y => 0.0, Dx(Mx.y) => 1.0], (0.0, 20.0), [g => 9.81, v => 10.0]) # 1 = dy/dx = (dy/dt)/(dx/dt) means equal initial horizontal and vertical velocities @@ -127,8 +127,8 @@ end M1 = ODESystem([ # crazy non-autonomous non-linear 2nd order ODE D(D(y)) ~ D(x)^2 + D(y^3) |> expand_derivatives # expand D(y^3) # TODO: make this test 3rd order D(x) ~ x^4 + y^5 + t^6 - ], t; name = :M) |> complete - M2 = change_independent_variable(M1, M1.x; dummies = true) + ], t; name = :M) + M2 = change_independent_variable(M1, x; dummies = true) # Compare to pen-and-paper result @independent_variables x @@ -144,18 +144,16 @@ end @testset "Change independent variable (errors)" begin @variables x(t) y z(y) w(t) v(t) M = ODESystem([D(x) ~ 0, v ~ x], t; name = :M) - @test_throws "incomplete" change_independent_variable(M, M.x) - M = complete(M) - @test_throws "singular" change_independent_variable(M, M.x) + @test_throws "singular" change_independent_variable(M, x) @test_throws "structurally simplified" change_independent_variable(structural_simplify(M), y) @test_throws "Got 0 equations:" change_independent_variable(M, w) @test_throws "Got 0 equations:" change_independent_variable(M, v) - M = ODESystem([D(x) ~ 1, v ~ 1], t; name = :M) |> complete - @test_throws "Got 2 equations:" change_independent_variable(M, M.x, [D(x) ~ 2]) + M = ODESystem([D(x) ~ 1, v ~ 1], t; name = :M) + @test_throws "Got 2 equations:" change_independent_variable(M, x, [D(x) ~ 2]) @test_throws "not a function of the independent variable" change_independent_variable(M, y) @test_throws "not a function of the independent variable" change_independent_variable(M, z) M = ODESystem([D(x) ~ 0, v ~ x], t; name = :M) @variables x(..) # require explicit argument - M = ODESystem([D(x(t)) ~ x(t-1)], t; name = :M) |> complete - @test_throws "DDE" change_independent_variable(M, M.x) + M = ODESystem([D(x(t)) ~ x(t-1)], t; name = :M) + @test_throws "DDE" change_independent_variable(M, x(t)) end From a27785418814fe917fdeb096943375e92349ac79 Mon Sep 17 00:00:00 2001 From: Herman Sletmoen Date: Thu, 6 Mar 2025 20:38:01 +0100 Subject: [PATCH 1084/1337] Warn user about subtle differences between variables after change of independent variable in documentation --- .../tutorials/change_independent_variable.md | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/docs/src/tutorials/change_independent_variable.md b/docs/src/tutorials/change_independent_variable.md index f196c3cb64..b308bd3f82 100644 --- a/docs/src/tutorials/change_independent_variable.md +++ b/docs/src/tutorials/change_independent_variable.md @@ -23,11 +23,8 @@ D = Differential(t) @parameters g = 9.81 v # gravitational acceleration and constant horizontal velocity M1 = ODESystem([ D(D(y)) ~ -g, D(x) ~ v # constant horizontal velocity -], t; defaults = [ - y => 0.0 # start on the ground -], initialization_eqs = [ - #x ~ 0.0, # TODO: handle? # hide - D(x) ~ D(y) # equal initial horizontal and vertical velocity (45 °) +], t; initialization_eqs = [ + D(x) ~ D(y) # equal initial horizontal and vertical velocity (45°) ], name = :M) M1s = structural_simplify(M1) ``` @@ -43,15 +40,26 @@ We will demonstrate the last method by changing the independent variable from $t This transformation is well-defined for any non-zero horizontal velocity $v$. ```@example changeivar M2 = change_independent_variable(M1, x; dummies = true) -@assert M2.x isa Num # hide M2s = structural_simplify(M2; allow_symbolic = true) +# a sanity test on the 10 x/y variables that are accessible to the user # hide +@assert allequal([x, M1s.x]) # hide +@assert allequal([M2.x, M2s.x]) # hide +@assert allequal([y, M1s.y]) # hide +@assert allunique([M1.x, M1.y, M2.y, M2s.y]) # hide +M2s # display this # hide ``` The derivatives are now with respect to the new independent variable $x$, which can be accessed with `M2.x`. + +!!! warn + At this point `x`, `M1.x`, `M1s.x`, `M2.x`, `M2s.x` are *three* different variables. + Meanwhile `y`, `M1.y`, `M1s.y`, `M2.y` and `M2s.y` are *four* different variables. + It can be instructive to inspect these yourself to see their subtle differences. + Notice how the number of equations has also decreased from three to two, as $\mathrm{d}x/\mathrm{d}t$ has been turned into an observed equation. It is straightforward to evolve the ODE for 10 meters and plot the resulting trajectory $y(x)$: ```@example changeivar using OrdinaryDiffEq, Plots -prob = ODEProblem(M2s, [], [0.0, 10.0], [v => 8.0]) # throw 10 meters with x-velocity 8 m/s +prob = ODEProblem(M2s, [M2s.y => 0.0], [0.0, 10.0], [v => 8.0]) # throw 10 meters with x-velocity 8 m/s sol = solve(prob, Tsit5()) plot(sol; idxs = M2.y) # must index by M2.y = y(x); not M1.y = y(t)! ``` From aa29107b95d0fdf109df73b67cf49a07125eb471 Mon Sep 17 00:00:00 2001 From: Herman Sletmoen Date: Thu, 6 Mar 2025 20:51:00 +0100 Subject: [PATCH 1085/1337] Update change_independent_variable docstring --- src/systems/diffeqs/basic_transformations.jl | 28 ++++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/systems/diffeqs/basic_transformations.jl b/src/systems/diffeqs/basic_transformations.jl index a6ce1fcc14..60159023f7 100644 --- a/src/systems/diffeqs/basic_transformations.jl +++ b/src/systems/diffeqs/basic_transformations.jl @@ -60,30 +60,30 @@ This or other additional equations can also be specified through `eqs`. The transformation is well-defined when the mapping between the new and old independent variables are one-to-one. This is satisfied if one is a strictly increasing function of the other (e.g. ``du(t)/dt > 0`` or ``du(t)/dt < 0``). -Keyword arguments -================= -If `dummies`, derivatives of the new independent variable with respect to the old one are expressed through dummy equations; otherwise they are explicitly inserted into the equations. -If `simplify`, these dummy expressions are simplified and often give a tidier transformation. -If `fold`, internal substitutions will evaluate numerical expressions. +# Keyword arguments + +- `dummies`: Whether derivatives of the new independent variable with respect to the old one are expressed through dummy equations or explicitly inserted into the equations. +- `simplify`: Whether expanded derivative expressions are simplified. This can give a tidier transformation. +- `fold`: Whether internal substitutions will evaluate numerical expressions. Additional keyword arguments `kwargs...` are forwarded to the constructor that rebuilds `sys`. -Usage before structural simplification -====================================== +# Usage before structural simplification + The variable change must take place before structural simplification. Subsequently, consider passing `allow_symbolic = true` to `structural_simplify(sys)` to reduce the number of unknowns, with the understanding that the transformation is well-defined. -Usage with non-autonomous systems -================================= -If `sys` is autonomous (i.e. ``t`` appears explicitly in its equations), it is often desirable to also pass an algebraic equation relating the new and old independent variables (e.g. ``t = f(u(t))``). +# Usage with non-autonomous systems + +If `sys` is non-autonomous (i.e. ``t`` appears explicitly in its equations), it is often desirable to also pass an algebraic equation relating the new and old independent variables (e.g. ``t = f(u(t))``). Otherwise the transformed system will be underdetermined and cannot be structurally simplified without additional changes. -Usage with hierarchical systems -=============================== +# Usage with hierarchical systems + It is recommended that `iv` is a non-namespaced variable in `sys`. This means it can belong to the top-level system or be a variable in a subsystem declared with `GlobalScope`. -Example -======= +# Example + Consider a free fall with constant horizontal velocity. Physics naturally describes position as a function of time. By changing the independent variable, it can be reformulated for vertical position as a function of horizontal position: From ea5a0032165e56a2cf61985d664031e7b3409ce6 Mon Sep 17 00:00:00 2001 From: Herman Sletmoen Date: Thu, 6 Mar 2025 20:57:47 +0100 Subject: [PATCH 1086/1337] Explicitly test that c*D(x) ~ something fails, but add TODO to fix it --- test/basic_transformations.jl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/basic_transformations.jl b/test/basic_transformations.jl index 0aa679a817..ea781536ef 100644 --- a/test/basic_transformations.jl +++ b/test/basic_transformations.jl @@ -148,6 +148,8 @@ end @test_throws "structurally simplified" change_independent_variable(structural_simplify(M), y) @test_throws "Got 0 equations:" change_independent_variable(M, w) @test_throws "Got 0 equations:" change_independent_variable(M, v) + M = ODESystem([2 * D(x) ~ 1, v ~ x], t; name = :M) # TODO: allow equations like this + @test_throws "Got 0 equations:" change_independent_variable(M, x) M = ODESystem([D(x) ~ 1, v ~ 1], t; name = :M) @test_throws "Got 2 equations:" change_independent_variable(M, x, [D(x) ~ 2]) @test_throws "not a function of the independent variable" change_independent_variable(M, y) From 0e41e045616cc260ce61b430607ef33ccf44eedb Mon Sep 17 00:00:00 2001 From: Herman Sletmoen Date: Fri, 7 Mar 2025 19:30:05 +0100 Subject: [PATCH 1087/1337] Prepare test for array variables (until expand_derivatives bug is fixed) --- src/systems/diffeqs/basic_transformations.jl | 8 +++++--- test/basic_transformations.jl | 8 +++++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/systems/diffeqs/basic_transformations.jl b/src/systems/diffeqs/basic_transformations.jl index 60159023f7..0806a105bb 100644 --- a/src/systems/diffeqs/basic_transformations.jl +++ b/src/systems/diffeqs/basic_transformations.jl @@ -121,9 +121,11 @@ function change_independent_variable(sys::AbstractODESystem, iv, eqs = []; dummi # 1) Utility that performs the chain rule on an expression, e.g. (d/dt)(f(t)) -> (d/dt)(f(u(t))) -> df(u(t))/du(t) * du(t)/dt function chain_rule(ex) - for var_of_iv1 in vars(ex; op = Nothing) # loop over all variables in expression, e.g. f(t) (op = Nothing so "D(f(t))" yields only "f(t)" and not "D(f(t))"; we want to replace *inside* derivative signs) - if iscall(var_of_iv1) && isequal(only(arguments(var_of_iv1)), iv1) && !isequal(var_of_iv1, iv2_of_iv1) # handle e.g. f(t) -> f(u(t)), but not u(t) -> u(u(t)) - var_of_iv2 = substitute(var_of_iv1, iv1 => iv2_of_iv1) # e.g. f(t) -> f(u(t)) + for var in vars(ex; op = Nothing) # loop over all variables in expression (op = Nothing prevents interpreting "D(f(t))" as one big variable) + is_function_of_iv1 = iscall(var) && isequal(only(arguments(var)), iv1) # is the expression of the form f(t)? + if is_function_of_iv1 && !isequal(var, iv2_of_iv1) # substitute f(t) -> f(u(t)), but not u(t) -> u(u(t)) + var_of_iv1 = var # e.g. f(t) + var_of_iv2 = substitute(var_of_iv1, iv1 => iv2_of_iv1) # e.g. f(u(t)) ex = substitute(ex, var_of_iv1 => var_of_iv2) end end diff --git a/test/basic_transformations.jl b/test/basic_transformations.jl index ea781536ef..cfbcea01b1 100644 --- a/test/basic_transformations.jl +++ b/test/basic_transformations.jl @@ -102,10 +102,12 @@ end end @testset "Change independent variable (simple)" begin - @variables x(t) - Mt = ODESystem([D(x) ~ 2*x], t; name = :M) + @variables x(t) y1(t) # y(t)[1:1] # TODO: use array variables y(t)[1:2] when fixed: https://github.com/JuliaSymbolics/Symbolics.jl/issues/1383 + Mt = ODESystem([D(x) ~ 2*x, D(y1) ~ y1], t; name = :M) Mx = change_independent_variable(Mt, x; dummies = true) - @test (@variables x xˍt(x) xˍtt(x); Set(equations(Mx)) == Set([xˍt ~ 2*x, xˍtt ~ 2*xˍt])) + @variables x xˍt(x) xˍtt(x) y1(x) # y(x)[1:1] # TODO: array variables + Dx = Differential(x) + @test (Set(equations(Mx)) == Set([xˍt ~ 2*x, xˍtt ~ 2*xˍt, xˍt*Dx(y1) ~ y1])) end @testset "Change independent variable (free fall)" begin From d66026947da0f471b8028348effce85e06bcc630 Mon Sep 17 00:00:00 2001 From: Herman Sletmoen Date: Fri, 7 Mar 2025 20:22:49 +0100 Subject: [PATCH 1088/1337] Test change_independent_variable with registered functions and callable parameters --- test/basic_transformations.jl | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/test/basic_transformations.jl b/test/basic_transformations.jl index cfbcea01b1..3853debcd4 100644 --- a/test/basic_transformations.jl +++ b/test/basic_transformations.jl @@ -1,4 +1,4 @@ -using ModelingToolkit, OrdinaryDiffEq, Test +using ModelingToolkit, OrdinaryDiffEq, DataInterpolations, Test @independent_variables t D = Differential(t) @@ -143,6 +143,34 @@ end ]) end +@testset "Change independent variable (registered function / callable parameter)" begin + @independent_variables t + D = Differential(t) + @variables x(t) y(t) + @parameters f::LinearInterpolation (fc::LinearInterpolation)(..) # non-callable and callable + callme(interp::LinearInterpolation, input) = interp(input) + @register_symbolic callme(interp::LinearInterpolation, input) + M1 = ODESystem([ + D(x) ~ 2*t + D(y) ~ 1*fc(t) + 2*fc(x) + 3*fc(y) + 1*callme(f, t) + 2*callme(f, x) + 3*callme(f, y) + ], t; name = :M) + + # Ensure that interpolations are called with the same variables + M2 = change_independent_variable(M1, x, [t ~ √(x)]) + @variables x y(x) t(x) + Dx = Differential(x) + @test Set(equations(M2)) == Set([ + t ~ √(x) + 2*t*Dx(y) ~ 1*fc(t) + 2*fc(x) + 3*fc(y) + 1*callme(f, t) + 2*callme(f, x) + 3*callme(f, y) + ]) + + _f = LinearInterpolation([1.0, 1.0], [-100.0, +100.0]) # constant value 1 + M2s = structural_simplify(M2; allow_symbolic = true) + prob = ODEProblem(M2s, [M2s.y => 0.0], (1.0, 4.0), [fc => _f, f => _f]) + sol = solve(prob, Tsit5(); abstol = 1e-5) + @test isapprox(sol(4.0, idxs=M2.y), 12.0; atol = 1e-5) # Anal solution is D(y) ~ 12 => y(t) ~ 12*t + C => y(x) ~ 12*√(x) + C. With y(x=1)=0 => 12*(√(x)-1), so y(x=4) ~ 12 +end + @testset "Change independent variable (errors)" begin @variables x(t) y z(y) w(t) v(t) M = ODESystem([D(x) ~ 0, v ~ x], t; name = :M) From 10793d28f28b5ce8bad2b3f3ffe2dc439f41e845 Mon Sep 17 00:00:00 2001 From: Herman Sletmoen Date: Fri, 7 Mar 2025 21:02:02 +0100 Subject: [PATCH 1089/1337] Optionally add differential equation for old independent variable --- .../tutorials/change_independent_variable.md | 5 ++++ src/systems/diffeqs/basic_transformations.jl | 23 ++++++++++++++----- test/basic_transformations.jl | 6 ++--- 3 files changed, 25 insertions(+), 9 deletions(-) diff --git a/docs/src/tutorials/change_independent_variable.md b/docs/src/tutorials/change_independent_variable.md index b308bd3f82..35a065e706 100644 --- a/docs/src/tutorials/change_independent_variable.md +++ b/docs/src/tutorials/change_independent_variable.md @@ -64,6 +64,11 @@ sol = solve(prob, Tsit5()) plot(sol; idxs = M2.y) # must index by M2.y = y(x); not M1.y = y(t)! ``` +!!! tip "Usage tips" + Look up the documentation of [`change_independent_variable`](@ref) for tips on how to use it. + + For example, if you also need $t(x)$, you can tell it to add a differential equation for the old independent variable in terms of the new one using the [inverse function rule](https://en.wikipedia.org/wiki/Inverse_function_rule) (here $\mathrm{d}t/\mathrm{d}x = 1 / (\mathrm{d}x/\mathrm{d}t)$). If you know an analytical expression between the independent variables (here $t = x/v$), you can also pass it directly to the function to avoid the extra differential equation. + ## 2. Alleviating stiffness by changing to logarithmic time In cosmology, the [Friedmann equations](https://en.wikipedia.org/wiki/Friedmann_equations) describe the expansion of the universe. diff --git a/src/systems/diffeqs/basic_transformations.jl b/src/systems/diffeqs/basic_transformations.jl index 0806a105bb..5e3c09d1d4 100644 --- a/src/systems/diffeqs/basic_transformations.jl +++ b/src/systems/diffeqs/basic_transformations.jl @@ -51,7 +51,7 @@ function liouville_transform(sys::AbstractODESystem; kwargs...) end """ - change_independent_variable(sys::AbstractODESystem, iv, eqs = []; dummies = false, simplify = true, fold = false, kwargs...) + change_independent_variable(sys::AbstractODESystem, iv, eqs = []; dummies = false, add_old_diff = false, simplify = true, fold = false, kwargs...) Transform the independent variable (e.g. ``t``) of the ODE system `sys` to a dependent variable `iv` (e.g. ``u(t)``). An equation in `sys` must define the rate of change of the new independent variable (e.g. ``du(t)/dt``). @@ -63,6 +63,7 @@ This is satisfied if one is a strictly increasing function of the other (e.g. `` # Keyword arguments - `dummies`: Whether derivatives of the new independent variable with respect to the old one are expressed through dummy equations or explicitly inserted into the equations. +- `add_old_diff`: Whether to add a differential equation for the old independent variable in terms of the new one using the inverse function rule. - `simplify`: Whether expanded derivative expressions are simplified. This can give a tidier transformation. - `fold`: Whether internal substitutions will evaluate numerical expressions. Additional keyword arguments `kwargs...` are forwarded to the constructor that rebuilds `sys`. @@ -99,7 +100,7 @@ julia> unknowns(M′) y(x) ``` """ -function change_independent_variable(sys::AbstractODESystem, iv, eqs = []; dummies = false, simplify = true, fold = false, kwargs...) +function change_independent_variable(sys::AbstractODESystem, iv, eqs = []; dummies = false, add_old_diff = false, simplify = true, fold = false, kwargs...) iv2_of_iv1 = unwrap(iv) # e.g. u(t) iv1 = get_iv(sys) # e.g. t @@ -148,24 +149,34 @@ function change_independent_variable(sys::AbstractODESystem, iv, eqs = []; dummi # 3) If requested, insert extra dummy equations for e.g. du/dt, d²u/dt², ... # Otherwise, replace all these derivatives by their explicit expressions + unks = get_unknowns(sys) if dummies div2 = substitute(default_toterm(D1(iv2_of_iv1)), iv1 => iv2) # e.g. uˍt(u) ddiv2 = substitute(default_toterm(D1(D1(iv2_of_iv1))), iv1 => iv2) # e.g. uˍtt(u) div2, ddiv2 = GlobalScope.([div2, ddiv2]) # do not namespace dummies in new system eqs = [[div2 ~ div2_of_iv1, ddiv2 ~ ddiv2_of_iv1]; eqs] # add dummy equations - @set! sys.unknowns = [get_unknowns(sys); [div2, ddiv2]] # add dummy variables + unks = [unks; [div2, ddiv2]] # add dummy variables else div2 = div2_of_iv1 ddiv2 = ddiv2_of_iv1 end + + # 4) If requested, add a differential equation for the old independent variable as a function of the old one + if add_old_diff + div1lhs = substitute(D2(iv1_of_iv2), iv2 => iv2_of_iv1) # e.g. (d/du(t))(t(u(t))); will be transformed to (d/du)(t(u)) by chain rule + div1rhs = 1 / div2 # du/dt = 1/(dt/du) (https://en.wikipedia.org/wiki/Inverse_function_rule) + eqs = [eqs; div1lhs ~ div1rhs] + unks = [unks; iv1_of_iv2] + end @set! sys.eqs = eqs # add extra equations we derived before starting transformation process + @set! sys.unknowns = unks # add dummy variables and old independent variable as a function of the new one - # 4) Transform everything from old to new independent variable, e.g. t -> u. + # 5) Transform everything from old to new independent variable, e.g. t -> u. # Substitution order matters! Must begin with highest order to get D(D(u(t))) -> u_tt(u). # If we had started with the lowest order, we would get D(D(u(t))) -> D(u_t(u)) -> 0! iv1_to_iv2_subs = [ # a vector ensures substitution order - D1(D1(iv2_of_iv1)) => ddiv2 # order 2, e.g. D(D(u(t))) -> u_tt(u) - D1(iv2_of_iv1) => div2 # order 1, e.g. D(u(t)) -> u_t(u) + D1(D1(iv2_of_iv1)) => ddiv2 # order 2, e.g. D(D(u(t))) -> u_tt(u) or explicit expression + D1(iv2_of_iv1) => div2 # order 1, e.g. D(u(t)) -> u_t(u) or explicit expression iv2_of_iv1 => iv2 # order 0, e.g. u(t) -> u iv1 => iv1_of_iv2 # in case sys was autonomous, e.g. t -> t(u) ] diff --git a/test/basic_transformations.jl b/test/basic_transformations.jl index 3853debcd4..ac3720469a 100644 --- a/test/basic_transformations.jl +++ b/test/basic_transformations.jl @@ -114,12 +114,12 @@ end @variables x(t) y(t) @parameters g v # gravitational acceleration and constant horizontal velocity Mt = ODESystem([D(D(y)) ~ -g, D(x) ~ v], t; name = :M) # gives (x, y) as function of t, ... - Mx = change_independent_variable(Mt, x; dummies = false) # ... but we want y as a function of x + Mx = change_independent_variable(Mt, x; add_old_diff = true) # ... but we want y as a function of x Mx = structural_simplify(Mx; allow_symbolic = true) Dx = Differential(Mx.x) - prob = ODEProblem(Mx, [Mx.y => 0.0, Dx(Mx.y) => 1.0], (0.0, 20.0), [g => 9.81, v => 10.0]) # 1 = dy/dx = (dy/dt)/(dx/dt) means equal initial horizontal and vertical velocities + prob = ODEProblem(Mx, [Mx.y => 0.0, Dx(Mx.y) => 1.0, Mx.t => 0.0], (0.0, 20.0), [g => 9.81, v => 10.0]) # 1 = dy/dx = (dy/dt)/(dx/dt) means equal initial horizontal and vertical velocities sol = solve(prob, Tsit5(); reltol = 1e-5) - @test all(isapprox.(sol[Mx.y], sol[Mx.x - g*(Mx.x/v)^2/2]; atol = 1e-10)) # compare to analytical solution (x(t) = v*t, y(t) = v*t - g*t^2/2) + @test all(isapprox.(sol[Mx.y], sol[Mx.x - g*(Mx.t)^2/2]; atol = 1e-10)) # compare to analytical solution (x(t) = v*t, y(t) = v*t - g*t^2/2) end @testset "Change independent variable (crazy analytical example)" begin From 2b0998b74409f3cf04aebeae2652a113fd1ec5bd Mon Sep 17 00:00:00 2001 From: Herman Sletmoen Date: Sun, 9 Mar 2025 00:47:27 +0100 Subject: [PATCH 1090/1337] Rewrite change_independent_variable to handle any derivative order and nonlinear equations --- .../tutorials/change_independent_variable.md | 4 +- src/systems/diffeqs/basic_transformations.jl | 120 +++++++----------- test/basic_transformations.jl | 56 ++++---- 3 files changed, 77 insertions(+), 103 deletions(-) diff --git a/docs/src/tutorials/change_independent_variable.md b/docs/src/tutorials/change_independent_variable.md index 35a065e706..8f9ee0cbd2 100644 --- a/docs/src/tutorials/change_independent_variable.md +++ b/docs/src/tutorials/change_independent_variable.md @@ -39,7 +39,7 @@ There are at least three ways of answering this: We will demonstrate the last method by changing the independent variable from $t$ to $x$. This transformation is well-defined for any non-zero horizontal velocity $v$. ```@example changeivar -M2 = change_independent_variable(M1, x; dummies = true) +M2 = change_independent_variable(M1, x) M2s = structural_simplify(M2; allow_symbolic = true) # a sanity test on the 10 x/y variables that are accessible to the user # hide @assert allequal([x, M1s.x]) # hide @@ -110,7 +110,7 @@ To do this, we will change the independent variable in two stages; from $t$ to $ Notice that $\mathrm{d}a/\mathrm{d}t > 0$ provided that $\Omega > 0$, and $\mathrm{d}b/\mathrm{d}a > 0$, so the transformation is well-defined. First, we transform from $t$ to $a$: ```@example changeivar -M2 = change_independent_variable(M1, M1.a; dummies = true) +M2 = change_independent_variable(M1, M1.a) ``` Unlike the original, notice that this system is *non-autonomous* because the independent variable $a$ appears explicitly in the equations! This means that to change the independent variable from $a$ to $b$, we must provide not only the rate of change relation $db(a)/da = \exp(-b)$, but *also* the equation $a(b) = \exp(b)$ so $a$ can be eliminated in favor of $b$: diff --git a/src/systems/diffeqs/basic_transformations.jl b/src/systems/diffeqs/basic_transformations.jl index 5e3c09d1d4..c05e5e4394 100644 --- a/src/systems/diffeqs/basic_transformations.jl +++ b/src/systems/diffeqs/basic_transformations.jl @@ -51,22 +51,19 @@ function liouville_transform(sys::AbstractODESystem; kwargs...) end """ - change_independent_variable(sys::AbstractODESystem, iv, eqs = []; dummies = false, add_old_diff = false, simplify = true, fold = false, kwargs...) + change_independent_variable(sys::AbstractODESystem, iv, eqs = []; add_old_diff = false, simplify = true, fold = false, kwargs...) Transform the independent variable (e.g. ``t``) of the ODE system `sys` to a dependent variable `iv` (e.g. ``u(t)``). -An equation in `sys` must define the rate of change of the new independent variable (e.g. ``du(t)/dt``). -This or other additional equations can also be specified through `eqs`. - The transformation is well-defined when the mapping between the new and old independent variables are one-to-one. This is satisfied if one is a strictly increasing function of the other (e.g. ``du(t)/dt > 0`` or ``du(t)/dt < 0``). +Any extra equations `eqs` involving the new and old independent variables will be taken into account in the transformation. + # Keyword arguments -- `dummies`: Whether derivatives of the new independent variable with respect to the old one are expressed through dummy equations or explicitly inserted into the equations. -- `add_old_diff`: Whether to add a differential equation for the old independent variable in terms of the new one using the inverse function rule. +- `add_old_diff`: Whether to add a differential equation for the old independent variable in terms of the new one using the inverse function rule ``dt/du = 1/(du/dt)``. - `simplify`: Whether expanded derivative expressions are simplified. This can give a tidier transformation. - `fold`: Whether internal substitutions will evaluate numerical expressions. -Additional keyword arguments `kwargs...` are forwarded to the constructor that rebuilds `sys`. # Usage before structural simplification @@ -77,6 +74,7 @@ Subsequently, consider passing `allow_symbolic = true` to `structural_simplify(s If `sys` is non-autonomous (i.e. ``t`` appears explicitly in its equations), it is often desirable to also pass an algebraic equation relating the new and old independent variables (e.g. ``t = f(u(t))``). Otherwise the transformed system will be underdetermined and cannot be structurally simplified without additional changes. +If an algebraic relation is not known, consider using `add_old_diff`. # Usage with hierarchical systems @@ -91,16 +89,20 @@ By changing the independent variable, it can be reformulated for vertical positi ```julia julia> @variables x(t) y(t); -julia> @named M = ODESystem([D(D(y)) ~ -9.81, D(x) ~ 10.0], t); +julia> @named M = ODESystem([D(D(y)) ~ -9.81, D(D(x)) ~ 0.0], t); + +julia> M = change_independent_variable(M, x); -julia> M′ = change_independent_variable(complete(M), x); +julia> M = structural_simplify(M; allow_symbolic = true); -julia> unknowns(M′) -1-element Vector{SymbolicUtils.BasicSymbolic{Real}}: +julia> unknowns(M) +3-element Vector{SymbolicUtils.BasicSymbolic{Real}}: + xˍt(x) y(x) + yˍx(x) ``` """ -function change_independent_variable(sys::AbstractODESystem, iv, eqs = []; dummies = false, add_old_diff = false, simplify = true, fold = false, kwargs...) +function change_independent_variable(sys::AbstractODESystem, iv, eqs = []; add_old_diff = false, simplify = true, fold = false, kwargs...) iv2_of_iv1 = unwrap(iv) # e.g. u(t) iv1 = get_iv(sys) # e.g. t @@ -115,78 +117,45 @@ function change_independent_variable(sys::AbstractODESystem, iv, eqs = []; dummi iv1name = nameof(iv1) # e.g. :t iv2name = nameof(operation(iv2_of_iv1)) # e.g. :u iv2, = @independent_variables $iv2name # e.g. u - iv1_of_iv2, = @variables $iv1name(iv2) # inverse in case sys is autonomous; e.g. t(u) - iv1_of_iv2 = GlobalScope(iv1_of_iv2) # do not namespace old independent variable as new dependent variable + iv1_of_iv2, = GlobalScope.(@variables $iv1name(iv2)) # inverse, e.g. t(u), global because iv1 has no namespacing in sys D1 = Differential(iv1) # e.g. d/d(t) - D2 = Differential(iv2_of_iv1) # e.g. d/d(u(t)) + div2_of_iv1 = GlobalScope(default_toterm(D1(iv2_of_iv1))) # e.g. uˍt(t) + div2_of_iv2 = substitute(div2_of_iv1, iv1 => iv2) # e.g. uˍt(u) + div2_of_iv2_of_iv1 = substitute(div2_of_iv2, iv2 => iv2_of_iv1) # e.g. uˍt(u(t)) + + # If requested, add a differential equation for the old independent variable as a function of the old one + if add_old_diff + eqs = [eqs; Differential(iv2)(iv1_of_iv2) ~ 1 / div2_of_iv2] # e.g. dt(u)/du ~ 1 / uˍt(u) (https://en.wikipedia.org/wiki/Inverse_function_rule) + end + @set! sys.eqs = [get_eqs(sys); eqs] # add extra equations we derived before starting transformation process + @set! sys.unknowns = [get_unknowns(sys); [iv1, div2_of_iv1]] # add new variables, will be transformed to e.g. t(u) and uˍt(u) # add dummy variables and old independent variable as a function of the new one - # 1) Utility that performs the chain rule on an expression, e.g. (d/dt)(f(t)) -> (d/dt)(f(u(t))) -> df(u(t))/du(t) * du(t)/dt - function chain_rule(ex) + # Create a utility that performs the chain rule on an expression, followed by insertion of the new independent variable + # e.g. (d/dt)(f(t)) -> (d/dt)(f(u(t))) -> df(u(t))/du(t) * du(t)/dt -> df(u)/du * uˍt(u) + # Then use it to transform everything in the system! + function transform(ex) + # 1) Replace the argument of every function; e.g. f(t) -> f(u(t)) for var in vars(ex; op = Nothing) # loop over all variables in expression (op = Nothing prevents interpreting "D(f(t))" as one big variable) is_function_of_iv1 = iscall(var) && isequal(only(arguments(var)), iv1) # is the expression of the form f(t)? - if is_function_of_iv1 && !isequal(var, iv2_of_iv1) # substitute f(t) -> f(u(t)), but not u(t) -> u(u(t)) + if is_function_of_iv1 && !isequal(var, iv2_of_iv1) # prevent e.g. u(t) -> u(u(t)) var_of_iv1 = var # e.g. f(t) - var_of_iv2 = substitute(var_of_iv1, iv1 => iv2_of_iv1) # e.g. f(u(t)) - ex = substitute(ex, var_of_iv1 => var_of_iv2) + var_of_iv2_of_iv1 = substitute(var_of_iv1, iv1 => iv2_of_iv1) # e.g. f(u(t)) + ex = substitute(ex, var_of_iv1 => var_of_iv2_of_iv1; fold) end end - ex = expand_derivatives(ex, simplify) # expand chain rule, e.g. (d/dt)(f(u(t)))) -> df(u(t))/du(t) * du(t)/dt - return ex - end - - # 2) Find e.g. du/dt in equations, then calculate e.g. d²u/dt², ... - eqs = [eqs; get_eqs(sys)] # all equations (system-defined + user-provided) we may use - idxs = findall(eq -> isequal(eq.lhs, D1(iv2_of_iv1)), eqs) - if length(idxs) != 1 - error("Exactly one equation for $D1($iv2_of_iv1) was not specified! Got $(length(idxs)) equations:\n", join(eqs[idxs], '\n')) - end - div2_of_iv1_eq = popat!(eqs, only(idxs)) # get and remove e.g. du/dt = ... (may be added back later as a dummy) - div2_of_iv1 = chain_rule(div2_of_iv1_eq.rhs) - if isequal(div2_of_iv1, 0) # e.g. du/dt ~ 0 - error("Independent variable transformation $(div2_of_iv1_eq) is singular!") - end - ddiv2_of_iv1 = chain_rule(D1(div2_of_iv1)) # TODO: implement higher orders (order >= 3) derivatives with a loop - - # 3) If requested, insert extra dummy equations for e.g. du/dt, d²u/dt², ... - # Otherwise, replace all these derivatives by their explicit expressions - unks = get_unknowns(sys) - if dummies - div2 = substitute(default_toterm(D1(iv2_of_iv1)), iv1 => iv2) # e.g. uˍt(u) - ddiv2 = substitute(default_toterm(D1(D1(iv2_of_iv1))), iv1 => iv2) # e.g. uˍtt(u) - div2, ddiv2 = GlobalScope.([div2, ddiv2]) # do not namespace dummies in new system - eqs = [[div2 ~ div2_of_iv1, ddiv2 ~ ddiv2_of_iv1]; eqs] # add dummy equations - unks = [unks; [div2, ddiv2]] # add dummy variables - else - div2 = div2_of_iv1 - ddiv2 = ddiv2_of_iv1 - end - - # 4) If requested, add a differential equation for the old independent variable as a function of the old one - if add_old_diff - div1lhs = substitute(D2(iv1_of_iv2), iv2 => iv2_of_iv1) # e.g. (d/du(t))(t(u(t))); will be transformed to (d/du)(t(u)) by chain rule - div1rhs = 1 / div2 # du/dt = 1/(dt/du) (https://en.wikipedia.org/wiki/Inverse_function_rule) - eqs = [eqs; div1lhs ~ div1rhs] - unks = [unks; iv1_of_iv2] - end - @set! sys.eqs = eqs # add extra equations we derived before starting transformation process - @set! sys.unknowns = unks # add dummy variables and old independent variable as a function of the new one - - # 5) Transform everything from old to new independent variable, e.g. t -> u. - # Substitution order matters! Must begin with highest order to get D(D(u(t))) -> u_tt(u). - # If we had started with the lowest order, we would get D(D(u(t))) -> D(u_t(u)) -> 0! - iv1_to_iv2_subs = [ # a vector ensures substitution order - D1(D1(iv2_of_iv1)) => ddiv2 # order 2, e.g. D(D(u(t))) -> u_tt(u) or explicit expression - D1(iv2_of_iv1) => div2 # order 1, e.g. D(u(t)) -> u_t(u) or explicit expression - iv2_of_iv1 => iv2 # order 0, e.g. u(t) -> u - iv1 => iv1_of_iv2 # in case sys was autonomous, e.g. t -> t(u) - ] - function transform(ex) - ex = chain_rule(ex) - for sub in iv1_to_iv2_subs - ex = substitute(ex, sub; fold) + # 2) Expand with chain rule until nothing changes anymore + orgex = nothing + while !isequal(ex, orgex) + orgex = ex # save original + ex = expand_derivatives(ex, simplify) # expand chain rule, e.g. (d/dt)(f(u(t)))) -> df(u(t))/du(t) * du(t)/dt + ex = substitute(ex, D1(iv2_of_iv1) => div2_of_iv2_of_iv1; fold) # e.g. du(t)/dt -> uˍt(u(t)) end + # 3) Set new independent variable + ex = substitute(ex, iv2_of_iv1 => iv2; fold) # set e.g. u(t) -> u everywhere + ex = substitute(ex, iv1 => iv1_of_iv2; fold) # set e.g. t -> t(u) everywhere return ex end + function transform(sys::AbstractODESystem) eqs = map(transform, get_eqs(sys)) unknowns = map(transform, get_unknowns(sys)) @@ -203,7 +172,7 @@ function change_independent_variable(sys::AbstractODESystem, iv, eqs = []; dummi wascomplete = iscomplete(sys) # save before reconstructing system sys = typeof(sys)( # recreate system with transformed fields eqs, iv2, unknowns, ps; observed, initialization_eqs, parameter_dependencies, defaults, guesses, - assertions, name = nameof(sys), description = description(sys), kwargs... + assertions, name = nameof(sys), description = description(sys) ) systems = map(transform, systems) # recurse through subsystems sys = compose(sys, systems) # rebuild hierarchical system @@ -213,5 +182,6 @@ function change_independent_variable(sys::AbstractODESystem, iv, eqs = []; dummi end return sys end + return transform(sys) end diff --git a/test/basic_transformations.jl b/test/basic_transformations.jl index ac3720469a..8bf60ed68a 100644 --- a/test/basic_transformations.jl +++ b/test/basic_transformations.jl @@ -30,8 +30,9 @@ end eqs1 = [D(D(x)) ~ D(x) + x, D(y) ~ 1] M1 = ODESystem(eqs1, t; name = :M) M2 = change_independent_variable(M1, y) - eqs2 = substitute(equations(M2), M2.y => M1.t) # system should be equivalent when parametrized with y (since D(y) ~ 1), so substitute back ... - @test eqs1[1] == only(eqs2) # ... and check that the equations are unmodified + @variables y x(y) yˍt(y) + Dy = Differential(y) + @test Set(equations(M2)) == Set([yˍt^2*(Dy^2)(x) + yˍt*Dy(yˍt)*Dy(x) ~ x + Dy(x)*yˍt, yˍt ~ 1]) end @testset "Change independent variable" begin @@ -44,7 +45,7 @@ end ] initialization_eqs = [x ~ 1.0, y ~ 1.0, D(y) ~ 0.0] M1 = ODESystem(eqs, t; initialization_eqs, name = :M) - M2 = change_independent_variable(M1, s; dummies = true) + M2 = change_independent_variable(M1, s) M1 = structural_simplify(M1; allow_symbolic = true) M2 = structural_simplify(M2; allow_symbolic = true) @@ -77,16 +78,15 @@ end M1 = compose(M1, r, m, Λ) # Apply in two steps, where derivatives are defined at each step: first t -> a, then a -> b - M2 = change_independent_variable(M1, M1.a; dummies = true) + M2 = change_independent_variable(M1, M1.a) M2c = complete(M2) # just for the following equation comparison (without namespacing) - a, ȧ, Ω, Ωr, Ωm, ΩΛ, ϕ, aˍt, aˍtt = M2c.a, M2c.ȧ, M2c.Ω, M2c.r.Ω, M2c.m.Ω, M2c.Λ.Ω, M2c.ϕ, M2c.aˍt, M2c.aˍtt + a, ȧ, Ω, Ωr, Ωm, ΩΛ, ϕ, aˍt = M2c.a, M2c.ȧ, M2c.Ω, M2c.r.Ω, M2c.m.Ω, M2c.Λ.Ω, M2c.ϕ, M2c.aˍt Da = Differential(a) @test Set(equations(M2)) == Set([ - aˍt ~ ȧ # 1st order dummy equation - aˍtt ~ Da(ȧ) * aˍt # 2nd order dummy equation + aˍt ~ ȧ # dummy equation Ω ~ Ωr + Ωm + ΩΛ ȧ ~ √(Ω) * a^2 - aˍtt*Da(ϕ) + aˍt^2*(Da^2)(ϕ) ~ -3*aˍt^2/a*Da(ϕ) + Da(aˍt)*Da(ϕ)*aˍt + aˍt^2*(Da^2)(ϕ) ~ -3*aˍt^2/a*Da(ϕ) aˍt*Da(Ωr) ~ -4*Ωr*aˍt/a aˍt*Da(Ωm) ~ -3*Ωm*aˍt/a aˍt*Da(ΩΛ) ~ 0 @@ -104,13 +104,13 @@ end @testset "Change independent variable (simple)" begin @variables x(t) y1(t) # y(t)[1:1] # TODO: use array variables y(t)[1:2] when fixed: https://github.com/JuliaSymbolics/Symbolics.jl/issues/1383 Mt = ODESystem([D(x) ~ 2*x, D(y1) ~ y1], t; name = :M) - Mx = change_independent_variable(Mt, x; dummies = true) + Mx = change_independent_variable(Mt, x) @variables x xˍt(x) xˍtt(x) y1(x) # y(x)[1:1] # TODO: array variables Dx = Differential(x) - @test (Set(equations(Mx)) == Set([xˍt ~ 2*x, xˍtt ~ 2*xˍt, xˍt*Dx(y1) ~ y1])) + @test (Set(equations(Mx)) == Set([xˍt ~ 2*x, xˍt*Dx(y1) ~ y1])) end -@testset "Change independent variable (free fall)" begin +@testset "Change independent variable (free fall with 1st order horizontal equation)" begin @variables x(t) y(t) @parameters g v # gravitational acceleration and constant horizontal velocity Mt = ODESystem([D(D(y)) ~ -g, D(x) ~ v], t; name = :M) # gives (x, y) as function of t, ... @@ -122,6 +122,18 @@ end @test all(isapprox.(sol[Mx.y], sol[Mx.x - g*(Mx.t)^2/2]; atol = 1e-10)) # compare to analytical solution (x(t) = v*t, y(t) = v*t - g*t^2/2) end +@testset "Change independent variable (free fall with 2nd order horizontal equation)" begin + @variables x(t) y(t) + @parameters g # gravitational acceleration + Mt = ODESystem([D(D(y)) ~ -g, D(D(x)) ~ 0], t; name = :M) # gives (x, y) as function of t, ... + Mx = change_independent_variable(Mt, x; add_old_diff = true) # ... but we want y as a function of x + Mx = structural_simplify(Mx; allow_symbolic = true) + Dx = Differential(Mx.x) + prob = ODEProblem(Mx, [Mx.y => 0.0, Dx(Mx.y) => 1.0, Mx.t => 0.0, Mx.xˍt => 10.0], (0.0, 20.0), [g => 9.81]) # 1 = dy/dx = (dy/dt)/(dx/dt) means equal initial horizontal and vertical velocities + sol = solve(prob, Tsit5(); reltol = 1e-5) + @test all(isapprox.(sol[Mx.y], sol[Mx.x - g*(Mx.t)^2/2]; atol = 1e-10)) # compare to analytical solution (x(t) = v*t, y(t) = v*t - g*t^2/2) +end + @testset "Change independent variable (crazy analytical example)" begin @independent_variables t D = Differential(t) @@ -130,16 +142,15 @@ end D(D(y)) ~ D(x)^2 + D(y^3) |> expand_derivatives # expand D(y^3) # TODO: make this test 3rd order D(x) ~ x^4 + y^5 + t^6 ], t; name = :M) - M2 = change_independent_variable(M1, x; dummies = true) + M2 = change_independent_variable(M1, x) # Compare to pen-and-paper result @independent_variables x Dx = Differential(x) @variables xˍt(x) xˍtt(x) y(x) t(x) @test Set(equations(M2)) == Set([ - xˍt^2*(Dx^2)(y) + xˍtt*Dx(y) ~ xˍt^2 + 3*y^2*Dx(y)*xˍt # from D(D(y)) - xˍt ~ x^4 + y^5 + t^6 # 1st order dummy equation - xˍtt ~ 4*x^3*xˍt + 5*y^4*Dx(y)*xˍt + 6*t^5 # 2nd order dummy equation + xˍt^2*(Dx^2)(y) + xˍt*Dx(xˍt)*Dx(y) ~ xˍt^2 + 3*y^2*Dx(y)*xˍt # from D(D(y)) + xˍt ~ x^4 + y^5 + t^6 # dummy equation ]) end @@ -157,11 +168,12 @@ end # Ensure that interpolations are called with the same variables M2 = change_independent_variable(M1, x, [t ~ √(x)]) - @variables x y(x) t(x) + @variables x xˍt(x) y(x) t(x) Dx = Differential(x) @test Set(equations(M2)) == Set([ t ~ √(x) - 2*t*Dx(y) ~ 1*fc(t) + 2*fc(x) + 3*fc(y) + 1*callme(f, t) + 2*callme(f, x) + 3*callme(f, y) + xˍt ~ 2*t + xˍt*Dx(y) ~ 1*fc(t) + 2*fc(x) + 3*fc(y) + 1*callme(f, t) + 2*callme(f, x) + 3*callme(f, y) ]) _f = LinearInterpolation([1.0, 1.0], [-100.0, +100.0]) # constant value 1 @@ -173,18 +185,10 @@ end @testset "Change independent variable (errors)" begin @variables x(t) y z(y) w(t) v(t) - M = ODESystem([D(x) ~ 0, v ~ x], t; name = :M) - @test_throws "singular" change_independent_variable(M, x) + M = ODESystem([D(x) ~ 1, v ~ x], t; name = :M) @test_throws "structurally simplified" change_independent_variable(structural_simplify(M), y) - @test_throws "Got 0 equations:" change_independent_variable(M, w) - @test_throws "Got 0 equations:" change_independent_variable(M, v) - M = ODESystem([2 * D(x) ~ 1, v ~ x], t; name = :M) # TODO: allow equations like this - @test_throws "Got 0 equations:" change_independent_variable(M, x) - M = ODESystem([D(x) ~ 1, v ~ 1], t; name = :M) - @test_throws "Got 2 equations:" change_independent_variable(M, x, [D(x) ~ 2]) @test_throws "not a function of the independent variable" change_independent_variable(M, y) @test_throws "not a function of the independent variable" change_independent_variable(M, z) - M = ODESystem([D(x) ~ 0, v ~ x], t; name = :M) @variables x(..) # require explicit argument M = ODESystem([D(x(t)) ~ x(t-1)], t; name = :M) @test_throws "DDE" change_independent_variable(M, x(t)) From f2a1a5ae580487e3725ca85fbd9e1083719a1b48 Mon Sep 17 00:00:00 2001 From: Herman Sletmoen Date: Sun, 9 Mar 2025 12:25:15 +0100 Subject: [PATCH 1091/1337] Test 3rd order nonlinear system --- test/basic_transformations.jl | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/test/basic_transformations.jl b/test/basic_transformations.jl index 8bf60ed68a..dd6d677b60 100644 --- a/test/basic_transformations.jl +++ b/test/basic_transformations.jl @@ -134,24 +134,24 @@ end @test all(isapprox.(sol[Mx.y], sol[Mx.x - g*(Mx.t)^2/2]; atol = 1e-10)) # compare to analytical solution (x(t) = v*t, y(t) = v*t - g*t^2/2) end -@testset "Change independent variable (crazy analytical example)" begin +@testset "Change independent variable (crazy 3rd order nonlinear system)" begin @independent_variables t D = Differential(t) @variables x(t) y(t) - M1 = ODESystem([ # crazy non-autonomous non-linear 2nd order ODE - D(D(y)) ~ D(x)^2 + D(y^3) |> expand_derivatives # expand D(y^3) # TODO: make this test 3rd order - D(x) ~ x^4 + y^5 + t^6 + M1 = ODESystem([ + (D^3)(y) ~ D(x)^2 + (D^2)(y^2) |> expand_derivatives + D(x)^2 + D(y)^2 ~ x^4 + y^5 + t^6 ], t; name = :M) - M2 = change_independent_variable(M1, x) + M2 = change_independent_variable(M1, x; add_old_diff = true) + @test_nowarn structural_simplify(M2) # Compare to pen-and-paper result - @independent_variables x + @variables x xˍt(x) xˍt(x) y(x) t(x) Dx = Differential(x) - @variables xˍt(x) xˍtt(x) y(x) t(x) - @test Set(equations(M2)) == Set([ - xˍt^2*(Dx^2)(y) + xˍt*Dx(xˍt)*Dx(y) ~ xˍt^2 + 3*y^2*Dx(y)*xˍt # from D(D(y)) - xˍt ~ x^4 + y^5 + t^6 # dummy equation - ]) + areequivalent(eq1, eq2) = isequal(expand(eq1.lhs - eq2.lhs), 0) && isequal(expand(eq1.rhs - eq2.rhs), 0) + @test areequivalent(equations(M2)[1], xˍt^3*(Dx^3)(y) + xˍt^2*Dx(y)*(Dx^2)(xˍt) + xˍt*Dx(y)*(Dx(xˍt))^2 + 3*xˍt^2*(Dx^2)(y)*Dx(xˍt) ~ xˍt^2 + 2*xˍt^2*Dx(y)^2 + 2*xˍt^2*y*(Dx^2)(y) + 2*y*Dx(y)*Dx(xˍt)*xˍt) + @test areequivalent(equations(M2)[2], xˍt^2 + xˍt^2*Dx(y)^2 ~ x^4 + y^5 + t^6) + @test areequivalent(equations(M2)[3], Dx(t) ~ 1 / xˍt) end @testset "Change independent variable (registered function / callable parameter)" begin From aaae59df56fa8a200317420fba65579d56e12ecc Mon Sep 17 00:00:00 2001 From: Herman Sletmoen Date: Sun, 9 Mar 2025 12:33:09 +0100 Subject: [PATCH 1092/1337] Clean up and format --- .../tutorials/change_independent_variable.md | 78 +++++++----- src/ModelingToolkit.jl | 3 +- src/systems/diffeqs/basic_transformations.jl | 49 ++++--- test/basic_transformations.jl | 120 +++++++++++------- 4 files changed, 150 insertions(+), 100 deletions(-) diff --git a/docs/src/tutorials/change_independent_variable.md b/docs/src/tutorials/change_independent_variable.md index 8f9ee0cbd2..e1243fb2ee 100644 --- a/docs/src/tutorials/change_independent_variable.md +++ b/docs/src/tutorials/change_independent_variable.md @@ -2,11 +2,11 @@ Ordinary differential equations describe the rate of change of some dependent variables with respect to one independent variable. For the modeler it is often most natural to write down the equations with a particular independent variable, say time $t$. -In many cases there are good reasons for reparametrizing ODEs in terms of a different independent variable: +However, in many cases there are good reasons for changing the independent variable: -1. One may want $y(x)$ as a function of $x$ instead of $(x(t), y(t))$ as a function of $t$ -2. Some differential equations vary more nicely (e.g. less stiff or better behaved) with respect to one independent variable than another. -3. It can reduce the number of equations that must be solved (e.g. $y(x)$ is one equation, while $(x(t), y(t))$ are two). + 1. One may want $y(x)$ as a function of $x$ instead of $(x(t), y(t))$ as a function of $t$ + 2. Some differential equations vary more nicely (e.g. less stiff) with respect to one independent variable than another. + 3. It can reduce the number of equations that must be solved (e.g. $y(x)$ is one equation, while $(x(t), y(t))$ are two). To manually change the independent variable of an ODE, one must rewrite all equations in terms of a new variable and transform differentials with the chain rule. This is mechanical and error-prone. @@ -14,30 +14,34 @@ ModelingToolkit provides the utility function [`change_independent_variable`](@r ## 1. Get one dependent variable as a function of another -Consider a projectile shot with some initial velocity in a gravitational field. +Consider a projectile shot with some initial velocity in a vertical gravitational field with constant horizontal velocity. + ```@example changeivar using ModelingToolkit @independent_variables t D = Differential(t) @variables x(t) y(t) -@parameters g = 9.81 v # gravitational acceleration and constant horizontal velocity -M1 = ODESystem([ - D(D(y)) ~ -g, D(x) ~ v # constant horizontal velocity -], t; initialization_eqs = [ - D(x) ~ D(y) # equal initial horizontal and vertical velocity (45°) -], name = :M) +@parameters g=9.81 v # gravitational acceleration and horizontal velocity +eqs = [D(D(y)) ~ -g, D(x) ~ v] +initialization_eqs = [D(x) ~ D(y)] # 45° initial angle +M1 = ODESystem(eqs, t; initialization_eqs, name = :M) M1s = structural_simplify(M1) +@assert length(equations(M1s)) == 3 # hide +M1s # hide ``` + This is the standard parametrization that arises naturally from kinematics and Newton's laws. It expresses the position $(x(t), y(t))$ as a function of time $t$. But suppose we want to determine whether the projectile hits a target 10 meters away. There are at least three ways of answering this: -* Solve the ODE for $(x(t), y(t))$ and use a callback to terminate when $x$ reaches 10 meters, and evaluate $y$ at the final time. -* Solve the ODE for $(x(t), y(t))$ and use root finding to find the time when $x$ reaches 10 meters, and evaluate $y$ at that time. -* Solve the ODE for $y(x)$ and evaluate it at 10 meters. + + - Solve the ODE for $(x(t), y(t))$ and use a callback to terminate when $x$ reaches 10 meters, and evaluate $y$ at the final time. + - Solve the ODE for $(x(t), y(t))$ and use root finding to find the time when $x$ reaches 10 meters, and evaluate $y$ at that time. + - Solve the ODE for $y(x)$ and evaluate it at 10 meters. We will demonstrate the last method by changing the independent variable from $t$ to $x$. -This transformation is well-defined for any non-zero horizontal velocity $v$. +This transformation is well-defined for any non-zero horizontal velocity $v$, so $x$ and $t$ are one-to-one. + ```@example changeivar M2 = change_independent_variable(M1, x) M2s = structural_simplify(M2; allow_symbolic = true) @@ -46,17 +50,21 @@ M2s = structural_simplify(M2; allow_symbolic = true) @assert allequal([M2.x, M2s.x]) # hide @assert allequal([y, M1s.y]) # hide @assert allunique([M1.x, M1.y, M2.y, M2s.y]) # hide +@assert length(equations(M2s)) == 2 # hide M2s # display this # hide ``` + The derivatives are now with respect to the new independent variable $x$, which can be accessed with `M2.x`. !!! warn + At this point `x`, `M1.x`, `M1s.x`, `M2.x`, `M2s.x` are *three* different variables. Meanwhile `y`, `M1.y`, `M1s.y`, `M2.y` and `M2s.y` are *four* different variables. It can be instructive to inspect these yourself to see their subtle differences. Notice how the number of equations has also decreased from three to two, as $\mathrm{d}x/\mathrm{d}t$ has been turned into an observed equation. It is straightforward to evolve the ODE for 10 meters and plot the resulting trajectory $y(x)$: + ```@example changeivar using OrdinaryDiffEq, Plots prob = ODEProblem(M2s, [M2s.y => 0.0], [0.0, 10.0], [v => 8.0]) # throw 10 meters with x-velocity 8 m/s @@ -65,67 +73,75 @@ plot(sol; idxs = M2.y) # must index by M2.y = y(x); not M1.y = y(t)! ``` !!! tip "Usage tips" + Look up the documentation of [`change_independent_variable`](@ref) for tips on how to use it. - + For example, if you also need $t(x)$, you can tell it to add a differential equation for the old independent variable in terms of the new one using the [inverse function rule](https://en.wikipedia.org/wiki/Inverse_function_rule) (here $\mathrm{d}t/\mathrm{d}x = 1 / (\mathrm{d}x/\mathrm{d}t)$). If you know an analytical expression between the independent variables (here $t = x/v$), you can also pass it directly to the function to avoid the extra differential equation. ## 2. Alleviating stiffness by changing to logarithmic time In cosmology, the [Friedmann equations](https://en.wikipedia.org/wiki/Friedmann_equations) describe the expansion of the universe. In terms of conformal time $t$, they can be written + ```@example changeivar @variables a(t) Ω(t) a = GlobalScope(a) # global var needed by all species function species(w; kw...) - eqs = [D(Ω) ~ -3(1 + w) * D(a)/a * Ω] + eqs = [D(Ω) ~ -3(1 + w) * D(a) / a * Ω] return ODESystem(eqs, t, [Ω], []; kw...) end -@named r = species(1//3) # radiation +@named r = species(1 // 3) # radiation @named m = species(0) # matter @named Λ = species(-1) # dark energy / cosmological constant -eqs = [ - Ω ~ r.Ω + m.Ω + Λ.Ω # total energy density - D(a) ~ √(Ω) * a^2 -] -initialization_eqs = [ - Λ.Ω + r.Ω + m.Ω ~ 1 -] +eqs = [Ω ~ r.Ω + m.Ω + Λ.Ω, D(a) ~ √(Ω) * a^2] +initialization_eqs = [Λ.Ω + r.Ω + m.Ω ~ 1] M1 = ODESystem(eqs, t, [Ω, a], []; initialization_eqs, name = :M) M1 = compose(M1, r, m, Λ) M1s = structural_simplify(M1) ``` + Of course, we can solve this ODE as it is: + ```@example changeivar prob = ODEProblem(M1s, [M1s.a => 1.0, M1s.r.Ω => 5e-5, M1s.m.Ω => 0.3], (0.0, -3.3), []) sol = solve(prob, Tsit5(); reltol = 1e-5) @assert Symbol(sol.retcode) == :Unstable # surrounding text assumes this was unstable # hide -plot(sol, idxs = [M1.a, M1.r.Ω/M1.Ω, M1.m.Ω/M1.Ω, M1.Λ.Ω/M1.Ω]) +plot(sol, idxs = [M1.a, M1.r.Ω / M1.Ω, M1.m.Ω / M1.Ω, M1.Λ.Ω / M1.Ω]) ``` -The solver becomes unstable due to stiffness. + +But the solver becomes unstable due to stiffness. Also notice the interesting dynamics taking place towards the end of the integration (in the early universe), which gets compressed into a very small time interval. -These ODEs would benefit from being defined with respect to a logarithmic "time" that better captures the evolution of the universe through *orders of magnitude* of time. +These ODEs would benefit from being defined with respect to a logarithmic "time" that better captures the evolution of the universe through *orders of magnitude* of time, rather than linear time. It is therefore common to write these ODEs in terms of $b = \ln a$. -To do this, we will change the independent variable in two stages; from $t$ to $a$ to $b$. -Notice that $\mathrm{d}a/\mathrm{d}t > 0$ provided that $\Omega > 0$, and $\mathrm{d}b/\mathrm{d}a > 0$, so the transformation is well-defined. +To do this, we will change the independent variable in two stages; first from $t$ to $a$, and then from $a$ to $b$. +Notice that $\mathrm{d}a/\mathrm{d}t > 0$ provided that $\Omega > 0$, and $\mathrm{d}b/\mathrm{d}a > 0$, so the transformation is well-defined since $t \leftrightarrow a \leftrightarrow b$ are one-to-one. First, we transform from $t$ to $a$: + ```@example changeivar M2 = change_independent_variable(M1, M1.a) +@assert !ModelingToolkit.isautonomous(M2) # hide +M2 # hide ``` + Unlike the original, notice that this system is *non-autonomous* because the independent variable $a$ appears explicitly in the equations! This means that to change the independent variable from $a$ to $b$, we must provide not only the rate of change relation $db(a)/da = \exp(-b)$, but *also* the equation $a(b) = \exp(b)$ so $a$ can be eliminated in favor of $b$: + ```@example changeivar a = M2.a Da = Differential(a) @variables b(a) M3 = change_independent_variable(M2, b, [Da(b) ~ exp(-b), a ~ exp(b)]) ``` + We can now solve and plot the ODE in terms of $b$: + ```@example changeivar M3s = structural_simplify(M3; allow_symbolic = true) prob = ODEProblem(M3s, [M3s.r.Ω => 5e-5, M3s.m.Ω => 0.3], (0, -15), []) sol = solve(prob, Tsit5()) @assert Symbol(sol.retcode) == :Success # surrounding text assumes the solution was successful # hide -plot(sol, idxs = [M3.r.Ω/M3.Ω, M3.m.Ω/M3.Ω, M3.Λ.Ω/M3.Ω]) +plot(sol, idxs = [M3.r.Ω / M3.Ω, M3.m.Ω / M3.Ω, M3.Λ.Ω / M3.Ω]) ``` + Notice that the variables vary "more nicely" with respect to $b$ than $t$, making the solver happier and avoiding numerical problems. diff --git a/src/ModelingToolkit.jl b/src/ModelingToolkit.jl index 51156ba993..41f9292c73 100644 --- a/src/ModelingToolkit.jl +++ b/src/ModelingToolkit.jl @@ -255,7 +255,8 @@ export isinput, isoutput, getbounds, hasbounds, getguess, hasguess, isdisturbanc tunable_parameters, isirreducible, getdescription, hasdescription, hasunit, getunit, hasconnect, getconnect, hasmisc, getmisc, state_priority -export ode_order_lowering, dae_order_lowering, liouville_transform, change_independent_variable +export ode_order_lowering, dae_order_lowering, liouville_transform, + change_independent_variable export PDESystem export Differential, expand_derivatives, @derivatives export Equation, ConstrainedEquation diff --git a/src/systems/diffeqs/basic_transformations.jl b/src/systems/diffeqs/basic_transformations.jl index c05e5e4394..a08c83ffb6 100644 --- a/src/systems/diffeqs/basic_transformations.jl +++ b/src/systems/diffeqs/basic_transformations.jl @@ -47,11 +47,17 @@ function liouville_transform(sys::AbstractODESystem; kwargs...) neweq = D(trJ) ~ trJ * -tr(calculate_jacobian(sys)) neweqs = [equations(sys); neweq] vars = [unknowns(sys); trJ] - ODESystem(neweqs, t, vars, parameters(sys); checks = false, name = nameof(sys), kwargs...) + ODESystem( + neweqs, t, vars, parameters(sys); + checks = false, name = nameof(sys), kwargs... + ) end """ - change_independent_variable(sys::AbstractODESystem, iv, eqs = []; add_old_diff = false, simplify = true, fold = false, kwargs...) + change_independent_variable( + sys::AbstractODESystem, iv, eqs = []; + add_old_diff = false, simplify = true, fold = false + ) Transform the independent variable (e.g. ``t``) of the ODE system `sys` to a dependent variable `iv` (e.g. ``u(t)``). The transformation is well-defined when the mapping between the new and old independent variables are one-to-one. @@ -68,13 +74,13 @@ Any extra equations `eqs` involving the new and old independent variables will b # Usage before structural simplification The variable change must take place before structural simplification. -Subsequently, consider passing `allow_symbolic = true` to `structural_simplify(sys)` to reduce the number of unknowns, with the understanding that the transformation is well-defined. +In following calls to `structural_simplify`, consider passing `allow_symbolic = true` to avoid undesired constraint equations between between dummy variables. # Usage with non-autonomous systems -If `sys` is non-autonomous (i.e. ``t`` appears explicitly in its equations), it is often desirable to also pass an algebraic equation relating the new and old independent variables (e.g. ``t = f(u(t))``). -Otherwise the transformed system will be underdetermined and cannot be structurally simplified without additional changes. -If an algebraic relation is not known, consider using `add_old_diff`. +If `sys` is non-autonomous (i.e. ``t`` appears explicitly in its equations), consider passing an algebraic equation relating the new and old independent variables (e.g. ``t = f(u(t))``). +Otherwise the transformed system can be underdetermined. +If an algebraic relation is not known, consider using `add_old_diff` instead. # Usage with hierarchical systems @@ -102,7 +108,10 @@ julia> unknowns(M) yˍx(x) ``` """ -function change_independent_variable(sys::AbstractODESystem, iv, eqs = []; add_old_diff = false, simplify = true, fold = false, kwargs...) +function change_independent_variable( + sys::AbstractODESystem, iv, eqs = []; + add_old_diff = false, simplify = true, fold = false +) iv2_of_iv1 = unwrap(iv) # e.g. u(t) iv1 = get_iv(sys) # e.g. t @@ -114,6 +123,7 @@ function change_independent_variable(sys::AbstractODESystem, iv, eqs = []; add_o error("Variable $iv is not a function of the independent variable $iv1 of the system $(nameof(sys))!") end + # Set up intermediate and final variables for the transformation iv1name = nameof(iv1) # e.g. :t iv2name = nameof(operation(iv2_of_iv1)) # e.g. :u iv2, = @independent_variables $iv2name # e.g. u @@ -127,23 +137,22 @@ function change_independent_variable(sys::AbstractODESystem, iv, eqs = []; add_o if add_old_diff eqs = [eqs; Differential(iv2)(iv1_of_iv2) ~ 1 / div2_of_iv2] # e.g. dt(u)/du ~ 1 / uˍt(u) (https://en.wikipedia.org/wiki/Inverse_function_rule) end - @set! sys.eqs = [get_eqs(sys); eqs] # add extra equations we derived before starting transformation process - @set! sys.unknowns = [get_unknowns(sys); [iv1, div2_of_iv1]] # add new variables, will be transformed to e.g. t(u) and uˍt(u) # add dummy variables and old independent variable as a function of the new one + @set! sys.eqs = [get_eqs(sys); eqs] # add extra equations we derived + @set! sys.unknowns = [get_unknowns(sys); [iv1, div2_of_iv1]] # add new variables, will be transformed to e.g. t(u) and uˍt(u) - # Create a utility that performs the chain rule on an expression, followed by insertion of the new independent variable - # e.g. (d/dt)(f(t)) -> (d/dt)(f(u(t))) -> df(u(t))/du(t) * du(t)/dt -> df(u)/du * uˍt(u) - # Then use it to transform everything in the system! + # Create a utility that performs the chain rule on an expression, followed by insertion of the new independent variable: + # e.g. (d/dt)(f(t)) -> (d/dt)(f(u(t))) -> df(u(t))/du(t) * du(t)/dt -> df(u)/du * uˍt(u) function transform(ex) # 1) Replace the argument of every function; e.g. f(t) -> f(u(t)) for var in vars(ex; op = Nothing) # loop over all variables in expression (op = Nothing prevents interpreting "D(f(t))" as one big variable) - is_function_of_iv1 = iscall(var) && isequal(only(arguments(var)), iv1) # is the expression of the form f(t)? + is_function_of_iv1 = iscall(var) && isequal(only(arguments(var)), iv1) # of the form f(t)? if is_function_of_iv1 && !isequal(var, iv2_of_iv1) # prevent e.g. u(t) -> u(u(t)) var_of_iv1 = var # e.g. f(t) var_of_iv2_of_iv1 = substitute(var_of_iv1, iv1 => iv2_of_iv1) # e.g. f(u(t)) ex = substitute(ex, var_of_iv1 => var_of_iv2_of_iv1; fold) end end - # 2) Expand with chain rule until nothing changes anymore + # 2) Repeatedly expand chain rule until nothing changes anymore orgex = nothing while !isequal(ex, orgex) orgex = ex # save original @@ -156,22 +165,25 @@ function change_independent_variable(sys::AbstractODESystem, iv, eqs = []; add_o return ex end + # Use the utility function to transform everything in the system! function transform(sys::AbstractODESystem) eqs = map(transform, get_eqs(sys)) unknowns = map(transform, get_unknowns(sys)) unknowns = filter(var -> !isequal(var, iv2), unknowns) # remove e.g. u ps = map(transform, get_ps(sys)) - ps = filter(!isinitial, ps) # remove Initial(...) # # TODO: shouldn't have to touch this + ps = filter(!isinitial, ps) # remove Initial(...) # TODO: shouldn't have to touch this observed = map(transform, get_observed(sys)) initialization_eqs = map(transform, get_initialization_eqs(sys)) parameter_dependencies = map(transform, get_parameter_dependencies(sys)) - defaults = Dict(transform(var) => transform(val) for (var, val) in get_defaults(sys)) + defaults = Dict(transform(var) => transform(val) + for (var, val) in get_defaults(sys)) guesses = Dict(transform(var) => transform(val) for (var, val) in get_guesses(sys)) - assertions = Dict(transform(condition) => msg for (condition, msg) in get_assertions(sys)) + assertions = Dict(transform(ass) => msg for (ass, msg) in get_assertions(sys)) systems = get_systems(sys) # save before reconstructing system wascomplete = iscomplete(sys) # save before reconstructing system sys = typeof(sys)( # recreate system with transformed fields - eqs, iv2, unknowns, ps; observed, initialization_eqs, parameter_dependencies, defaults, guesses, + eqs, iv2, unknowns, ps; observed, initialization_eqs, + parameter_dependencies, defaults, guesses, assertions, name = nameof(sys), description = description(sys) ) systems = map(transform, systems) # recurse through subsystems @@ -182,6 +194,5 @@ function change_independent_variable(sys::AbstractODESystem, iv, eqs = []; add_o end return sys end - return transform(sys) end diff --git a/test/basic_transformations.jl b/test/basic_transformations.jl index dd6d677b60..544a89cd29 100644 --- a/test/basic_transformations.jl +++ b/test/basic_transformations.jl @@ -32,16 +32,19 @@ end M2 = change_independent_variable(M1, y) @variables y x(y) yˍt(y) Dy = Differential(y) - @test Set(equations(M2)) == Set([yˍt^2*(Dy^2)(x) + yˍt*Dy(yˍt)*Dy(x) ~ x + Dy(x)*yˍt, yˍt ~ 1]) + @test Set(equations(M2)) == Set([ + yˍt^2 * (Dy^2)(x) + yˍt * Dy(yˍt) * Dy(x) ~ x + Dy(x) * yˍt, + yˍt ~ 1 + ]) end @testset "Change independent variable" begin @variables x(t) y(t) z(t) s(t) eqs = [ - D(x) ~ y - D(D(y)) ~ 2 * x * D(y) - z ~ x + D(y) - D(s) ~ 1 / (2*s) + D(x) ~ y, + D(D(y)) ~ 2 * x * D(y), + z ~ x + D(y), + D(s) ~ 1 / (2 * s) ] initialization_eqs = [x ~ 1.0, y ~ 1.0, D(y) ~ 0.0] M1 = ODESystem(eqs, t; initialization_eqs, name = :M) @@ -55,8 +58,8 @@ end sol2 = solve(prob2, Tsit5(); reltol = 1e-10, abstol = 1e-10) ts = range(0.0, 1.0, length = 50) ss = .√(ts) - @test all(isapprox.(sol1(ts, idxs=M1.x), sol2(ss, idxs=M2.x); atol = 1e-7)) && - all(isapprox.(sol1(ts, idxs=M1.y), sol2(ss, idxs=M2.y); atol = 1e-7)) + @test all(isapprox.(sol1(ts, idxs = M1.x), sol2(ss, idxs = M2.x); atol = 1e-7)) && + all(isapprox.(sol1(ts, idxs = M1.y), sol2(ss, idxs = M2.y); atol = 1e-7)) end @testset "Change independent variable (Friedmann equation)" begin @@ -64,15 +67,15 @@ end D = Differential(t) @variables a(t) ȧ(t) Ω(t) ϕ(t) a, ȧ = GlobalScope.([a, ȧ]) - species(w; kw...) = ODESystem([D(Ω) ~ -3(1 + w) * D(a)/a * Ω], t, [Ω], []; kw...) - @named r = species(1//3) + species(w; kw...) = ODESystem([D(Ω) ~ -3(1 + w) * D(a) / a * Ω], t, [Ω], []; kw...) + @named r = species(1 // 3) @named m = species(0) @named Λ = species(-1) eqs = [ - Ω ~ r.Ω + m.Ω + Λ.Ω - D(a) ~ ȧ - ȧ ~ √(Ω) * a^2 - D(D(ϕ)) ~ -3*D(a)/a*D(ϕ) + Ω ~ r.Ω + m.Ω + Λ.Ω, + D(a) ~ ȧ, + ȧ ~ √(Ω) * a^2, + D(D(ϕ)) ~ -3 * D(a) / a * D(ϕ) ] M1 = ODESystem(eqs, t, [Ω, a, ȧ, ϕ], []; name = :M) M1 = compose(M1, r, m, Λ) @@ -80,20 +83,22 @@ end # Apply in two steps, where derivatives are defined at each step: first t -> a, then a -> b M2 = change_independent_variable(M1, M1.a) M2c = complete(M2) # just for the following equation comparison (without namespacing) - a, ȧ, Ω, Ωr, Ωm, ΩΛ, ϕ, aˍt = M2c.a, M2c.ȧ, M2c.Ω, M2c.r.Ω, M2c.m.Ω, M2c.Λ.Ω, M2c.ϕ, M2c.aˍt + a, ȧ, Ω, ϕ, aˍt = M2c.a, M2c.ȧ, M2c.Ω, M2c.ϕ, M2c.aˍt + Ωr, Ωm, ΩΛ = M2c.r.Ω, M2c.m.Ω, M2c.Λ.Ω Da = Differential(a) @test Set(equations(M2)) == Set([ - aˍt ~ ȧ # dummy equation - Ω ~ Ωr + Ωm + ΩΛ - ȧ ~ √(Ω) * a^2 - Da(aˍt)*Da(ϕ)*aˍt + aˍt^2*(Da^2)(ϕ) ~ -3*aˍt^2/a*Da(ϕ) - aˍt*Da(Ωr) ~ -4*Ωr*aˍt/a - aˍt*Da(Ωm) ~ -3*Ωm*aˍt/a - aˍt*Da(ΩΛ) ~ 0 + aˍt ~ ȧ, # dummy equation + Ω ~ Ωr + Ωm + ΩΛ, + ȧ ~ √(Ω) * a^2, + Da(aˍt) * Da(ϕ) * aˍt + aˍt^2 * (Da^2)(ϕ) ~ -3 * aˍt^2 / a * Da(ϕ), + aˍt * Da(Ωr) ~ -4 * Ωr * aˍt / a, + aˍt * Da(Ωm) ~ -3 * Ωm * aˍt / a, + aˍt * Da(ΩΛ) ~ 0 ]) @variables b(M2.a) - M3 = change_independent_variable(M2, b, [Differential(M2.a)(b) ~ exp(-b), M2.a ~ exp(b)]) + extraeqs = [Differential(M2.a)(b) ~ exp(-b), M2.a ~ exp(b)] + M3 = change_independent_variable(M2, b, extraeqs) M1 = structural_simplify(M1) M2 = structural_simplify(M2; allow_symbolic = true) @@ -103,55 +108,69 @@ end @testset "Change independent variable (simple)" begin @variables x(t) y1(t) # y(t)[1:1] # TODO: use array variables y(t)[1:2] when fixed: https://github.com/JuliaSymbolics/Symbolics.jl/issues/1383 - Mt = ODESystem([D(x) ~ 2*x, D(y1) ~ y1], t; name = :M) + Mt = ODESystem([D(x) ~ 2 * x, D(y1) ~ y1], t; name = :M) Mx = change_independent_variable(Mt, x) @variables x xˍt(x) xˍtt(x) y1(x) # y(x)[1:1] # TODO: array variables Dx = Differential(x) - @test (Set(equations(Mx)) == Set([xˍt ~ 2*x, xˍt*Dx(y1) ~ y1])) + @test Set(equations(Mx)) == Set([xˍt ~ 2 * x, xˍt * Dx(y1) ~ y1]) end @testset "Change independent variable (free fall with 1st order horizontal equation)" begin @variables x(t) y(t) - @parameters g v # gravitational acceleration and constant horizontal velocity + @parameters g=9.81 v # gravitational acceleration and constant horizontal velocity Mt = ODESystem([D(D(y)) ~ -g, D(x) ~ v], t; name = :M) # gives (x, y) as function of t, ... Mx = change_independent_variable(Mt, x; add_old_diff = true) # ... but we want y as a function of x Mx = structural_simplify(Mx; allow_symbolic = true) Dx = Differential(Mx.x) - prob = ODEProblem(Mx, [Mx.y => 0.0, Dx(Mx.y) => 1.0, Mx.t => 0.0], (0.0, 20.0), [g => 9.81, v => 10.0]) # 1 = dy/dx = (dy/dt)/(dx/dt) means equal initial horizontal and vertical velocities + u0 = [Mx.y => 0.0, Dx(Mx.y) => 1.0, Mx.t => 0.0] + p = [v => 10.0] + prob = ODEProblem(Mx, u0, (0.0, 20.0), p) # 1 = dy/dx = (dy/dt)/(dx/dt) means equal initial horizontal and vertical velocities sol = solve(prob, Tsit5(); reltol = 1e-5) - @test all(isapprox.(sol[Mx.y], sol[Mx.x - g*(Mx.t)^2/2]; atol = 1e-10)) # compare to analytical solution (x(t) = v*t, y(t) = v*t - g*t^2/2) + @test all(isapprox.(sol[Mx.y], sol[Mx.x - g * (Mx.t)^2 / 2]; atol = 1e-10)) # compare to analytical solution (x(t) = v*t, y(t) = v*t - g*t^2/2) end @testset "Change independent variable (free fall with 2nd order horizontal equation)" begin @variables x(t) y(t) - @parameters g # gravitational acceleration + @parameters g = 9.81 # gravitational acceleration Mt = ODESystem([D(D(y)) ~ -g, D(D(x)) ~ 0], t; name = :M) # gives (x, y) as function of t, ... Mx = change_independent_variable(Mt, x; add_old_diff = true) # ... but we want y as a function of x Mx = structural_simplify(Mx; allow_symbolic = true) Dx = Differential(Mx.x) - prob = ODEProblem(Mx, [Mx.y => 0.0, Dx(Mx.y) => 1.0, Mx.t => 0.0, Mx.xˍt => 10.0], (0.0, 20.0), [g => 9.81]) # 1 = dy/dx = (dy/dt)/(dx/dt) means equal initial horizontal and vertical velocities + u0 = [Mx.y => 0.0, Dx(Mx.y) => 1.0, Mx.t => 0.0, Mx.xˍt => 10.0] + prob = ODEProblem(Mx, u0, (0.0, 20.0), []) # 1 = dy/dx = (dy/dt)/(dx/dt) means equal initial horizontal and vertical velocities sol = solve(prob, Tsit5(); reltol = 1e-5) - @test all(isapprox.(sol[Mx.y], sol[Mx.x - g*(Mx.t)^2/2]; atol = 1e-10)) # compare to analytical solution (x(t) = v*t, y(t) = v*t - g*t^2/2) + @test all(isapprox.(sol[Mx.y], sol[Mx.x - g * (Mx.t)^2 / 2]; atol = 1e-10)) # compare to analytical solution (x(t) = v*t, y(t) = v*t - g*t^2/2) end @testset "Change independent variable (crazy 3rd order nonlinear system)" begin @independent_variables t D = Differential(t) @variables x(t) y(t) - M1 = ODESystem([ - (D^3)(y) ~ D(x)^2 + (D^2)(y^2) |> expand_derivatives + eqs = [ + (D^3)(y) ~ D(x)^2 + (D^2)(y^2) |> expand_derivatives, D(x)^2 + D(y)^2 ~ x^4 + y^5 + t^6 - ], t; name = :M) + ] + M1 = ODESystem(eqs, t; name = :M) M2 = change_independent_variable(M1, x; add_old_diff = true) @test_nowarn structural_simplify(M2) # Compare to pen-and-paper result @variables x xˍt(x) xˍt(x) y(x) t(x) Dx = Differential(x) - areequivalent(eq1, eq2) = isequal(expand(eq1.lhs - eq2.lhs), 0) && isequal(expand(eq1.rhs - eq2.rhs), 0) - @test areequivalent(equations(M2)[1], xˍt^3*(Dx^3)(y) + xˍt^2*Dx(y)*(Dx^2)(xˍt) + xˍt*Dx(y)*(Dx(xˍt))^2 + 3*xˍt^2*(Dx^2)(y)*Dx(xˍt) ~ xˍt^2 + 2*xˍt^2*Dx(y)^2 + 2*xˍt^2*y*(Dx^2)(y) + 2*y*Dx(y)*Dx(xˍt)*xˍt) - @test areequivalent(equations(M2)[2], xˍt^2 + xˍt^2*Dx(y)^2 ~ x^4 + y^5 + t^6) - @test areequivalent(equations(M2)[3], Dx(t) ~ 1 / xˍt) + areequivalent(eq1, eq2) = isequal(expand(eq1.lhs - eq2.lhs), 0) && + isequal(expand(eq1.rhs - eq2.rhs), 0) + eq1lhs = xˍt^3 * (Dx^3)(y) + xˍt^2 * Dx(y) * (Dx^2)(xˍt) + + xˍt * Dx(y) * (Dx(xˍt))^2 + + 3 * xˍt^2 * (Dx^2)(y) * Dx(xˍt) + eq1rhs = xˍt^2 + 2 * xˍt^2 * Dx(y)^2 + + 2 * xˍt^2 * y * (Dx^2)(y) + + 2 * y * Dx(y) * Dx(xˍt) * xˍt + eq1 = eq1lhs ~ eq1rhs + eq2 = xˍt^2 + xˍt^2 * Dx(y)^2 ~ x^4 + y^5 + t^6 + eq3 = Dx(t) ~ 1 / xˍt + @test areequivalent(equations(M2)[1], eq1) + @test areequivalent(equations(M2)[2], eq2) + @test areequivalent(equations(M2)[3], eq3) end @testset "Change independent variable (registered function / callable parameter)" begin @@ -161,35 +180,38 @@ end @parameters f::LinearInterpolation (fc::LinearInterpolation)(..) # non-callable and callable callme(interp::LinearInterpolation, input) = interp(input) @register_symbolic callme(interp::LinearInterpolation, input) - M1 = ODESystem([ - D(x) ~ 2*t - D(y) ~ 1*fc(t) + 2*fc(x) + 3*fc(y) + 1*callme(f, t) + 2*callme(f, x) + 3*callme(f, y) - ], t; name = :M) + eqs = [ + D(x) ~ 2t, + D(y) ~ 1fc(t) + 2fc(x) + 3fc(y) + 1callme(f, t) + 2callme(f, x) + 3callme(f, y) + ] + M1 = ODESystem(eqs, t; name = :M) # Ensure that interpolations are called with the same variables M2 = change_independent_variable(M1, x, [t ~ √(x)]) @variables x xˍt(x) y(x) t(x) Dx = Differential(x) @test Set(equations(M2)) == Set([ - t ~ √(x) - xˍt ~ 2*t - xˍt*Dx(y) ~ 1*fc(t) + 2*fc(x) + 3*fc(y) + 1*callme(f, t) + 2*callme(f, x) + 3*callme(f, y) + t ~ √(x), + xˍt ~ 2t, + xˍt * Dx(y) ~ 1fc(t) + 2fc(x) + 3fc(y) + + 1callme(f, t) + 2callme(f, x) + 3callme(f, y) ]) _f = LinearInterpolation([1.0, 1.0], [-100.0, +100.0]) # constant value 1 M2s = structural_simplify(M2; allow_symbolic = true) prob = ODEProblem(M2s, [M2s.y => 0.0], (1.0, 4.0), [fc => _f, f => _f]) sol = solve(prob, Tsit5(); abstol = 1e-5) - @test isapprox(sol(4.0, idxs=M2.y), 12.0; atol = 1e-5) # Anal solution is D(y) ~ 12 => y(t) ~ 12*t + C => y(x) ~ 12*√(x) + C. With y(x=1)=0 => 12*(√(x)-1), so y(x=4) ~ 12 + @test isapprox(sol(4.0, idxs = M2.y), 12.0; atol = 1e-5) # Anal solution is D(y) ~ 12 => y(t) ~ 12*t + C => y(x) ~ 12*√(x) + C. With y(x=1)=0 => 12*(√(x)-1), so y(x=4) ~ 12 end @testset "Change independent variable (errors)" begin @variables x(t) y z(y) w(t) v(t) M = ODESystem([D(x) ~ 1, v ~ x], t; name = :M) - @test_throws "structurally simplified" change_independent_variable(structural_simplify(M), y) - @test_throws "not a function of the independent variable" change_independent_variable(M, y) - @test_throws "not a function of the independent variable" change_independent_variable(M, z) + Ms = structural_simplify(M) + @test_throws "structurally simplified" change_independent_variable(Ms, y) + @test_throws "not a function of" change_independent_variable(M, y) + @test_throws "not a function of" change_independent_variable(M, z) @variables x(..) # require explicit argument - M = ODESystem([D(x(t)) ~ x(t-1)], t; name = :M) + M = ODESystem([D(x(t)) ~ x(t - 1)], t; name = :M) @test_throws "DDE" change_independent_variable(M, x(t)) end From a98397ca205309603e87f2c12433ffd49cfd0296 Mon Sep 17 00:00:00 2001 From: vyudu Date: Mon, 10 Mar 2025 15:15:38 -0400 Subject: [PATCH 1093/1337] remove Next and Prev --- src/discretedomain.jl | 13 ------------- .../discrete_system/implicit_discrete_system.jl | 6 +++--- 2 files changed, 3 insertions(+), 16 deletions(-) diff --git a/src/discretedomain.jl b/src/discretedomain.jl index 0d8d38e15d..7260237053 100644 --- a/src/discretedomain.jl +++ b/src/discretedomain.jl @@ -70,19 +70,6 @@ Base.literal_pow(f::typeof(^), D::Shift, ::Val{n}) where {n} = Shift(D.t, D.step hasshift(eq::Equation) = hasshift(eq.lhs) || hasshift(eq.rhs) -""" - Next(x) - -An alias for Shift(t, 1)(x). -""" -Next(x) = Shift(t, 1)(x) -""" - Prev(x) - -An alias for Shift(t, -1)(x). -""" -Prev(x) = Shift(t, -1)(x) - """ hasshift(O) diff --git a/src/systems/discrete_system/implicit_discrete_system.jl b/src/systems/discrete_system/implicit_discrete_system.jl index ae5524768b..327c82c47f 100644 --- a/src/systems/discrete_system/implicit_discrete_system.jl +++ b/src/systems/discrete_system/implicit_discrete_system.jl @@ -268,17 +268,17 @@ function generate_function( iv = get_iv(sys) # Algebraic equations get shifted forward 1, to match with differential equations exprs = map(equations(sys)) do eq - _iszero(eq.lhs) ? distribute_shift(Next(eq.rhs)) : (eq.rhs - eq.lhs) + _iszero(eq.lhs) ? distribute_shift(Shift(iv, 1)(eq.rhs)) : (eq.rhs - eq.lhs) end # Handle observables in algebraic equations, since they are shifted obs = observed(sys) - shifted_obs = Symbolics.Equation[distribute_shift(Next(eq)) for eq in obs] + shifted_obs = Symbolics.Equation[distribute_shift(Shift(iv, 1)(eq)) for eq in obs] obsidxs = observed_equations_used_by(sys, exprs; obs = shifted_obs) extra_assignments = [Assignment(shifted_obs[i].lhs, shifted_obs[i].rhs) for i in obsidxs] - u_next = map(Next, dvs) + u_next = map(Shift(iv, 1), dvs) u = dvs build_function_wrapper( sys, exprs, u_next, u, ps..., iv; p_start = 3, extra_assignments, kwargs...) From d40fd6e69f21919d0984f61bf990100653121b99 Mon Sep 17 00:00:00 2001 From: vyudu Date: Mon, 10 Mar 2025 15:16:42 -0400 Subject: [PATCH 1094/1337] remove Next and Prev docs --- docs/src/systems/DiscreteSystem.md | 2 -- docs/src/systems/ImplicitDiscreteSystem.md | 2 -- src/doc | 1 + 3 files changed, 1 insertion(+), 4 deletions(-) create mode 100644 src/doc diff --git a/docs/src/systems/DiscreteSystem.md b/docs/src/systems/DiscreteSystem.md index c00cc7f3c9..f8a71043ab 100644 --- a/docs/src/systems/DiscreteSystem.md +++ b/docs/src/systems/DiscreteSystem.md @@ -31,6 +31,4 @@ DiscreteFunction(sys::DiscreteSystem, args...) ```@docs; canonical=false Shift -Prev -Next ``` diff --git a/docs/src/systems/ImplicitDiscreteSystem.md b/docs/src/systems/ImplicitDiscreteSystem.md index 268d8ec13c..d69f88f106 100644 --- a/docs/src/systems/ImplicitDiscreteSystem.md +++ b/docs/src/systems/ImplicitDiscreteSystem.md @@ -31,6 +31,4 @@ ImplicitDiscreteFunction(sys::ImplicitDiscreteSystem, args...) ```@docs; canonical=false Shift -Prev -Next ``` diff --git a/src/doc b/src/doc new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/src/doc @@ -0,0 +1 @@ + From e87b804aa6012a3570c22c27739825b984cd6869 Mon Sep 17 00:00:00 2001 From: Ashutosh Bharambe Date: Wed, 12 Mar 2025 09:37:51 +0000 Subject: [PATCH 1095/1337] fix: return `Expr` for `expression=true` in `build_explicit_observed_function` --- src/systems/diffeqs/odesystem.jl | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/systems/diffeqs/odesystem.jl b/src/systems/diffeqs/odesystem.jl index 33cdc59909..2660cdbd85 100644 --- a/src/systems/diffeqs/odesystem.jl +++ b/src/systems/diffeqs/odesystem.jl @@ -567,12 +567,18 @@ function build_explicit_observed_function(sys, ts; sys, ts, args...; p_start, p_end, filter_observed = obsfilter, output_type, mkarray, try_namespaced = true, expression = Val{true}) if fns isa Tuple + if expression + return return_inplace ? fns : fns[1] + end oop, iip = eval_or_rgf.(fns; eval_expression, eval_module) f = GeneratedFunctionWrapper{( p_start + is_dde(sys), length(args) - length(ps) + 1 + is_dde(sys), is_split(sys))}( oop, iip) return return_inplace ? (f, f) : f else + if expression + return fns + end f = eval_or_rgf(fns; eval_expression, eval_module) f = GeneratedFunctionWrapper{( p_start + is_dde(sys), length(args) - length(ps) + 1 + is_dde(sys), is_split(sys))}( From ddb385a7073b03a7225dbec6fa72082d7895164b Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 12 Mar 2025 15:31:51 +0530 Subject: [PATCH 1096/1337] feat: add `ignored_connections` field to `ODESystem` --- src/systems/abstractsystem.jl | 1 + src/systems/diffeqs/odesystem.jl | 11 +++++++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index 8945d4022b..2004bbce95 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -878,6 +878,7 @@ for prop in [:eqs :assertions :solved_unknowns :split_idxs + :ignored_connections :parent :is_dde :tstops diff --git a/src/systems/diffeqs/odesystem.jl b/src/systems/diffeqs/odesystem.jl index 33cdc59909..491da2a8d6 100644 --- a/src/systems/diffeqs/odesystem.jl +++ b/src/systems/diffeqs/odesystem.jl @@ -189,6 +189,12 @@ struct ODESystem <: AbstractODESystem """ split_idxs::Union{Nothing, Vector{Vector{Int}}} """ + The connections to ignore (since they're removed by analysis point transformations). + The first element of the tuple are systems that can't be in the same connection set, + and the second are variables (for the trivial form of `connect`). + """ + ignored_connections::Union{Nothing, Tuple{Vector{ODESystem}, Vector{BasicSymbolic}}} + """ The hierarchical parent system before simplification. """ parent::Any @@ -203,7 +209,8 @@ struct ODESystem <: AbstractODESystem tstops = [], tearing_state = nothing, substitutions = nothing, complete = false, index_cache = nothing, discrete_subsystems = nothing, solved_unknowns = nothing, - split_idxs = nothing, parent = nothing; checks::Union{Bool, Int} = true) + split_idxs = nothing, ignored_connections = nothing, parent = nothing; + checks::Union{Bool, Int} = true) if checks == true || (checks & CheckComponents) > 0 check_independent_variables([iv]) check_variables(dvs, iv) @@ -221,7 +228,7 @@ struct ODESystem <: AbstractODESystem initializesystem, initialization_eqs, schedule, connector_type, preface, cevents, devents, parameter_dependencies, assertions, metadata, gui_metadata, is_dde, tstops, tearing_state, substitutions, complete, index_cache, - discrete_subsystems, solved_unknowns, split_idxs, parent) + discrete_subsystems, solved_unknowns, split_idxs, ignored_connections, parent) end end From 0a405f1b9fef71b831a61e6f294aecb49b845368 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 12 Mar 2025 15:32:56 +0530 Subject: [PATCH 1097/1337] feat: track connections removed by analysis point transformations --- src/systems/analysis_points.jl | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/src/systems/analysis_points.jl b/src/systems/analysis_points.jl index 135bf81729..70c41b50c9 100644 --- a/src/systems/analysis_points.jl +++ b/src/systems/analysis_points.jl @@ -412,6 +412,27 @@ function get_analysis_variable(var, name, iv; perturb = true) return pvar, default end +function with_analysis_point_ignored(sys::AbstractSystem, ap::AnalysisPoint) + has_ignored_connections(sys) || return sys + ignored = get_ignored_connections(sys) + if ignored === nothing + ignored = (ODESystem[], BasicSymbolic[]) + else + ignored = copy.(ignored) + end + if ap.outputs === nothing + error("Empty analysis point") + end + for x in ap.outputs + if x isa ODESystem + push!(ignored[1], x) + else + push!(ignored[2], unwrap(x)) + end + end + return @set sys.ignored_connections = ignored +end + #### PRIMITIVE TRANSFORMATIONS const DOC_WILL_REMOVE_AP = """ @@ -469,7 +490,9 @@ function apply_transformation(tf::Break, sys::AbstractSystem) ap = breaksys_eqs[ap_idx].rhs deleteat!(breaksys_eqs, ap_idx) - tf.add_input || return sys, () + breaksys = with_analysis_point_ignored(breaksys, ap) + + tf.add_input || return breaksys, () ap_ivar = ap_var(ap.input) new_var, new_def = get_analysis_variable(ap_ivar, nameof(ap), get_iv(sys)) @@ -511,7 +534,7 @@ function apply_transformation(tf::GetInput, sys::AbstractSystem) ap_idx === nothing && error("Analysis point $(nameof(tf.ap)) not found in system $(nameof(sys)).") # get the anlysis point - ap_sys_eqs = copy(get_eqs(ap_sys)) + ap_sys_eqs = get_eqs(ap_sys) ap = ap_sys_eqs[ap_idx].rhs # input variable @@ -570,6 +593,7 @@ function apply_transformation(tf::PerturbOutput, sys::AbstractSystem) ap = ap_sys_eqs[ap_idx].rhs # remove analysis point deleteat!(ap_sys_eqs, ap_idx) + ap_sys = with_analysis_point_ignored(ap_sys, ap) # add equations involving new variable ap_ivar = ap_var(ap.input) @@ -634,7 +658,7 @@ function apply_transformation(tf::AddVariable, sys::AbstractSystem) ap_idx = analysis_point_index(ap_sys, tf.ap) ap_idx === nothing && error("Analysis point $(nameof(tf.ap)) not found in system $(nameof(sys)).") - ap_sys_eqs = copy(get_eqs(ap_sys)) + ap_sys_eqs = get_eqs(ap_sys) ap = ap_sys_eqs[ap_idx].rhs # add equations involving new variable From 8629c6af85300476374410a06f89254dd3c343c8 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 12 Mar 2025 15:33:43 +0530 Subject: [PATCH 1098/1337] fix: ignore connections removed by AP transforms in `expand_connections` --- src/systems/abstractsystem.jl | 69 ++++++++++++++++++++++++++ src/systems/connectors.jl | 92 +++++++++++++++++++++++++++++++---- 2 files changed, 151 insertions(+), 10 deletions(-) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index 2004bbce95..c0b3601e21 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -1395,6 +1395,75 @@ function assertions(sys::AbstractSystem) return merge(asserts, namespaced_asserts) end +const HierarchyVariableT = Vector{Union{BasicSymbolic, Symbol}} +const HierarchySystemT = Vector{Union{AbstractSystem, Symbol}} +""" +The type returned from `as_hierarchy`. +""" +const HierarchyT = Union{HierarchyVariableT, HierarchySystemT} + +""" + $(TYPEDSIGNATURES) + +The inverse operation of `as_hierarchy`. +""" +function from_hierarchy(hierarchy::HierarchyT) + namefn = hierarchy[1] isa AbstractSystem ? nameof : getname + foldl(@view hierarchy[2:end]; init = hierarchy[1]) do sys, name + rename(sys, Symbol(name, NAMESPACE_SEPARATOR, namefn(sys))) + end +end + +""" + $(TYPEDSIGNATURES) + +Represent a namespaced system (or variable) `sys` as a hierarchy. Return a vector, where +the first element is the unnamespaced system (variable) and subsequent elements are +`Symbol`s representing the parents of the unnamespaced system (variable) in order from +inner to outer. +""" +function as_hierarchy(sys::Union{AbstractSystem, BasicSymbolic})::HierarchyT + namefn = sys isa AbstractSystem ? nameof : getname + # get the hierarchy + hierarchy = namespace_hierarchy(namefn(sys)) + # rename the system with unnamespaced name + newsys = rename(sys, hierarchy[end]) + # and remove it from the list + pop!(hierarchy) + # reverse it to go from inner to outer + reverse!(hierarchy) + # concatenate + T = sys isa AbstractSystem ? AbstractSystem : BasicSymbolic + return Union{Symbol, T}[newsys; hierarchy] +end + +""" + $(TYPEDSIGNATURES) + +Get the connections to ignore for `sys` and its subsystems. The returned value is a +`Tuple` similar in structure to the `ignored_connections` field. Each system (variable) +in the first (second) element of the tuple is also passed through `as_hierarchy`. +""" +function ignored_connections(sys::AbstractSystem) + has_ignored_connections(sys) || return (HierarchySystemT[], HierarchyVariableT[]) + + ics = get_ignored_connections(sys) + if ics === nothing + ics = (HierarchySystemT[], HierarchyVariableT[]) + end + # turn into hierarchies + ics = (map(as_hierarchy, ics[1]), map(as_hierarchy, ics[2])) + systems = get_systems(sys) + # for each subsystem, get its ignored connections, add the name of the subsystem + # to the hierarchy and concatenate corresponding buffers of the result + result = mapreduce(Broadcast.BroadcastFunction(vcat), systems; init = ics) do subsys + sub_ics = ignored_connections(subsys) + (map(Base.Fix2(push!, nameof(subsys)), sub_ics[1]), + map(Base.Fix2(push!, nameof(subsys)), sub_ics[2])) + end + return (Vector{HierarchySystemT}(result[1]), Vector{HierarchyVariableT}(result[2])) +end + """ $(TYPEDSIGNATURES) diff --git a/src/systems/connectors.jl b/src/systems/connectors.jl index db3349f546..f2ef5ac0f6 100644 --- a/src/systems/connectors.jl +++ b/src/systems/connectors.jl @@ -395,16 +395,43 @@ function generate_connection_set( connectionsets = ConnectionSet[] domain_csets = ConnectionSet[] sys = generate_connection_set!( - connectionsets, domain_csets, sys, find, replace, scalarize) + connectionsets, domain_csets, sys, find, replace, scalarize, nothing, + # include systems to be ignored + ignored_connections(sys)[1]) csets = merge(connectionsets) domain_csets = merge([csets; domain_csets], true) sys, (csets, domain_csets) end +""" + $(TYPEDSIGNATURES) + +Generate connection sets from `connect` equations. + +# Arguments + +- `connectionsets` is the list of connection sets to be populated by recursively + descending `sys`. +- `domain_csets` is the list of connection sets for domain connections. +- `sys` is the system whose equations are to be searched. +- `namespace` is a system representing the namespace in which `sys` exists, or `nothing` + for no namespace (if `sys` is top-level). +- `ignored_systems` is a list of systems (in the format returned by `as_hierarchy`) to + be ignored when generating connections. This is typically because the connections + they are used in were removed by analysis point transformations. +""" function generate_connection_set!(connectionsets, domain_csets, - sys::AbstractSystem, find, replace, scalarize, namespace = nothing) + sys::AbstractSystem, find, replace, scalarize, namespace = nothing, ignored_systems = []) subsys = get_systems(sys) + # turn hierarchies into namespaced systems + namespaced_ignored = from_hierarchy.(ignored_systems) + # filter the subsystems of `sys` to exclude ignored ones + filtered_subsys = filter(subsys) do ss + all(namespaced_ignored) do igsys + nameof(igsys) != nameof(ss) + end + end isouter = generate_isouter(sys) eqs′ = get_eqs(sys) @@ -430,9 +457,21 @@ function generate_connection_set!(connectionsets, domain_csets, neweq isa AbstractArray ? append!(eqs, neweq) : push!(eqs, neweq) else if lhs isa Connection && get_systems(lhs) === :domain - connection2set!(domain_csets, namespace, get_systems(rhs), isouter) + # don't consider systems that should be ignored + systems_to_connect = filter(get_systems(rhs)) do ss + all(namespaced_ignored) do igsys + nameof(igsys) != nameof(ss) + end + end + connection2set!(domain_csets, namespace, systems_to_connect, isouter) elseif isconnection(rhs) - push!(cts, get_systems(rhs)) + # ignore required systems + systems_to_connect = filter(get_systems(rhs)) do ss + all(namespaced_ignored) do igsys + nameof(igsys) != nameof(ss) + end + end + push!(cts, systems_to_connect) else # split connections and equations if eq.lhs isa AbstractArray || eq.rhs isa AbstractArray @@ -446,7 +485,8 @@ function generate_connection_set!(connectionsets, domain_csets, # all connectors are eventually inside connectors. T = ConnectionElement - for s in subsys + # only generate connection sets for systems that are not ignored + for s in filtered_subsys isconnector(s) || continue is_domain_connector(s) && continue for v in unknowns(s) @@ -465,12 +505,32 @@ function generate_connection_set!(connectionsets, domain_csets, end @set! sys.systems = map( s -> generate_connection_set!(connectionsets, domain_csets, s, - find, replace, scalarize, - renamespace(namespace, s)), + find, replace, scalarize, renamespace(namespace, s), + ignored_systems_for_subsystem(s, ignored_systems)), subsys) @set! sys.eqs = eqs end +""" + $(TYPEDSIGNATURES) + +Given a subsystem `subsys` of a parent system and a list of systems (variables) to be +ignored by `generate_connection_set!` (`expand_variable_connections`), filter +`ignored_systems` to only include those present in the subtree of `subsys` and update +their hierarchy to not include `subsys`. +""" +function ignored_systems_for_subsystem( + subsys::AbstractSystem, ignored_systems::Vector{<:HierarchyT}) + result = eltype(ignored_systems)[] + for igsys in ignored_systems + if igsys[end] == nameof(subsys) + push!(result, copy(igsys)) + pop!(result[end]) + end + end + return result +end + function Base.merge(csets::AbstractVector{<:ConnectionSet}, allouter = false) ele2idx = Dict{ConnectionElement, Int}() idx2ele = ConnectionElement[] @@ -576,7 +636,11 @@ end Recursively descend through the hierarchy of `sys` and expand all connection equations of causal variables. Return the modified system. """ -function expand_variable_connections(sys::AbstractSystem) +function expand_variable_connections(sys::AbstractSystem; ignored_variables = nothing) + if ignored_variables === nothing + ignored_variables = ignored_connections(sys)[2] + end + namespaced_ignored = from_hierarchy.(ignored_variables) eqs = copy(get_eqs(sys)) valid_idxs = trues(length(eqs)) additional_eqs = Equation[] @@ -584,8 +648,13 @@ function expand_variable_connections(sys::AbstractSystem) for (i, eq) in enumerate(eqs) eq.lhs isa Connection || continue connection = eq.rhs - elements = connection.systems + elements = get_systems(connection) is_causal_variable_connection(connection) || continue + elements = filter(elements) do el + all(namespaced_ignored) do var + getname(var) != getname(el.var) + end + end valid_idxs[i] = false elements = map(x -> x.var, elements) @@ -595,7 +664,10 @@ function expand_variable_connections(sys::AbstractSystem) end end eqs = [eqs[valid_idxs]; additional_eqs] - subsystems = map(expand_variable_connections, get_systems(sys)) + subsystems = map(get_systems(sys)) do subsys + expand_variable_connections(subsys; + ignored_variables = ignored_systems_for_subsystem(subsys, ignored_variables)) + end @set! sys.eqs = eqs @set! sys.systems = subsystems return sys From 5507939e48bf98d34d14d8a4599337c5c4d52a61 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 12 Mar 2025 15:33:57 +0530 Subject: [PATCH 1099/1337] test: test analysis point transformations with duplicate connections --- test/downstream/analysis_points.jl | 46 ++++++++++++++++++++++++++---- 1 file changed, 40 insertions(+), 6 deletions(-) diff --git a/test/downstream/analysis_points.jl b/test/downstream/analysis_points.jl index 7a433cb504..6a02bc25b4 100644 --- a/test/downstream/analysis_points.jl +++ b/test/downstream/analysis_points.jl @@ -80,18 +80,50 @@ import ControlSystemsBase as CS @test tf(So) ≈ tf(Si) end -@testset "Analysis points with subsystems" begin +@testset "Duplicate `connect` statements across subsystems with AP transforms - standard `connect`" begin @named P = FirstOrder(k = 1, T = 1) @named C = Gain(; k = 1) @named add = Blocks.Add(k2 = -1) eqs = [connect(P.output, :plant_output, add.input2) connect(add.output, C.input) - connect(C.output, :plant_input, P.input)] + connect(C.output, P.input)] + + sys_inner = ODESystem(eqs, t, systems = [P, C, add], name = :inner) + + @named r = Constant(k = 1) + @named F = FirstOrder(k = 1, T = 3) + + eqs = [connect(r.output, F.input) + connect(sys_inner.P.output, sys_inner.add.input2) + connect(sys_inner.C.output, :plant_input, sys_inner.P.input) + connect(F.output, sys_inner.add.input1)] + sys_outer = ODESystem(eqs, t, systems = [F, sys_inner, r], name = :outer) + + # test first that the structural_simplify works correctly + ssys = structural_simplify(sys_outer) + prob = ODEProblem(ssys, Pair[], (0, 10)) + @test_nowarn solve(prob, Rodas5()) - # eqs = [connect(P.output, add.input2) - # connect(add.output, C.input) - # connect(C.output, P.input)] + matrices, _ = get_sensitivity(sys_outer, sys_outer.plant_input) + lsys = sminreal(ss(matrices...)) + @test lsys.A[] == -2 + @test lsys.B[] * lsys.C[] == -1 # either one negative + @test lsys.D[] == 1 + + matrices_So, _ = get_sensitivity(sys_outer, sys_outer.inner.plant_output) + lsyso = sminreal(ss(matrices_So...)) + @test lsys == lsyso || lsys == -1 * lsyso * (-1) # Output and input sensitivities are equal for SISO systems +end + +@testset "Duplicate `connect` statements across subsystems with AP transforms - causal variable `connect`" begin + @named P = FirstOrder(k = 1, T = 1) + @named C = Gain(; k = 1) + @named add = Blocks.Add(k2 = -1) + + eqs = [connect(P.output.u, :plant_output, add.input2.u) + connect(add.output, C.input) + connect(C.output.u, P.input.u)] sys_inner = ODESystem(eqs, t, systems = [P, C, add], name = :inner) @@ -99,6 +131,8 @@ end @named F = FirstOrder(k = 1, T = 3) eqs = [connect(r.output, F.input) + connect(sys_inner.P.output.u, sys_inner.add.input2.u) + connect(sys_inner.C.output.u, :plant_input, sys_inner.P.input.u) connect(F.output, sys_inner.add.input1)] sys_outer = ODESystem(eqs, t, systems = [F, sys_inner, r], name = :outer) @@ -107,7 +141,7 @@ end prob = ODEProblem(ssys, Pair[], (0, 10)) @test_nowarn solve(prob, Rodas5()) - matrices, _ = get_sensitivity(sys_outer, sys_outer.inner.plant_input) + matrices, _ = get_sensitivity(sys_outer, sys_outer.plant_input) lsys = sminreal(ss(matrices...)) @test lsys.A[] == -2 @test lsys.B[] * lsys.C[] == -1 # either one negative From 6b7939aa56a316b38f84861b93e7968318184e1e Mon Sep 17 00:00:00 2001 From: Ashutosh Bharambe Date: Wed, 12 Mar 2025 11:21:13 +0000 Subject: [PATCH 1100/1337] test: add tests to check Expr is returned --- test/odesystem.jl | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/test/odesystem.jl b/test/odesystem.jl index 87be5bbc54..00f7e76b01 100644 --- a/test/odesystem.jl +++ b/test/odesystem.jl @@ -578,6 +578,11 @@ obsfn = ModelingToolkit.build_explicit_observed_function( outersys, bar(3outersys.sys.ms, 3outersys.sys.p)) @test_nowarn obsfn(sol.u[1], prob.p, sol.t[1]) +obsfn_expr = ModelingToolkit.build_explicit_observed_function( + outersys, bar(3outersys.sys.ms, 3outersys.sys.p), expression = true) +@test obsfn_expr isa Expr + + # x/x @variables x(t) @named sys = ODESystem([D(x) ~ x / x], t) @@ -1225,6 +1230,11 @@ end buffer = zeros(3) @test_nowarn obsfn(buffer, [1.0], ps, 3.0) @test buffer ≈ [2.0, 3.0, 4.0] + + obsfn_expr_oop, obsfn_expr_iip = ModelingToolkit.build_explicit_observed_function( + sys, [x + 1, x + P, x + t], return_inplace = true, expression = true) + @test obsfn_expr_oop isa Expr + @test obsfn_expr_iip isa Expr end # https://github.com/SciML/ModelingToolkit.jl/issues/2818 From 8386886e226d1a3a7e70f4856cc2b8f985ea0ef7 Mon Sep 17 00:00:00 2001 From: Ashutosh Bharambe Date: Wed, 12 Mar 2025 11:57:27 +0000 Subject: [PATCH 1101/1337] chore: fix formatting --- src/systems/diffeqs/odesystem.jl | 4 ++-- test/odesystem.jl | 5 ++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/systems/diffeqs/odesystem.jl b/src/systems/diffeqs/odesystem.jl index 2660cdbd85..9a082abf5e 100644 --- a/src/systems/diffeqs/odesystem.jl +++ b/src/systems/diffeqs/odesystem.jl @@ -576,8 +576,8 @@ function build_explicit_observed_function(sys, ts; oop, iip) return return_inplace ? (f, f) : f else - if expression - return fns + if expression + return fns end f = eval_or_rgf(fns; eval_expression, eval_module) f = GeneratedFunctionWrapper{( diff --git a/test/odesystem.jl b/test/odesystem.jl index 00f7e76b01..804aca4fd9 100644 --- a/test/odesystem.jl +++ b/test/odesystem.jl @@ -579,10 +579,9 @@ obsfn = ModelingToolkit.build_explicit_observed_function( @test_nowarn obsfn(sol.u[1], prob.p, sol.t[1]) obsfn_expr = ModelingToolkit.build_explicit_observed_function( - outersys, bar(3outersys.sys.ms, 3outersys.sys.p), expression = true) + outersys, bar(3outersys.sys.ms, 3outersys.sys.p), expression = true) @test obsfn_expr isa Expr - # x/x @variables x(t) @named sys = ODESystem([D(x) ~ x / x], t) @@ -1233,7 +1232,7 @@ end obsfn_expr_oop, obsfn_expr_iip = ModelingToolkit.build_explicit_observed_function( sys, [x + 1, x + P, x + t], return_inplace = true, expression = true) - @test obsfn_expr_oop isa Expr + @test obsfn_expr_oop isa Expr @test obsfn_expr_iip isa Expr end From 26668fd0c7e8c8de5d10e24b11bec022ad0fc236 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 12 Mar 2025 18:42:38 +0530 Subject: [PATCH 1102/1337] fix: handle causal variable AP ignoring part of normal `connect` --- src/systems/connectors.jl | 98 ++++++++++++++++++++++-------- test/downstream/analysis_points.jl | 36 +++++++++++ 2 files changed, 107 insertions(+), 27 deletions(-) diff --git a/src/systems/connectors.jl b/src/systems/connectors.jl index f2ef5ac0f6..94971e38ee 100644 --- a/src/systems/connectors.jl +++ b/src/systems/connectors.jl @@ -315,7 +315,34 @@ function ori(sys) end end -function connection2set!(connectionsets, namespace, ss, isouter) +""" + $(TYPEDSIGNATURES) + +Populate `connectionsets` with connections between the connectors `ss`, all of which are +namespaced by `namespace`. + +# Keyword Arguments +- `ignored_connects`: A tuple of the systems and variables for which connections should be + ignored. Of the format returned from `as_hierarchy`. +- `namespaced_ignored_systems`: The `from_hierarchy` versions of entries in + `ignored_connects[1]`, purely to avoid unnecessary recomputation. +""" +function connection2set!(connectionsets, namespace, ss, isouter; + ignored_connects = (HierarchySystemT[], HierarchyVariableT[]), + namespaced_ignored_systems = ODESystem[]) + ignored_systems, ignored_variables = ignored_connects + # ignore specified systems + ss = filter(ss) do s + all(namespaced_ignored_systems) do igsys + nameof(igsys) != nameof(s) + end + end + # `ignored_variables` for each `s` in `ss` + corresponding_ignored_variables = map( + Base.Fix2(ignored_systems_for_subsystem, ignored_variables), ss) + corresponding_namespaced_ignored_variables = map( + Broadcast.BroadcastFunction(from_hierarchy), corresponding_ignored_variables) + regular_ss = [] domain_ss = nothing for s in ss @@ -340,9 +367,12 @@ function connection2set!(connectionsets, namespace, ss, isouter) for (i, s) in enumerate(ss) sts = unknowns(s) io = isouter(s) - for (j, v) in enumerate(sts) + _ignored_variables = corresponding_ignored_variables[i] + _namespaced_ignored_variables = corresponding_namespaced_ignored_variables[i] + for v in sts vtype = get_connection_type(v) (vtype === Flow && isequal(v, dv)) || continue + any(isequal(v), _namespaced_ignored_variables) && continue push!(cset, T(LazyNamespace(namespace, domain_ss), dv, false)) push!(cset, T(LazyNamespace(namespace, s), v, io)) end @@ -360,6 +390,12 @@ function connection2set!(connectionsets, namespace, ss, isouter) end sts1 = Set(sts1v) num_unknowns = length(sts1) + + # we don't filter here because `csets` should include the full set of unknowns. + # not all of `ss` will have the same (or any) variables filtered so the ones + # that aren't should still go in the right cset. Since `sts1` is only used for + # validating that all systems being connected are of the same type, it has + # unfiltered entries. csets = [T[] for _ in 1:num_unknowns] # Add 9 orientation variables if connection is between multibody frames for (i, s) in enumerate(ss) unknown_vars = unknowns(s) @@ -372,7 +408,10 @@ function connection2set!(connectionsets, namespace, ss, isouter) all(Base.Fix2(in, sts1), unknown_vars)) || connection_error(ss)) io = isouter(s) + # don't `filter!` here so that `j` points to the correct cset regardless of + # which variables are filtered. for (j, v) in enumerate(unknown_vars) + any(isequal(v), corresponding_namespaced_ignored_variables[i]) && continue push!(csets[j], T(LazyNamespace(namespace, s), v, io)) end end @@ -397,7 +436,7 @@ function generate_connection_set( sys = generate_connection_set!( connectionsets, domain_csets, sys, find, replace, scalarize, nothing, # include systems to be ignored - ignored_connections(sys)[1]) + ignored_connections(sys)) csets = merge(connectionsets) domain_csets = merge([csets; domain_csets], true) @@ -417,18 +456,23 @@ Generate connection sets from `connect` equations. - `sys` is the system whose equations are to be searched. - `namespace` is a system representing the namespace in which `sys` exists, or `nothing` for no namespace (if `sys` is top-level). -- `ignored_systems` is a list of systems (in the format returned by `as_hierarchy`) to - be ignored when generating connections. This is typically because the connections - they are used in were removed by analysis point transformations. +- `ignored_connects` is a tuple. The first (second) element is a list of systems + (variables) in the format returned by `as_hierarchy` to be ignored when generating + connections. This is typically because the connections they are used in were removed by + analysis point transformations. """ function generate_connection_set!(connectionsets, domain_csets, - sys::AbstractSystem, find, replace, scalarize, namespace = nothing, ignored_systems = []) + sys::AbstractSystem, find, replace, scalarize, namespace = nothing, + ignored_connects = (HierarchySystemT[], HierarchyVariableT[])) subsys = get_systems(sys) + ignored_systems, ignored_variables = ignored_connects # turn hierarchies into namespaced systems - namespaced_ignored = from_hierarchy.(ignored_systems) + namespaced_ignored_systems = from_hierarchy.(ignored_systems) + namespaced_ignored_variables = from_hierarchy.(ignored_variables) + namespaced_ignored = (namespaced_ignored_systems, namespaced_ignored_variables) # filter the subsystems of `sys` to exclude ignored ones filtered_subsys = filter(subsys) do ss - all(namespaced_ignored) do igsys + all(namespaced_ignored_systems) do igsys nameof(igsys) != nameof(ss) end end @@ -457,21 +501,10 @@ function generate_connection_set!(connectionsets, domain_csets, neweq isa AbstractArray ? append!(eqs, neweq) : push!(eqs, neweq) else if lhs isa Connection && get_systems(lhs) === :domain - # don't consider systems that should be ignored - systems_to_connect = filter(get_systems(rhs)) do ss - all(namespaced_ignored) do igsys - nameof(igsys) != nameof(ss) - end - end - connection2set!(domain_csets, namespace, systems_to_connect, isouter) + connection2set!(domain_csets, namespace, get_systems(rhs), isouter; + ignored_connects, namespaced_ignored_systems) elseif isconnection(rhs) - # ignore required systems - systems_to_connect = filter(get_systems(rhs)) do ss - all(namespaced_ignored) do igsys - nameof(igsys) != nameof(ss) - end - end - push!(cts, systems_to_connect) + push!(cts, get_systems(rhs)) else # split connections and equations if eq.lhs isa AbstractArray || eq.rhs isa AbstractArray @@ -489,14 +522,19 @@ function generate_connection_set!(connectionsets, domain_csets, for s in filtered_subsys isconnector(s) || continue is_domain_connector(s) && continue + _ignored_variables = ignored_systems_for_subsystem(s, ignored_variables) + _namespaced_ignored_variables = from_hierarchy.(_ignored_variables) for v in unknowns(s) Flow === get_connection_type(v) || continue + # ignore specified variables + any(isequal(v), _namespaced_ignored_variables) && continue push!(connectionsets, ConnectionSet([T(LazyNamespace(namespace, s), v, false)])) end end for ct in cts - connection2set!(connectionsets, namespace, ct, isouter) + connection2set!(connectionsets, namespace, ct, isouter; + ignored_connects, namespaced_ignored_systems) end # pre order traversal @@ -506,7 +544,7 @@ function generate_connection_set!(connectionsets, domain_csets, @set! sys.systems = map( s -> generate_connection_set!(connectionsets, domain_csets, s, find, replace, scalarize, renamespace(namespace, s), - ignored_systems_for_subsystem(s, ignored_systems)), + ignored_systems_for_subsystem.((s,), ignored_connects)), subsys) @set! sys.eqs = eqs end @@ -522,10 +560,16 @@ their hierarchy to not include `subsys`. function ignored_systems_for_subsystem( subsys::AbstractSystem, ignored_systems::Vector{<:HierarchyT}) result = eltype(ignored_systems)[] + # in case `subsys` is namespaced, get its hierarchy and compare suffixes + # instead of the just the last element + suffix = reverse!(namespace_hierarchy(nameof(subsys))) + N = length(suffix) for igsys in ignored_systems - if igsys[end] == nameof(subsys) + if igsys[(end - N + 1):end] == suffix push!(result, copy(igsys)) - pop!(result[end]) + for i in 1:N + pop!(result[end]) + end end end return result diff --git a/test/downstream/analysis_points.jl b/test/downstream/analysis_points.jl index 6a02bc25b4..b5a4ba9c84 100644 --- a/test/downstream/analysis_points.jl +++ b/test/downstream/analysis_points.jl @@ -152,6 +152,42 @@ end @test lsys == lsyso || lsys == -1 * lsyso * (-1) # Output and input sensitivities are equal for SISO systems end +@testset "Duplicate `connect` statements across subsystems with AP transforms - mixed `connect`" begin + @named P = FirstOrder(k = 1, T = 1) + @named C = Gain(; k = 1) + @named add = Blocks.Add(k2 = -1) + + eqs = [connect(P.output.u, :plant_output, add.input2.u) + connect(add.output, C.input) + connect(C.output, P.input)] + + sys_inner = ODESystem(eqs, t, systems = [P, C, add], name = :inner) + + @named r = Constant(k = 1) + @named F = FirstOrder(k = 1, T = 3) + + eqs = [connect(r.output, F.input) + connect(sys_inner.P.output, sys_inner.add.input2) + connect(sys_inner.C.output.u, :plant_input, sys_inner.P.input.u) + connect(F.output, sys_inner.add.input1)] + sys_outer = ODESystem(eqs, t, systems = [F, sys_inner, r], name = :outer) + + # test first that the structural_simplify works correctly + ssys = structural_simplify(sys_outer) + prob = ODEProblem(ssys, Pair[], (0, 10)) + @test_nowarn solve(prob, Rodas5()) + + matrices, _ = get_sensitivity(sys_outer, sys_outer.plant_input) + lsys = sminreal(ss(matrices...)) + @test lsys.A[] == -2 + @test lsys.B[] * lsys.C[] == -1 # either one negative + @test lsys.D[] == 1 + + matrices_So, _ = get_sensitivity(sys_outer, sys_outer.inner.plant_output) + lsyso = sminreal(ss(matrices_So...)) + @test lsys == lsyso || lsys == -1 * lsyso * (-1) # Output and input sensitivities are equal for SISO systems +end + @testset "multilevel system with loop openings" begin @named P_inner = FirstOrder(k = 1, T = 1) @named feedback = Feedback() From 1de31d06ef7357ac661ed563249a2e6a81de20f7 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 12 Mar 2025 18:42:52 +0530 Subject: [PATCH 1103/1337] test: test multi-input `connect` with single-input AP --- test/downstream/analysis_points.jl | 98 ++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) diff --git a/test/downstream/analysis_points.jl b/test/downstream/analysis_points.jl index b5a4ba9c84..62961f181a 100644 --- a/test/downstream/analysis_points.jl +++ b/test/downstream/analysis_points.jl @@ -340,3 +340,101 @@ end G = CS.ss(matrices...) |> sminreal @test tf(G) ≈ tf(CS.feedback(Ps, Cs)) end + +function normal_test_system() + @named F1 = FirstOrder(k = 1, T = 1) + @named F2 = FirstOrder(k = 1, T = 1) + @named add = Blocks.Add(k1 = 1, k2 = 2) + @named back = Feedback() + + eqs_normal = [connect(back.output, :ap, F1.input) + connect(back.output, F2.input) + connect(F1.output, add.input1) + connect(F2.output, add.input2) + connect(add.output, back.input2)] + @named normal_inner = ODESystem(eqs_normal, t; systems = [F1, F2, add, back]) + + @named step = Step() + eqs2_normal = [ + connect(step.output, normal_inner.back.input1) + ] + @named sys_normal = ODESystem(eqs2_normal, t; systems = [normal_inner, step]) +end + +sys_normal = normal_test_system() + +prob = ODEProblem(structural_simplify(sys_normal), [], (0.0, 10.0)) +@test SciMLBase.successful_retcode(solve(prob, Rodas5P())) +matrices_normal, _ = get_sensitivity(sys_normal, sys_normal.normal_inner.ap) + +@testset "Analysis point overriding part of connection - normal connect" begin + @named F1 = FirstOrder(k = 1, T = 1) + @named F2 = FirstOrder(k = 1, T = 1) + @named add = Blocks.Add(k1 = 1, k2 = 2) + @named back = Feedback() + + eqs = [connect(back.output, F1.input, F2.input) + connect(F1.output, add.input1) + connect(F2.output, add.input2) + connect(add.output, back.input2)] + @named inner = ODESystem(eqs, t; systems = [F1, F2, add, back]) + + @named step = Step() + eqs2 = [connect(step.output, inner.back.input1) + connect(inner.back.output, :ap, inner.F1.input)] + @named sys = ODESystem(eqs2, t; systems = [inner, step]) + + prob = ODEProblem(structural_simplify(sys), [], (0.0, 10.0)) + @test SciMLBase.successful_retcode(solve(prob, Rodas5P())) + + matrices, _ = get_sensitivity(sys, sys.ap) + @test matrices == matrices_normal +end + +@testset "Analysis point overriding part of connection - variable connect" begin + @named F1 = FirstOrder(k = 1, T = 1) + @named F2 = FirstOrder(k = 1, T = 1) + @named add = Blocks.Add(k1 = 1, k2 = 2) + @named back = Feedback() + + eqs = [connect(back.output.u, F1.input.u, F2.input.u) + connect(F1.output, add.input1) + connect(F2.output, add.input2) + connect(add.output, back.input2)] + @named inner = ODESystem(eqs, t; systems = [F1, F2, add, back]) + + @named step = Step() + eqs2 = [connect(step.output, inner.back.input1) + connect(inner.back.output.u, :ap, inner.F1.input.u)] + @named sys = ODESystem(eqs2, t; systems = [inner, step]) + + prob = ODEProblem(structural_simplify(sys), [], (0.0, 10.0)) + @test SciMLBase.successful_retcode(solve(prob, Rodas5P())) + + matrices, _ = get_sensitivity(sys, sys.ap) + @test matrices == matrices_normal +end + +@testset "Analysis point overriding part of connection - mixed connect" begin + @named F1 = FirstOrder(k = 1, T = 1) + @named F2 = FirstOrder(k = 1, T = 1) + @named add = Blocks.Add(k1 = 1, k2 = 2) + @named back = Feedback() + + eqs = [connect(back.output, F1.input, F2.input) + connect(F1.output, add.input1) + connect(F2.output, add.input2) + connect(add.output, back.input2)] + @named inner = ODESystem(eqs, t; systems = [F1, F2, add, back]) + + @named step = Step() + eqs2 = [connect(step.output, inner.back.input1) + connect(inner.back.output.u, :ap, inner.F1.input.u)] + @named sys = ODESystem(eqs2, t; systems = [inner, step]) + + prob = ODEProblem(structural_simplify(sys), [], (0.0, 10.0)) + @test SciMLBase.successful_retcode(solve(prob, Rodas5P())) + + matrices, _ = get_sensitivity(sys, sys.ap) + @test matrices == matrices_normal +end From d3a8f8dce842e93c17470b5cd676b252247eba6a Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 13 Mar 2025 00:25:41 +0530 Subject: [PATCH 1104/1337] test: relax initializationsystem test --- test/initializationsystem.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/initializationsystem.jl b/test/initializationsystem.jl index 4267f1d857..93054eb5f4 100644 --- a/test/initializationsystem.jl +++ b/test/initializationsystem.jl @@ -473,7 +473,7 @@ prob = ODEProblem(pend, [x => 1], (0.0, 1.5), [g => 1], unsimp = generate_initializesystem(pend; u0map = [x => 1], initialization_eqs = [y ~ 1]) sys = structural_simplify(unsimp; fully_determined = false) -@test length(equations(sys)) == 3 +@test length(equations(sys)) in (3, 4) # could be either depending on tearing # Extend two systems with initialization equations and guesses # https://github.com/SciML/ModelingToolkit.jl/issues/2845 From 508cd8a0ecce4e476111e28b98248f89cc8c3f28 Mon Sep 17 00:00:00 2001 From: Ashutosh Bharambe Date: Thu, 13 Mar 2025 09:33:28 +0000 Subject: [PATCH 1105/1337] test: add testsets for obs func expr --- test/odesystem.jl | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/test/odesystem.jl b/test/odesystem.jl index 804aca4fd9..0beee10238 100644 --- a/test/odesystem.jl +++ b/test/odesystem.jl @@ -578,10 +578,11 @@ obsfn = ModelingToolkit.build_explicit_observed_function( outersys, bar(3outersys.sys.ms, 3outersys.sys.p)) @test_nowarn obsfn(sol.u[1], prob.p, sol.t[1]) -obsfn_expr = ModelingToolkit.build_explicit_observed_function( - outersys, bar(3outersys.sys.ms, 3outersys.sys.p), expression = true) -@test obsfn_expr isa Expr - +@testset "Observed Function: Expression" begin + obsfn_expr = ModelingToolkit.build_explicit_observed_function( + outersys, bar(3outersys.sys.ms, 3outersys.sys.p), expression = true) + @test obsfn_expr isa Expr +end # x/x @variables x(t) @named sys = ODESystem([D(x) ~ x / x], t) @@ -1230,10 +1231,12 @@ end @test_nowarn obsfn(buffer, [1.0], ps, 3.0) @test buffer ≈ [2.0, 3.0, 4.0] - obsfn_expr_oop, obsfn_expr_iip = ModelingToolkit.build_explicit_observed_function( - sys, [x + 1, x + P, x + t], return_inplace = true, expression = true) - @test obsfn_expr_oop isa Expr - @test obsfn_expr_iip isa Expr + @testset "Observed Function: Expression" begin + obsfn_expr_oop, obsfn_expr_iip = ModelingToolkit.build_explicit_observed_function( + sys, [x + 1, x + P, x + t], return_inplace = true, expression = true) + @test obsfn_expr_oop isa Expr + @test obsfn_expr_iip isa Expr + end end # https://github.com/SciML/ModelingToolkit.jl/issues/2818 From 7fb06a6babc08c5294fd6e3895c1ef1fb649e956 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 27 Feb 2025 15:36:01 +0530 Subject: [PATCH 1106/1337] feat: implement `SciMLBase.detect_cycles` --- src/systems/problem_utils.jl | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/systems/problem_utils.jl b/src/systems/problem_utils.jl index 85dfded21c..93a0749acb 100644 --- a/src/systems/problem_utils.jl +++ b/src/systems/problem_utils.jl @@ -897,6 +897,13 @@ function Base.showerror(io::IO, e::InvalidKeyError) println(io, "pmap: $(join(e.params, ", "))") end +function SciMLBase.detect_cycles(sys::AbstractSystem, varmap::Dict{Any, Any}, vars) + varmap = AnyDict(unwrap(k) => unwrap(v) for (k, v) in varmap) + vars = map(unwrap, vars) + cycles = check_substitution_cycles(varmap, vars) + return !isempty(cycles) +end + ############## # Legacy functions for backward compatibility ############## From a7a9b13df526231c214240562b71580cfde83e42 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 21 Feb 2025 12:33:38 +0530 Subject: [PATCH 1107/1337] fix: fix `DEF` parameters for `split = true, flatten = false` systems --- src/systems/abstractsystem.jl | 4 +++- test/split_parameters.jl | 37 +++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index c0b3601e21..dfc25c65ec 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -769,7 +769,9 @@ function complete(sys::AbstractSystem; split = true, flatten = true) end if split && has_index_cache(sys) @set! sys.index_cache = IndexCache(sys) - all_ps = get_ps(sys) + # Ideally we'd do `get_ps` but if `flatten = false` + # we don't get all of them. So we call `parameters`. + all_ps = parameters(sys; initial_parameters = true) if !isempty(all_ps) # reorder parameters by portions ps_split = reorder_parameters(sys, all_ps) diff --git a/test/split_parameters.jl b/test/split_parameters.jl index 0a4f9fb25d..9b00097d54 100644 --- a/test/split_parameters.jl +++ b/test/split_parameters.jl @@ -275,3 +275,40 @@ end @test_nowarn sol = solve(prob) end end + +@testset "" begin + @mtkmodel SubSystem begin + @parameters begin + c = 1 + end + @variables begin + x(t) + end + @equations begin + D(x) ~ c * x + end + end + + @mtkmodel System begin + @components begin + subsys = SubSystem() + end + @parameters begin + k = 1 + end + @variables begin + y(t) + end + @equations begin + D(y) ~ k * y + subsys.x + end + end + + @named sys = System() + sysref = complete(sys) + sys2 = complete(sys; split = true, flatten = false) + ps = Set(full_parameters(sys2)) + @test sysref.k in ps + @test sysref.subsys.c in ps + @test length(ps) == 2 +end From ac76d5a91c23211cd48aa4336f6e37af23f5f3d4 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 24 Feb 2025 12:20:13 +0530 Subject: [PATCH 1108/1337] fix: use `split = false, flatten = false` when `complete`ing parent in `structural_simplify` --- src/systems/systems.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/systems/systems.jl b/src/systems/systems.jl index 9664e38cb6..367e1cd4cd 100644 --- a/src/systems/systems.jl +++ b/src/systems/systems.jl @@ -50,7 +50,7 @@ function structural_simplify( newsys = pass(newsys) end if newsys isa ODESystem || has_parent(newsys) - @set! newsys.parent = complete(sys; split, flatten = false) + @set! newsys.parent = complete(sys; split = false, flatten = false) end newsys = complete(newsys; split) if newsys′ isa Tuple From 9d35146bf62a341a4e7585a55da5f3ac4faa722f Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 27 Feb 2025 17:43:43 +0530 Subject: [PATCH 1109/1337] test: reduce step size for CS FMUs in test --- test/fmi/fmi.jl | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/test/fmi/fmi.jl b/test/fmi/fmi.jl index 3db1c3f0e8..98c93398ff 100644 --- a/test/fmi/fmi.jl +++ b/test/fmi/fmi.jl @@ -32,7 +32,7 @@ const FMU_DIR = joinpath(@__DIR__, "fmus") @testset "v2, CS" begin fmu = loadFMU("SpringPendulum1D", "Dymola", "2022x"; type = :CS) @named inner = MTK.FMIComponent( - Val(2); fmu, communication_step_size = 0.001, type = :CS) + Val(2); fmu, communication_step_size = 1e-5, type = :CS) @variables x(t) = 1.0 @mtkbuild sys = ODESystem([D(x) ~ x], t; systems = [inner]) test_no_inputs_outputs(sys) @@ -66,7 +66,7 @@ const FMU_DIR = joinpath(@__DIR__, "fmus") @testset "v3, CS" begin fmu = loadFMU("SpringPendulum1D", "Dymola", "2023x", "3.0"; type = :CS) @named inner = MTK.FMIComponent( - Val(3); fmu, communication_step_size = 0.001, type = :CS) + Val(3); fmu, communication_step_size = 1e-5, type = :CS) @variables x(t) = 1.0 @mtkbuild sys = ODESystem([D(x) ~ x], t; systems = [inner]) test_no_inputs_outputs(sys) @@ -210,7 +210,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-4, type = :CS, + Val(3); fmu, communication_step_size = 1e-6, type = :CS, reinitializealg = BrownFullBasicInit()) @test MTK.isinput(sspace.u) @test MTK.isoutput(sspace.y) @@ -259,9 +259,9 @@ end @testset "v2, CS" begin fmu = loadFMU(joinpath(FMU_DIR, "SimpleAdder.fmu"); type = :CS) @named adder1 = MTK.FMIComponent( - Val(2); fmu, type = :CS, communication_step_size = 1e-3) + Val(2); fmu, type = :CS, communication_step_size = 1e-5) @named adder2 = MTK.FMIComponent( - Val(2); fmu, type = :CS, communication_step_size = 1e-3) + Val(2); fmu, type = :CS, communication_step_size = 1e-5) sys, prob = build_looped_adders(adder1, adder2) sol = solve(prob, Tsit5(); @@ -300,9 +300,9 @@ end @testset "v3, CS" begin fmu = loadFMU(joinpath(FMU_DIR, "StateSpace.fmu"); type = :CS) @named sspace1 = MTK.FMIComponent( - Val(3); fmu, type = :CS, communication_step_size = 1e-4) + Val(3); fmu, type = :CS, communication_step_size = 1e-5) @named sspace2 = MTK.FMIComponent( - Val(3); fmu, type = :CS, communication_step_size = 1e-4) + Val(3); fmu, type = :CS, communication_step_size = 1e-5) sys, prob = build_looped_sspace(sspace1, sspace2) sol = solve(prob, Rodas5P(autodiff = false); reltol = 1e-8) @test SciMLBase.successful_retcode(sol) From cf8b3a3ea6785d769d44ca776286876e2acb2600 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 28 Feb 2025 15:52:00 +0530 Subject: [PATCH 1110/1337] fix: handle symbolic values for parameters passed to `linearize` --- src/linearization.jl | 5 ++++- test/downstream/linearize.jl | 8 ++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/linearization.jl b/src/linearization.jl index 411620e8f3..70ae5360a8 100644 --- a/src/linearization.jl +++ b/src/linearization.jl @@ -590,9 +590,12 @@ function linearize(sys, lin_fun::LinearizationFunction; t = 0.0, p = DiffEqBase.NullParameters()) prob = LinearizationProblem(lin_fun, t) op = anydict(op) - evaluate_varmap!(op, unknowns(sys)) + evaluate_varmap!(op, keys(op)) for (k, v) in op v === nothing && continue + if symbolic_type(v) != NotSymbolic() || is_array_of_symbolics(v) + v = getu(prob, v)(prob) + end if is_parameter(prob, Initial(k)) setu(prob, Initial(k))(prob, v) else diff --git a/test/downstream/linearize.jl b/test/downstream/linearize.jl index 898040fb4f..fbae0656f9 100644 --- a/test/downstream/linearize.jl +++ b/test/downstream/linearize.jl @@ -329,3 +329,11 @@ end sys, [x], []; op = Dict(x => 1.0), guesses = Dict(y => 1.0), allow_input_derivatives = true) end + +@testset "Symbolic values for parameters in `linearize`" begin + @named tank_noi = Tank_noi() + @unpack md_i, h, m, ρ, A, K = tank_noi + m_ss = 2.4000000003229878 + @test_nowarn linearize( + tank_noi, [md_i], [h]; op = Dict(m => m_ss, md_i => 2, ρ => A / K, A => 5)) +end From 0cf9aadcd7c8330bc7f429a0e09f6a1de5ee8eac Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 28 Feb 2025 16:42:23 +0530 Subject: [PATCH 1111/1337] fix: warn if the operating point given to `linearization_function` is empty. --- src/linearization.jl | 4 ++++ test/downstream/linearize.jl | 8 ++++++++ 2 files changed, 12 insertions(+) diff --git a/src/linearization.jl b/src/linearization.jl index 70ae5360a8..501582f188 100644 --- a/src/linearization.jl +++ b/src/linearization.jl @@ -42,8 +42,12 @@ function linearization_function(sys::AbstractSystem, inputs, eval_expression = false, eval_module = @__MODULE__, warn_initialize_determined = true, guesses = Dict(), + warn_empty_op = true, kwargs...) op = Dict(op) + if isempty(op) + @warn "An empty operating point was passed to `linearization_function`. An operating point containing the variables that will be changed in `linearize` should be provided. Disable this warning by passing `warn_empty_op = false`." + end inputs isa AbstractVector || (inputs = [inputs]) outputs isa AbstractVector || (outputs = [outputs]) inputs = mapreduce(vcat, inputs; init = []) do var diff --git a/test/downstream/linearize.jl b/test/downstream/linearize.jl index fbae0656f9..704964daae 100644 --- a/test/downstream/linearize.jl +++ b/test/downstream/linearize.jl @@ -337,3 +337,11 @@ end @test_nowarn linearize( tank_noi, [md_i], [h]; op = Dict(m => m_ss, md_i => 2, ρ => A / K, A => 5)) end + +@testset "Warn on empty operating point" begin + @named tank_noi = Tank_noi() + @unpack md_i, h, m = tank_noi + m_ss = 2.4000000003229878 + @test_warn ["empty operating point", "warn_empty_op"] linearize( + tank_noi, [md_i], [h]; p = [md_i => 1.0]) +end From 89dec69df33b4b404613b4226eaa8c599e9f2611 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 7 Mar 2025 14:06:31 +0530 Subject: [PATCH 1112/1337] test: fix warning in `@test_nowarn` --- test/downstream/linearize.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/downstream/linearize.jl b/test/downstream/linearize.jl index 704964daae..0336709173 100644 --- a/test/downstream/linearize.jl +++ b/test/downstream/linearize.jl @@ -262,7 +262,7 @@ closed_loop = ODESystem(connections, t, systems = [model, pid, filt, sensor, r, filt.xd => 0.0 ]) -@test_nowarn linearize(closed_loop, :r, :y) +@test_nowarn linearize(closed_loop, :r, :y; warn_empty_op = false) # https://discourse.julialang.org/t/mtk-change-in-linearize/115760/3 @mtkmodel Tank_noi begin From c354feba39892aaef06ec84c3385ded1b07b2ec7 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 13 Mar 2025 16:34:53 +0530 Subject: [PATCH 1113/1337] fix: respect `warn_empty_op` --- src/linearization.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/linearization.jl b/src/linearization.jl index 501582f188..f58f3b4253 100644 --- a/src/linearization.jl +++ b/src/linearization.jl @@ -45,7 +45,7 @@ function linearization_function(sys::AbstractSystem, inputs, warn_empty_op = true, kwargs...) op = Dict(op) - if isempty(op) + if isempty(op) && warn_empty_op @warn "An empty operating point was passed to `linearization_function`. An operating point containing the variables that will be changed in `linearize` should be provided. Disable this warning by passing `warn_empty_op = false`." end inputs isa AbstractVector || (inputs = [inputs]) From 3d5fdd83eb3b76ee14c5d5d3d0672a4d08acf406 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 13 Mar 2025 16:57:45 +0530 Subject: [PATCH 1114/1337] test: isolate tests into new testset --- test/odesystem.jl | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/test/odesystem.jl b/test/odesystem.jl index 0beee10238..970a9afaad 100644 --- a/test/odesystem.jl +++ b/test/odesystem.jl @@ -578,11 +578,6 @@ obsfn = ModelingToolkit.build_explicit_observed_function( outersys, bar(3outersys.sys.ms, 3outersys.sys.p)) @test_nowarn obsfn(sol.u[1], prob.p, sol.t[1]) -@testset "Observed Function: Expression" begin - obsfn_expr = ModelingToolkit.build_explicit_observed_function( - outersys, bar(3outersys.sys.ms, 3outersys.sys.p), expression = true) - @test obsfn_expr isa Expr -end # x/x @variables x(t) @named sys = ODESystem([D(x) ~ x / x], t) @@ -1230,13 +1225,6 @@ end buffer = zeros(3) @test_nowarn obsfn(buffer, [1.0], ps, 3.0) @test buffer ≈ [2.0, 3.0, 4.0] - - @testset "Observed Function: Expression" begin - obsfn_expr_oop, obsfn_expr_iip = ModelingToolkit.build_explicit_observed_function( - sys, [x + 1, x + P, x + t], return_inplace = true, expression = true) - @test obsfn_expr_oop isa Expr - @test obsfn_expr_iip isa Expr - end end # https://github.com/SciML/ModelingToolkit.jl/issues/2818 @@ -1736,3 +1724,15 @@ end @mtkbuild ode = ODESystem(D(x(t)) ~ mat * x(t), t; constraints = cons) @test length(constraints(ModelingToolkit.get_constraintsystem(ode))) == 5 end + +@testset "`build_explicit_observed_function` with `expression = true` returns `Expr`" begin + @variables x(t) + @mtkbuild sys = ODESystem(D(x) ~ 2x, t) + obsfn_expr = ModelingToolkit.build_explicit_observed_function( + sys, 2x + 1, expression = true) + @test obsfn_expr isa Expr + obsfn_expr_oop, obsfn_expr_iip = ModelingToolkit.build_explicit_observed_function( + sys, [x + 1, x + 2, x + t], return_inplace = true, expression = true) + @test obsfn_expr_oop isa Expr + @test obsfn_expr_iip isa Expr +end From d157ebebd80cc7e878fbb82066bef28fc1c65630 Mon Sep 17 00:00:00 2001 From: Fredrik Bagge Carlson Date: Thu, 13 Mar 2025 14:29:05 +0100 Subject: [PATCH 1115/1337] error message threw error message `sys` had gone away from the function body, so it had to be grabbed from elsewhere --- src/linearization.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/linearization.jl b/src/linearization.jl index 411620e8f3..1bd24db113 100644 --- a/src/linearization.jl +++ b/src/linearization.jl @@ -349,7 +349,7 @@ function CommonSolve.solve(prob::LinearizationProblem; allow_input_derivatives = if !iszero(Bs) if !allow_input_derivatives der_inds = findall(vec(any(!=(0), Bs, dims = 1))) - error("Input derivatives appeared in expressions (-g_z\\g_u != 0), the following inputs appeared differentiated: $(inputs(sys)[der_inds]). Call `linearize` with keyword argument `allow_input_derivatives = true` to allow this and have the returned `B` matrix be of double width ($(2nu)), where the last $nu inputs are the derivatives of the first $nu inputs.") + error("Input derivatives appeared in expressions (-g_z\\g_u != 0), the following inputs appeared differentiated: $(inputs(prob.f.prob.f.sys)[der_inds]). Call `linearize` with keyword argument `allow_input_derivatives = true` to allow this and have the returned `B` matrix be of double width ($(2nu)), where the last $nu inputs are the derivatives of the first $nu inputs.") end B = [B [zeros(nx, nu); Bs]] D = [D zeros(ny, nu)] From 186a7d3de1cc87847983096a586e3da83b8b57d5 Mon Sep 17 00:00:00 2001 From: Torkel Loman Date: Sat, 15 Mar 2025 19:38:29 +0000 Subject: [PATCH 1116/1337] add toplevel arguments --- src/systems/abstractsystem.jl | 22 ++++++++++++---------- src/systems/callbacks.jl | 32 +++++++++++++++++--------------- 2 files changed, 29 insertions(+), 25 deletions(-) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index c0b3601e21..010456a094 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -1293,10 +1293,10 @@ Get the unknown variables of the system `sys` and its subsystems. See also [`ModelingToolkit.get_unknowns`](@ref). """ -function unknowns(sys::AbstractSystem) +function unknowns(sys::AbstractSystem; toplevel = false) sts = get_unknowns(sys) systems = get_systems(sys) - nonunique_unknowns = if isempty(systems) + nonunique_unknowns = if toplevel || isempty(systems) sts else system_unknowns = reduce(vcat, namespace_variables.(systems)) @@ -1320,7 +1320,7 @@ Get the parameters of the system `sys` and its subsystems. See also [`@parameters`](@ref) and [`ModelingToolkit.get_ps`](@ref). """ -function parameters(sys::AbstractSystem; initial_parameters = false) +function parameters(sys::AbstractSystem; initial_parameters = false, toplevel = false) ps = get_ps(sys) if ps == SciMLBase.NullParameters() return [] @@ -1329,8 +1329,8 @@ function parameters(sys::AbstractSystem; initial_parameters = false) ps = first.(ps) end systems = get_systems(sys) - result = unique(isempty(systems) ? ps : - [ps; reduce(vcat, namespace_parameters.(systems))]) + result = unique(toplevel || isempty(systems) ? + ps : [ps; reduce(vcat, namespace_parameters.(systems))]) if !initial_parameters if is_time_dependent(sys) # time-dependent systems have `Initial` parameters for all their @@ -1490,8 +1490,9 @@ function controls(sys::AbstractSystem) isempty(systems) ? ctrls : [ctrls; reduce(vcat, namespace_controls.(systems))] end -function observed(sys::AbstractSystem) +function observed(sys::AbstractSystem; toplevel = false) obs = get_observed(sys) + toplevel && return obs systems = get_systems(sys) [obs; reduce(vcat, @@ -1510,7 +1511,7 @@ If they are not explicitly provided, variables and parameters are initialized to See also [`initialization_equations`](@ref), [`parameter_dependencies`](@ref) and [`ModelingToolkit.get_defaults`](@ref). """ -function defaults(sys::AbstractSystem) +function defaults(sys::AbstractSystem; toplevel = false) systems = get_systems(sys) defs = get_defaults(sys) # `mapfoldr` is really important!!! We should prefer the base model for @@ -1519,7 +1520,8 @@ function defaults(sys::AbstractSystem) # `compose(ODESystem(...; defaults=defs), ...)` # # Thus, right associativity is required and crucial for correctness. - isempty(systems) ? defs : mapfoldr(namespace_defaults, merge, systems; init = defs) + (toplevel ||isempty(systems)) ? + defs : mapfoldr(namespace_defaults, merge, systems; init = defs) end function defaults_and_guesses(sys::AbstractSystem) @@ -1549,10 +1551,10 @@ It is often the most useful way to inspect the equations of a system. See also [`full_equations`](@ref) and [`ModelingToolkit.get_eqs`](@ref). """ -function equations(sys::AbstractSystem) +function equations(sys::AbstractSystem; toplevel = false) eqs = get_eqs(sys) systems = get_systems(sys) - if isempty(systems) + if toplevel || isempty(systems) return eqs else eqs = Equation[eqs; diff --git a/src/systems/callbacks.jl b/src/systems/callbacks.jl index 5cefe65651..5b9dab6fb0 100644 --- a/src/systems/callbacks.jl +++ b/src/systems/callbacks.jl @@ -94,17 +94,17 @@ A [`ContinuousCallback`](@ref SciMLBase.ContinuousCallback) specified symbolical as well as the positive-edge `affect` and negative-edge `affect_neg` that apply when *any* of `eq` are satisfied. By default `affect_neg = affect`; to only get rising edges specify `affect_neg = nothing`. -Assume without loss of generality that the equation is of the form `c(u,p,t) ~ 0`; we denote the integrator state as `i.u`. +Assume without loss of generality that the equation is of the form `c(u,p,t) ~ 0`; we denote the integrator state as `i.u`. For compactness, we define `prev_sign = sign(c(u[t-1], p[t-1], t-1))` and `cur_sign = sign(c(u[t], p[t], t))`. -A condition edge will be detected and the callback will be invoked iff `prev_sign * cur_sign <= 0`. +A condition edge will be detected and the callback will be invoked iff `prev_sign * cur_sign <= 0`. The positive edge `affect` will be triggered iff an edge is detected and if `prev_sign < 0`; similarly, `affect_neg` will be -triggered iff an edge is detected and `prev_sign > 0`. +triggered iff an edge is detected and `prev_sign > 0`. -Inter-sample condition activation is not guaranteed; for example if we use the dirac delta function as `c` to insert a +Inter-sample condition activation is not guaranteed; for example if we use the dirac delta function as `c` to insert a sharp discontinuity between integrator steps (which in this example would not normally be identified by adaptivity) then the condition is not guaranteed to be triggered. -Once detected the integrator will "wind back" through a root-finding process to identify the point when the condition became active; the method used +Once detected the integrator will "wind back" through a root-finding process to identify the point when the condition became active; the method used is specified by `rootfind` from [`SciMLBase.RootfindOpt`](@ref). If we denote the time when the condition becomes active as `tc`, the value in the integrator after windback will be: * `u[tc-epsilon], p[tc-epsilon], tc` if `LeftRootFind` is used, @@ -116,7 +116,7 @@ it passed through 0. Multiple callbacks in the same system with different `rootfind` operations will be grouped by their `rootfind` value into separate VectorContinuousCallbacks in the enumeration order of `SciMLBase.RootfindOpt`. This may cause some callbacks to not fire if several become -active at the same instant. See the `SciMLBase` documentation for more information on the semantic rules. +active at the same instant. See the `SciMLBase` documentation for more information on the semantic rules. Affects (i.e. `affect` and `affect_neg`) can be specified as either: * A list of equations that should be applied when the callback is triggered (e.g. `x ~ 3, y ~ 7`) which must be of the form `unknown ~ observed value` where each `unknown` appears only once. Equations will be applied in the order that they appear in the vector; parameters and state updates will become immediately visible to following equations. @@ -327,14 +327,15 @@ The `SymbolicContinuousCallback`s in the returned vector are structs with two fi `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) - obs = get_continuous_events(sys) - filter(!isempty, obs) +function continuous_events(sys::AbstractSystem; toplevel = false) + cbs = get_continuous_events(sys) + filter(!isempty, cbs) + toplevel && return cbs systems = get_systems(sys) - cbs = [obs; + cbs = [cbs; reduce(vcat, - (map(o -> namespace_callback(o, s), continuous_events(s)) + (map(cb -> namespace_callback(cb, s), continuous_events(s)) for s in systems), init = SymbolicContinuousCallback[])] filter(!isempty, cbs) @@ -482,10 +483,11 @@ The `SymbolicDiscreteCallback`s in the returned vector are structs with two fiel `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) - obs = get_discrete_events(sys) +function discrete_events(sys::AbstractSystem; toplevel = false) + cbs = get_discrete_events(sys) + toplevel && return cbs systems = get_systems(sys) - cbs = [obs; + cbs = [cbs; reduce(vcat, (map(o -> namespace_callback(o, s), discrete_events(s)) for s in systems), init = SymbolicDiscreteCallback[])] @@ -688,7 +690,7 @@ function generate_rootfinding_callback(sys::AbstractTimeDependentSystem, generate_rootfinding_callback(cbs, sys, dvs, ps; kwargs...) end """ -Generate a single rootfinding callback; this happens if there is only one equation in `cbs` passed to +Generate a single rootfinding callback; this happens if there is only one equation in `cbs` passed to generate_rootfinding_callback and thus we can produce a ContinuousCallback instead of a VectorContinuousCallback. """ function generate_single_rootfinding_callback( From 91f938a530a20c4a76dbf5cb5f5d803f696e750b Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Sun, 16 Mar 2025 01:14:42 +0530 Subject: [PATCH 1117/1337] fix: don't use `===` for comparison in variable metadata tests --- test/test_variable_metadata.jl | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/test/test_variable_metadata.jl b/test/test_variable_metadata.jl index 8fca96a23d..aaf6addb59 100644 --- a/test/test_variable_metadata.jl +++ b/test/test_variable_metadata.jl @@ -54,30 +54,30 @@ end # Guess @variables y [guess = 0] -@test getguess(y) === 0 -@test hasguess(y) === true +@test getguess(y) == 0 +@test hasguess(y) == true @test ModelingToolkit.dump_variable_metadata(y).guess == 0 # Default @variables y = 0 -@test ModelingToolkit.getdefault(y) === 0 -@test ModelingToolkit.hasdefault(y) === true +@test ModelingToolkit.getdefault(y) == 0 +@test ModelingToolkit.hasdefault(y) == true @test ModelingToolkit.dump_variable_metadata(y).default == 0 # Issue#2653 @variables y[1:3] [guess = ones(3)] @test getguess(y) == ones(3) -@test hasguess(y) === true +@test hasguess(y) == true @test ModelingToolkit.dump_variable_metadata(y).guess == ones(3) for i in 1:3 @test getguess(y[i]) == 1.0 - @test hasguess(y[i]) === true + @test hasguess(y[i]) == true @test ModelingToolkit.dump_variable_metadata(y[i]).guess == 1.0 end @variables y -@test hasguess(y) === false +@test hasguess(y) == false @test !haskey(ModelingToolkit.dump_variable_metadata(y), :guess) # Disturbance From 1d872e04a7e727cf4f2e4d6c1bc59bb5748339e8 Mon Sep 17 00:00:00 2001 From: Torkel Loman Date: Sun, 16 Mar 2025 12:11:54 +0000 Subject: [PATCH 1118/1337] update with aayush code to handle flattened systems --- src/systems/abstractsystem.jl | 29 +++++++++++++++++++++++++++++ src/systems/callbacks.jl | 12 ++++++++++-- 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index 010456a094..6f72991297 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -1292,8 +1292,12 @@ $(TYPEDSIGNATURES) Get the unknown variables of the system `sys` and its subsystems. See also [`ModelingToolkit.get_unknowns`](@ref). + +Arguments: +- `toplevel = false`: if set to true, do not return the continuous events of the subsystems. """ function unknowns(sys::AbstractSystem; toplevel = false) + toplevel && (sys = recursive_get_parent(sys)) sts = get_unknowns(sys) systems = get_systems(sys) nonunique_unknowns = if toplevel || isempty(systems) @@ -1319,8 +1323,12 @@ $(TYPEDSIGNATURES) Get the parameters of the system `sys` and its subsystems. See also [`@parameters`](@ref) and [`ModelingToolkit.get_ps`](@ref). + +Arguments: +- `toplevel = false`: if set to true, do not return the continuous events of the subsystems. """ function parameters(sys::AbstractSystem; initial_parameters = false, toplevel = false) + toplevel && (sys = recursive_get_parent(sys)) ps = get_ps(sys) if ps == SciMLBase.NullParameters() return [] @@ -1353,6 +1361,22 @@ function dependent_parameters(sys::AbstractSystem) return map(eq -> eq.lhs, parameter_dependencies(sys)) end +""" + recursive_get_parent(sys::AbstractSystem) + +Loops through parent systems to find the original parent system. + +Warning: +- Curently only used (and tested) in the context of accessor functions (e.g. `parameters`), +specifically in the context of the `toplevel` keyword argument. +""" +function recursive_get_parent(sys::AbstractSystem) + if ModelingToolkit.has_parent(sys) && (p = ModelingToolkit.get_parent(sys)) !== nothing + return recursive_get_parent(p) + end + return sys +end + """ $(TYPEDSIGNATURES) Get the parameter dependencies of the system `sys` and its subsystems. @@ -1491,6 +1515,7 @@ function controls(sys::AbstractSystem) end function observed(sys::AbstractSystem; toplevel = false) + toplevel && (sys = recursive_get_parent(sys)) obs = get_observed(sys) toplevel && return obs systems = get_systems(sys) @@ -1550,8 +1575,12 @@ It may include some abbreviations and aliases of observables. It is often the most useful way to inspect the equations of a system. See also [`full_equations`](@ref) and [`ModelingToolkit.get_eqs`](@ref). + +Arguments: +- `toplevel = false`: if set to true, do not return the continuous events of the subsystems. """ function equations(sys::AbstractSystem; toplevel = false) + toplevel && (sys = recursive_get_parent(sys)) eqs = get_eqs(sys) systems = get_systems(sys) if toplevel || isempty(systems) diff --git a/src/systems/callbacks.jl b/src/systems/callbacks.jl index 5b9dab6fb0..715b12999a 100644 --- a/src/systems/callbacks.jl +++ b/src/systems/callbacks.jl @@ -320,14 +320,18 @@ function namespace_callback(cb::SymbolicContinuousCallback, s)::SymbolicContinuo end """ - continuous_events(sys::AbstractSystem)::Vector{SymbolicContinuousCallback} + continuous_events(sys::AbstractSystem; toplevel = false)::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`. + +Arguments: +- `toplevel = false`: if set to true, do not return the continuous events of the subsystems. """ function continuous_events(sys::AbstractSystem; toplevel = false) + toplevel && (sys = recursive_get_parent(sys)) cbs = get_continuous_events(sys) filter(!isempty, cbs) toplevel && return cbs @@ -476,14 +480,18 @@ SymbolicDiscreteCallbacks(cbs::Vector{<:SymbolicDiscreteCallback}) = cbs SymbolicDiscreteCallbacks(::Nothing) = SymbolicDiscreteCallback[] """ - discrete_events(sys::AbstractSystem) :: Vector{SymbolicDiscreteCallback} + discrete_events(sys::AbstractSystem; toplevel = false) :: 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`. + +Arguments: +- `toplevel = false`: if set to true, do not return the discrete events of the subsystems. """ function discrete_events(sys::AbstractSystem; toplevel = false) + toplevel && (sys = recursive_get_parent(sys)) cbs = get_discrete_events(sys) toplevel && return cbs systems = get_systems(sys) From 06419376dcfea0105e68bdacb5121365b7f08f8a Mon Sep 17 00:00:00 2001 From: Torkel Loman Date: Sun, 16 Mar 2025 12:11:59 +0000 Subject: [PATCH 1119/1337] add tests --- test/accessor_functions.jl | 142 +++++++++++++++++++++++++++++++++++++ test/runtests.jl | 1 + 2 files changed, 143 insertions(+) create mode 100644 test/accessor_functions.jl diff --git a/test/accessor_functions.jl b/test/accessor_functions.jl new file mode 100644 index 0000000000..78c00b00fc --- /dev/null +++ b/test/accessor_functions.jl @@ -0,0 +1,142 @@ +### Preparations ### + +# Fetch packages. +using ModelingToolkit, Test +using ModelingToolkit: t_nounits as t, D_nounits as D +import ModelingToolkit: get_ps, get_unknowns, get_observed, get_eqs, get_continuous_events, + get_discrete_events, namespace_equations + +# Creates helper functions. +function all_sets_equal(args...) + for arg in args[2:end] + issetequal(args[1], arg) || return false + end + return true +end +function sym_issubset(set1, set2) + for sym1 in set1 + any(isequal(sym1, sym2) for sym2 in set2) || return false + end + return true +end + +### Basic Tests ### + +# Checks `toplevel = false` argument for various accessors (currently only for `ODESystem`s). +# Compares to `toplevel = true` version, and `get_` functions. +# Checks accessors for parameters, unknowns, equations, observables, and events. +let + # Prepares model components. + @parameters p_top p_mid1 p_mid2 p_bot d + @variables X_top(t) X_mid1(t) X_mid2(t) X_bot(t) Y(t) O(t) + + # Creates the systems (individual and hierarchical). + eqs_top = [ + D(X_top) ~ p_top - d*X_top, + D(Y) ~ log(X_top) - Y^2 + 3.0, + O ~ (p_top + d)*X_top + Y + ] + eqs_mid1 = [ + D(X_mid1) ~ p_mid1 - d*X_mid1^2, + D(Y) ~ D(X_mid1) - Y^3, + O ~ (p_mid1 + d)*X_mid1 + Y + ] + eqs_mid2 = [ + D(X_mid2) ~ p_mid2 - d*X_mid2, + X_mid2^3 ~ log(X_mid2 + Y) - Y^2 + 3.0, + O ~ (p_mid2 + d)*X_mid2 + Y + ] + eqs_bot = [ + D(X_bot) ~ p_bot - d*X_bot, + D(Y) ~ -Y^3, + O ~ (p_bot + d)*X_bot + Y + ] + cevs = [[t ~ 1.0] => [Y ~ Y + 2.0]] + devs = [(t == 2.0) => [Y ~ Y + 2.0]] + @named sys_bot = ODESystem(eqs_bot, t; systems = [], continuous_events = cevs, discrete_events = devs) + @named sys_mid2 = ODESystem(eqs_mid2, t; systems = [], continuous_events = cevs, discrete_events = devs) + @named sys_mid1 = ODESystem(eqs_mid1, t; systems = [sys_bot], continuous_events = cevs, discrete_events = devs) + @named sys_top = ODESystem(eqs_top, t; systems = [sys_mid1, sys_mid2], continuous_events = cevs, discrete_events = devs) + sys_bot_comp = complete(sys_bot) + sys_mid2_comp = complete(sys_mid2) + sys_mid1_comp = complete(sys_mid1) + sys_top_comp = complete(sys_top) + sys_bot_ss = structural_simplify(sys_bot) + sys_mid2_ss = structural_simplify(sys_mid2) + sys_mid1_ss = structural_simplify(sys_mid1) + sys_top_ss = structural_simplify(sys_top) + + # Checks `parameters` for `toplevel = false`. + @test all_sets_equal(parameters.([sys_bot, sys_bot_comp, sys_bot_ss])..., [d, p_bot]) + @test all_sets_equal(parameters.([sys_mid1, sys_mid1_comp, sys_mid1_ss])..., [d, p_mid1, sys_bot.d, sys_bot.p_bot]) + @test all_sets_equal(parameters.([sys_mid2, sys_mid2_comp, sys_mid2_ss])..., [d, p_mid2]) + @test all_sets_equal(parameters.([sys_top, sys_top_comp, sys_top_ss])..., [d, p_top, sys_mid1.d, sys_mid1.p_mid1, sys_mid1.sys_bot.d, sys_mid1.sys_bot.p_bot, sys_mid2.d, sys_mid2.p_mid2]) + + # Checks `parameters`` for `toplevel = true`. Compares to known parameters and also checks that + # these are subset of what `get_ps` returns. + @test all_sets_equal(parameters.([sys_bot, sys_bot_comp, sys_bot_ss]; toplevel = true)..., [d, p_bot]) + @test_broken all_sets_equal(parameters.([sys_mid1, sys_mid1_comp, sys_mid1_ss]; toplevel = true)..., [d, p_mid1]) + @test all_sets_equal(parameters.([sys_mid2, sys_mid2_comp, sys_mid2_ss]; toplevel = true)..., [d, p_mid2]) + @test_broken all_sets_equal(parameters.([sys_top, sys_top_comp, sys_top_ss]; toplevel = true)..., [d, p_top]) + @test all(sym_issubset(parameters(sys; toplevel = true), get_ps(sys)) for sys in [sys_bot, sys_mid2, sys_mid1, sys_top]) + + # Checks `unknowns` for `toplevel = false`. O(t) is eliminated by `structural_simplify` and + # must be considered separately. + @test all_sets_equal(unknowns.([sys_bot, sys_bot_comp])..., [O, Y, X_bot]) + @test all_sets_equal(unknowns.([sys_bot_ss])..., [Y, X_bot]) + @test all_sets_equal(unknowns.([sys_mid1, sys_mid1_comp])..., [O, Y, X_mid1, sys_bot.Y, sys_bot.O, sys_bot.X_bot]) + @test all_sets_equal(unknowns.([sys_mid1_ss])..., [Y, X_mid1, sys_bot.Y, sys_bot.X_bot]) + @test all_sets_equal(unknowns.([sys_mid2, sys_mid2_comp])..., [O, Y, X_mid2]) + @test all_sets_equal(unknowns.([sys_mid2_ss])..., [Y, X_mid2]) + @test all_sets_equal(unknowns.([sys_top, sys_top_comp])..., [O, Y, X_top, sys_mid1.O, sys_mid1.Y, sys_mid1.X_mid1, sys_mid1.sys_bot.O, sys_mid1.sys_bot.Y, sys_mid1.sys_bot.X_bot, sys_mid2.O, sys_mid2.Y, sys_mid2.X_mid2]) + @test all_sets_equal(unknowns.([sys_top_ss])..., [Y, X_top, sys_mid1.Y, sys_mid1.X_mid1, sys_mid1.sys_bot.Y, sys_mid1.sys_bot.X_bot, sys_mid2.Y, sys_mid2.X_mid2]) + + # Checks `unknowns` for `toplevel = true`. Note that O is not eliminated here (as we go back + # to original parent system). Also checks that outputs are subsets of what `get_ps` returns.. + @test all_sets_equal(unknowns.([sys_bot, sys_bot_comp, sys_bot_ss]; toplevel = true)..., [O, Y, X_bot]) + @test all_sets_equal(unknowns.([sys_mid1, sys_mid1_comp]; toplevel = true)..., [O, Y, X_mid1]) + @test all_sets_equal(unknowns.([sys_mid2, sys_mid2_comp]; toplevel = true)..., [O, Y, X_mid2]) + @test all_sets_equal(unknowns.([sys_top, sys_top_comp]; toplevel = true)..., [O, Y, X_top]) + @test all(sym_issubset(unknowns(sys; toplevel = true), get_unknowns(sys)) for sys in [sys_bot, sys_mid1, sys_mid2, sys_top]) + + # Checks `equations` for `toplevel = false`. Do not check ss equations as these might potentially + # be structurally simplified to new equations. + @test all_sets_equal(equations.([sys_bot, sys_bot_comp])..., eqs_bot) + @test all_sets_equal(equations.([sys_mid1, sys_mid1_comp])..., [eqs_mid1; namespace_equations(sys_bot)]) + @test all_sets_equal(equations.([sys_mid2, sys_mid2_comp])..., eqs_mid2) + @test all_sets_equal(equations.([sys_top, sys_top_comp])..., [eqs_top; namespace_equations(sys_mid1); namespace_equations(sys_mid2)]) + + # Checks `equations` for `toplevel = true`. Do not check ss equations directly as these + # might potentially be structurally simplified to new equations. + @test all_sets_equal(equations.([sys_bot, sys_bot_comp]; toplevel = true)..., eqs_bot) + @test all_sets_equal(equations.([sys_mid1, sys_mid1_comp]; toplevel = true)..., eqs_mid1) + @test all_sets_equal(equations.([sys_mid2, sys_mid2_comp]; toplevel = true)..., eqs_mid2) + @test all_sets_equal(equations.([sys_top, sys_top_comp]; toplevel = true)..., eqs_top) + @test all(sym_issubset(equations(sys; toplevel = true), get_eqs(sys)) for sys in [sys_bot, sys_mid2, sys_mid1, sys_top]) + + # Checks `observed for `toplevel = false`. For non-ss systems, there are no observables. + @test all_sets_equal(observed.([sys_bot, sys_bot_comp, sys_mid1, sys_mid1_comp, sys_mid2, sys_mid2_comp, sys_top, sys_top_comp])..., []) + @test issetequal(observed(sys_bot_ss), eqs_bot[3:3]) + @test issetequal(observed(sys_mid1_ss), [eqs_mid1[3:3]; namespace_equations(sys_bot)[3:3]]) + @test issetequal(observed(sys_mid2_ss), eqs_mid2[3:3]) + @test issetequal(observed(sys_top_ss), [eqs_top[3:3]; namespace_equations(sys_mid1)[3:3:6]; namespace_equations(sys_mid2)[3:3]]) + + # Checks `observed` for `toplevel = true`. Again, for non-ss systems, there are no observables. + # Also checks that `toplevel = true` yields subset of `get_observed`. + @test all_sets_equal(observed.([sys_bot, sys_bot_comp, sys_mid1, sys_mid1_comp, sys_mid2, sys_mid2_comp, sys_top, sys_top_comp]; toplevel = true)..., []) + @test_broken issetequal(observed(sys_bot_ss; toplevel = true), eqs_bot[3:3]) + @test_broken issetequal(observed(sys_mid1_ss; toplevel = true), eqs_mid1[3:3]) + @test_broken issetequal(observed(sys_mid2_ss; toplevel = true), eqs_mid2[3:3]) + @test_broken issetequal(observed(sys_top_ss; toplevel = true), eqs_top[3:3]) + @test all(sym_issubset(observed(sys; toplevel = true), get_observed(sys)) for sys in [sys_bot, sys_mid2, sys_mid1, sys_top]) + + # Checks `continuous_events` and `discrete_events` for `toplevel = true` (more straightforward + # as I stored the same singe event in all systems). Don't check for `toplevel = false` as + # technically not needed for these tests and name spacing the events is a mess. + mtk_cev = ModelingToolkit.SymbolicContinuousCallback.(cevs)[1] + mtk_dev = ModelingToolkit.SymbolicDiscreteCallback.(devs)[1] + @test all_sets_equal(continuous_events.([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]; toplevel = true)..., [mtk_cev]) + @test all_sets_equal(discrete_events.([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]; toplevel = true)..., [mtk_dev]) + @test all(sym_issubset(continuous_events(sys; toplevel = true), get_continuous_events(sys)) for sys in [sys_bot, sys_mid2, sys_mid1, sys_top]) + @test all(sym_issubset(discrete_events(sys; toplevel = true), get_discrete_events(sys)) for sys in [sys_bot, sys_mid2, sys_mid1, sys_top]) +end diff --git a/test/runtests.jl b/test/runtests.jl index e0ef4b8640..4537c27255 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -61,6 +61,7 @@ end @safetestset "Constants Test" include("constants.jl") @safetestset "Parameter Dependency Test" include("parameter_dependencies.jl") @safetestset "Equation Type Accessors Test" include("equation_type_accessors.jl") + @safetestset "System Accessor Functions Test" include("accessor_functions.jl") @safetestset "Equations with complex values" include("complex.jl") end end From 78672024c585e6fd320d502023a99e93c11263c6 Mon Sep 17 00:00:00 2001 From: Torkel Loman Date: Sun, 16 Mar 2025 12:19:50 +0000 Subject: [PATCH 1120/1337] formatting --- src/systems/abstractsystem.jl | 4 +- test/accessor_functions.jl | 142 ++++++++++++++++++++++++---------- 2 files changed, 101 insertions(+), 45 deletions(-) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index 6f72991297..969f5d89ec 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -1545,8 +1545,8 @@ function defaults(sys::AbstractSystem; toplevel = false) # `compose(ODESystem(...; defaults=defs), ...)` # # Thus, right associativity is required and crucial for correctness. - (toplevel ||isempty(systems)) ? - defs : mapfoldr(namespace_defaults, merge, systems; init = defs) + (toplevel || isempty(systems)) ? + defs : mapfoldr(namespace_defaults, merge, systems; init = defs) end function defaults_and_guesses(sys::AbstractSystem) diff --git a/test/accessor_functions.jl b/test/accessor_functions.jl index 78c00b00fc..2402c53da7 100644 --- a/test/accessor_functions.jl +++ b/test/accessor_functions.jl @@ -4,7 +4,7 @@ using ModelingToolkit, Test using ModelingToolkit: t_nounits as t, D_nounits as D import ModelingToolkit: get_ps, get_unknowns, get_observed, get_eqs, get_continuous_events, - get_discrete_events, namespace_equations + get_discrete_events, namespace_equations # Creates helper functions. function all_sets_equal(args...) @@ -32,31 +32,35 @@ let # Creates the systems (individual and hierarchical). eqs_top = [ - D(X_top) ~ p_top - d*X_top, + D(X_top) ~ p_top - d * X_top, D(Y) ~ log(X_top) - Y^2 + 3.0, - O ~ (p_top + d)*X_top + Y + O ~ (p_top + d) * X_top + Y ] eqs_mid1 = [ - D(X_mid1) ~ p_mid1 - d*X_mid1^2, + D(X_mid1) ~ p_mid1 - d * X_mid1^2, D(Y) ~ D(X_mid1) - Y^3, - O ~ (p_mid1 + d)*X_mid1 + Y + O ~ (p_mid1 + d) * X_mid1 + Y ] eqs_mid2 = [ - D(X_mid2) ~ p_mid2 - d*X_mid2, + D(X_mid2) ~ p_mid2 - d * X_mid2, X_mid2^3 ~ log(X_mid2 + Y) - Y^2 + 3.0, - O ~ (p_mid2 + d)*X_mid2 + Y + O ~ (p_mid2 + d) * X_mid2 + Y ] eqs_bot = [ - D(X_bot) ~ p_bot - d*X_bot, + D(X_bot) ~ p_bot - d * X_bot, D(Y) ~ -Y^3, - O ~ (p_bot + d)*X_bot + Y + O ~ (p_bot + d) * X_bot + Y ] cevs = [[t ~ 1.0] => [Y ~ Y + 2.0]] devs = [(t == 2.0) => [Y ~ Y + 2.0]] - @named sys_bot = ODESystem(eqs_bot, t; systems = [], continuous_events = cevs, discrete_events = devs) - @named sys_mid2 = ODESystem(eqs_mid2, t; systems = [], continuous_events = cevs, discrete_events = devs) - @named sys_mid1 = ODESystem(eqs_mid1, t; systems = [sys_bot], continuous_events = cevs, discrete_events = devs) - @named sys_top = ODESystem(eqs_top, t; systems = [sys_mid1, sys_mid2], continuous_events = cevs, discrete_events = devs) + @named sys_bot = ODESystem( + eqs_bot, t; systems = [], continuous_events = cevs, discrete_events = devs) + @named sys_mid2 = ODESystem( + eqs_mid2, t; systems = [], continuous_events = cevs, discrete_events = devs) + @named sys_mid1 = ODESystem( + eqs_mid1, t; systems = [sys_bot], continuous_events = cevs, discrete_events = devs) + @named sys_top = ODESystem(eqs_top, t; systems = [sys_mid1, sys_mid2], + continuous_events = cevs, discrete_events = devs) sys_bot_comp = complete(sys_bot) sys_mid2_comp = complete(sys_mid2) sys_mid1_comp = complete(sys_mid1) @@ -68,75 +72,127 @@ let # Checks `parameters` for `toplevel = false`. @test all_sets_equal(parameters.([sys_bot, sys_bot_comp, sys_bot_ss])..., [d, p_bot]) - @test all_sets_equal(parameters.([sys_mid1, sys_mid1_comp, sys_mid1_ss])..., [d, p_mid1, sys_bot.d, sys_bot.p_bot]) - @test all_sets_equal(parameters.([sys_mid2, sys_mid2_comp, sys_mid2_ss])..., [d, p_mid2]) - @test all_sets_equal(parameters.([sys_top, sys_top_comp, sys_top_ss])..., [d, p_top, sys_mid1.d, sys_mid1.p_mid1, sys_mid1.sys_bot.d, sys_mid1.sys_bot.p_bot, sys_mid2.d, sys_mid2.p_mid2]) + @test all_sets_equal(parameters.([sys_mid1, sys_mid1_comp, sys_mid1_ss])..., + [d, p_mid1, sys_bot.d, sys_bot.p_bot]) + @test all_sets_equal( + parameters.([sys_mid2, sys_mid2_comp, sys_mid2_ss])..., [d, p_mid2]) + @test all_sets_equal(parameters.([sys_top, sys_top_comp, sys_top_ss])..., + [d, p_top, sys_mid1.d, sys_mid1.p_mid1, sys_mid1.sys_bot.d, + sys_mid1.sys_bot.p_bot, sys_mid2.d, sys_mid2.p_mid2]) # Checks `parameters`` for `toplevel = true`. Compares to known parameters and also checks that # these are subset of what `get_ps` returns. - @test all_sets_equal(parameters.([sys_bot, sys_bot_comp, sys_bot_ss]; toplevel = true)..., [d, p_bot]) - @test_broken all_sets_equal(parameters.([sys_mid1, sys_mid1_comp, sys_mid1_ss]; toplevel = true)..., [d, p_mid1]) - @test all_sets_equal(parameters.([sys_mid2, sys_mid2_comp, sys_mid2_ss]; toplevel = true)..., [d, p_mid2]) - @test_broken all_sets_equal(parameters.([sys_top, sys_top_comp, sys_top_ss]; toplevel = true)..., [d, p_top]) - @test all(sym_issubset(parameters(sys; toplevel = true), get_ps(sys)) for sys in [sys_bot, sys_mid2, sys_mid1, sys_top]) + @test all_sets_equal( + parameters.([sys_bot, sys_bot_comp, sys_bot_ss]; toplevel = true)..., [d, p_bot]) + @test_broken all_sets_equal( + parameters.([sys_mid1, sys_mid1_comp, sys_mid1_ss]; toplevel = true)..., + [d, p_mid1]) + @test all_sets_equal( + parameters.([sys_mid2, sys_mid2_comp, sys_mid2_ss]; toplevel = true)..., + [d, p_mid2]) + @test_broken all_sets_equal( + parameters.([sys_top, sys_top_comp, sys_top_ss]; toplevel = true)..., [d, p_top]) + @test all(sym_issubset(parameters(sys; toplevel = true), get_ps(sys)) + for sys in [sys_bot, sys_mid2, sys_mid1, sys_top]) # Checks `unknowns` for `toplevel = false`. O(t) is eliminated by `structural_simplify` and # must be considered separately. @test all_sets_equal(unknowns.([sys_bot, sys_bot_comp])..., [O, Y, X_bot]) @test all_sets_equal(unknowns.([sys_bot_ss])..., [Y, X_bot]) - @test all_sets_equal(unknowns.([sys_mid1, sys_mid1_comp])..., [O, Y, X_mid1, sys_bot.Y, sys_bot.O, sys_bot.X_bot]) + @test all_sets_equal(unknowns.([sys_mid1, sys_mid1_comp])..., + [O, Y, X_mid1, sys_bot.Y, sys_bot.O, sys_bot.X_bot]) @test all_sets_equal(unknowns.([sys_mid1_ss])..., [Y, X_mid1, sys_bot.Y, sys_bot.X_bot]) @test all_sets_equal(unknowns.([sys_mid2, sys_mid2_comp])..., [O, Y, X_mid2]) @test all_sets_equal(unknowns.([sys_mid2_ss])..., [Y, X_mid2]) - @test all_sets_equal(unknowns.([sys_top, sys_top_comp])..., [O, Y, X_top, sys_mid1.O, sys_mid1.Y, sys_mid1.X_mid1, sys_mid1.sys_bot.O, sys_mid1.sys_bot.Y, sys_mid1.sys_bot.X_bot, sys_mid2.O, sys_mid2.Y, sys_mid2.X_mid2]) - @test all_sets_equal(unknowns.([sys_top_ss])..., [Y, X_top, sys_mid1.Y, sys_mid1.X_mid1, sys_mid1.sys_bot.Y, sys_mid1.sys_bot.X_bot, sys_mid2.Y, sys_mid2.X_mid2]) + @test all_sets_equal(unknowns.([sys_top, sys_top_comp])..., + [O, Y, X_top, sys_mid1.O, sys_mid1.Y, sys_mid1.X_mid1, + sys_mid1.sys_bot.O, sys_mid1.sys_bot.Y, sys_mid1.sys_bot.X_bot, + sys_mid2.O, sys_mid2.Y, sys_mid2.X_mid2]) + @test all_sets_equal(unknowns.([sys_top_ss])..., + [Y, X_top, sys_mid1.Y, sys_mid1.X_mid1, sys_mid1.sys_bot.Y, + sys_mid1.sys_bot.X_bot, sys_mid2.Y, sys_mid2.X_mid2]) # Checks `unknowns` for `toplevel = true`. Note that O is not eliminated here (as we go back # to original parent system). Also checks that outputs are subsets of what `get_ps` returns.. - @test all_sets_equal(unknowns.([sys_bot, sys_bot_comp, sys_bot_ss]; toplevel = true)..., [O, Y, X_bot]) - @test all_sets_equal(unknowns.([sys_mid1, sys_mid1_comp]; toplevel = true)..., [O, Y, X_mid1]) - @test all_sets_equal(unknowns.([sys_mid2, sys_mid2_comp]; toplevel = true)..., [O, Y, X_mid2]) - @test all_sets_equal(unknowns.([sys_top, sys_top_comp]; toplevel = true)..., [O, Y, X_top]) - @test all(sym_issubset(unknowns(sys; toplevel = true), get_unknowns(sys)) for sys in [sys_bot, sys_mid1, sys_mid2, sys_top]) + @test all_sets_equal( + unknowns.([sys_bot, sys_bot_comp, sys_bot_ss]; toplevel = true)..., [O, Y, X_bot]) + @test all_sets_equal( + unknowns.([sys_mid1, sys_mid1_comp]; toplevel = true)..., [O, Y, X_mid1]) + @test all_sets_equal( + unknowns.([sys_mid2, sys_mid2_comp]; toplevel = true)..., [O, Y, X_mid2]) + @test all_sets_equal( + unknowns.([sys_top, sys_top_comp]; toplevel = true)..., [O, Y, X_top]) + @test all(sym_issubset(unknowns(sys; toplevel = true), get_unknowns(sys)) + for sys in [sys_bot, sys_mid1, sys_mid2, sys_top]) # Checks `equations` for `toplevel = false`. Do not check ss equations as these might potentially # be structurally simplified to new equations. @test all_sets_equal(equations.([sys_bot, sys_bot_comp])..., eqs_bot) - @test all_sets_equal(equations.([sys_mid1, sys_mid1_comp])..., [eqs_mid1; namespace_equations(sys_bot)]) + @test all_sets_equal( + equations.([sys_mid1, sys_mid1_comp])..., [eqs_mid1; namespace_equations(sys_bot)]) @test all_sets_equal(equations.([sys_mid2, sys_mid2_comp])..., eqs_mid2) - @test all_sets_equal(equations.([sys_top, sys_top_comp])..., [eqs_top; namespace_equations(sys_mid1); namespace_equations(sys_mid2)]) + @test all_sets_equal(equations.([sys_top, sys_top_comp])..., + [eqs_top; namespace_equations(sys_mid1); namespace_equations(sys_mid2)]) # Checks `equations` for `toplevel = true`. Do not check ss equations directly as these # might potentially be structurally simplified to new equations. @test all_sets_equal(equations.([sys_bot, sys_bot_comp]; toplevel = true)..., eqs_bot) - @test all_sets_equal(equations.([sys_mid1, sys_mid1_comp]; toplevel = true)..., eqs_mid1) - @test all_sets_equal(equations.([sys_mid2, sys_mid2_comp]; toplevel = true)..., eqs_mid2) + @test all_sets_equal( + equations.([sys_mid1, sys_mid1_comp]; toplevel = true)..., eqs_mid1) + @test all_sets_equal( + equations.([sys_mid2, sys_mid2_comp]; toplevel = true)..., eqs_mid2) @test all_sets_equal(equations.([sys_top, sys_top_comp]; toplevel = true)..., eqs_top) - @test all(sym_issubset(equations(sys; toplevel = true), get_eqs(sys)) for sys in [sys_bot, sys_mid2, sys_mid1, sys_top]) + @test all(sym_issubset(equations(sys; toplevel = true), get_eqs(sys)) + for sys in [sys_bot, sys_mid2, sys_mid1, sys_top]) # Checks `observed for `toplevel = false`. For non-ss systems, there are no observables. - @test all_sets_equal(observed.([sys_bot, sys_bot_comp, sys_mid1, sys_mid1_comp, sys_mid2, sys_mid2_comp, sys_top, sys_top_comp])..., []) + @test all_sets_equal( + observed.([sys_bot, sys_bot_comp, sys_mid1, sys_mid1_comp, + sys_mid2, sys_mid2_comp, sys_top, sys_top_comp])..., + []) @test issetequal(observed(sys_bot_ss), eqs_bot[3:3]) - @test issetequal(observed(sys_mid1_ss), [eqs_mid1[3:3]; namespace_equations(sys_bot)[3:3]]) + @test issetequal( + observed(sys_mid1_ss), [eqs_mid1[3:3]; namespace_equations(sys_bot)[3:3]]) @test issetequal(observed(sys_mid2_ss), eqs_mid2[3:3]) - @test issetequal(observed(sys_top_ss), [eqs_top[3:3]; namespace_equations(sys_mid1)[3:3:6]; namespace_equations(sys_mid2)[3:3]]) + @test issetequal(observed(sys_top_ss), + [eqs_top[3:3]; namespace_equations(sys_mid1)[3:3:6]; + namespace_equations(sys_mid2)[3:3]]) # Checks `observed` for `toplevel = true`. Again, for non-ss systems, there are no observables. # Also checks that `toplevel = true` yields subset of `get_observed`. - @test all_sets_equal(observed.([sys_bot, sys_bot_comp, sys_mid1, sys_mid1_comp, sys_mid2, sys_mid2_comp, sys_top, sys_top_comp]; toplevel = true)..., []) + @test all_sets_equal( + observed.( + [sys_bot, sys_bot_comp, sys_mid1, sys_mid1_comp, + sys_mid2, sys_mid2_comp, sys_top, sys_top_comp]; + toplevel = true)..., + []) @test_broken issetequal(observed(sys_bot_ss; toplevel = true), eqs_bot[3:3]) @test_broken issetequal(observed(sys_mid1_ss; toplevel = true), eqs_mid1[3:3]) @test_broken issetequal(observed(sys_mid2_ss; toplevel = true), eqs_mid2[3:3]) @test_broken issetequal(observed(sys_top_ss; toplevel = true), eqs_top[3:3]) - @test all(sym_issubset(observed(sys; toplevel = true), get_observed(sys)) for sys in [sys_bot, sys_mid2, sys_mid1, sys_top]) + @test all(sym_issubset(observed(sys; toplevel = true), get_observed(sys)) + for sys in [sys_bot, sys_mid2, sys_mid1, sys_top]) # Checks `continuous_events` and `discrete_events` for `toplevel = true` (more straightforward # as I stored the same singe event in all systems). Don't check for `toplevel = false` as # technically not needed for these tests and name spacing the events is a mess. mtk_cev = ModelingToolkit.SymbolicContinuousCallback.(cevs)[1] mtk_dev = ModelingToolkit.SymbolicDiscreteCallback.(devs)[1] - @test all_sets_equal(continuous_events.([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]; toplevel = true)..., [mtk_cev]) - @test all_sets_equal(discrete_events.([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]; toplevel = true)..., [mtk_dev]) - @test all(sym_issubset(continuous_events(sys; toplevel = true), get_continuous_events(sys)) for sys in [sys_bot, sys_mid2, sys_mid1, sys_top]) - @test all(sym_issubset(discrete_events(sys; toplevel = true), get_discrete_events(sys)) for sys in [sys_bot, sys_mid2, sys_mid1, sys_top]) + @test all_sets_equal( + continuous_events.( + [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]; + toplevel = true)..., + [mtk_cev]) + @test all_sets_equal( + discrete_events.( + [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]; + toplevel = true)..., + [mtk_dev]) + @test all(sym_issubset( + continuous_events(sys; toplevel = true), get_continuous_events(sys)) + for sys in [sys_bot, sys_mid2, sys_mid1, sys_top]) + @test all(sym_issubset(discrete_events(sys; toplevel = true), get_discrete_events(sys)) + for sys in [sys_bot, sys_mid2, sys_mid1, sys_top]) end From 3e07adf208fbeb90dfc1e73faa14f3ffb2b70e4d Mon Sep 17 00:00:00 2001 From: Torkel Loman Date: Sun, 16 Mar 2025 13:20:04 +0000 Subject: [PATCH 1121/1337] create separate _toplevel function versions isntead --- src/systems/abstractsystem.jl | 77 ++++++++++++++++++++--------------- src/systems/callbacks.jl | 48 +++++++++++++++------- 2 files changed, 79 insertions(+), 46 deletions(-) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index 969f5d89ec..d23289d964 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -1292,15 +1292,11 @@ $(TYPEDSIGNATURES) Get the unknown variables of the system `sys` and its subsystems. See also [`ModelingToolkit.get_unknowns`](@ref). - -Arguments: -- `toplevel = false`: if set to true, do not return the continuous events of the subsystems. """ -function unknowns(sys::AbstractSystem; toplevel = false) - toplevel && (sys = recursive_get_parent(sys)) +function unknowns(sys::AbstractSystem) sts = get_unknowns(sys) systems = get_systems(sys) - nonunique_unknowns = if toplevel || isempty(systems) + nonunique_unknowns = if isempty(systems) sts else system_unknowns = reduce(vcat, namespace_variables.(systems)) @@ -1317,6 +1313,19 @@ function unknowns(sys::AbstractSystem; toplevel = false) unique(nonunique_unknowns) end +""" + unknowns_toplevel(sys::AbstractSystem) + +Replicates the behaviour of `unknowns`, but ignores unknowns of subsystems. +""" +function unknowns_toplevel(sys::AbstractSystem) + if has_parent(sys) && (parent = get_parent(sys)) !== nothing + return unknowns_toplevel(parent) + end + return get_unknowns(sys) +end + + """ $(TYPEDSIGNATURES) @@ -1324,11 +1333,8 @@ Get the parameters of the system `sys` and its subsystems. See also [`@parameters`](@ref) and [`ModelingToolkit.get_ps`](@ref). -Arguments: -- `toplevel = false`: if set to true, do not return the continuous events of the subsystems. """ -function parameters(sys::AbstractSystem; initial_parameters = false, toplevel = false) - toplevel && (sys = recursive_get_parent(sys)) +function parameters(sys::AbstractSystem; initial_parameters = false) ps = get_ps(sys) if ps == SciMLBase.NullParameters() return [] @@ -1337,7 +1343,7 @@ function parameters(sys::AbstractSystem; initial_parameters = false, toplevel = ps = first.(ps) end systems = get_systems(sys) - result = unique(toplevel || isempty(systems) ? + result = unique(isempty(systems) ? ps : [ps; reduce(vcat, namespace_parameters.(systems))]) if !initial_parameters if is_time_dependent(sys) @@ -1362,19 +1368,15 @@ function dependent_parameters(sys::AbstractSystem) end """ - recursive_get_parent(sys::AbstractSystem) + parameters_toplevel(sys::AbstractSystem) -Loops through parent systems to find the original parent system. - -Warning: -- Curently only used (and tested) in the context of accessor functions (e.g. `parameters`), -specifically in the context of the `toplevel` keyword argument. +Replicates the behaviour of `parameters`, but ignores parameters of subsystems. """ -function recursive_get_parent(sys::AbstractSystem) - if ModelingToolkit.has_parent(sys) && (p = ModelingToolkit.get_parent(sys)) !== nothing - return recursive_get_parent(p) +function parameters_toplevel(sys::AbstractSystem) + if has_parent(sys) && (parent = get_parent(sys)) !== nothing + return parameters_toplevel(parent) end - return sys + return get_ps(sys) end """ @@ -1514,10 +1516,8 @@ function controls(sys::AbstractSystem) isempty(systems) ? ctrls : [ctrls; reduce(vcat, namespace_controls.(systems))] end -function observed(sys::AbstractSystem; toplevel = false) - toplevel && (sys = recursive_get_parent(sys)) +function observed(sys::AbstractSystem) obs = get_observed(sys) - toplevel && return obs systems = get_systems(sys) [obs; reduce(vcat, @@ -1536,7 +1536,7 @@ If they are not explicitly provided, variables and parameters are initialized to See also [`initialization_equations`](@ref), [`parameter_dependencies`](@ref) and [`ModelingToolkit.get_defaults`](@ref). """ -function defaults(sys::AbstractSystem; toplevel = false) +function defaults(sys::AbstractSystem) systems = get_systems(sys) defs = get_defaults(sys) # `mapfoldr` is really important!!! We should prefer the base model for @@ -1545,8 +1545,7 @@ function defaults(sys::AbstractSystem; toplevel = false) # `compose(ODESystem(...; defaults=defs), ...)` # # Thus, right associativity is required and crucial for correctness. - (toplevel || isempty(systems)) ? - defs : mapfoldr(namespace_defaults, merge, systems; init = defs) + isempty(systems) ? defs : mapfoldr(namespace_defaults, merge, systems; init = defs) end function defaults_and_guesses(sys::AbstractSystem) @@ -1576,14 +1575,11 @@ It is often the most useful way to inspect the equations of a system. See also [`full_equations`](@ref) and [`ModelingToolkit.get_eqs`](@ref). -Arguments: -- `toplevel = false`: if set to true, do not return the continuous events of the subsystems. """ -function equations(sys::AbstractSystem; toplevel = false) - toplevel && (sys = recursive_get_parent(sys)) +function equations(sys::AbstractSystem) eqs = get_eqs(sys) systems = get_systems(sys) - if toplevel || isempty(systems) + if isempty(systems) return eqs else eqs = Equation[eqs; @@ -1594,6 +1590,23 @@ function equations(sys::AbstractSystem; toplevel = false) end end + +""" + equations_toplevel(sys::AbstractSystem) + +Replicates the behaviour of `equations`, but ignores equations of subsystems. + +Notes: +- Cannot be applied to non-complete systems. +""" +function equations_toplevel(sys::AbstractSystem) + iscomplete(sys) && error("Cannot apply `equations_toplevel` to complete systems.") + if has_parent(sys) && (parent = get_parent(sys)) !== nothing + return equations_toplevel(parent) + end + return get_eqs(sys) +end + """ $(TYPEDSIGNATURES) diff --git a/src/systems/callbacks.jl b/src/systems/callbacks.jl index 715b12999a..710ccc4946 100644 --- a/src/systems/callbacks.jl +++ b/src/systems/callbacks.jl @@ -320,21 +320,16 @@ function namespace_callback(cb::SymbolicContinuousCallback, s)::SymbolicContinuo end """ - continuous_events(sys::AbstractSystem; toplevel = false)::Vector{SymbolicContinuousCallback} + 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`. - -Arguments: -- `toplevel = false`: if set to true, do not return the continuous events of the subsystems. """ -function continuous_events(sys::AbstractSystem; toplevel = false) - toplevel && (sys = recursive_get_parent(sys)) +function continuous_events(sys::AbstractSystem) cbs = get_continuous_events(sys) filter(!isempty, cbs) - toplevel && return cbs systems = get_systems(sys) cbs = [cbs; @@ -361,6 +356,21 @@ function vars!(vars, cb::SymbolicContinuousCallback; op = Differential) return vars end +""" + continuous_events_toplevel(sys::AbstractSystem) + +Replicates the behaviour of `continuous_events`, but ignores events of subsystems. + +Notes: +- Cannot be applied to non-complete systems. +""" +function continuous_events_toplevel(sys::AbstractSystem) + if has_parent(sys) && (parent = get_parent(sys)) !== nothing + return continuous_events_toplevel(parent) + end + return get_continuous_events(sys) +end + #################################### discrete events ##################################### struct SymbolicDiscreteCallback @@ -480,20 +490,15 @@ SymbolicDiscreteCallbacks(cbs::Vector{<:SymbolicDiscreteCallback}) = cbs SymbolicDiscreteCallbacks(::Nothing) = SymbolicDiscreteCallback[] """ - discrete_events(sys::AbstractSystem; toplevel = false) :: Vector{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`. - -Arguments: -- `toplevel = false`: if set to true, do not return the discrete events of the subsystems. """ -function discrete_events(sys::AbstractSystem; toplevel = false) - toplevel && (sys = recursive_get_parent(sys)) +function discrete_events(sys::AbstractSystem) cbs = get_discrete_events(sys) - toplevel && return cbs systems = get_systems(sys) cbs = [cbs; reduce(vcat, @@ -524,6 +529,21 @@ function vars!(vars, cb::SymbolicDiscreteCallback; op = Differential) return vars end +""" + discrete_events_toplevel(sys::AbstractSystem) + +Replicates the behaviour of `discrete_events`, but ignores events of subsystems. + +Notes: +- Cannot be applied to non-complete systems. +""" +function discrete_events_toplevel(sys::AbstractSystem) + if has_parent(sys) && (parent = get_parent(sys)) !== nothing + return discrete_events_toplevel(parent) + end + return get_discrete_events(sys) +end + ################################# compilation functions #################################### # handles ensuring that affect! functions work with integrator arguments From 04d8dda22eeb39005015c2831c9acf018f7096de Mon Sep 17 00:00:00 2001 From: Torkel Loman Date: Sun, 16 Mar 2025 13:20:11 +0000 Subject: [PATCH 1122/1337] update for new tests --- test/accessor_functions.jl | 97 ++++++++++++++------------------------ 1 file changed, 35 insertions(+), 62 deletions(-) diff --git a/test/accessor_functions.jl b/test/accessor_functions.jl index 2402c53da7..1ad4ef174d 100644 --- a/test/accessor_functions.jl +++ b/test/accessor_functions.jl @@ -5,6 +5,8 @@ using ModelingToolkit, Test using ModelingToolkit: t_nounits as t, D_nounits as D import ModelingToolkit: get_ps, get_unknowns, get_observed, get_eqs, get_continuous_events, get_discrete_events, namespace_equations +import ModelingToolkit: parameters_toplevel, unknowns_toplevel, equations_toplevel, + continuous_events_toplevel, discrete_events_toplevel # Creates helper functions. function all_sets_equal(args...) @@ -23,8 +25,9 @@ end ### Basic Tests ### # Checks `toplevel = false` argument for various accessors (currently only for `ODESystem`s). -# Compares to `toplevel = true` version, and `get_` functions. +# Compares to `` version, and `get_` functions. # Checks accessors for parameters, unknowns, equations, observables, and events. +# Some tests looks funny (caused by the formatter). let # Prepares model components. @parameters p_top p_mid1 p_mid2 p_bot d @@ -70,7 +73,7 @@ let sys_mid1_ss = structural_simplify(sys_mid1) sys_top_ss = structural_simplify(sys_top) - # Checks `parameters` for `toplevel = false`. + # Checks `parameters1. @test all_sets_equal(parameters.([sys_bot, sys_bot_comp, sys_bot_ss])..., [d, p_bot]) @test all_sets_equal(parameters.([sys_mid1, sys_mid1_comp, sys_mid1_ss])..., [d, p_mid1, sys_bot.d, sys_bot.p_bot]) @@ -80,22 +83,22 @@ let [d, p_top, sys_mid1.d, sys_mid1.p_mid1, sys_mid1.sys_bot.d, sys_mid1.sys_bot.p_bot, sys_mid2.d, sys_mid2.p_mid2]) - # Checks `parameters`` for `toplevel = true`. Compares to known parameters and also checks that + # Checks `parameters_toplevel`. Compares to known parameters and also checks that # these are subset of what `get_ps` returns. @test all_sets_equal( - parameters.([sys_bot, sys_bot_comp, sys_bot_ss]; toplevel = true)..., [d, p_bot]) + parameters_toplevel.([sys_bot, sys_bot_comp, sys_bot_ss])..., [d, p_bot]) @test_broken all_sets_equal( - parameters.([sys_mid1, sys_mid1_comp, sys_mid1_ss]; toplevel = true)..., + parameters_toplevel.([sys_mid1, sys_mid1_comp, sys_mid1_ss])..., [d, p_mid1]) @test all_sets_equal( - parameters.([sys_mid2, sys_mid2_comp, sys_mid2_ss]; toplevel = true)..., + parameters_toplevel.([sys_mid2, sys_mid2_comp, sys_mid2_ss])..., [d, p_mid2]) @test_broken all_sets_equal( - parameters.([sys_top, sys_top_comp, sys_top_ss]; toplevel = true)..., [d, p_top]) - @test all(sym_issubset(parameters(sys; toplevel = true), get_ps(sys)) + parameters_toplevel.([sys_top, sys_top_comp, sys_top_ss])..., [d, p_top]) + @test all(sym_issubset(parameters_toplevel(sys), get_ps(sys)) for sys in [sys_bot, sys_mid2, sys_mid1, sys_top]) - # Checks `unknowns` for `toplevel = false`. O(t) is eliminated by `structural_simplify` and + # Checks `unknowns`. O(t) is eliminated by `structural_simplify` and # must be considered separately. @test all_sets_equal(unknowns.([sys_bot, sys_bot_comp])..., [O, Y, X_bot]) @test all_sets_equal(unknowns.([sys_bot_ss])..., [Y, X_bot]) @@ -112,20 +115,20 @@ let [Y, X_top, sys_mid1.Y, sys_mid1.X_mid1, sys_mid1.sys_bot.Y, sys_mid1.sys_bot.X_bot, sys_mid2.Y, sys_mid2.X_mid2]) - # Checks `unknowns` for `toplevel = true`. Note that O is not eliminated here (as we go back - # to original parent system). Also checks that outputs are subsets of what `get_ps` returns.. + # Checks `unknowns_toplevel`. Note that O is not eliminated here (as we go back + # to original parent system). Also checks that outputs are subsets of what `get_unknowns` returns. @test all_sets_equal( - unknowns.([sys_bot, sys_bot_comp, sys_bot_ss]; toplevel = true)..., [O, Y, X_bot]) + unknowns_toplevel.([sys_bot, sys_bot_comp, sys_bot_ss])..., [O, Y, X_bot]) @test all_sets_equal( - unknowns.([sys_mid1, sys_mid1_comp]; toplevel = true)..., [O, Y, X_mid1]) + unknowns_toplevel.([sys_mid1, sys_mid1_comp])..., [O, Y, X_mid1]) @test all_sets_equal( - unknowns.([sys_mid2, sys_mid2_comp]; toplevel = true)..., [O, Y, X_mid2]) + unknowns_toplevel.([sys_mid2, sys_mid2_comp])..., [O, Y, X_mid2]) @test all_sets_equal( - unknowns.([sys_top, sys_top_comp]; toplevel = true)..., [O, Y, X_top]) - @test all(sym_issubset(unknowns(sys; toplevel = true), get_unknowns(sys)) + unknowns_toplevel.([sys_top, sys_top_comp])..., [O, Y, X_top]) + @test all(sym_issubset(unknowns_toplevel(sys), get_unknowns(sys)) for sys in [sys_bot, sys_mid1, sys_mid2, sys_top]) - # Checks `equations` for `toplevel = false`. Do not check ss equations as these might potentially + # Checks `equations`. Do not check ss equations as these might potentially # be structurally simplified to new equations. @test all_sets_equal(equations.([sys_bot, sys_bot_comp])..., eqs_bot) @test all_sets_equal( @@ -134,65 +137,35 @@ let @test all_sets_equal(equations.([sys_top, sys_top_comp])..., [eqs_top; namespace_equations(sys_mid1); namespace_equations(sys_mid2)]) - # Checks `equations` for `toplevel = true`. Do not check ss equations directly as these - # might potentially be structurally simplified to new equations. - @test all_sets_equal(equations.([sys_bot, sys_bot_comp]; toplevel = true)..., eqs_bot) + # Checks `equations_toplevel`. Do not check ss equations directly as these + # might potentially be structurally simplified to new equations. Do not check + @test all_sets_equal(equations_toplevel.([sys_bot])..., eqs_bot) @test all_sets_equal( - equations.([sys_mid1, sys_mid1_comp]; toplevel = true)..., eqs_mid1) + equations_toplevel.([sys_mid1])..., eqs_mid1) @test all_sets_equal( - equations.([sys_mid2, sys_mid2_comp]; toplevel = true)..., eqs_mid2) - @test all_sets_equal(equations.([sys_top, sys_top_comp]; toplevel = true)..., eqs_top) - @test all(sym_issubset(equations(sys; toplevel = true), get_eqs(sys)) + equations_toplevel.([sys_mid2])..., eqs_mid2) + @test all_sets_equal(equations_toplevel.([sys_top])..., eqs_top) + @test all(sym_issubset(equations_toplevel(sys), get_eqs(sys)) for sys in [sys_bot, sys_mid2, sys_mid1, sys_top]) - # Checks `observed for `toplevel = false`. For non-ss systems, there are no observables. - @test all_sets_equal( - observed.([sys_bot, sys_bot_comp, sys_mid1, sys_mid1_comp, - sys_mid2, sys_mid2_comp, sys_top, sys_top_comp])..., - []) - @test issetequal(observed(sys_bot_ss), eqs_bot[3:3]) - @test issetequal( - observed(sys_mid1_ss), [eqs_mid1[3:3]; namespace_equations(sys_bot)[3:3]]) - @test issetequal(observed(sys_mid2_ss), eqs_mid2[3:3]) - @test issetequal(observed(sys_top_ss), - [eqs_top[3:3]; namespace_equations(sys_mid1)[3:3:6]; - namespace_equations(sys_mid2)[3:3]]) - - # Checks `observed` for `toplevel = true`. Again, for non-ss systems, there are no observables. - # Also checks that `toplevel = true` yields subset of `get_observed`. - @test all_sets_equal( - observed.( - [sys_bot, sys_bot_comp, sys_mid1, sys_mid1_comp, - sys_mid2, sys_mid2_comp, sys_top, sys_top_comp]; - toplevel = true)..., - []) - @test_broken issetequal(observed(sys_bot_ss; toplevel = true), eqs_bot[3:3]) - @test_broken issetequal(observed(sys_mid1_ss; toplevel = true), eqs_mid1[3:3]) - @test_broken issetequal(observed(sys_mid2_ss; toplevel = true), eqs_mid2[3:3]) - @test_broken issetequal(observed(sys_top_ss; toplevel = true), eqs_top[3:3]) - @test all(sym_issubset(observed(sys; toplevel = true), get_observed(sys)) - for sys in [sys_bot, sys_mid2, sys_mid1, sys_top]) - - # Checks `continuous_events` and `discrete_events` for `toplevel = true` (more straightforward - # as I stored the same singe event in all systems). Don't check for `toplevel = false` as + # Checks `continuous_events_toplevel` and `discrete_events_toplevel` (straightforward + # as I stored the same singe event in all systems). Don't check for non-toplevel cases as # technically not needed for these tests and name spacing the events is a mess. mtk_cev = ModelingToolkit.SymbolicContinuousCallback.(cevs)[1] mtk_dev = ModelingToolkit.SymbolicDiscreteCallback.(devs)[1] @test all_sets_equal( - continuous_events.( + 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]; - toplevel = true)..., + sys_mid2, sys_mid2_comp, sys_mid2_ss, sys_top, sys_top_comp, sys_top_ss])..., [mtk_cev]) @test all_sets_equal( - discrete_events.( + 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]; - toplevel = true)..., + sys_mid2, sys_mid2_comp, sys_mid2_ss, sys_top, sys_top_comp, sys_top_ss])..., [mtk_dev]) @test all(sym_issubset( - continuous_events(sys; toplevel = true), get_continuous_events(sys)) + continuous_events_toplevel(sys), get_continuous_events(sys)) for sys in [sys_bot, sys_mid2, sys_mid1, sys_top]) - @test all(sym_issubset(discrete_events(sys; toplevel = true), get_discrete_events(sys)) + @test all(sym_issubset(discrete_events_toplevel(sys), get_discrete_events(sys)) for sys in [sys_bot, sys_mid2, sys_mid1, sys_top]) end From 9e459e20266712154d8ac17818ab211abe8c1b4a Mon Sep 17 00:00:00 2001 From: Torkel Loman Date: Sun, 16 Mar 2025 13:20:31 +0000 Subject: [PATCH 1123/1337] formating --- src/systems/abstractsystem.jl | 2 -- test/accessor_functions.jl | 6 +++--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index d23289d964..de69338ec4 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -1325,7 +1325,6 @@ function unknowns_toplevel(sys::AbstractSystem) return get_unknowns(sys) end - """ $(TYPEDSIGNATURES) @@ -1590,7 +1589,6 @@ function equations(sys::AbstractSystem) end end - """ equations_toplevel(sys::AbstractSystem) diff --git a/test/accessor_functions.jl b/test/accessor_functions.jl index 1ad4ef174d..d930d202e5 100644 --- a/test/accessor_functions.jl +++ b/test/accessor_functions.jl @@ -156,15 +156,15 @@ let @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])..., + sys_mid2, sys_mid2_comp, sys_mid2_ss, sys_top, sys_top_comp, sys_top_ss])..., [mtk_cev]) @test all_sets_equal( discrete_events_toplevel.( [sys_bot, sys_bot_comp, sys_bot_ss, sys_mid1, sys_mid1_comp, sys_mid1_ss, - sys_mid2, sys_mid2_comp, sys_mid2_ss, sys_top, sys_top_comp, sys_top_ss])..., + sys_mid2, sys_mid2_comp, sys_mid2_ss, sys_top, sys_top_comp, sys_top_ss])..., [mtk_dev]) @test all(sym_issubset( - continuous_events_toplevel(sys), get_continuous_events(sys)) + continuous_events_toplevel(sys), get_continuous_events(sys)) for sys in [sys_bot, sys_mid2, sys_mid1, sys_top]) @test all(sym_issubset(discrete_events_toplevel(sys), get_discrete_events(sys)) for sys in [sys_bot, sys_mid2, sys_mid1, sys_top]) From e151999d5aeccf70fb1415df71f24b799c760ade Mon Sep 17 00:00:00 2001 From: Torkel Loman Date: Sun, 16 Mar 2025 13:29:11 +0000 Subject: [PATCH 1124/1337] add docs --- docs/src/basics/AbstractSystem.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/src/basics/AbstractSystem.md b/docs/src/basics/AbstractSystem.md index e68a7bb94e..e1ff695245 100644 --- a/docs/src/basics/AbstractSystem.md +++ b/docs/src/basics/AbstractSystem.md @@ -69,6 +69,8 @@ Optionally, a system could have: Note that if you know a system is an `AbstractTimeDependentSystem` you could use `get_iv` to get the unique independent variable directly, rather than using `independent_variables(sys)[1]`, which is clunky and may cause problems if `sys` is an `AbstractMultivariateSystem` because there may be more than one independent variable. `AbstractTimeIndependentSystem`s do not have a method `get_iv`, and `independent_variables(sys)` will return a size-zero result for such. For an `AbstractMultivariateSystem`, `get_ivs` is equivalent. +For the `parameters`, `unknowns`, `continuous_events`, and `discrete_events` accessors there are corresponding `parameters_toplevel`, `unknowns_toplevel`, `continuous_events_toplevel`, and `discrete_events_toplevel` accessors which works similarly, but ignores the content of subsystems. Furthermore, a `equations_toplevel` version of `equations` exists as well, however, it can only be applied to non-complete systems. + A system could also have caches: - `get_jac(sys)`: The Jacobian of a system. From be8d20d8a96c53eb8271dc3fba96401e34dc2284 Mon Sep 17 00:00:00 2001 From: Torkel Loman Date: Sun, 16 Mar 2025 13:31:12 +0000 Subject: [PATCH 1125/1337] format fixes --- src/systems/abstractsystem.jl | 5 ++--- src/systems/callbacks.jl | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index de69338ec4..9f0a2b61d4 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -1342,8 +1342,8 @@ function parameters(sys::AbstractSystem; initial_parameters = false) ps = first.(ps) end systems = get_systems(sys) - result = unique(isempty(systems) ? - ps : [ps; reduce(vcat, namespace_parameters.(systems))]) + result = unique(isempty(systems) ? ps : + [ps; reduce(vcat, namespace_parameters.(systems))]) if !initial_parameters if is_time_dependent(sys) # time-dependent systems have `Initial` parameters for all their @@ -1573,7 +1573,6 @@ It may include some abbreviations and aliases of observables. It is often the most useful way to inspect the equations of a system. See also [`full_equations`](@ref) and [`ModelingToolkit.get_eqs`](@ref). - """ function equations(sys::AbstractSystem) eqs = get_eqs(sys) diff --git a/src/systems/callbacks.jl b/src/systems/callbacks.jl index 710ccc4946..937264d083 100644 --- a/src/systems/callbacks.jl +++ b/src/systems/callbacks.jl @@ -502,7 +502,7 @@ function discrete_events(sys::AbstractSystem) systems = get_systems(sys) cbs = [cbs; reduce(vcat, - (map(o -> namespace_callback(o, s), discrete_events(s)) for s in systems), + (map(cb -> namespace_callback(cb, s), discrete_events(s)) for s in systems), init = SymbolicDiscreteCallback[])] cbs end From 3054252a929feaddac923838f397786083a74751 Mon Sep 17 00:00:00 2001 From: Christopher Rackauckas Date: Mon, 17 Mar 2025 06:43:18 -0100 Subject: [PATCH 1126/1337] Update .typos.toml --- .typos.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.typos.toml b/.typos.toml index b8c07088b4..951470242d 100644 --- a/.typos.toml +++ b/.typos.toml @@ -4,4 +4,5 @@ nd = "nd" Strat = "Strat" eles = "eles" ser = "ser" -isconnection = "isconnection" \ No newline at end of file +isconnection = "isconnection" +Ue = "Ue" From d90d02bdc940a0a435a82c09d6fdb1762b4f189a Mon Sep 17 00:00:00 2001 From: Christopher Rackauckas Date: Mon, 17 Mar 2025 06:45:08 -0100 Subject: [PATCH 1127/1337] Update analysis_points.jl --- src/systems/analysis_points.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/systems/analysis_points.jl b/src/systems/analysis_points.jl index 70c41b50c9..16dee02934 100644 --- a/src/systems/analysis_points.jl +++ b/src/systems/analysis_points.jl @@ -533,7 +533,7 @@ function apply_transformation(tf::GetInput, sys::AbstractSystem) ap_idx = analysis_point_index(ap_sys, tf.ap) ap_idx === nothing && error("Analysis point $(nameof(tf.ap)) not found in system $(nameof(sys)).") - # get the anlysis point + # get the analysis point ap_sys_eqs = get_eqs(ap_sys) ap = ap_sys_eqs[ap_idx].rhs @@ -587,7 +587,7 @@ function apply_transformation(tf::PerturbOutput, sys::AbstractSystem) ap_idx = analysis_point_index(ap_sys, tf.ap) ap_idx === nothing && error("Analysis point $(nameof(tf.ap)) not found in system $(nameof(sys)).") - # modified quations + # modified equations ap_sys_eqs = copy(get_eqs(ap_sys)) @set! ap_sys.eqs = ap_sys_eqs ap = ap_sys_eqs[ap_idx].rhs @@ -899,7 +899,7 @@ result of `apply_transformation`. # Keyword Arguments - `system_modifier`: a function which takes the modified system and returns a new system - with any required further modifications peformed. + with any required further modifications performed. """ function open_loop(sys, ap::Union{Symbol, AnalysisPoint}; system_modifier = identity) ap = only(canonicalize_ap(sys, ap)) From 03f765744ba2413d62fb81ba4d4b22cd140b5359 Mon Sep 17 00:00:00 2001 From: Christopher Rackauckas Date: Mon, 17 Mar 2025 06:45:35 -0100 Subject: [PATCH 1128/1337] Update abstractsystem.jl --- src/systems/abstractsystem.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index dfc25c65ec..90d461aa87 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -622,7 +622,7 @@ end """ Initial(x) -The `Initial` operator. Used by initializaton to store constant constraints on variables +The `Initial` operator. Used by initialization to store constant constraints on variables of a system. See the documentation section on initialization for more information. """ struct Initial <: Symbolics.Operator end From 9bac25fa252d00b6910045a2f2e592493bc489e9 Mon Sep 17 00:00:00 2001 From: Christopher Rackauckas Date: Mon, 17 Mar 2025 06:46:10 -0100 Subject: [PATCH 1129/1337] Update if_lifting.jl --- src/systems/if_lifting.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/systems/if_lifting.jl b/src/systems/if_lifting.jl index 53e1ca4957..da069cc76e 100644 --- a/src/systems/if_lifting.jl +++ b/src/systems/if_lifting.jl @@ -85,7 +85,7 @@ function (cw::CondRewriter)(expr, dep) return (expr, true, true) # other singleton symbolic variables elseif !iscall(expr) - @warn "Automatic conversion of if statments to events requires use of a limited conditional grammar; see the documentation. Skipping due to $expr" + @warn "Automatic conversion of if statements to events requires use of a limited conditional grammar; see the documentation. Skipping due to $expr" return (expr, true, true) # error case => conservative assumption is that both true and false have to be evaluated elseif operation(expr) == Base.:(|) # OR of two conditions a, b = arguments(expr) @@ -405,7 +405,7 @@ const CONDITION_SIMPLIFIER = Rewriters.Fixpoint(Rewriters.Postwalk(Rewriters.Cha (@rule ifelse(false, (~x), (~y)) => (~y))]))) """ -If lifting converts (nested) if statements into a series of continous events + a logically equivalent if statement + parameters. +If lifting converts (nested) if statements into a series of continuous events + a logically equivalent if statement + parameters. Lifting proceeds through the following process: * rewrite comparisons to be of the form eqn [op] 0; subtract the RHS from the LHS From 14f173664652fd8101b9e68b2563c3ac246d8167 Mon Sep 17 00:00:00 2001 From: Christopher Rackauckas Date: Mon, 17 Mar 2025 06:46:45 -0100 Subject: [PATCH 1130/1337] Update .typos.toml --- .typos.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/.typos.toml b/.typos.toml index 951470242d..a933260fb5 100644 --- a/.typos.toml +++ b/.typos.toml @@ -6,3 +6,4 @@ eles = "eles" ser = "ser" isconnection = "isconnection" Ue = "Ue" +Derivate = "Derivate" From d0f1dc0e2c4e99585fb39db8f23f1414c304d45f Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 17 Mar 2025 15:07:34 +0530 Subject: [PATCH 1131/1337] build: bump minor version --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index ad5b7f9d28..2b204c5ed7 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "ModelingToolkit" uuid = "961ee093-0014-501f-94e3-6117800e7a78" authors = ["Yingbo Ma ", "Chris Rackauckas and contributors"] -version = "9.65.0" +version = "9.66.0" [deps] AbstractTrees = "1520ce14-60c1-5f80-bbc7-55ef81b5835c" From 56ec62202fa7f4f6d4250d140ae8fedb5a22a7c9 Mon Sep 17 00:00:00 2001 From: Torkel Loman Date: Mon, 17 Mar 2025 10:42:35 +0000 Subject: [PATCH 1132/1337] Update src/systems/abstractsystem.jl Co-authored-by: Aayush Sabharwal --- src/systems/abstractsystem.jl | 1 - 1 file changed, 1 deletion(-) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index 9f0a2b61d4..9e6e875283 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -1331,7 +1331,6 @@ $(TYPEDSIGNATURES) Get the parameters of the system `sys` and its subsystems. See also [`@parameters`](@ref) and [`ModelingToolkit.get_ps`](@ref). - """ function parameters(sys::AbstractSystem; initial_parameters = false) ps = get_ps(sys) From 4536f9800d2ac6ffe4ecd8fa917701b209e0f002 Mon Sep 17 00:00:00 2001 From: Torkel Loman Date: Mon, 17 Mar 2025 10:43:06 +0000 Subject: [PATCH 1133/1337] Update docs/src/basics/AbstractSystem.md Co-authored-by: Aayush Sabharwal --- docs/src/basics/AbstractSystem.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/basics/AbstractSystem.md b/docs/src/basics/AbstractSystem.md index e1ff695245..3aa43e6d25 100644 --- a/docs/src/basics/AbstractSystem.md +++ b/docs/src/basics/AbstractSystem.md @@ -69,7 +69,7 @@ Optionally, a system could have: Note that if you know a system is an `AbstractTimeDependentSystem` you could use `get_iv` to get the unique independent variable directly, rather than using `independent_variables(sys)[1]`, which is clunky and may cause problems if `sys` is an `AbstractMultivariateSystem` because there may be more than one independent variable. `AbstractTimeIndependentSystem`s do not have a method `get_iv`, and `independent_variables(sys)` will return a size-zero result for such. For an `AbstractMultivariateSystem`, `get_ivs` is equivalent. -For the `parameters`, `unknowns`, `continuous_events`, and `discrete_events` accessors there are corresponding `parameters_toplevel`, `unknowns_toplevel`, `continuous_events_toplevel`, and `discrete_events_toplevel` accessors which works similarly, but ignores the content of subsystems. Furthermore, a `equations_toplevel` version of `equations` exists as well, however, it can only be applied to non-complete systems. +For the `parameters`, `unknowns`, `continuous_events`, and `discrete_events` accessors there are corresponding `parameters_toplevel`, `unknowns_toplevel`, `continuous_events_toplevel`, and `discrete_events_toplevel` accessors which work similarly, but ignore the content of subsystems. Furthermore, a `equations_toplevel` version of `equations` exists as well, however, it can only be applied to non-complete systems. A system could also have caches: From 9a19c1d4c2b0fe75c6537936455831ba46ceef24 Mon Sep 17 00:00:00 2001 From: Torkel Loman Date: Mon, 17 Mar 2025 12:45:40 +0000 Subject: [PATCH 1134/1337] broken tests no longer broken --- test/accessor_functions.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/accessor_functions.jl b/test/accessor_functions.jl index d930d202e5..7ce477155b 100644 --- a/test/accessor_functions.jl +++ b/test/accessor_functions.jl @@ -87,13 +87,13 @@ let # these are subset of what `get_ps` returns. @test all_sets_equal( parameters_toplevel.([sys_bot, sys_bot_comp, sys_bot_ss])..., [d, p_bot]) - @test_broken all_sets_equal( + @test all_sets_equal( parameters_toplevel.([sys_mid1, sys_mid1_comp, sys_mid1_ss])..., [d, p_mid1]) @test all_sets_equal( parameters_toplevel.([sys_mid2, sys_mid2_comp, sys_mid2_ss])..., [d, p_mid2]) - @test_broken all_sets_equal( + @test all_sets_equal( parameters_toplevel.([sys_top, sys_top_comp, sys_top_ss])..., [d, p_top]) @test all(sym_issubset(parameters_toplevel(sys), get_ps(sys)) for sys in [sys_bot, sys_mid2, sys_mid1, sys_top]) From 14e810de1a18b35c515873c4af19bd10e708c730 Mon Sep 17 00:00:00 2001 From: vyudu Date: Wed, 12 Mar 2025 16:50:03 -0400 Subject: [PATCH 1135/1337] fix: add better warning for missing guess --- src/systems/diffeqs/abstractodesystem.jl | 2 +- src/systems/problem_utils.jl | 31 ++++++++++++++++++++---- src/variables.jl | 2 +- 3 files changed, 28 insertions(+), 7 deletions(-) diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index 01f847d5d6..f686ebcede 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -1504,5 +1504,5 @@ function InitializationProblem{iip, specialize}(sys::AbstractSystem, else NonlinearLeastSquaresProblem end - TProb(isys, u0map, parammap; kwargs..., build_initializeprob = false) + TProb(isys, u0map, parammap; kwargs..., build_initializeprob = false, is_initializeprob = true) end diff --git a/src/systems/problem_utils.jl b/src/systems/problem_utils.jl index 93a0749acb..9aaf9eedab 100644 --- a/src/systems/problem_utils.jl +++ b/src/systems/problem_utils.jl @@ -325,6 +325,22 @@ function Base.showerror(io::IO, err::UnexpectedSymbolicValueInVarmap) """) end +struct MissingGuessError <: Exception + sym::Any + val::Any +end + +function Base.showerror(io::IO, err::MissingGuessError) + println(io, + """ + The problem cannot be initialized without providing an additional numeric \ + guess to serve as a starting point for solving for the initial state. Please \ + provide another numeric value to `guesses` in the problem constructor. + + This error was thrown because symbolic value $(err.val) was found for variable $(err.sym). + """) +end + """ $(TYPEDSIGNATURES) @@ -342,10 +358,11 @@ Keyword arguments: [`missingvars`](@ref) to perform the check. - `allow_symbolic` allows the returned array to contain symbolic values. If this is `true`, `promotetoconcrete` is set to `false`. +- `is_initializeprob`: Whether the parent problem is an initialization problem. """ function better_varmap_to_vars(varmap::AbstractDict, vars::Vector; tofloat = true, use_union = true, container_type = Array, - toterm = default_toterm, promotetoconcrete = nothing, check = true, allow_symbolic = false) + toterm = default_toterm, promotetoconcrete = nothing, check = true, allow_symbolic = false, is_initializeprob = false) isempty(vars) && return nothing if check @@ -356,7 +373,11 @@ function better_varmap_to_vars(varmap::AbstractDict, vars::Vector; if !allow_symbolic for (sym, val) in zip(vars, vals) symbolic_type(val) == NotSymbolic() && continue - throw(UnexpectedSymbolicValueInVarmap(sym, val)) + if is_initializeprob + throw(MissingGuessError(sym, val)) + else + throw(UnexpectedSymbolicValueInVarmap(sym, val)) + end end end @@ -704,7 +725,7 @@ Keyword arguments: - `fully_determined`: Override whether the initialization system is fully determined. - `check_initialization_units`: Enable or disable unit checks when constructing the initialization problem. -- `tofloat`, `use_union`: Passed to [`better_varmap_to_vars`](@ref) for building `u0` (and +- `tofloat`, `use_union`, `is_initializeprob`: Passed to [`better_varmap_to_vars`](@ref) for building `u0` (and possibly `p`). - `u0_constructor`: A function to apply to the `u0` value returned from `better_varmap_to_vars` to construct the final `u0` value. @@ -742,7 +763,7 @@ function process_SciMLProblem( circular_dependency_max_cycles = 10, substitution_limit = 100, use_scc = true, force_initialization_time_independent = false, algebraic_only = false, - allow_incomplete = false, kwargs...) + allow_incomplete = false, is_initializeprob = false, kwargs...) dvs = unknowns(sys) ps = parameters(sys; initial_parameters = true) iv = has_iv(sys) ? get_iv(sys) : nothing @@ -815,7 +836,7 @@ function process_SciMLProblem( u0 = better_varmap_to_vars( op, dvs; tofloat = true, use_union = false, - container_type = u0Type, allow_symbolic = symbolic_u0) + container_type = u0Type, allow_symbolic = symbolic_u0, is_initializeprob) if u0 !== nothing u0 = u0_constructor(u0) diff --git a/src/variables.jl b/src/variables.jl index ddc33fa838..3f070fea3c 100644 --- a/src/variables.jl +++ b/src/variables.jl @@ -219,7 +219,7 @@ end function Base.showerror(io::IO, e::MissingVariablesError) println(io, MISSING_VARIABLES_MESSAGE) - println(io, e.vars) + println(io, join(e.vars, ", ")) end function _varmap_to_vars(varmap::Dict, varlist; defaults = Dict(), check = false, From 59d277bded4f93f0448c765e97a32e7f4d6d7cd4 Mon Sep 17 00:00:00 2001 From: vyudu Date: Wed, 12 Mar 2025 18:55:04 -0400 Subject: [PATCH 1136/1337] throw error only in case of initializationproblem --- src/systems/problem_utils.jl | 12 ++++++++---- test/initial_values.jl | 14 +++++++++++++- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/src/systems/problem_utils.jl b/src/systems/problem_utils.jl index 9aaf9eedab..2cae9deb3b 100644 --- a/src/systems/problem_utils.jl +++ b/src/systems/problem_utils.jl @@ -333,9 +333,13 @@ end function Base.showerror(io::IO, err::MissingGuessError) println(io, """ - The problem cannot be initialized without providing an additional numeric \ - guess to serve as a starting point for solving for the initial state. Please \ - provide another numeric value to `guesses` in the problem constructor. + Unable to resolve numeric guesses for all of the variables in the system. \ + This may be because your guesses are cyclic. + + In order for the problem to be initialized, all of the variables must have \ + a numeric value to serve as a starting point for the nonlinear solve. \ + Please provide one or more additional numeric guesses to `guesses` in \ + the problem constructor. This error was thrown because symbolic value $(err.val) was found for variable $(err.sym). """) @@ -358,7 +362,7 @@ Keyword arguments: [`missingvars`](@ref) to perform the check. - `allow_symbolic` allows the returned array to contain symbolic values. If this is `true`, `promotetoconcrete` is set to `false`. -- `is_initializeprob`: Whether the parent problem is an initialization problem. +- `is_initializeprob, guesses`: Used to determine whether the system is missing guesses. """ function better_varmap_to_vars(varmap::AbstractDict, vars::Vector; tofloat = true, use_union = true, container_type = Array, diff --git a/test/initial_values.jl b/test/initial_values.jl index 2db552c3ba..67e9da1ffb 100644 --- a/test/initial_values.jl +++ b/test/initial_values.jl @@ -172,7 +172,7 @@ end [p => 2q, q => 3p]; warn_cyclic_dependency = true) catch end - @test_throws ModelingToolkit.UnexpectedSymbolicValueInVarmap ODEProblem( + @test_throws ModelingToolkit.MissingGuessError ODEProblem( sys, [x => 1, y => 2], (0.0, 1.0), [p => 2q, q => 3p]) end @@ -199,3 +199,15 @@ end @test SciMLBase.successful_retcode(sol) @test sol.u[1] ≈ [1.0, 1.0, 0.5, 0.5] end + +@testset "Missing/cyclic guesses throws error" begin + @parameters g + @variables x(t) y(t) [state_priority = 10] λ(t) + eqs = [D(D(x)) ~ λ * x + D(D(y)) ~ λ * y - g + x^2 + y^2 ~ 1] + @mtkbuild pend = ODESystem(eqs, t) + + @test_throws ModelingToolkit.MissingGuessError ODEProblem(pend, [x => 1], (0, 1), [g => 1], guesses = [y => λ, λ => y + 1]) + ODEProblem(pend, [x => 1], (0, 1), [g => 1], guesses = [y => λ, λ => 0.5]) +end From 6ddc1a7489fccc70a93ab4442751de699e83ae97 Mon Sep 17 00:00:00 2001 From: vyudu Date: Thu, 13 Mar 2025 21:36:40 -0400 Subject: [PATCH 1137/1337] adjust error message --- src/systems/problem_utils.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/systems/problem_utils.jl b/src/systems/problem_utils.jl index 2cae9deb3b..1a4ea523b8 100644 --- a/src/systems/problem_utils.jl +++ b/src/systems/problem_utils.jl @@ -338,7 +338,7 @@ function Base.showerror(io::IO, err::MissingGuessError) In order for the problem to be initialized, all of the variables must have \ a numeric value to serve as a starting point for the nonlinear solve. \ - Please provide one or more additional numeric guesses to `guesses` in \ + Please provide an additional numeric guess to `guesses` in \ the problem constructor. This error was thrown because symbolic value $(err.val) was found for variable $(err.sym). From f8846518a5a3c6ccf4c43364fbc82ab41e8727fc Mon Sep 17 00:00:00 2001 From: vyudu Date: Mon, 17 Mar 2025 11:36:22 -0400 Subject: [PATCH 1138/1337] print all missing values --- src/systems/problem_utils.jl | 33 +++++++++++++++++++-------------- test/initial_values.jl | 6 ++++++ 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/src/systems/problem_utils.jl b/src/systems/problem_utils.jl index 1a4ea523b8..877b16bb69 100644 --- a/src/systems/problem_utils.jl +++ b/src/systems/problem_utils.jl @@ -326,23 +326,24 @@ function Base.showerror(io::IO, err::UnexpectedSymbolicValueInVarmap) end struct MissingGuessError <: Exception - sym::Any - val::Any + syms::Vector{Any} + vals::Vector{Any} end function Base.showerror(io::IO, err::MissingGuessError) println(io, """ Unable to resolve numeric guesses for all of the variables in the system. \ - This may be because your guesses are cyclic. + This may be because your guesses are cyclic. In order for the problem to be \ + initialized, all of the variables must have a numeric value to serve as a \ + starting point for the nonlinear solve. - In order for the problem to be initialized, all of the variables must have \ - a numeric value to serve as a starting point for the nonlinear solve. \ - Please provide an additional numeric guess to `guesses` in \ - the problem constructor. - - This error was thrown because symbolic value $(err.val) was found for variable $(err.sym). + Symbolic values were found for the following variables/parameters in the map; \ + please provide additional numeric guesses so they can resolve to numbers: """) + for (sym, val) in zip(err.syms, err.vals) + println(sym, " => ", val) + end end """ @@ -375,13 +376,17 @@ function better_varmap_to_vars(varmap::AbstractDict, vars::Vector; end vals = map(x -> varmap[x], vars) if !allow_symbolic + missingsyms = Any[] + missingvals = Any[] for (sym, val) in zip(vars, vals) symbolic_type(val) == NotSymbolic() && continue - if is_initializeprob - throw(MissingGuessError(sym, val)) - else - throw(UnexpectedSymbolicValueInVarmap(sym, val)) - end + push!(missingsyms, sym) + push!(missingvals, val) + end + + if !isempty(missingsyms) + is_initializeprob ? throw(MissingGuessError(missingsyms, missingvals)) : + throw(UnexpectedSymbolicValueInVarmap(missingsyms[1], missingvals[1])) end end diff --git a/test/initial_values.jl b/test/initial_values.jl index 67e9da1ffb..7ad3dc4497 100644 --- a/test/initial_values.jl +++ b/test/initial_values.jl @@ -210,4 +210,10 @@ end @test_throws ModelingToolkit.MissingGuessError ODEProblem(pend, [x => 1], (0, 1), [g => 1], guesses = [y => λ, λ => y + 1]) ODEProblem(pend, [x => 1], (0, 1), [g => 1], guesses = [y => λ, λ => 0.5]) + + # Throw multiple if multiple are missing + @variables a(t) b(t) c(t) d(t) e(t) + eqs = [D(a) ~ b, D(b) ~ c, D(c) ~ d, D(d) ~ e, D(e) ~ 1] + @mtkbuild sys = ODESystem(eqs, t) + @test_throws ["a(t) => ", "c(t) => "] ODEProblem(sys, [e => 2, a => b, b => a + 1, c => d, d => c + 1], (0, 1)) end From d680da20c30c97090b6c8eefb34c2abe2140eb35 Mon Sep 17 00:00:00 2001 From: vyudu Date: Mon, 17 Mar 2025 12:25:51 -0400 Subject: [PATCH 1139/1337] refine message, fix test --- src/systems/problem_utils.jl | 13 +++++-------- test/initial_values.jl | 2 +- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/src/systems/problem_utils.jl b/src/systems/problem_utils.jl index 877b16bb69..e9b836ddf7 100644 --- a/src/systems/problem_utils.jl +++ b/src/systems/problem_utils.jl @@ -333,17 +333,14 @@ end function Base.showerror(io::IO, err::MissingGuessError) println(io, """ - Unable to resolve numeric guesses for all of the variables in the system. \ - This may be because your guesses are cyclic. In order for the problem to be \ - initialized, all of the variables must have a numeric value to serve as a \ - starting point for the nonlinear solve. - - Symbolic values were found for the following variables/parameters in the map; \ - please provide additional numeric guesses so they can resolve to numbers: + Cyclic guesses detected in the system. Symbolic values were found for the following variables/parameters in the map: \ """) for (sym, val) in zip(err.syms, err.vals) - println(sym, " => ", val) + println(io, "$sym => $val") end + println(io, + """ + In order to resolve this, please provide additional numeric guesses so that the chain can be resolved to assign numeric values to each variable. """) end """ diff --git a/test/initial_values.jl b/test/initial_values.jl index 7ad3dc4497..66230557ab 100644 --- a/test/initial_values.jl +++ b/test/initial_values.jl @@ -215,5 +215,5 @@ end @variables a(t) b(t) c(t) d(t) e(t) eqs = [D(a) ~ b, D(b) ~ c, D(c) ~ d, D(d) ~ e, D(e) ~ 1] @mtkbuild sys = ODESystem(eqs, t) - @test_throws ["a(t) => ", "c(t) => "] ODEProblem(sys, [e => 2, a => b, b => a + 1, c => d, d => c + 1], (0, 1)) + @test_throws ["a(t)", "c(t)"] ODEProblem(sys, [e => 2, a => b, b => a + 1, c => d, d => c + 1], (0, 1)) end From be21e4b3281af492eb72723fda8e455a94048e48 Mon Sep 17 00:00:00 2001 From: Vincent Du <54586336+vyudu@users.noreply.github.com> Date: Fri, 7 Mar 2025 16:20:00 -0500 Subject: [PATCH 1140/1337] fix: propagate `tofloat`, `use_union` to `better_varmap_to_vars` fix mergre conflict --- src/systems/problem_utils.jl | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/systems/problem_utils.jl b/src/systems/problem_utils.jl index e9b836ddf7..4b612e62c7 100644 --- a/src/systems/problem_utils.jl +++ b/src/systems/problem_utils.jl @@ -363,7 +363,7 @@ Keyword arguments: - `is_initializeprob, guesses`: Used to determine whether the system is missing guesses. """ function better_varmap_to_vars(varmap::AbstractDict, vars::Vector; - tofloat = true, use_union = true, container_type = Array, + tofloat = true, container_type = Array, toterm = default_toterm, promotetoconcrete = nothing, check = true, allow_symbolic = false, is_initializeprob = false) isempty(vars) && return nothing @@ -841,8 +841,13 @@ function process_SciMLProblem( evaluate_varmap!(op, dvs; limit = substitution_limit) u0 = better_varmap_to_vars( +<<<<<<< HEAD op, dvs; tofloat = true, use_union = false, container_type = u0Type, allow_symbolic = symbolic_u0, is_initializeprob) +======= + op, dvs; tofloat, use_union, + container_type = u0Type, allow_symbolic = symbolic_u0) +>>>>>>> e31ae1bcc9 (fix: propagate `tofloat`, `use_union` to `better_varmap_to_vars`) if u0 !== nothing u0 = u0_constructor(u0) @@ -875,7 +880,7 @@ function process_SciMLProblem( du0map = to_varmap(du0map, ddvs) merge!(op, du0map) du0 = varmap_to_vars(op, ddvs; toterm = identity, - tofloat = true) + tofloat) kwargs = merge(kwargs, (; ddvs)) else du0 = nothing From adf70fba23b70281a36a322ef4b407dc9007828c Mon Sep 17 00:00:00 2001 From: Vincent Du <54586336+vyudu@users.noreply.github.com> Date: Fri, 7 Mar 2025 16:24:43 -0500 Subject: [PATCH 1141/1337] test: add typecheck --- test/jumpsystem.jl | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/test/jumpsystem.jl b/test/jumpsystem.jl index 0fd6dc4af0..7d525c871e 100644 --- a/test/jumpsystem.jl +++ b/test/jumpsystem.jl @@ -533,3 +533,21 @@ let Xsamp /= Nsims @test abs(Xsamp - Xf(0.2, p) < 0.05 * Xf(0.2, p)) end + +@testset "JumpProcess simulation should be Int64 valued (#3446)" begin + @parameters p d + @variables X(t) + rate1 = p + rate2 = X*d + affect1 = [X ~ X + 1] + affect2 = [X ~ X - 1] + j1 = ConstantRateJump(rate1, affect1) + j2 = ConstantRateJump(rate2, affect2) + + # Works. + @mtkbuild js = JumpSystem([j1, j2], t, [X], [p,d]) + dprob = DiscreteProblem(js, [X => 15], (0.0, 10.), [p => 2.0, d => 0.5]) + jprob = JumpProblem(js, dprob, Direct()) + sol = solve(jprob, SSAStepper()) + @test eltype(sol[X]) === Int64 +end From 4aa47d6cfb883ea72929050fc3a17ca9f26f2838 Mon Sep 17 00:00:00 2001 From: vyudu Date: Thu, 13 Mar 2025 15:23:55 -0400 Subject: [PATCH 1142/1337] format --- test/jumpsystem.jl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/jumpsystem.jl b/test/jumpsystem.jl index 7d525c871e..d77e37f516 100644 --- a/test/jumpsystem.jl +++ b/test/jumpsystem.jl @@ -537,16 +537,16 @@ end @testset "JumpProcess simulation should be Int64 valued (#3446)" begin @parameters p d @variables X(t) - rate1 = p - rate2 = X*d + rate1 = p + rate2 = X * d affect1 = [X ~ X + 1] affect2 = [X ~ X - 1] j1 = ConstantRateJump(rate1, affect1) j2 = ConstantRateJump(rate2, affect2) # Works. - @mtkbuild js = JumpSystem([j1, j2], t, [X], [p,d]) - dprob = DiscreteProblem(js, [X => 15], (0.0, 10.), [p => 2.0, d => 0.5]) + @mtkbuild js = JumpSystem([j1, j2], t, [X], [p, d]) + dprob = DiscreteProblem(js, [X => 15], (0.0, 10.0), [p => 2.0, d => 0.5]) jprob = JumpProblem(js, dprob, Direct()) sol = solve(jprob, SSAStepper()) @test eltype(sol[X]) === Int64 From fca98e08e0e3edf8229b1cee594bc0e98b8be6ba Mon Sep 17 00:00:00 2001 From: vyudu Date: Fri, 14 Mar 2025 09:48:57 -0400 Subject: [PATCH 1143/1337] fix: don't propagate for u0 --- src/systems/problem_utils.jl | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/systems/problem_utils.jl b/src/systems/problem_utils.jl index 4b612e62c7..334944d50a 100644 --- a/src/systems/problem_utils.jl +++ b/src/systems/problem_utils.jl @@ -351,11 +351,10 @@ in `varmap`. Does not perform symbolic substitution in the values of `varmap`. Keyword arguments: - `tofloat`: Convert values to floating point numbers using `float`. -- `use_union`: Use a `Union`-typed array if the values have heterogeneous types. - `container_type`: The type of container to use for the values. - `toterm`: The `toterm` method to use for converting symbolics. - `promotetoconcrete`: whether the promote to a concrete buffer (respecting - `tofloat` and `use_union`). Defaults to `container_type <: AbstractArray`. + `tofloat`). Defaults to `container_type <: AbstractArray`. - `check`: Error if any variables in `vars` do not have a mapping in `varmap`. Uses [`missingvars`](@ref) to perform the check. - `allow_symbolic` allows the returned array to contain symbolic values. If this is `true`, @@ -393,7 +392,7 @@ function better_varmap_to_vars(varmap::AbstractDict, vars::Vector; promotetoconcrete === nothing && (promotetoconcrete = container_type <: AbstractArray) if promotetoconcrete && !allow_symbolic - vals = promote_to_concrete(vals; tofloat = tofloat, use_union = use_union) + vals = promote_to_concrete(vals; tofloat = tofloat, use_union = false) end if isempty(vals) @@ -841,11 +840,15 @@ function process_SciMLProblem( evaluate_varmap!(op, dvs; limit = substitution_limit) u0 = better_varmap_to_vars( +<<<<<<< HEAD <<<<<<< HEAD op, dvs; tofloat = true, use_union = false, container_type = u0Type, allow_symbolic = symbolic_u0, is_initializeprob) ======= op, dvs; tofloat, use_union, +======= + op, dvs; tofloat, use_union = false, +>>>>>>> 6951e652f2 (fix: don't propagate for u0) container_type = u0Type, allow_symbolic = symbolic_u0) >>>>>>> e31ae1bcc9 (fix: propagate `tofloat`, `use_union` to `better_varmap_to_vars`) From 9c959704162b4b3ec9b514db62230d70b08c3d16 Mon Sep 17 00:00:00 2001 From: vyudu Date: Mon, 17 Mar 2025 10:42:41 -0400 Subject: [PATCH 1144/1337] remove instances of --- .../discrete_system/discrete_system.jl | 1 - .../implicit_discrete_system.jl | 1 - src/systems/jumps/jumpsystem.jl | 11 ++----- .../optimization/optimizationsystem.jl | 14 ++++----- src/systems/parameter_buffer.jl | 2 +- src/systems/problem_utils.jl | 31 +++++++++++++------ src/variables.jl | 4 +-- test/odesystem.jl | 6 ++-- test/split_parameters.jl | 2 +- 9 files changed, 37 insertions(+), 35 deletions(-) diff --git a/src/systems/discrete_system/discrete_system.jl b/src/systems/discrete_system/discrete_system.jl index 5c7d77ec83..952c8b71b0 100644 --- a/src/systems/discrete_system/discrete_system.jl +++ b/src/systems/discrete_system/discrete_system.jl @@ -302,7 +302,6 @@ function SciMLBase.DiscreteProblem( parammap = SciMLBase.NullParameters(); eval_module = @__MODULE__, eval_expression = false, - use_union = false, kwargs... ) if !iscomplete(sys) diff --git a/src/systems/discrete_system/implicit_discrete_system.jl b/src/systems/discrete_system/implicit_discrete_system.jl index 327c82c47f..a9cddcef49 100644 --- a/src/systems/discrete_system/implicit_discrete_system.jl +++ b/src/systems/discrete_system/implicit_discrete_system.jl @@ -321,7 +321,6 @@ function SciMLBase.ImplicitDiscreteProblem( parammap = SciMLBase.NullParameters(); eval_module = @__MODULE__, eval_expression = false, - use_union = false, kwargs... ) if !iscomplete(sys) diff --git a/src/systems/jumps/jumpsystem.jl b/src/systems/jumps/jumpsystem.jl index 81f0e1371d..b95b035bf6 100644 --- a/src/systems/jumps/jumpsystem.jl +++ b/src/systems/jumps/jumpsystem.jl @@ -383,7 +383,6 @@ end ```julia DiffEqBase.DiscreteProblem(sys::JumpSystem, u0map, tspan, parammap = DiffEqBase.NullParameters; - use_union = true, kwargs...) ``` @@ -403,7 +402,6 @@ dprob = DiscreteProblem(complete(js), u₀map, tspan, parammap) """ function DiffEqBase.DiscreteProblem(sys::JumpSystem, u0map, tspan::Union{Tuple, Nothing}, parammap = DiffEqBase.NullParameters(); - use_union = true, eval_expression = false, eval_module = @__MODULE__, kwargs...) @@ -416,7 +414,7 @@ function DiffEqBase.DiscreteProblem(sys::JumpSystem, u0map, tspan::Union{Tuple, end _f, u0, p = process_SciMLProblem(EmptySciMLFunction, sys, u0map, parammap; - t = tspan === nothing ? nothing : tspan[1], use_union, tofloat = false, check_length = false, build_initializeprob = false) + t = tspan === nothing ? nothing : tspan[1], tofloat = false, check_length = false, build_initializeprob = false) f = DiffEqBase.DISCRETE_INPLACE_DEFAULT observedfun = ObservedFunctionCache( @@ -451,14 +449,13 @@ struct DiscreteProblemExpr{iip} end function DiscreteProblemExpr{iip}(sys::JumpSystem, u0map, tspan::Union{Tuple, Nothing}, parammap = DiffEqBase.NullParameters(); - use_union = true, kwargs...) where {iip} if !iscomplete(sys) error("A completed `JumpSystem` is required. Call `complete` or `structural_simplify` on the system before creating a `DiscreteProblemExpr`") end _, u0, p = process_SciMLProblem(EmptySciMLFunction, sys, u0map, parammap; - t = tspan === nothing ? nothing : tspan[1], use_union, tofloat = false, check_length = false) + t = tspan === nothing ? nothing : tspan[1], tofloat = false, check_length = false) # identity function to make syms works quote f = DiffEqBase.DISCRETE_INPLACE_DEFAULT @@ -475,7 +472,6 @@ end ```julia DiffEqBase.ODEProblem(sys::JumpSystem, u0map, tspan, parammap = DiffEqBase.NullParameters; - use_union = true, kwargs...) ``` @@ -497,7 +493,6 @@ oprob = ODEProblem(complete(js), u₀map, tspan, parammap) """ function DiffEqBase.ODEProblem(sys::JumpSystem, u0map, tspan::Union{Tuple, Nothing}, parammap = DiffEqBase.NullParameters(); - use_union = false, eval_expression = false, eval_module = @__MODULE__, kwargs...) @@ -517,7 +512,7 @@ function DiffEqBase.ODEProblem(sys::JumpSystem, u0map, tspan::Union{Tuple, Nothi build_initializeprob = false, kwargs...) else _, u0, p = process_SciMLProblem(EmptySciMLFunction, sys, u0map, parammap; - t = tspan === nothing ? nothing : tspan[1], use_union, tofloat = false, + t = tspan === nothing ? nothing : tspan[1], tofloat = false, check_length = false) f = (du, u, p, t) -> (du .= 0; nothing) observedfun = ObservedFunctionCache(sys; eval_expression, eval_module, diff --git a/src/systems/optimization/optimizationsystem.jl b/src/systems/optimization/optimizationsystem.jl index c1c2237852..4d01bd31c9 100644 --- a/src/systems/optimization/optimizationsystem.jl +++ b/src/systems/optimization/optimizationsystem.jl @@ -295,7 +295,6 @@ function DiffEqBase.OptimizationProblem{iip}(sys::OptimizationSystem, u0map, cons_sparse = false, checkbounds = false, linenumbers = true, parallel = SerialForm(), eval_expression = false, eval_module = @__MODULE__, - use_union = false, checks = true, kwargs...) where {iip} if !iscomplete(sys) @@ -338,10 +337,10 @@ function DiffEqBase.OptimizationProblem{iip}(sys::OptimizationSystem, u0map, elseif has_index_cache(sys) && get_index_cache(sys) !== nothing p = MTKParameters(sys, parammap, u0map) else - p = varmap_to_vars(parammap, ps; defaults = defs, tofloat = false, use_union) + p = varmap_to_vars(parammap, ps; defaults = defs, tofloat = false) end - lb = varmap_to_vars(dvs .=> lb, dvs; defaults = defs, tofloat = false, use_union) - ub = varmap_to_vars(dvs .=> ub, dvs; defaults = defs, tofloat = false, use_union) + lb = varmap_to_vars(dvs .=> lb, dvs; defaults = defs, tofloat = false) + ub = varmap_to_vars(dvs .=> ub, dvs; defaults = defs, tofloat = false) if !isnothing(lb) && all(lb .== -Inf) && !isnothing(ub) && all(ub .== Inf) lb = nothing @@ -538,7 +537,6 @@ function OptimizationProblemExpr{iip}(sys::OptimizationSystem, u0map, checkbounds = false, linenumbers = false, parallel = SerialForm(), eval_expression = false, eval_module = @__MODULE__, - use_union = false, kwargs...) where {iip} if !iscomplete(sys) error("A completed `OptimizationSystem` is required. Call `complete` or `structural_simplify` on the system before creating a `OptimizationProblemExpr`") @@ -578,10 +576,10 @@ function OptimizationProblemExpr{iip}(sys::OptimizationSystem, u0map, if has_index_cache(sys) && get_index_cache(sys) !== nothing p = MTKParameters(sys, parammap, u0map) else - p = varmap_to_vars(parammap, ps; defaults = defs, tofloat = false, use_union) + p = varmap_to_vars(parammap, ps; defaults = defs, tofloat = false) end - lb = varmap_to_vars(dvs .=> lb, dvs; defaults = defs, tofloat = false, use_union) - ub = varmap_to_vars(dvs .=> ub, dvs; defaults = defs, tofloat = false, use_union) + lb = varmap_to_vars(dvs .=> lb, dvs; defaults = defs, tofloat = false) + ub = varmap_to_vars(dvs .=> ub, dvs; defaults = defs, tofloat = false) if !isnothing(lb) && all(lb .== -Inf) && !isnothing(ub) && all(ub .== Inf) lb = nothing diff --git a/src/systems/parameter_buffer.jl b/src/systems/parameter_buffer.jl index e132a5e596..4f1bd1a234 100644 --- a/src/systems/parameter_buffer.jl +++ b/src/systems/parameter_buffer.jl @@ -27,7 +27,7 @@ This requires that `complete` has been called on the system (usually via the default behavior). """ function MTKParameters( - sys::AbstractSystem, p, u0 = Dict(); tofloat = false, use_union = false, + sys::AbstractSystem, p, u0 = Dict(); tofloat = false, t0 = nothing, substitution_limit = 1000) ic = if has_index_cache(sys) && get_index_cache(sys) !== nothing get_index_cache(sys) diff --git a/src/systems/problem_utils.jl b/src/systems/problem_utils.jl index 334944d50a..a5d9b02dbb 100644 --- a/src/systems/problem_utils.jl +++ b/src/systems/problem_utils.jl @@ -363,7 +363,11 @@ Keyword arguments: """ function better_varmap_to_vars(varmap::AbstractDict, vars::Vector; tofloat = true, container_type = Array, +<<<<<<< HEAD toterm = default_toterm, promotetoconcrete = nothing, check = true, allow_symbolic = false, is_initializeprob = false) +======= + toterm = default_toterm, promotetoconcrete = nothing, check = true, allow_symbolic = false) +>>>>>>> a5e1e0239a (remove instances of) isempty(vars) && return nothing if check @@ -730,8 +734,12 @@ Keyword arguments: - `fully_determined`: Override whether the initialization system is fully determined. - `check_initialization_units`: Enable or disable unit checks when constructing the initialization problem. +<<<<<<< HEAD - `tofloat`, `use_union`, `is_initializeprob`: Passed to [`better_varmap_to_vars`](@ref) for building `u0` (and possibly `p`). +======= +- `tofloat`: Passed to [`better_varmap_to_vars`](@ref) for building `u0` (and possibly `p`). +>>>>>>> a5e1e0239a (remove instances of) - `u0_constructor`: A function to apply to the `u0` value returned from `better_varmap_to_vars` to construct the final `u0` value. - `du0map`: A map of derivatives to values. See `implicit_dae`. @@ -761,7 +769,7 @@ function process_SciMLProblem( implicit_dae = false, t = nothing, guesses = AnyDict(), warn_initialize_determined = true, initialization_eqs = [], eval_expression = false, eval_module = @__MODULE__, fully_determined = nothing, - check_initialization_units = false, tofloat = true, use_union = false, + check_initialization_units = false, tofloat = true, u0_constructor = identity, du0map = nothing, check_length = true, symbolic_u0 = false, warn_cyclic_dependency = false, circular_dependency_max_cycle_length = length(all_symbols(sys)), @@ -841,6 +849,7 @@ function process_SciMLProblem( u0 = better_varmap_to_vars( <<<<<<< HEAD +<<<<<<< HEAD <<<<<<< HEAD op, dvs; tofloat = true, use_union = false, container_type = u0Type, allow_symbolic = symbolic_u0, is_initializeprob) @@ -851,6 +860,9 @@ function process_SciMLProblem( >>>>>>> 6951e652f2 (fix: don't propagate for u0) container_type = u0Type, allow_symbolic = symbolic_u0) >>>>>>> e31ae1bcc9 (fix: propagate `tofloat`, `use_union` to `better_varmap_to_vars`) +======= + op, dvs; tofloat, container_type = u0Type, allow_symbolic = symbolic_u0) +>>>>>>> a5e1e0239a (remove instances of) if u0 !== nothing u0 = u0_constructor(u0) @@ -875,7 +887,7 @@ function process_SciMLProblem( if is_split(sys) p = MTKParameters(sys, op) else - p = better_varmap_to_vars(op, ps; tofloat, use_union, container_type = pType) + p = better_varmap_to_vars(op, ps; tofloat, container_type = pType) end if implicit_dae && du0map !== nothing @@ -944,7 +956,7 @@ end ############## """ - u0, p, defs = get_u0_p(sys, u0map, parammap; use_union=true, tofloat=true) + u0, p, defs = get_u0_p(sys, u0map, parammap; tofloat=true) Take dictionaries with initial conditions and parameters and convert them to numeric arrays `u0` and `p`. Also return the merged dictionary `defs` containing the entire operating point. """ @@ -952,7 +964,6 @@ function get_u0_p(sys, u0map, parammap = nothing; t0 = nothing, - use_union = true, tofloat = true, symbolic_u0 = false) dvs = unknowns(sys) @@ -991,11 +1002,11 @@ function get_u0_p(sys, end if symbolic_u0 - u0 = varmap_to_vars(u0map, dvs; defaults = defs, tofloat = false, use_union = false) + u0 = varmap_to_vars(u0map, dvs; defaults = defs, tofloat = false) else - u0 = varmap_to_vars(u0map, dvs; defaults = defs, tofloat = true, use_union) + u0 = varmap_to_vars(u0map, dvs; defaults = defs, tofloat = true) end - p = varmap_to_vars(parammap, ps; defaults = defs, tofloat, use_union) + p = varmap_to_vars(parammap, ps; defaults = defs, tofloat) p = p === nothing ? SciMLBase.NullParameters() : p t0 !== nothing && delete!(defs, get_iv(sys)) u0, p, defs @@ -1003,7 +1014,7 @@ end function get_u0( sys, u0map, parammap = nothing; symbolic_u0 = false, - toterm = default_toterm, t0 = nothing, use_union = true) + toterm = default_toterm, t0 = nothing) dvs = unknowns(sys) ps = parameters(sys) defs = defaults(sys) @@ -1026,9 +1037,9 @@ function get_u0( defs = mergedefaults(defs, obsmap, u0map, dvs) if symbolic_u0 u0 = varmap_to_vars( - u0map, dvs; defaults = defs, tofloat = false, use_union = false, toterm) + u0map, dvs; defaults = defs, tofloat = false, toterm) else - u0 = varmap_to_vars(u0map, dvs; defaults = defs, tofloat = true, use_union, toterm) + u0 = varmap_to_vars(u0map, dvs; defaults = defs, tofloat = true, toterm) end t0 !== nothing && delete!(defs, get_iv(sys)) return u0, defs diff --git a/src/variables.jl b/src/variables.jl index 3f070fea3c..dae044f1ec 100644 --- a/src/variables.jl +++ b/src/variables.jl @@ -158,7 +158,7 @@ applicable. """ function varmap_to_vars(varmap, varlist; defaults = Dict(), check = true, toterm = default_toterm, promotetoconcrete = nothing, - tofloat = true, use_union = true) + tofloat = true) varlist = collect(map(unwrap, varlist)) # Edge cases where one of the arguments is effectively empty. @@ -194,7 +194,7 @@ function varmap_to_vars(varmap, varlist; defaults = Dict(), check = true, promotetoconcrete === nothing && (promotetoconcrete = container_type <: AbstractArray) if promotetoconcrete - vals = promote_to_concrete(vals; tofloat = tofloat, use_union = use_union) + vals = promote_to_concrete(vals; tofloat = tofloat, use_union = false) end if isempty(vals) diff --git a/test/odesystem.jl b/test/odesystem.jl index 970a9afaad..7c4c4e7ead 100644 --- a/test/odesystem.jl +++ b/test/odesystem.jl @@ -736,7 +736,7 @@ let pmap = [k1 => 1, k2 => 1] tspan = (0.0, 1.0) - prob = ODEProblem(sys, u0map, tspan, pmap) + prob = ODEProblem(sys, u0map, tspan, pmap); @test eltype(vcat(prob.p...)) === Float64 prob = ODEProblem(sys, u0map, tspan, pmap) @@ -745,7 +745,7 @@ let # No longer supported, Tuple used instead # pmap = Pair{Any, Union{Int, Float64}}[k1 => 1, k2 => 1.0] # tspan = (0.0, 1.0) - # prob = ODEProblem(sys, u0map, tspan, pmap, use_union = true) + # prob = ODEProblem(sys, u0map, tspan, pmap) # @test eltype(prob.p) === Union{Float64, Int} end @@ -1208,7 +1208,7 @@ end sys = structural_simplify(ODESystem([D(x) ~ P], t, [x], [P]; name = :sys)) function x_at_1(P) - prob = ODEProblem(sys, [x => P], (0.0, 1.0), [sys.P => P], use_union = false) + prob = ODEProblem(sys, [x => P], (0.0, 1.0), [sys.P => P]) return solve(prob, Tsit5())(1.0) end diff --git a/test/split_parameters.jl b/test/split_parameters.jl index 9b00097d54..2ecf6d3aca 100644 --- a/test/split_parameters.jl +++ b/test/split_parameters.jl @@ -123,7 +123,7 @@ sol = solve(prob, ImplicitEuler()); # ------------------------ Mixed Type Conserved prob = ODEProblem( - sys, [], tspan, []; tofloat = false, use_union = true, build_initializeprob = false) + sys, [], tspan, []; tofloat = false, build_initializeprob = false) @test prob.p isa Vector{Union{Float64, Int64}} sol = solve(prob, ImplicitEuler()); From 58f93c45dd2c01cee8e9f3b4059802e5bf0ec0f8 Mon Sep 17 00:00:00 2001 From: vyudu Date: Mon, 17 Mar 2025 10:59:29 -0400 Subject: [PATCH 1145/1337] revert --- src/variables.jl | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/variables.jl b/src/variables.jl index dae044f1ec..f3dd16819d 100644 --- a/src/variables.jl +++ b/src/variables.jl @@ -158,7 +158,7 @@ applicable. """ function varmap_to_vars(varmap, varlist; defaults = Dict(), check = true, toterm = default_toterm, promotetoconcrete = nothing, - tofloat = true) + tofloat = true, use_union = true) varlist = collect(map(unwrap, varlist)) # Edge cases where one of the arguments is effectively empty. @@ -186,15 +186,14 @@ function varmap_to_vars(varmap, varlist; defaults = Dict(), check = true, vals = if eltype(varmap) <: Pair # `varmap` is a dict or an array of pairs varmap = todict(varmap) - _varmap_to_vars(varmap, varlist; defaults = defaults, check = check, - toterm = toterm) + _varmap_to_vars(varmap, varlist; defaults, check, toterm) else # plain array-like initialization varmap end promotetoconcrete === nothing && (promotetoconcrete = container_type <: AbstractArray) if promotetoconcrete - vals = promote_to_concrete(vals; tofloat = tofloat, use_union = false) + vals = promote_to_concrete(vals; tofloat, use_union) end if isempty(vals) From 97161d43d026a630998d2a5241fffc9d33d06d5a Mon Sep 17 00:00:00 2001 From: vyudu Date: Mon, 17 Mar 2025 11:44:17 -0400 Subject: [PATCH 1146/1337] test_broken for now --- test/split_parameters.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/split_parameters.jl b/test/split_parameters.jl index 2ecf6d3aca..1c1701fb2d 100644 --- a/test/split_parameters.jl +++ b/test/split_parameters.jl @@ -125,7 +125,7 @@ sol = solve(prob, ImplicitEuler()); prob = ODEProblem( sys, [], tspan, []; tofloat = false, build_initializeprob = false) -@test prob.p isa Vector{Union{Float64, Int64}} +@test_broken prob.p isa Vector{Union{Float64, Int64}} sol = solve(prob, ImplicitEuler()); @test sol.retcode == ReturnCode.Success From af4dc93a84a4e69628bcdc7a1001474817857ad3 Mon Sep 17 00:00:00 2001 From: vyudu Date: Tue, 18 Mar 2025 10:56:58 -0400 Subject: [PATCH 1147/1337] revert legacy functions, remove test --- src/systems/problem_utils.jl | 15 ++++++++------- test/split_parameters.jl | 1 - 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/systems/problem_utils.jl b/src/systems/problem_utils.jl index a5d9b02dbb..92d867db69 100644 --- a/src/systems/problem_utils.jl +++ b/src/systems/problem_utils.jl @@ -956,7 +956,7 @@ end ############## """ - u0, p, defs = get_u0_p(sys, u0map, parammap; tofloat=true) + u0, p, defs = get_u0_p(sys, u0map, parammap; use_union=true, tofloat=true) Take dictionaries with initial conditions and parameters and convert them to numeric arrays `u0` and `p`. Also return the merged dictionary `defs` containing the entire operating point. """ @@ -965,6 +965,7 @@ function get_u0_p(sys, parammap = nothing; t0 = nothing, tofloat = true, + use_union = true, symbolic_u0 = false) dvs = unknowns(sys) ps = parameters(sys; initial_parameters = true) @@ -1002,11 +1003,11 @@ function get_u0_p(sys, end if symbolic_u0 - u0 = varmap_to_vars(u0map, dvs; defaults = defs, tofloat = false) + u0 = varmap_to_vars(u0map, dvs; defaults = defs, tofloat = false, use_union = false) else - u0 = varmap_to_vars(u0map, dvs; defaults = defs, tofloat = true) + u0 = varmap_to_vars(u0map, dvs; defaults = defs, tofloat = true, use_union) end - p = varmap_to_vars(parammap, ps; defaults = defs, tofloat) + p = varmap_to_vars(parammap, ps; defaults = defs, tofloat, use_union) p = p === nothing ? SciMLBase.NullParameters() : p t0 !== nothing && delete!(defs, get_iv(sys)) u0, p, defs @@ -1014,7 +1015,7 @@ end function get_u0( sys, u0map, parammap = nothing; symbolic_u0 = false, - toterm = default_toterm, t0 = nothing) + toterm = default_toterm, t0 = nothing, use_union = true) dvs = unknowns(sys) ps = parameters(sys) defs = defaults(sys) @@ -1037,9 +1038,9 @@ function get_u0( defs = mergedefaults(defs, obsmap, u0map, dvs) if symbolic_u0 u0 = varmap_to_vars( - u0map, dvs; defaults = defs, tofloat = false, toterm) + u0map, dvs; defaults = defs, tofloat = false, use_union = false, toterm) else - u0 = varmap_to_vars(u0map, dvs; defaults = defs, tofloat = true, toterm) + u0 = varmap_to_vars(u0map, dvs; defaults = defs, tofloat = true, use_union, toterm) end t0 !== nothing && delete!(defs, get_iv(sys)) return u0, defs diff --git a/test/split_parameters.jl b/test/split_parameters.jl index 1c1701fb2d..74f8d41d73 100644 --- a/test/split_parameters.jl +++ b/test/split_parameters.jl @@ -125,7 +125,6 @@ sol = solve(prob, ImplicitEuler()); prob = ODEProblem( sys, [], tspan, []; tofloat = false, build_initializeprob = false) -@test_broken prob.p isa Vector{Union{Float64, Int64}} sol = solve(prob, ImplicitEuler()); @test sol.retcode == ReturnCode.Success From eadea4031ed977763a75b5ac019f5b9adb206b2a Mon Sep 17 00:00:00 2001 From: vyudu Date: Tue, 18 Mar 2025 10:58:42 -0400 Subject: [PATCH 1148/1337] format --- test/odesystem.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/odesystem.jl b/test/odesystem.jl index 7c4c4e7ead..78218c5107 100644 --- a/test/odesystem.jl +++ b/test/odesystem.jl @@ -736,7 +736,7 @@ let pmap = [k1 => 1, k2 => 1] tspan = (0.0, 1.0) - prob = ODEProblem(sys, u0map, tspan, pmap); + prob = ODEProblem(sys, u0map, tspan, pmap) @test eltype(vcat(prob.p...)) === Float64 prob = ODEProblem(sys, u0map, tspan, pmap) From 8805bb3a2a089cb63b9711cb203470a3f838f4b9 Mon Sep 17 00:00:00 2001 From: vyudu Date: Tue, 18 Mar 2025 11:06:54 -0400 Subject: [PATCH 1149/1337] format --- src/systems/diffeqs/abstractodesystem.jl | 3 +- src/systems/problem_utils.jl | 47 +++++++----------------- test/initial_values.jl | 6 ++- 3 files changed, 19 insertions(+), 37 deletions(-) diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index f686ebcede..f39c76246d 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -1504,5 +1504,6 @@ function InitializationProblem{iip, specialize}(sys::AbstractSystem, else NonlinearLeastSquaresProblem end - TProb(isys, u0map, parammap; kwargs..., build_initializeprob = false, is_initializeprob = true) + TProb(isys, u0map, parammap; kwargs..., + build_initializeprob = false, is_initializeprob = true) end diff --git a/src/systems/problem_utils.jl b/src/systems/problem_utils.jl index 92d867db69..0eaab993a3 100644 --- a/src/systems/problem_utils.jl +++ b/src/systems/problem_utils.jl @@ -330,17 +330,17 @@ struct MissingGuessError <: Exception vals::Vector{Any} end -function Base.showerror(io::IO, err::MissingGuessError) - println(io, - """ - Cyclic guesses detected in the system. Symbolic values were found for the following variables/parameters in the map: \ - """) +function Base.showerror(io::IO, err::MissingGuessError) + println(io, + """ + Cyclic guesses detected in the system. Symbolic values were found for the following variables/parameters in the map: \ + """) for (sym, val) in zip(err.syms, err.vals) println(io, "$sym => $val") end println(io, - """ - In order to resolve this, please provide additional numeric guesses so that the chain can be resolved to assign numeric values to each variable. """) + """ + In order to resolve this, please provide additional numeric guesses so that the chain can be resolved to assign numeric values to each variable. """) end """ @@ -363,11 +363,8 @@ Keyword arguments: """ function better_varmap_to_vars(varmap::AbstractDict, vars::Vector; tofloat = true, container_type = Array, -<<<<<<< HEAD - toterm = default_toterm, promotetoconcrete = nothing, check = true, allow_symbolic = false, is_initializeprob = false) -======= - toterm = default_toterm, promotetoconcrete = nothing, check = true, allow_symbolic = false) ->>>>>>> a5e1e0239a (remove instances of) + toterm = default_toterm, promotetoconcrete = nothing, check = true, + allow_symbolic = false, is_initializeprob = false) isempty(vars) && return nothing if check @@ -385,8 +382,8 @@ function better_varmap_to_vars(varmap::AbstractDict, vars::Vector; end if !isempty(missingsyms) - is_initializeprob ? throw(MissingGuessError(missingsyms, missingvals)) : - throw(UnexpectedSymbolicValueInVarmap(missingsyms[1], missingvals[1])) + is_initializeprob ? throw(MissingGuessError(missingsyms, missingvals)) : + throw(UnexpectedSymbolicValueInVarmap(missingsyms[1], missingvals[1])) end end @@ -734,12 +731,7 @@ Keyword arguments: - `fully_determined`: Override whether the initialization system is fully determined. - `check_initialization_units`: Enable or disable unit checks when constructing the initialization problem. -<<<<<<< HEAD -- `tofloat`, `use_union`, `is_initializeprob`: Passed to [`better_varmap_to_vars`](@ref) for building `u0` (and - possibly `p`). -======= -- `tofloat`: Passed to [`better_varmap_to_vars`](@ref) for building `u0` (and possibly `p`). ->>>>>>> a5e1e0239a (remove instances of) +- `tofloat`, `is_initializeprob`: Passed to [`better_varmap_to_vars`](@ref) for building `u0` (and possibly `p`). - `u0_constructor`: A function to apply to the `u0` value returned from `better_varmap_to_vars` to construct the final `u0` value. - `du0map`: A map of derivatives to values. See `implicit_dae`. @@ -848,21 +840,8 @@ function process_SciMLProblem( evaluate_varmap!(op, dvs; limit = substitution_limit) u0 = better_varmap_to_vars( -<<<<<<< HEAD -<<<<<<< HEAD -<<<<<<< HEAD - op, dvs; tofloat = true, use_union = false, + op, dvs; tofloat = true, container_type = u0Type, allow_symbolic = symbolic_u0, is_initializeprob) -======= - op, dvs; tofloat, use_union, -======= - op, dvs; tofloat, use_union = false, ->>>>>>> 6951e652f2 (fix: don't propagate for u0) - container_type = u0Type, allow_symbolic = symbolic_u0) ->>>>>>> e31ae1bcc9 (fix: propagate `tofloat`, `use_union` to `better_varmap_to_vars`) -======= - op, dvs; tofloat, container_type = u0Type, allow_symbolic = symbolic_u0) ->>>>>>> a5e1e0239a (remove instances of) if u0 !== nothing u0 = u0_constructor(u0) diff --git a/test/initial_values.jl b/test/initial_values.jl index 66230557ab..f413c1a018 100644 --- a/test/initial_values.jl +++ b/test/initial_values.jl @@ -208,12 +208,14 @@ end x^2 + y^2 ~ 1] @mtkbuild pend = ODESystem(eqs, t) - @test_throws ModelingToolkit.MissingGuessError ODEProblem(pend, [x => 1], (0, 1), [g => 1], guesses = [y => λ, λ => y + 1]) + @test_throws ModelingToolkit.MissingGuessError ODEProblem( + pend, [x => 1], (0, 1), [g => 1], guesses = [y => λ, λ => y + 1]) ODEProblem(pend, [x => 1], (0, 1), [g => 1], guesses = [y => λ, λ => 0.5]) # Throw multiple if multiple are missing @variables a(t) b(t) c(t) d(t) e(t) eqs = [D(a) ~ b, D(b) ~ c, D(c) ~ d, D(d) ~ e, D(e) ~ 1] @mtkbuild sys = ODESystem(eqs, t) - @test_throws ["a(t)", "c(t)"] ODEProblem(sys, [e => 2, a => b, b => a + 1, c => d, d => c + 1], (0, 1)) + @test_throws ["a(t)", "c(t)"] ODEProblem( + sys, [e => 2, a => b, b => a + 1, c => d, d => c + 1], (0, 1)) end From 6b9dc98651ed6a908bba4357039e34b204ede7c8 Mon Sep 17 00:00:00 2001 From: vyudu Date: Tue, 18 Mar 2025 11:11:37 -0400 Subject: [PATCH 1150/1337] fix removed tofloat = true --- src/systems/problem_utils.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/systems/problem_utils.jl b/src/systems/problem_utils.jl index 0eaab993a3..f9f8f4df4c 100644 --- a/src/systems/problem_utils.jl +++ b/src/systems/problem_utils.jl @@ -874,7 +874,7 @@ function process_SciMLProblem( du0map = to_varmap(du0map, ddvs) merge!(op, du0map) du0 = varmap_to_vars(op, ddvs; toterm = identity, - tofloat) + tofloat = true) kwargs = merge(kwargs, (; ddvs)) else du0 = nothing From 408b86b0c9cf79077e4cac7f99f3828b00889c5a Mon Sep 17 00:00:00 2001 From: vyudu Date: Tue, 18 Mar 2025 12:37:59 -0400 Subject: [PATCH 1151/1337] fix JumpSystem tofloat test --- src/systems/problem_utils.jl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/systems/problem_utils.jl b/src/systems/problem_utils.jl index f9f8f4df4c..4964ac2986 100644 --- a/src/systems/problem_utils.jl +++ b/src/systems/problem_utils.jl @@ -840,7 +840,7 @@ function process_SciMLProblem( evaluate_varmap!(op, dvs; limit = substitution_limit) u0 = better_varmap_to_vars( - op, dvs; tofloat = true, + op, dvs; tofloat, container_type = u0Type, allow_symbolic = symbolic_u0, is_initializeprob) if u0 !== nothing @@ -874,7 +874,7 @@ function process_SciMLProblem( du0map = to_varmap(du0map, ddvs) merge!(op, du0map) du0 = varmap_to_vars(op, ddvs; toterm = identity, - tofloat = true) + tofloat) kwargs = merge(kwargs, (; ddvs)) else du0 = nothing @@ -984,7 +984,7 @@ function get_u0_p(sys, if symbolic_u0 u0 = varmap_to_vars(u0map, dvs; defaults = defs, tofloat = false, use_union = false) else - u0 = varmap_to_vars(u0map, dvs; defaults = defs, tofloat = true, use_union) + u0 = varmap_to_vars(u0map, dvs; defaults = defs, tofloat, use_union) end p = varmap_to_vars(parammap, ps; defaults = defs, tofloat, use_union) p = p === nothing ? SciMLBase.NullParameters() : p @@ -1019,7 +1019,7 @@ function get_u0( u0 = varmap_to_vars( u0map, dvs; defaults = defs, tofloat = false, use_union = false, toterm) else - u0 = varmap_to_vars(u0map, dvs; defaults = defs, tofloat = true, use_union, toterm) + u0 = varmap_to_vars(u0map, dvs; defaults = defs, tofloat, use_union, toterm) end t0 !== nothing && delete!(defs, get_iv(sys)) return u0, defs From 7f821e21fecdfdf857dc220fd4856d5c6388a9b3 Mon Sep 17 00:00:00 2001 From: Herman Sletmoen Date: Tue, 18 Mar 2025 20:09:10 +0100 Subject: [PATCH 1152/1337] Use t_nounits, D_nounits --- docs/src/tutorials/change_independent_variable.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/src/tutorials/change_independent_variable.md b/docs/src/tutorials/change_independent_variable.md index e1243fb2ee..4b7ba037fe 100644 --- a/docs/src/tutorials/change_independent_variable.md +++ b/docs/src/tutorials/change_independent_variable.md @@ -18,8 +18,7 @@ Consider a projectile shot with some initial velocity in a vertical gravitationa ```@example changeivar using ModelingToolkit -@independent_variables t -D = Differential(t) +using ModelingToolkit: t_nounits as t, D_nounits as D @variables x(t) y(t) @parameters g=9.81 v # gravitational acceleration and horizontal velocity eqs = [D(D(y)) ~ -g, D(x) ~ v] @@ -128,7 +127,7 @@ Unlike the original, notice that this system is *non-autonomous* because the ind This means that to change the independent variable from $a$ to $b$, we must provide not only the rate of change relation $db(a)/da = \exp(-b)$, but *also* the equation $a(b) = \exp(b)$ so $a$ can be eliminated in favor of $b$: ```@example changeivar -a = M2.a +a = M2.a # get independent variable of M2 Da = Differential(a) @variables b(a) M3 = change_independent_variable(M2, b, [Da(b) ~ exp(-b), a ~ exp(b)]) From cabd06063c3cdf7119d4b72cce10ed0a77b885ac Mon Sep 17 00:00:00 2001 From: vyudu Date: Tue, 18 Mar 2025 16:21:37 -0400 Subject: [PATCH 1153/1337] fix get_u0 test --- src/systems/problem_utils.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/systems/problem_utils.jl b/src/systems/problem_utils.jl index 4964ac2986..b94a360f93 100644 --- a/src/systems/problem_utils.jl +++ b/src/systems/problem_utils.jl @@ -1019,7 +1019,7 @@ function get_u0( u0 = varmap_to_vars( u0map, dvs; defaults = defs, tofloat = false, use_union = false, toterm) else - u0 = varmap_to_vars(u0map, dvs; defaults = defs, tofloat, use_union, toterm) + u0 = varmap_to_vars(u0map, dvs; defaults = defs, tofloat = true, use_union, toterm) end t0 !== nothing && delete!(defs, get_iv(sys)) return u0, defs From 8a577e971b7c7cd57b17fbc8e5a2f6c5f5e0ae6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hans=20W=C3=BCrfel?= Date: Wed, 19 Mar 2025 13:11:40 +0100 Subject: [PATCH 1154/1337] prevent invalidation of mtkmodel macro code --- src/systems/model_parsing.jl | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/systems/model_parsing.jl b/src/systems/model_parsing.jl index 1976e89aa2..cb72e2e416 100644 --- a/src/systems/model_parsing.jl +++ b/src/systems/model_parsing.jl @@ -511,9 +511,10 @@ function generate_var!(dict, a, b, varclass, mod; end # Use the `t` defined in the `mod`. When it is unavailable, generate a new `t` with a warning. -function get_t(mod, t) +get_t(mod, t) = invokelatest(_get_t, mod, t) +function _get_t(mod, t) try - get_var(mod, t) + _get_var(mod, t) catch e if e isa UndefVarError @warn("Could not find a predefined `t` in `$mod`; generating a new one within this model.\nConsider defining it or importing `t` (or `t_nounits`, `t_unitful` as `t`) from ModelingToolkit.") @@ -588,7 +589,8 @@ function set_var_metadata(a, ms) a, metadata_with_exprs end -function get_var(mod::Module, b) +get_var(mod, b) = invokelatest(_get_var, mod, b) +function _get_var(mod::Module, b) if b isa Symbol isdefined(mod, b) && return getproperty(mod, b) isdefined(@__MODULE__, b) && return getproperty(@__MODULE__, b) @@ -1355,7 +1357,8 @@ push_something!(v, x...) = push_something!.(Ref(v), x) define_blocks(branch) = [Expr(branch), Expr(branch), Expr(branch), Expr(branch)] -function parse_top_level_branch(condition, x, y = nothing, branch = :if) +Base.@nospecializeinfer function parse_top_level_branch(condition, x::Expr, y::Union{Expr,Nothing} = nothing, branch::Symbol = :if) + @nospecialize condition x y blocks::Vector{Union{Expr, Nothing}} = component_blk, equations_blk, parameter_blk, variable_blk = define_blocks(branch) for arg in x From 36837e2315fb93c8bbeff1de0fa36a4fa696e0d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hans=20W=C3=BCrfel?= Date: Wed, 19 Mar 2025 14:05:16 +0100 Subject: [PATCH 1155/1337] add precompile workload --- src/ModelingToolkit.jl | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/ModelingToolkit.jl b/src/ModelingToolkit.jl index bbaeea1a40..8e84ac14f4 100644 --- a/src/ModelingToolkit.jl +++ b/src/ModelingToolkit.jl @@ -224,6 +224,25 @@ PrecompileTools.@compile_workload begin @variables x(ModelingToolkit.t_nounits) @named sys = ODESystem([ModelingToolkit.D_nounits(x) ~ -x], ModelingToolkit.t_nounits) prob = ODEProblem(structural_simplify(sys), [x => 30.0], (0, 100), [], jac = true) + @mtkmodel __testmod__ begin + @structural_parameters begin + structp = false + end + @variables begin + x(t) = 0.0, [description="foo", guess=1.0] + end + @parameters begin + a = 1.0, [description="bar"] + if structp + b=2*a, [description="if"] + else + c + end + end + @equations begin + x ~ a + b + end + end; end export AbstractTimeDependentSystem, From 8d3779036b0310c7406abfc5a8799dcabeaf0c5f Mon Sep 17 00:00:00 2001 From: Christopher Rackauckas Date: Wed, 19 Mar 2025 12:24:20 -0100 Subject: [PATCH 1156/1337] Update Downstream.yml --- .github/workflows/Downstream.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/Downstream.yml b/.github/workflows/Downstream.yml index 9c5d05e022..5026f798a3 100644 --- a/.github/workflows/Downstream.yml +++ b/.github/workflows/Downstream.yml @@ -27,7 +27,7 @@ jobs: package: - {user: SciML, repo: SciMLBase.jl, group: Downstream} - {user: SciML, repo: Catalyst.jl, group: All} - - {user: SciML, repo: CellMLToolkit.jl, group: All} + - {user: SciML, repo: CellMLToolkit.jl, group: Core} - {user: SciML, repo: SBMLToolkit.jl, group: All} - {user: SciML, repo: NeuralPDE.jl, group: NNPDE} - {user: SciML, repo: DataDrivenDiffEq.jl, group: Downstream} From abb589547a43a978c92cffd6e4256e5f54fb0cf4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hans=20W=C3=BCrfel?= Date: Wed, 19 Mar 2025 15:16:02 +0100 Subject: [PATCH 1157/1337] don't invalidate parse constants --- src/systems/model_parsing.jl | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/systems/model_parsing.jl b/src/systems/model_parsing.jl index cb72e2e416..f395e73b43 100644 --- a/src/systems/model_parsing.jl +++ b/src/systems/model_parsing.jl @@ -640,7 +640,7 @@ function parse_constants!(exprs, dict, body, mod) type = getfield(mod, type) b = _type_check!(get_var(mod, b), a, type, :constants) push!(exprs, - :($(Symbolics._parse_vars( + :($(Symbolics_parse_vars( :constants, type, [:($a = $b), metadata], toconstant)))) dict[:constants][a] = Dict(:value => b, :type => type) if @isdefined metadata @@ -651,7 +651,7 @@ function parse_constants!(exprs, dict, body, mod) end Expr(:(=), a, Expr(:tuple, b, metadata)) => begin push!(exprs, - :($(Symbolics._parse_vars( + :($(Symbolics_parse_vars( :constants, Real, [:($a = $b), metadata], toconstant)))) dict[:constants][a] = Dict{Symbol, Any}(:value => get_var(mod, b)) for data in metadata.args @@ -660,7 +660,7 @@ function parse_constants!(exprs, dict, body, mod) end Expr(:(=), a, b) => begin push!(exprs, - :($(Symbolics._parse_vars( + :($(Symbolics_parse_vars( :constants, Real, [:($a = $b)], toconstant)))) dict[:constants][a] = Dict(:value => get_var(mod, b)) end @@ -674,6 +674,8 @@ function parse_constants!(exprs, dict, body, mod) end end end +# HACK: _parse_vars seems invalidated, so we use invokelatest +Symbolics_parse_vars(x...) = invokelatest(Symbolics._parse_vars, x...) push_additional_defaults!(dict, a, b::Number) = dict[:defaults][a] = b push_additional_defaults!(dict, a, b::QuoteNode) = dict[:defaults][a] = b.value From a4f805d1b243b2795c4acd5695692cf2f232792b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hans=20W=C3=BCrfel?= Date: Wed, 19 Mar 2025 16:14:12 +0100 Subject: [PATCH 1158/1337] defering _pars_vars makes no real world difference --- src/systems/model_parsing.jl | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/systems/model_parsing.jl b/src/systems/model_parsing.jl index f395e73b43..cb72e2e416 100644 --- a/src/systems/model_parsing.jl +++ b/src/systems/model_parsing.jl @@ -640,7 +640,7 @@ function parse_constants!(exprs, dict, body, mod) type = getfield(mod, type) b = _type_check!(get_var(mod, b), a, type, :constants) push!(exprs, - :($(Symbolics_parse_vars( + :($(Symbolics._parse_vars( :constants, type, [:($a = $b), metadata], toconstant)))) dict[:constants][a] = Dict(:value => b, :type => type) if @isdefined metadata @@ -651,7 +651,7 @@ function parse_constants!(exprs, dict, body, mod) end Expr(:(=), a, Expr(:tuple, b, metadata)) => begin push!(exprs, - :($(Symbolics_parse_vars( + :($(Symbolics._parse_vars( :constants, Real, [:($a = $b), metadata], toconstant)))) dict[:constants][a] = Dict{Symbol, Any}(:value => get_var(mod, b)) for data in metadata.args @@ -660,7 +660,7 @@ function parse_constants!(exprs, dict, body, mod) end Expr(:(=), a, b) => begin push!(exprs, - :($(Symbolics_parse_vars( + :($(Symbolics._parse_vars( :constants, Real, [:($a = $b)], toconstant)))) dict[:constants][a] = Dict(:value => get_var(mod, b)) end @@ -674,8 +674,6 @@ function parse_constants!(exprs, dict, body, mod) end end end -# HACK: _parse_vars seems invalidated, so we use invokelatest -Symbolics_parse_vars(x...) = invokelatest(Symbolics._parse_vars, x...) push_additional_defaults!(dict, a, b::Number) = dict[:defaults][a] = b push_additional_defaults!(dict, a, b::QuoteNode) = dict[:defaults][a] = b.value From 6b5c445050f45d3d0f0a83ce83d6745e7bc8f12a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hans=20W=C3=BCrfel?= Date: Wed, 19 Mar 2025 20:02:00 +0100 Subject: [PATCH 1159/1337] despecialize parse_variable_Def --- src/systems/model_parsing.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/systems/model_parsing.jl b/src/systems/model_parsing.jl index cb72e2e416..40a1213466 100644 --- a/src/systems/model_parsing.jl +++ b/src/systems/model_parsing.jl @@ -246,8 +246,9 @@ end # The comments indicate the syntax matched by a block; either when parsed directly # when it is called recursively for parsing a part of an expression. # These variable definitions are part of test suite in `test/model_parsing.jl` -function parse_variable_def!(dict, mod, arg, varclass, kwargs, where_types; +Base.@nospecializeinfer function parse_variable_def!(dict, mod, arg, varclass, kwargs, where_types; def = nothing, type::Type = Real, meta = Dict{DataType, Expr}()) + @nospecialize arg isa LineNumberNode && return MLStyle.@match arg begin # Parses: `a` From a0b413f81168cbc69389f27c75baa4791203df55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hans=20W=C3=BCrfel?= Date: Wed, 19 Mar 2025 20:11:26 +0100 Subject: [PATCH 1160/1337] despecialize further --- src/systems/model_parsing.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/systems/model_parsing.jl b/src/systems/model_parsing.jl index 40a1213466..512e4e2971 100644 --- a/src/systems/model_parsing.jl +++ b/src/systems/model_parsing.jl @@ -1358,8 +1358,8 @@ push_something!(v, x...) = push_something!.(Ref(v), x) define_blocks(branch) = [Expr(branch), Expr(branch), Expr(branch), Expr(branch)] -Base.@nospecializeinfer function parse_top_level_branch(condition, x::Expr, y::Union{Expr,Nothing} = nothing, branch::Symbol = :if) - @nospecialize condition x y +Base.@nospecializeinfer function parse_top_level_branch(condition, x, y = nothing, branch::Symbol = :if) + @nospecialize blocks::Vector{Union{Expr, Nothing}} = component_blk, equations_blk, parameter_blk, variable_blk = define_blocks(branch) for arg in x From 9235009fcdd46b19a9bba516da10d14d32a5a2c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hans=20W=C3=BCrfel?= Date: Wed, 19 Mar 2025 20:21:25 +0100 Subject: [PATCH 1161/1337] roll back invalidation blocking invoke latest --- src/systems/model_parsing.jl | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/systems/model_parsing.jl b/src/systems/model_parsing.jl index 512e4e2971..01b29ebd29 100644 --- a/src/systems/model_parsing.jl +++ b/src/systems/model_parsing.jl @@ -512,10 +512,9 @@ function generate_var!(dict, a, b, varclass, mod; end # Use the `t` defined in the `mod`. When it is unavailable, generate a new `t` with a warning. -get_t(mod, t) = invokelatest(_get_t, mod, t) -function _get_t(mod, t) +function get_t(mod, t) try - _get_var(mod, t) + get_var(mod, t) catch e if e isa UndefVarError @warn("Could not find a predefined `t` in `$mod`; generating a new one within this model.\nConsider defining it or importing `t` (or `t_nounits`, `t_unitful` as `t`) from ModelingToolkit.") @@ -590,8 +589,7 @@ function set_var_metadata(a, ms) a, metadata_with_exprs end -get_var(mod, b) = invokelatest(_get_var, mod, b) -function _get_var(mod::Module, b) +function get_var(mod::Module, b) if b isa Symbol isdefined(mod, b) && return getproperty(mod, b) isdefined(@__MODULE__, b) && return getproperty(@__MODULE__, b) From bf5742a4dcf7d55e0f1b1fc9e33a18cf6e4199ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hans=20W=C3=BCrfel?= Date: Wed, 19 Mar 2025 20:22:04 +0100 Subject: [PATCH 1162/1337] extend workload --- src/ModelingToolkit.jl | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/ModelingToolkit.jl b/src/ModelingToolkit.jl index 8e84ac14f4..011654bb6a 100644 --- a/src/ModelingToolkit.jl +++ b/src/ModelingToolkit.jl @@ -225,11 +225,20 @@ PrecompileTools.@compile_workload begin @named sys = ODESystem([ModelingToolkit.D_nounits(x) ~ -x], ModelingToolkit.t_nounits) prob = ODEProblem(structural_simplify(sys), [x => 30.0], (0, 100), [], jac = true) @mtkmodel __testmod__ begin + @constants begin + c = 1.0 + end @structural_parameters begin structp = false end - @variables begin - x(t) = 0.0, [description="foo", guess=1.0] + if structp + @variables begin + x(t) = 0.0, [description="foo", guess=1.0] + end + else + @variables begin + x(t) = 0.0, [description="foo w/o structp", guess=1.0] + end end @parameters begin a = 1.0, [description="bar"] From ee2cac10cd5f5e43aad39bac019d684bceffbff2 Mon Sep 17 00:00:00 2001 From: Fredrik Bagge Carlson Date: Thu, 20 Mar 2025 03:27:15 +0100 Subject: [PATCH 1163/1337] Fix docstring for `InitializationProblem` It did not appear to match any of the constructors --- src/systems/diffeqs/abstractodesystem.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index f39c76246d..306aa59fc1 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -1306,7 +1306,7 @@ struct InitializationProblem{iip, specialization} end """ ```julia -InitializationProblem{iip}(sys::AbstractODESystem, u0map, tspan, +InitializationProblem{iip}(sys::AbstractODESystem, t, u0map, parammap = DiffEqBase.NullParameters(); version = nothing, tgrad = false, jac = false, From 3466dee76213669cd63c56cc36b0ab03258d308b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hans=20W=C3=BCrfel?= Date: Thu, 20 Mar 2025 07:12:14 +0100 Subject: [PATCH 1164/1337] apply formatter --- src/systems/model_parsing.jl | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/systems/model_parsing.jl b/src/systems/model_parsing.jl index 01b29ebd29..4632c1b889 100644 --- a/src/systems/model_parsing.jl +++ b/src/systems/model_parsing.jl @@ -246,7 +246,8 @@ end # The comments indicate the syntax matched by a block; either when parsed directly # when it is called recursively for parsing a part of an expression. # These variable definitions are part of test suite in `test/model_parsing.jl` -Base.@nospecializeinfer function parse_variable_def!(dict, mod, arg, varclass, kwargs, where_types; +Base.@nospecializeinfer function parse_variable_def!( + dict, mod, arg, varclass, kwargs, where_types; def = nothing, type::Type = Real, meta = Dict{DataType, Expr}()) @nospecialize arg isa LineNumberNode && return @@ -1356,7 +1357,8 @@ push_something!(v, x...) = push_something!.(Ref(v), x) define_blocks(branch) = [Expr(branch), Expr(branch), Expr(branch), Expr(branch)] -Base.@nospecializeinfer function parse_top_level_branch(condition, x, y = nothing, branch::Symbol = :if) +Base.@nospecializeinfer function parse_top_level_branch( + condition, x, y = nothing, branch::Symbol = :if) @nospecialize blocks::Vector{Union{Expr, Nothing}} = component_blk, equations_blk, parameter_blk, variable_blk = define_blocks(branch) From 3a137d67ec578d336dca4c54a4ef77c5a0a823d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hans=20W=C3=BCrfel?= Date: Thu, 20 Mar 2025 07:35:26 +0100 Subject: [PATCH 1165/1337] apply formatter --- src/ModelingToolkit.jl | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/ModelingToolkit.jl b/src/ModelingToolkit.jl index 011654bb6a..7f934a5769 100644 --- a/src/ModelingToolkit.jl +++ b/src/ModelingToolkit.jl @@ -233,17 +233,17 @@ PrecompileTools.@compile_workload begin end if structp @variables begin - x(t) = 0.0, [description="foo", guess=1.0] + x(t) = 0.0, [description = "foo", guess = 1.0] end else @variables begin - x(t) = 0.0, [description="foo w/o structp", guess=1.0] + x(t) = 0.0, [description = "foo w/o structp", guess = 1.0] end end @parameters begin - a = 1.0, [description="bar"] + a = 1.0, [description = "bar"] if structp - b=2*a, [description="if"] + b = 2 * a, [description = "if"] else c end @@ -251,7 +251,7 @@ PrecompileTools.@compile_workload begin @equations begin x ~ a + b end - end; + end end export AbstractTimeDependentSystem, From 1dcf7c690af613b5ea4d2ebb726b7df92365e6af Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 18 Mar 2025 16:23:16 +0530 Subject: [PATCH 1166/1337] refactor: store ignored analysis points as `IgnoredAnalysisPoint` --- src/systems/abstractsystem.jl | 64 ++++++++++++++++++++++++++++---- src/systems/diffeqs/odesystem.jl | 9 +++-- 2 files changed, 62 insertions(+), 11 deletions(-) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index 39ca1baed3..7fe99cb2eb 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -1421,9 +1421,34 @@ function assertions(sys::AbstractSystem) return merge(asserts, namespaced_asserts) end +""" + $(TYPEDEF) + +Information about an `AnalysisPoint` for which the corresponding connection must be +ignored during `expand_connections`, since the analysis point has been transformed. + +# Fields + +$(TYPEDFIELDS) +""" +struct IgnoredAnalysisPoint + """ + The input variable/connector. + """ + input::Union{BasicSymbolic, AbstractSystem} + """ + The output variables/connectors. + """ + outputs::Vector{Union{BasicSymbolic, AbstractSystem}} +end + const HierarchyVariableT = Vector{Union{BasicSymbolic, Symbol}} const HierarchySystemT = Vector{Union{AbstractSystem, Symbol}} """ +The type returned from `analysis_point_common_hierarchy`. +""" +const HierarchyAnalysisPointT = Vector{Union{IgnoredAnalysisPoint, Symbol}} +""" The type returned from `as_hierarchy`. """ const HierarchyT = Union{HierarchyVariableT, HierarchySystemT} @@ -1440,6 +1465,29 @@ function from_hierarchy(hierarchy::HierarchyT) end end +""" + $(TYPEDSIGNATURES) + +Represent an ignored analysis point as a namespaced hierarchy. The hierarchy is built +using the common hierarchy of all involved systems/variables. +""" +function analysis_point_common_hierarchy(ap::IgnoredAnalysisPoint)::HierarchyAnalysisPointT + isys = as_hierarchy(ap.input) + osyss = as_hierarchy.(ap.outputs) + suffix = Symbol[] + while isys[end] == osyss[1][end] && allequal(last.(osyss)) + push!(suffix, isys[end]) + pop!(isys) + pop!.(osyss) + end + isys = from_hierarchy(isys) + osyss = from_hierarchy.(osyss) + newap = IgnoredAnalysisPoint(isys, osyss) + hierarchy = HierarchyAnalysisPointT([suffix; newap]) + reverse!(hierarchy) + return hierarchy +end + """ $(TYPEDSIGNATURES) @@ -1466,19 +1514,20 @@ end """ $(TYPEDSIGNATURES) -Get the connections to ignore for `sys` and its subsystems. The returned value is a -`Tuple` similar in structure to the `ignored_connections` field. Each system (variable) -in the first (second) element of the tuple is also passed through `as_hierarchy`. +Get the analysis points to ignore for `sys` and its subsystems. The returned value is a +`Tuple` similar in structure to the `ignored_connections` field. """ function ignored_connections(sys::AbstractSystem) - has_ignored_connections(sys) || return (HierarchySystemT[], HierarchyVariableT[]) + has_ignored_connections(sys) || + return (HierarchyAnalysisPointT[], HierarchyAnalysisPointT[]) ics = get_ignored_connections(sys) if ics === nothing - ics = (HierarchySystemT[], HierarchyVariableT[]) + ics = (HierarchyAnalysisPointT[], HierarchyAnalysisPointT[]) end # turn into hierarchies - ics = (map(as_hierarchy, ics[1]), map(as_hierarchy, ics[2])) + ics = (map(analysis_point_common_hierarchy, ics[1]), + map(analysis_point_common_hierarchy, ics[2])) systems = get_systems(sys) # for each subsystem, get its ignored connections, add the name of the subsystem # to the hierarchy and concatenate corresponding buffers of the result @@ -1487,7 +1536,8 @@ function ignored_connections(sys::AbstractSystem) (map(Base.Fix2(push!, nameof(subsys)), sub_ics[1]), map(Base.Fix2(push!, nameof(subsys)), sub_ics[2])) end - return (Vector{HierarchySystemT}(result[1]), Vector{HierarchyVariableT}(result[2])) + return (Vector{HierarchyAnalysisPointT}(result[1]), + Vector{HierarchyAnalysisPointT}(result[2])) end """ diff --git a/src/systems/diffeqs/odesystem.jl b/src/systems/diffeqs/odesystem.jl index 78e8a0e93d..7be3b0c08a 100644 --- a/src/systems/diffeqs/odesystem.jl +++ b/src/systems/diffeqs/odesystem.jl @@ -189,11 +189,12 @@ struct ODESystem <: AbstractODESystem """ split_idxs::Union{Nothing, Vector{Vector{Int}}} """ - The connections to ignore (since they're removed by analysis point transformations). - The first element of the tuple are systems that can't be in the same connection set, - and the second are variables (for the trivial form of `connect`). + The analysis points removed by transformations, representing connections to be + ignored. The first element of the tuple analysis points connecting systems and + the second are ones connecting variables (for the trivial form of `connect`). """ - ignored_connections::Union{Nothing, Tuple{Vector{ODESystem}, Vector{BasicSymbolic}}} + ignored_connections::Union{ + Nothing, Tuple{Vector{IgnoredAnalysisPoint}, Vector{IgnoredAnalysisPoint}}} """ The hierarchical parent system before simplification. """ From fdcac6f395067962e4afee855e42524e545c5f6b Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 18 Mar 2025 16:23:40 +0530 Subject: [PATCH 1167/1337] refactor: unwrap in causal variable `connect` --- src/systems/analysis_points.jl | 3 ++- src/systems/connectors.jl | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/systems/analysis_points.jl b/src/systems/analysis_points.jl index 16dee02934..fe5364205b 100644 --- a/src/systems/analysis_points.jl +++ b/src/systems/analysis_points.jl @@ -213,7 +213,8 @@ function Symbolics.connect( outs::ConnectableSymbolicT...; verbose = true) allvars = (in, out, outs...) validate_causal_variables_connection(allvars) - return AnalysisPoint() ~ AnalysisPoint(in, name, [out; collect(outs)]; verbose) + return AnalysisPoint() ~ AnalysisPoint( + unwrap(in), name, unwrap.([out; collect(outs)]); verbose) end """ diff --git a/src/systems/connectors.jl b/src/systems/connectors.jl index 94971e38ee..2cd8d1a12e 100644 --- a/src/systems/connectors.jl +++ b/src/systems/connectors.jl @@ -152,7 +152,7 @@ function Symbolics.connect(var1::ConnectableSymbolicT, var2::ConnectableSymbolic vars::ConnectableSymbolicT...) allvars = (var1, var2, vars...) validate_causal_variables_connection(allvars) - return Equation(Connection(), Connection(map(SymbolicWithNameof, allvars))) + return Equation(Connection(), Connection(map(SymbolicWithNameof, unwrap.(allvars)))) end function flowvar(sys::AbstractSystem) From ce72751a302336c81164602a06bf4077baa15d80 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 18 Mar 2025 16:24:36 +0530 Subject: [PATCH 1168/1337] fix: only ignore systems in connection sets involving ignored analysis point --- src/systems/analysis_points.jl | 14 ++-- src/systems/connectors.jl | 144 +++++++++++++++++++++++---------- 2 files changed, 110 insertions(+), 48 deletions(-) diff --git a/src/systems/analysis_points.jl b/src/systems/analysis_points.jl index fe5364205b..798524da5d 100644 --- a/src/systems/analysis_points.jl +++ b/src/systems/analysis_points.jl @@ -417,20 +417,20 @@ function with_analysis_point_ignored(sys::AbstractSystem, ap::AnalysisPoint) has_ignored_connections(sys) || return sys ignored = get_ignored_connections(sys) if ignored === nothing - ignored = (ODESystem[], BasicSymbolic[]) + ignored = (IgnoredAnalysisPoint[], IgnoredAnalysisPoint[]) else ignored = copy.(ignored) end if ap.outputs === nothing error("Empty analysis point") end - for x in ap.outputs - if x isa ODESystem - push!(ignored[1], x) - else - push!(ignored[2], unwrap(x)) - end + + if ap.input isa AbstractSystem && all(x -> x isa AbstractSystem, ap.outputs) + push!(ignored[1], IgnoredAnalysisPoint(ap.input, ap.outputs)) + else + push!(ignored[2], IgnoredAnalysisPoint(ap.input, ap.outputs)) end + return @set sys.ignored_connections = ignored end diff --git a/src/systems/connectors.jl b/src/systems/connectors.jl index 2cd8d1a12e..ff4eb3e501 100644 --- a/src/systems/connectors.jl +++ b/src/systems/connectors.jl @@ -328,14 +328,12 @@ namespaced by `namespace`. `ignored_connects[1]`, purely to avoid unnecessary recomputation. """ function connection2set!(connectionsets, namespace, ss, isouter; - ignored_connects = (HierarchySystemT[], HierarchyVariableT[]), - namespaced_ignored_systems = ODESystem[]) - ignored_systems, ignored_variables = ignored_connects + ignored_systems = HierarchySystemT[], ignored_variables = HierarchyVariableT[]) + ns_ignored_systems = from_hierarchy.(ignored_systems) + ns_ignored_variables = from_hierarchy.(ignored_variables) # ignore specified systems ss = filter(ss) do s - all(namespaced_ignored_systems) do igsys - nameof(igsys) != nameof(s) - end + !any(x -> nameof(x) == nameof(s), ns_ignored_systems) end # `ignored_variables` for each `s` in `ss` corresponding_ignored_variables = map( @@ -434,15 +432,95 @@ function generate_connection_set( connectionsets = ConnectionSet[] domain_csets = ConnectionSet[] sys = generate_connection_set!( - connectionsets, domain_csets, sys, find, replace, scalarize, nothing, - # include systems to be ignored - ignored_connections(sys)) + connectionsets, domain_csets, sys, find, replace, scalarize, nothing, ignored_connections(sys)) csets = merge(connectionsets) domain_csets = merge([csets; domain_csets], true) sys, (csets, domain_csets) end +""" + $(TYPEDSIGNATURES) + +For a list of `systems` in a connect equation, return the subset of it to ignore (as a +list of hierarchical systems) based on `ignored_system_aps`, the analysis points to be +ignored. All analysis points in `ignored_system_aps` must contain systems (connectors) +as their input/outputs. +""" +function systems_to_ignore(ignored_system_aps::Vector{HierarchyAnalysisPointT}, + systems::Union{Vector{<:AbstractSystem}, Tuple{Vararg{<:AbstractSystem}}}) + to_ignore = HierarchySystemT[] + for ap in ignored_system_aps + # if `systems` contains the input of the AP, ignore any outputs of the AP present in it. + isys_hierarchy = HierarchySystemT([ap[1].input; @view ap[2:end]]) + isys = from_hierarchy(isys_hierarchy) + any(x -> nameof(x) == nameof(isys), systems) || continue + + for outsys in ap[1].outputs + osys_hierarchy = HierarchySystemT([outsys; @view ap[2:end]]) + osys = from_hierarchy(osys_hierarchy) + any(x -> nameof(x) == nameof(osys), systems) || continue + push!(to_ignore, HierarchySystemT(osys_hierarchy)) + end + end + + return to_ignore +end + +""" + $(TYPEDSIGNATURES) + +For a list of `systems` in a connect equation, return the subset of their variables to +ignore (as a list of hierarchical variables) based on `ignored_system_aps`, the analysis +points to be ignored. All analysis points in `ignored_system_aps` must contain variables +as their input/outputs. +""" +function variables_to_ignore(ignored_variable_aps::Vector{HierarchyAnalysisPointT}, + systems::Union{Vector{<:AbstractSystem}, Tuple{Vararg{<:AbstractSystem}}}) + to_ignore = HierarchyVariableT[] + for ap in ignored_variable_aps + ivar_hierarchy = HierarchyVariableT([ap[1].input; @view ap[2:end]]) + ivar = from_hierarchy(ivar_hierarchy) + any(x -> any(isequal(ivar), renamespace.((x,), unknowns(x))), systems) || continue + + for outvar in ap[1].outputs + ovar_hierarchy = HierarchyVariableT([as_hierarchy(outvar); @view ap[2:end]]) + ovar = from_hierarchy(ovar_hierarchy) + any(x -> any(isequal(ovar), renamespace.((x,), unknowns(x))), systems) || + continue + push!(to_ignore, HierarchyVariableT(ovar_hierarchy)) + end + end + return to_ignore +end + +""" + $(TYPEDSIGNATURES) + +For a list of variables `vars` in a connect equation, return the subset of them ignore +(as a list of symbolic variables) based on `ignored_system_aps`, the analysis points to +be ignored. All analysis points in `ignored_system_aps` must contain variables as their +input/outputs. +""" +function variables_to_ignore(ignored_variable_aps::Vector{HierarchyAnalysisPointT}, + vars::Union{Vector{<:BasicSymbolic}, Tuple{Vararg{<:BasicSymbolic}}}) + to_ignore = eltype(vars)[] + for ap in ignored_variable_aps + ivar_hierarchy = HierarchyVariableT([ap[1].input; @view ap[2:end]]) + ivar = from_hierarchy(ivar_hierarchy) + any(isequal(ivar), vars) || continue + + for outvar in ap[1].outputs + ovar_hierarchy = HierarchyVariableT([outvar; @view ap[2:end]]) + ovar = from_hierarchy(ovar_hierarchy) + any(isequal(ovar), vars) || continue + push!(to_ignore, ovar) + end + end + + return to_ignore +end + """ $(TYPEDSIGNATURES) @@ -456,26 +534,12 @@ Generate connection sets from `connect` equations. - `sys` is the system whose equations are to be searched. - `namespace` is a system representing the namespace in which `sys` exists, or `nothing` for no namespace (if `sys` is top-level). -- `ignored_connects` is a tuple. The first (second) element is a list of systems - (variables) in the format returned by `as_hierarchy` to be ignored when generating - connections. This is typically because the connections they are used in were removed by - analysis point transformations. """ function generate_connection_set!(connectionsets, domain_csets, sys::AbstractSystem, find, replace, scalarize, namespace = nothing, - ignored_connects = (HierarchySystemT[], HierarchyVariableT[])) + ignored_connects = (HierarchyAnalysisPointT[], HierarchyAnalysisPointT[])) subsys = get_systems(sys) - ignored_systems, ignored_variables = ignored_connects - # turn hierarchies into namespaced systems - namespaced_ignored_systems = from_hierarchy.(ignored_systems) - namespaced_ignored_variables = from_hierarchy.(ignored_variables) - namespaced_ignored = (namespaced_ignored_systems, namespaced_ignored_variables) - # filter the subsystems of `sys` to exclude ignored ones - filtered_subsys = filter(subsys) do ss - all(namespaced_ignored_systems) do igsys - nameof(igsys) != nameof(ss) - end - end + ignored_system_aps, ignored_variable_aps = ignored_connects isouter = generate_isouter(sys) eqs′ = get_eqs(sys) @@ -501,8 +565,12 @@ function generate_connection_set!(connectionsets, domain_csets, neweq isa AbstractArray ? append!(eqs, neweq) : push!(eqs, neweq) else if lhs isa Connection && get_systems(lhs) === :domain - connection2set!(domain_csets, namespace, get_systems(rhs), isouter; - ignored_connects, namespaced_ignored_systems) + connected_systems = get_systems(rhs) + connection2set!(domain_csets, namespace, connected_systems, isouter; + ignored_systems = systems_to_ignore( + ignored_system_aps, connected_systems), + ignored_variables = variables_to_ignore( + ignored_variable_aps, connected_systems)) elseif isconnection(rhs) push!(cts, get_systems(rhs)) else @@ -519,22 +587,19 @@ function generate_connection_set!(connectionsets, domain_csets, # all connectors are eventually inside connectors. T = ConnectionElement # only generate connection sets for systems that are not ignored - for s in filtered_subsys + for s in subsys isconnector(s) || continue is_domain_connector(s) && continue - _ignored_variables = ignored_systems_for_subsystem(s, ignored_variables) - _namespaced_ignored_variables = from_hierarchy.(_ignored_variables) for v in unknowns(s) Flow === get_connection_type(v) || continue - # ignore specified variables - any(isequal(v), _namespaced_ignored_variables) && continue push!(connectionsets, ConnectionSet([T(LazyNamespace(namespace, s), v, false)])) end end for ct in cts connection2set!(connectionsets, namespace, ct, isouter; - ignored_connects, namespaced_ignored_systems) + ignored_systems = systems_to_ignore(ignored_system_aps, ct), + ignored_variables = variables_to_ignore(ignored_variable_aps, ct)) end # pre order traversal @@ -558,14 +623,15 @@ ignored by `generate_connection_set!` (`expand_variable_connections`), filter their hierarchy to not include `subsys`. """ function ignored_systems_for_subsystem( - subsys::AbstractSystem, ignored_systems::Vector{<:HierarchyT}) + subsys::AbstractSystem, ignored_systems::Vector{<:Union{ + HierarchyT, HierarchyAnalysisPointT}}) result = eltype(ignored_systems)[] # in case `subsys` is namespaced, get its hierarchy and compare suffixes # instead of the just the last element suffix = reverse!(namespace_hierarchy(nameof(subsys))) N = length(suffix) for igsys in ignored_systems - if igsys[(end - N + 1):end] == suffix + if length(igsys) > N && igsys[(end - N + 1):end] == suffix push!(result, copy(igsys)) for i in 1:N pop!(result[end]) @@ -684,7 +750,6 @@ function expand_variable_connections(sys::AbstractSystem; ignored_variables = no if ignored_variables === nothing ignored_variables = ignored_connections(sys)[2] end - namespaced_ignored = from_hierarchy.(ignored_variables) eqs = copy(get_eqs(sys)) valid_idxs = trues(length(eqs)) additional_eqs = Equation[] @@ -694,14 +759,11 @@ function expand_variable_connections(sys::AbstractSystem; ignored_variables = no connection = eq.rhs elements = get_systems(connection) is_causal_variable_connection(connection) || continue - elements = filter(elements) do el - all(namespaced_ignored) do var - getname(var) != getname(el.var) - end - end valid_idxs[i] = false elements = map(x -> x.var, elements) + to_ignore = variables_to_ignore(ignored_variables, elements) + elements = setdiff(elements, to_ignore) outvar = first(elements) for invar in Iterators.drop(elements, 1) push!(additional_eqs, outvar ~ invar) From c54bfe94aa71f31f67ef67ed9e75c2c0a58ad960 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 18 Mar 2025 16:25:06 +0530 Subject: [PATCH 1169/1337] test: test analysis point transform not ignoring extra connections --- test/downstream/analysis_points.jl | 49 ++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/test/downstream/analysis_points.jl b/test/downstream/analysis_points.jl index 62961f181a..c175803e48 100644 --- a/test/downstream/analysis_points.jl +++ b/test/downstream/analysis_points.jl @@ -438,3 +438,52 @@ end matrices, _ = get_sensitivity(sys, sys.ap) @test matrices == matrices_normal end + +@testset "Ignored analysis points only affect relevant connection sets" begin + m1 = 1 + m2 = 1 + k = 1000 # Spring stiffness + c = 10 # Damping coefficient + + @named inertia1 = Inertia(; J = m1, phi = 0, w = 0) + @named inertia2 = Inertia(; J = m2, phi = 0, w = 0) + + @named spring = Spring(; c = k) + @named damper = Damper(; d = c) + + @named torque = Torque(use_support = false) + + function SystemModel(u = nothing; name = :model) + eqs = [connect(torque.flange, inertia1.flange_a) + connect(inertia1.flange_b, spring.flange_a, damper.flange_a) + connect(inertia2.flange_a, spring.flange_b, damper.flange_b)] + if u !== nothing + push!(eqs, connect(torque.tau, u.output)) + return @named model = ODESystem( + eqs, t; systems = [torque, inertia1, inertia2, spring, damper, u]) + end + ODESystem(eqs, t; systems = [torque, inertia1, inertia2, spring, damper], name) + end + + @named r = Step(start_time = 1) + @named pid = LimPID(k = 400, Ti = 0.5, Td = 1, u_max = 350) + @named filt = SecondOrder(d = 0.9, w = 10, x = 0, xd = 0) + @named sensor = AngleSensor() + @named add = Add() # To add the feedback and feedforward control signals + model = SystemModel() + @named inverse_model = SystemModel() + @named inverse_sensor = AngleSensor() + connections = [connect(r.output, :r, filt.input) # Name connection r to form an analysis point + connect(inverse_model.inertia1.flange_b, inverse_sensor.flange) # Attach the inverse sensor to the inverse model + connect(filt.output, pid.reference, inverse_sensor.phi) # the filtered reference now goes to both the PID controller and the inverse model input + connect(inverse_model.torque.tau, add.input1) + connect(pid.ctr_output, add.input2) + connect(add.output, :u, model.torque.tau) # Name connection u to form an analysis point + connect(model.inertia1.flange_b, sensor.flange) + connect(sensor.phi, :y, pid.measurement)] + closed_loop = ODESystem(connections, t, + systems = [model, inverse_model, pid, filt, sensor, inverse_sensor, r, add], + name = :closed_loop) + # just ensure the system simplifies + Blocks.get_sensitivity(closed_loop, :y) +end From c014d8c893a96f55f6a6419158b7f677f7d38dc1 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 18 Mar 2025 21:33:53 +0530 Subject: [PATCH 1170/1337] fix: be extra safe with unwrapping --- src/systems/analysis_points.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/systems/analysis_points.jl b/src/systems/analysis_points.jl index 798524da5d..eb43bc40ef 100644 --- a/src/systems/analysis_points.jl +++ b/src/systems/analysis_points.jl @@ -428,7 +428,7 @@ function with_analysis_point_ignored(sys::AbstractSystem, ap::AnalysisPoint) if ap.input isa AbstractSystem && all(x -> x isa AbstractSystem, ap.outputs) push!(ignored[1], IgnoredAnalysisPoint(ap.input, ap.outputs)) else - push!(ignored[2], IgnoredAnalysisPoint(ap.input, ap.outputs)) + push!(ignored[2], IgnoredAnalysisPoint(unwrap(ap.input), unwrap.(ap.outputs))) end return @set sys.ignored_connections = ignored From 861abffb1f8107fad6a294e06b673aa9a7bad77d Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 19 Mar 2025 13:51:29 +0530 Subject: [PATCH 1171/1337] test: improve analysis point test --- test/downstream/analysis_points.jl | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/test/downstream/analysis_points.jl b/test/downstream/analysis_points.jl index c175803e48..29b9aad512 100644 --- a/test/downstream/analysis_points.jl +++ b/test/downstream/analysis_points.jl @@ -485,5 +485,12 @@ end systems = [model, inverse_model, pid, filt, sensor, inverse_sensor, r, add], name = :closed_loop) # just ensure the system simplifies - Blocks.get_sensitivity(closed_loop, :y) + mats, _ = get_sensitivity(closed_loop, :y) + S = CS.ss(mats...) + fr = CS.freqrespv(S, [0.01, 1, 100]) + # https://github.com/SciML/ModelingToolkit.jl/pull/3469 + reference_fr = ComplexF64[-1.2505330104772838e-11 - 2.500062613816021e-9im, + -0.0024688370221621625 - 0.002279011866413123im, + 1.8100018764334602 + 0.3623845793211718im] + @test isapprox(fr, reference_fr) end From 41ce7f5ba5fefcb4b4e9ba140bcc4e2b968a8672 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 17 Mar 2025 16:33:16 +0530 Subject: [PATCH 1172/1337] fix: remove extra print statements --- src/systems/discrete_system/implicit_discrete_system.jl | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/systems/discrete_system/implicit_discrete_system.jl b/src/systems/discrete_system/implicit_discrete_system.jl index a9cddcef49..514576e927 100644 --- a/src/systems/discrete_system/implicit_discrete_system.jl +++ b/src/systems/discrete_system/implicit_discrete_system.jl @@ -333,10 +333,8 @@ function SciMLBase.ImplicitDiscreteProblem( u0map = to_varmap(u0map, dvs) u0map = shift_u0map_forward(sys, u0map, defaults(sys)) - @show u0map f, u0, p = process_SciMLProblem( ImplicitDiscreteFunction, sys, u0map, parammap; eval_expression, eval_module, kwargs...) - @show u0 kwargs = filter_kwargs(kwargs) ImplicitDiscreteProblem(f, u0, tspan, p; kwargs...) From 16ac26f9cf3aec79b4eb34123a68848a3006b130 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 17 Mar 2025 16:33:55 +0530 Subject: [PATCH 1173/1337] feat: add `Initial` parameters for unknowns to all non-initialization systems --- src/systems/abstractsystem.jl | 54 ++++++++++++----------------------- 1 file changed, 18 insertions(+), 36 deletions(-) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index 39ca1baed3..c9debc06c2 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -678,21 +678,22 @@ end function add_initialization_parameters(sys::AbstractSystem) @assert !has_systems(sys) || isempty(get_systems(sys)) + is_initializesystem(sys) && return sys + all_initialvars = Set{BasicSymbolic}() # time-independent systems don't initialize unknowns - if is_time_dependent(sys) - eqs = equations(sys) - if !(eqs isa Vector{Equation}) - eqs = Equation[x for x in eqs if x isa Equation] - end - obs, eqs = unhack_observed(observed(sys), eqs) - for x in Iterators.flatten((unknowns(sys), Iterators.map(eq -> eq.lhs, obs))) - x = unwrap(x) - if iscall(x) && operation(x) == getindex - push!(all_initialvars, arguments(x)[1]) - else - push!(all_initialvars, x) - end + # but may initialize parameters using guesses for unknowns + eqs = equations(sys) + if !(eqs isa Vector{Equation}) + eqs = Equation[x for x in eqs if x isa Equation] + end + obs, eqs = unhack_observed(observed(sys), eqs) + for x in Iterators.flatten((unknowns(sys), Iterators.map(eq -> eq.lhs, obs))) + x = unwrap(x) + if iscall(x) && operation(x) == getindex + push!(all_initialvars, arguments(x)[1]) + else + push!(all_initialvars, x) end end for eq in parameter_dependencies(sys) @@ -722,15 +723,8 @@ Returns true if the parameter `p` is of the form `Initial(x)`. """ function isinitial(p) p = unwrap(p) - if iscall(p) - operation(p) isa Initial && return true - if operation(p) === getindex - operation(arguments(p)[1]) isa Initial && return true - end - else - return false - end - return false + return iscall(p) && (operation(p) isa Initial || + operation(p) === getindex && isinitial(arguments(p)[1])) end """ @@ -1345,20 +1339,8 @@ function parameters(sys::AbstractSystem; initial_parameters = false) systems = get_systems(sys) result = unique(isempty(systems) ? ps : [ps; reduce(vcat, namespace_parameters.(systems))]) - if !initial_parameters - if is_time_dependent(sys) - # time-dependent systems have `Initial` parameters for all their - # unknowns/pdeps, all of which should be hidden. - filter!(x -> !iscall(x) || !isa(operation(x), Initial), result) - else - # time-independent systems only have `Initial` parameters for - # pdeps. Any other `Initial` parameters should be kept (e.g. initialization - # systems) - filter!( - x -> !iscall(x) || !isa(operation(x), Initial) || - !has_parameter_dependency_with_lhs(sys, only(arguments(x))), - result) - end + if !initial_parameters && !is_initializesystem(sys) + filter!(x -> !iscall(x) || !isa(operation(x), Initial), result) end return result end From d3a5386a2b2fd28f393398508791de4e1e3a9010 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 17 Mar 2025 16:34:49 +0530 Subject: [PATCH 1174/1337] feat: handle `Initial(x)` initialization_eqs for time-independent systems --- src/systems/nonlinear/initializesystem.jl | 384 +++++++++++++++------- src/systems/problem_utils.jl | 17 + 2 files changed, 280 insertions(+), 121 deletions(-) diff --git a/src/systems/nonlinear/initializesystem.jl b/src/systems/nonlinear/initializesystem.jl index 0f4016be44..c03524c61b 100644 --- a/src/systems/nonlinear/initializesystem.jl +++ b/src/systems/nonlinear/initializesystem.jl @@ -1,9 +1,9 @@ """ $(TYPEDSIGNATURES) -Generate `NonlinearSystem` which initializes an ODE problem from specified initial conditions of an `ODESystem`. +Generate `NonlinearSystem` which initializes a problem from specified initial conditions of an `AbstractTimeDependentSystem`. """ -function generate_initializesystem(sys::AbstractSystem; +function generate_initializesystem(sys::AbstractTimeDependentSystem; u0map = Dict(), pmap = Dict(), initialization_eqs = [], @@ -31,81 +31,70 @@ function generate_initializesystem(sys::AbstractSystem; idxs_diff = isdiffeq.(eqs) # PREPROCESSING - # If `=> nothing` in `u0map`, remove the key from `defs` u0map = copy(anydict(u0map)) - for (k, v) in u0map - v === nothing || continue - delete!(defs, k) - end - filter_missing_values!(u0map) pmap = anydict(pmap) - - # 1) Use algebraic equations of time-dependent systems as initialization constraints - if has_iv(sys) - idxs_alge = .!idxs_diff - append!(eqs_ics, eqs[idxs_alge]) # start equation list with algebraic equations - - eqs_diff = eqs[idxs_diff] - D = Differential(get_iv(sys)) - diffmap = merge( - Dict(eq.lhs => eq.rhs for eq in eqs_diff), - Dict(D(eq.lhs) => D(eq.rhs) for eq in trueobs) - ) - else - diffmap = Dict() - end - - if is_time_dependent(sys) - if has_schedule(sys) && (schedule = get_schedule(sys); !isnothing(schedule)) - # 2) process dummy derivatives and u0map into initialization system - # prepare map for dummy derivative substitution - for x in filter(x -> !isnothing(x[1]), schedule.dummy_sub) - # set dummy derivatives to default_dd_guess unless specified - push!(defs, x[1] => get(guesses, x[1], default_dd_guess)) - end - function process_u0map_with_dummysubs(y, x) - y = get(schedule.dummy_sub, y, y) - y = fixpoint_sub(y, diffmap) - # If we have `D(x) ~ x` and provide [D(x) => x, x => 1.0] to `u0map`, then - # without this condition `defs` would get `x => x` instead of retaining - # `x => 1.0`. - isequal(y, x) && return - if y ∈ vars_set - # variables specified in u0 overrides defaults - push!(defs, y => x) - elseif y isa Symbolics.Arr - # TODO: don't scalarize arrays - merge!(defs, Dict(scalarize(y .=> x))) - elseif y isa Symbolics.BasicSymbolic - # y is a derivative expression expanded; add it to the initialization equations - push!(eqs_ics, y ~ x) - else - error("Initialization expression $y is currently not supported. If its a higher order derivative expression, then only the dummy derivative expressions are supported.") - end - end - for (y, x) in u0map - if Symbolics.isarraysymbolic(y) - process_u0map_with_dummysubs.(collect(y), collect(x)) - else - process_u0map_with_dummysubs(y, x) - end + initsys_preprocessing!(u0map, defs) + + # 1) Use algebraic equations of system as initialization constraints + idxs_alge = .!idxs_diff + append!(eqs_ics, eqs[idxs_alge]) # start equation list with algebraic equations + + eqs_diff = eqs[idxs_diff] + D = Differential(get_iv(sys)) + diffmap = merge( + Dict(eq.lhs => eq.rhs for eq in eqs_diff), + Dict(D(eq.lhs) => D(eq.rhs) for eq in trueobs) + ) + + if has_schedule(sys) && (schedule = get_schedule(sys); !isnothing(schedule)) + # 2) process dummy derivatives and u0map into initialization system + # prepare map for dummy derivative substitution + for x in filter(x -> !isnothing(x[1]), schedule.dummy_sub) + # set dummy derivatives to default_dd_guess unless specified + push!(defs, x[1] => get(guesses, x[1], default_dd_guess)) + end + function process_u0map_with_dummysubs(y, x) + y = get(schedule.dummy_sub, y, y) + y = fixpoint_sub(y, diffmap) + # If we have `D(x) ~ x` and provide [D(x) => x, x => 1.0] to `u0map`, then + # without this condition `defs` would get `x => x` instead of retaining + # `x => 1.0`. + isequal(y, x) && return + if y ∈ vars_set + # variables specified in u0 overrides defaults + push!(defs, y => x) + elseif y isa Symbolics.Arr + # TODO: don't scalarize arrays + merge!(defs, Dict(scalarize(y .=> x))) + elseif y isa Symbolics.BasicSymbolic + # y is a derivative expression expanded; add it to the initialization equations + push!(eqs_ics, y ~ x) + else + error("Initialization expression $y is currently not supported. If its a higher order derivative expression, then only the dummy derivative expressions are supported.") end - else - # 2) System doesn't have a schedule, so dummy derivatives don't exist/aren't handled (SDESystem) - for (k, v) in u0map - defs[k] = v + end + for (y, x) in u0map + if Symbolics.isarraysymbolic(y) + process_u0map_with_dummysubs.(collect(y), collect(x)) + else + process_u0map_with_dummysubs(y, x) end end + else + # 2) System doesn't have a schedule, so dummy derivatives don't exist/aren't handled (SDESystem) + for (k, v) in u0map + defs[k] = v + end + end - # 3) process other variables - for var in vars - if var ∈ keys(defs) - push!(eqs_ics, var ~ defs[var]) - elseif var ∈ keys(guesses) - push!(defs, var => guesses[var]) - elseif check_defguess - error("Invalid setup: variable $(var) has no default value or initial guess") - end + # 3) process other variables + for var in vars + if var ∈ keys(defs) + push!(eqs_ics, var ~ defs[var]) + elseif var ∈ keys(guesses) + push!(defs, var => guesses[var]) + elseif check_defguess + error("Invalid setup: variable $(var) has no default value or initial guess") end end @@ -119,6 +108,179 @@ function generate_initializesystem(sys::AbstractSystem; end # 5) process parameters as initialization unknowns + paramsubs = setup_parameter_initialization!( + sys, pmap, defs, guesses, eqs_ics; check_defguess) + + # 6) parameter dependencies become equations, their LHS become unknowns + # non-numeric dependent parameters stay as parameter dependencies + new_parameter_deps = solve_parameter_dependencies!( + sys, paramsubs, eqs_ics, defs, guesses) + + # 7) handle values provided for dependent parameters similar to values for observed variables + handle_dependent_parameter_constraints!(sys, pmap, eqs_ics, paramsubs) + + # parameters do not include ones that became initialization unknowns + pars = Vector{SymbolicParam}(filter( + p -> !haskey(paramsubs, p), parameters(sys; initial_parameters = true))) + push!(pars, get_iv(sys)) + + # 8) use observed equations for guesses of observed variables if not provided + for eq in trueobs + haskey(defs, eq.lhs) && continue + any(x -> isequal(default_toterm(x), eq.lhs), keys(defs)) && continue + + defs[eq.lhs] = eq.rhs + end + append!(eqs_ics, trueobs) + + vars = [vars; collect(values(paramsubs))] + + # even if `p => tovar(p)` is in `paramsubs`, `isparameter(p[1]) === true` after substitution + # so add scalarized versions as well + scalarize_varmap!(paramsubs) + + eqs_ics = Symbolics.substitute.(eqs_ics, (paramsubs,)) + for k in keys(defs) + defs[k] = substitute(defs[k], paramsubs) + end + meta = InitializationSystemMetadata( + anydict(u0map), anydict(pmap), additional_guesses, + additional_initialization_eqs, extra_metadata, nothing) + return NonlinearSystem(eqs_ics, + vars, + pars; + defaults = defs, + checks = check_units, + parameter_dependencies = new_parameter_deps, + name, + metadata = meta, + kwargs...) +end + +""" +$(TYPEDSIGNATURES) + +Generate `NonlinearSystem` which initializes a problem from specified initial conditions of an `AbstractTimeDependentSystem`. +""" +function generate_initializesystem(sys::AbstractTimeIndependentSystem; + u0map = Dict(), + pmap = Dict(), + initialization_eqs = [], + guesses = Dict(), + algebraic_only = false, + check_units = true, check_defguess = false, + name = nameof(sys), extra_metadata = (;), kwargs...) + eqs = equations(sys) + trueobs, eqs = unhack_observed(observed(sys), eqs) + vars = unique([unknowns(sys); getfield.(trueobs, :lhs)]) + vars_set = Set(vars) # for efficient in-lookup + + eqs_ics = Equation[] + defs = copy(defaults(sys)) # copy so we don't modify sys.defaults + additional_guesses = anydict(guesses) + additional_initialization_eqs = Vector{Equation}(initialization_eqs) + guesses = merge(get_guesses(sys), additional_guesses) + + # PREPROCESSING + u0map = copy(anydict(u0map)) + pmap = anydict(pmap) + initsys_preprocessing!(u0map, defs) + + # Calculate valid `Initial` parameters. These are unknowns for + # which constant initial values were provided. By this point, + # they have been separated into `x => Initial(x)` in `u0map` + # and `Initial(x) => val` in `pmap`. + valid_initial_parameters = Set{BasicSymbolic}() + for (k, v) in u0map + isequal(Initial(k), v) || continue + push!(valid_initial_parameters, v) + end + + # get the initialization equations + if !algebraic_only + initialization_eqs = [get_initialization_eqs(sys); initialization_eqs] + end + + # only include initialization equations where all the involved `Initial` + # parameters are valid. + vs = Set() + initialization_eqs = filter(initialization_eqs) do eq + empty!(vs) + vars!(vs, eq; op = Initial) + non_params = filter(!isparameter, vs) + # error if non-parameters are present in the initialization equations + if !isempty(non_params) + throw(UnknownsInTimeIndependentInitializationError(eq, non_params)) + end + filter!(x -> iscall(x) && isinitial(x), vs) + invalid_initials = setdiff(vs, valid_initial_parameters) + return isempty(invalid_initials) + end + + append!(eqs_ics, initialization_eqs) + + # process parameters as initialization unknowns + paramsubs = setup_parameter_initialization!( + sys, pmap, defs, guesses, eqs_ics; check_defguess) + + # parameter dependencies become equations, their LHS become unknowns + # non-numeric dependent parameters stay as parameter dependencies + new_parameter_deps = solve_parameter_dependencies!( + sys, paramsubs, eqs_ics, defs, guesses) + + # handle values provided for dependent parameters similar to values for observed variables + handle_dependent_parameter_constraints!(sys, pmap, eqs_ics, paramsubs) + + # parameters do not include ones that became initialization unknowns + pars = Vector{SymbolicParam}(filter( + p -> !haskey(paramsubs, p), parameters(sys; initial_parameters = true))) + vars = collect(values(paramsubs)) + + # even if `p => tovar(p)` is in `paramsubs`, `isparameter(p[1]) === true` after substitution + # so add scalarized versions as well + scalarize_varmap!(paramsubs) + + eqs_ics = Symbolics.substitute.(eqs_ics, (paramsubs,)) + for k in keys(defs) + defs[k] = substitute(defs[k], paramsubs) + end + meta = InitializationSystemMetadata( + anydict(u0map), anydict(pmap), additional_guesses, + additional_initialization_eqs, extra_metadata, nothing) + return NonlinearSystem(eqs_ics, + vars, + pars; + defaults = defs, + checks = check_units, + parameter_dependencies = new_parameter_deps, + name, + metadata = meta, + kwargs...) +end + +""" + $(TYPEDSIGNATURES) + +Preprocessing step for initialization. Currently removes key `k` from `defs` and `u0map` +if `k => nothing` is present in `u0map`. +""" +function initsys_preprocessing!(u0map::AbstractDict, defs::AbstractDict) + for (k, v) in u0map + v === nothing || continue + delete!(defs, k) + end + filter_missing_values!(u0map) +end + +""" + $(TYPEDSIGNATURES) + +Update `defs` and `eqs_ics` appropriately for parameter initialization. Return a dictionary +mapping solvable parameters to their `tovar` variants. +""" +function setup_parameter_initialization!( + sys::AbstractSystem, pmap::AbstractDict, defs::AbstractDict, + guesses::AbstractDict, eqs_ics::Vector{Equation}; check_defguess = false) paramsubs = Dict() for p in parameters(sys) if is_parameter_solvable(p, pmap, defs, guesses) @@ -169,8 +331,17 @@ function generate_initializesystem(sys::AbstractSystem; end end - # 6) parameter dependencies become equations, their LHS become unknowns - # non-numeric dependent parameters stay as parameter dependencies + return paramsubs +end + +""" + $(TYPEDSIGNATURES) + +Add appropriate parameter dependencies as initialization equations. Return the new list of +parameter dependencies for the initialization system. +""" +function solve_parameter_dependencies!(sys::AbstractSystem, paramsubs::AbstractDict, + eqs_ics::Vector{Equation}, defs::AbstractDict, guesses::AbstractDict) new_parameter_deps = Equation[] for eq in parameter_dependencies(sys) if !is_variable_floatingpoint(eq.lhs) @@ -184,60 +355,23 @@ function generate_initializesystem(sys::AbstractSystem; push!(defs, varp => guessval) end - # 7) handle values provided for dependent parameters similar to values for observed variables + return new_parameter_deps +end + +""" + $(TYPEDSIGNATURES) + +Turn values provided for parameter dependencies into initialization equations. +""" +function handle_dependent_parameter_constraints!(sys::AbstractSystem, pmap::AbstractDict, + eqs_ics::Vector{Equation}, paramsubs::AbstractDict) for (k, v) in merge(defaults(sys), pmap) if is_variable_floatingpoint(k) && has_parameter_dependency_with_lhs(sys, k) push!(eqs_ics, paramsubs[k] ~ v) end end - # parameters do not include ones that became initialization unknowns - pars = Vector{SymbolicParam}(filter( - p -> !haskey(paramsubs, p), parameters(sys; initial_parameters = true))) - is_time_dependent(sys) && push!(pars, get_iv(sys)) - - if is_time_dependent(sys) - # 8) use observed equations for guesses of observed variables if not provided - for eq in trueobs - haskey(defs, eq.lhs) && continue - any(x -> isequal(default_toterm(x), eq.lhs), keys(defs)) && continue - - defs[eq.lhs] = eq.rhs - end - append!(eqs_ics, trueobs) - end - - if is_time_dependent(sys) - vars = [vars; collect(values(paramsubs))] - else - vars = collect(values(paramsubs)) - end - - # even if `p => tovar(p)` is in `paramsubs`, `isparameter(p[1]) === true` after substitution - # so add scalarized versions as well - for k in collect(keys(paramsubs)) - symbolic_type(k) == ArraySymbolic() || continue - for i in eachindex(k) - paramsubs[k[i]] = paramsubs[k][i] - end - end - - eqs_ics = Symbolics.substitute.(eqs_ics, (paramsubs,)) - for k in keys(defs) - defs[k] = substitute(defs[k], paramsubs) - end - meta = InitializationSystemMetadata( - anydict(u0map), anydict(pmap), additional_guesses, - additional_initialization_eqs, extra_metadata, nothing) - return NonlinearSystem(eqs_ics, - vars, - pars; - defaults = defs, - checks = check_units, - parameter_dependencies = new_parameter_deps, - name, - metadata = meta, - kwargs...) + return nothing end """ @@ -604,3 +738,11 @@ function unhack_observed(obseqs::Vector{Equation}, eqs::Vector{Equation}) end return obseqs, eqs end + +function UnknownsInTimeIndependentInitializationError(eq, non_params) + ArgumentError(""" + Initialization equations for time-independent systems can only contain parameters. \ + Found $non_params in $eq. If the equations refer to the initial guess for unknowns, \ + use the `Initial` operator. + """) +end diff --git a/src/systems/problem_utils.jl b/src/systems/problem_utils.jl index b94a360f93..74aed297d3 100644 --- a/src/systems/problem_utils.jl +++ b/src/systems/problem_utils.jl @@ -491,6 +491,22 @@ function filter_missing_values!(varmap::AbstractDict; missing_values = nothing) end end +""" + $(TYPEDSIGNATURES) + +For each `k => v` in `varmap` where `k` is an array (or array symbolic) add +`k[i] => v[i]` for all `i in eachindex(k)`. Return the modified `varmap`. +""" +function scalarize_varmap!(varmap::AbstractDict) + for k in collect(keys(varmap)) + symbolic_type(k) == ArraySymbolic() || continue + for i in eachindex(k) + varmap[k[i]] = varmap[k][i] + end + end + return varmap +end + struct GetUpdatedMTKParameters{G, S} # `getu` functor which gets parameters that are unknowns during initialization getpunknowns::G @@ -800,6 +816,7 @@ function process_SciMLProblem( op, missing_unknowns, missing_pars = build_operating_point!(sys, u0map, pmap, defs, cmap, dvs, ps) + # Main.@infiltrate sys isa NonlinearSystem if u0_constructor === identity && u0Type <: StaticArray u0_constructor = vals -> SymbolicUtils.Code.create_array( u0Type, eltype(vals), Val(1), Val(length(vals)), vals...) From ff7d28de118a43cfccd1bdc71760511229642687 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 17 Mar 2025 16:35:00 +0530 Subject: [PATCH 1175/1337] test: test `Initial(x)` initialization_eqs for time-independent systems --- test/initializationsystem.jl | 59 ++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/test/initializationsystem.jl b/test/initializationsystem.jl index 93054eb5f4..54b847c64a 100644 --- a/test/initializationsystem.jl +++ b/test/initializationsystem.jl @@ -1417,3 +1417,62 @@ end @test prob.ps[p1] ≈ 3.0 end end + +@testset "`Initial(X)` in time-independent systems: $Problem" for Problem in [ + NonlinearProblem, NonlinearLeastSquaresProblem] + @parameters k1 k2 + @variables X1(t) X2(t) + @parameters Γ[1:1]=missing [guess = [1.0]] + eqs = [ + 0 ~ k1 * (Γ[1] - X1) - k2 * X1 + ] + initialization_eqs = [ + X2 ~ Γ[1] - X1 + ] + @mtkbuild nlsys = NonlinearSystem(eqs, [X1, X2], [k1, k2, Γ]; initialization_eqs) + + @testset "throws if initialization_eqs contain unknowns" begin + u0 = [X1 => 1.0, X2 => 2.0] + ps = [k1 => 0.1, k2 => 0.2] + @test_throws ArgumentError Problem(nlsys, u0, ps) + end + + eqs = [0 ~ k1 * (Γ[1] - X1) - k2 * X1 + X2 ~ Γ[1] - X1] + initialization_eqs = [ + Initial(X2) ~ Γ[1] - Initial(X1) + ] + @mtkbuild nlsys = NonlinearSystem(eqs, [X1, X2], [k1, k2, Γ]; initialization_eqs) + + @testset "solves initialization" begin + u0 = [X1 => 1.0, X2 => 2.0] + ps = [k1 => 0.1, k2 => 0.2] + prob = Problem(nlsys, u0, ps) + @test state_values(prob.f.initialization_data.initializeprob) === nothing + @test prob.ps[Γ[1]] ≈ 3.0 + end + + @testset "respects explicitly provided value" begin + u0 = [] + ps = [k1 => 0.1, k2 => 0.2, Γ => [5.0]] + prob = Problem(nlsys, u0, ps) + @test prob.ps[Γ[1]] ≈ 5.0 + end + + @testset "fails initialization if inconsistent explicit value" begin + u0 = [X1 => 1.0, X2 => 2.0] + ps = [k1 => 0.1, k2 => 0.2, Γ => [5.0]] + prob = Problem(nlsys, u0, ps) + sol = solve(prob) + @test sol.retcode == SciMLBase.ReturnCode.InitialFailure + end + + @testset "Ignores initial equation if given insufficient u0" begin + u0 = [X2 => 2.0] + ps = [k1 => 0.1, k2 => 0.2, Γ => [5.0]] + prob = Problem(nlsys, u0, ps) + sol = solve(prob) + @test SciMLBase.successful_retcode(sol) + @test sol.ps[Γ[1]] ≈ 5.0 + end +end From 3e58b5a148795073c9f8d78f05e3af6754e55259 Mon Sep 17 00:00:00 2001 From: Christopher Rackauckas Date: Mon, 17 Mar 2025 11:29:37 -0100 Subject: [PATCH 1176/1337] Update src/systems/problem_utils.jl --- src/systems/problem_utils.jl | 1 - 1 file changed, 1 deletion(-) diff --git a/src/systems/problem_utils.jl b/src/systems/problem_utils.jl index 74aed297d3..902fb0f774 100644 --- a/src/systems/problem_utils.jl +++ b/src/systems/problem_utils.jl @@ -816,7 +816,6 @@ function process_SciMLProblem( op, missing_unknowns, missing_pars = build_operating_point!(sys, u0map, pmap, defs, cmap, dvs, ps) - # Main.@infiltrate sys isa NonlinearSystem if u0_constructor === identity && u0Type <: StaticArray u0_constructor = vals -> SymbolicUtils.Code.create_array( u0Type, eltype(vals), Val(1), Val(length(vals)), vals...) From 25159b8ed86d19aa6fb534f762631cceefc62a47 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 17 Mar 2025 17:55:27 +0530 Subject: [PATCH 1177/1337] refactor: remove extra empty file --- src/doc | 1 - 1 file changed, 1 deletion(-) delete mode 100644 src/doc diff --git a/src/doc b/src/doc deleted file mode 100644 index 8b13789179..0000000000 --- a/src/doc +++ /dev/null @@ -1 +0,0 @@ - From f05b93f8d068523ab1b0ebe5ad490f2dba0717be Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 18 Mar 2025 16:40:13 +0530 Subject: [PATCH 1178/1337] build: bump DiffEqBase compat --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 2b204c5ed7..278cf0b8e8 100644 --- a/Project.toml +++ b/Project.toml @@ -92,7 +92,7 @@ DataInterpolations = "6.4" DataStructures = "0.17, 0.18" DeepDiffs = "1" DelayDiffEq = "5.50" -DiffEqBase = "6.157" +DiffEqBase = "6.165.1" DiffEqCallbacks = "2.16, 3, 4" DiffEqNoiseProcess = "5" DiffRules = "0.1, 1.0" From d5e3d1aafa7b7608c2456b33d4a34dddc3e5a91b Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 18 Mar 2025 21:23:23 +0530 Subject: [PATCH 1179/1337] feat: add `supports_initialization` trait --- src/systems/abstractsystem.jl | 2 ++ src/systems/discrete_system/discrete_system.jl | 2 ++ src/systems/jumps/jumpsystem.jl | 2 ++ src/systems/optimization/constraints_system.jl | 2 ++ src/systems/optimization/optimizationsystem.jl | 2 ++ 5 files changed, 10 insertions(+) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index c9debc06c2..1acb19cf3d 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -676,6 +676,8 @@ function SymbolicUtils.maketerm(::Type{<:BasicSymbolic}, ::Initial, args, meta) return metadata(val, meta) end +supports_initialization(sys::AbstractSystem) = true + function add_initialization_parameters(sys::AbstractSystem) @assert !has_systems(sys) || isempty(get_systems(sys)) is_initializesystem(sys) && return sys diff --git a/src/systems/discrete_system/discrete_system.jl b/src/systems/discrete_system/discrete_system.jl index 952c8b71b0..f51ba638a4 100644 --- a/src/systems/discrete_system/discrete_system.jl +++ b/src/systems/discrete_system/discrete_system.jl @@ -421,3 +421,5 @@ end function DiscreteFunctionExpr(sys::DiscreteSystem, args...; kwargs...) DiscreteFunctionExpr{true}(sys, args...; kwargs...) end + +supports_initialization(::DiscreteSystem) = false diff --git a/src/systems/jumps/jumpsystem.jl b/src/systems/jumps/jumpsystem.jl index b95b035bf6..34fc26bd44 100644 --- a/src/systems/jumps/jumpsystem.jl +++ b/src/systems/jumps/jumpsystem.jl @@ -685,3 +685,5 @@ function (ratemap::JumpSysMajParamMapper{U, V, W})(maj::MassActionJump, newparam scale_rates && JumpProcesses.scalerates!(maj.scaled_rates, maj.reactant_stoch) nothing end + +supports_initialization(::JumpSystem) = false diff --git a/src/systems/optimization/constraints_system.jl b/src/systems/optimization/constraints_system.jl index 094640eb37..8a71c58043 100644 --- a/src/systems/optimization/constraints_system.jl +++ b/src/systems/optimization/constraints_system.jl @@ -246,3 +246,5 @@ function get_cmap(sys::ConstraintsSystem, exprs = nothing) cmap = map(x -> x ~ getdefault(x), cs) return cmap, cs end + +supports_initialization(::ConstraintsSystem) = false diff --git a/src/systems/optimization/optimizationsystem.jl b/src/systems/optimization/optimizationsystem.jl index 4d01bd31c9..391ac88e19 100644 --- a/src/systems/optimization/optimizationsystem.jl +++ b/src/systems/optimization/optimizationsystem.jl @@ -751,3 +751,5 @@ function structural_simplify(sys::OptimizationSystem; split = true, kwargs...) sys = complete(sys; split) return sys end + +supports_initialization(::OptimizationSystem) = false From 62b8c3cf3c9f55a0a676ef16cc35513c61d686ee Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 18 Mar 2025 21:23:34 +0530 Subject: [PATCH 1180/1337] fix: only add `Initial` parameters for systems that support initialization --- src/systems/abstractsystem.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index 1acb19cf3d..dd2a834e1c 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -680,6 +680,7 @@ supports_initialization(sys::AbstractSystem) = true function add_initialization_parameters(sys::AbstractSystem) @assert !has_systems(sys) || isempty(get_systems(sys)) + supports_initialization(sys) || return sys is_initializesystem(sys) && return sys all_initialvars = Set{BasicSymbolic}() From ca7b9c12c399e58da6c761ee0a58c4818f0bacb3 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 18 Mar 2025 21:23:48 +0530 Subject: [PATCH 1181/1337] fix: handle cyclic `u0map` in some cases --- src/systems/problem_utils.jl | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/systems/problem_utils.jl b/src/systems/problem_utils.jl index 902fb0f774..67096dda4e 100644 --- a/src/systems/problem_utils.jl +++ b/src/systems/problem_utils.jl @@ -623,10 +623,14 @@ function build_operating_point!(sys::AbstractSystem, end for k in keys(u0map) - u0map[k] = fixpoint_sub(u0map[k], neithermap) + v = fixpoint_sub(u0map[k], neithermap) + isequal(k, v) && continue + u0map[k] = v end for k in keys(pmap) - pmap[k] = fixpoint_sub(pmap[k], neithermap) + v = fixpoint_sub(pmap[k], neithermap) + isequal(k, v) && continue + pmap[k] = v end return op, missing_unknowns, missing_pars @@ -811,11 +815,11 @@ function process_SciMLProblem( else obs, _ = unhack_observed(observed(sys), Equation[x for x in eqs if x isa Equation]) end - is_time_dependent(sys) || add_observed_equations!(u0map, obs) op, missing_unknowns, missing_pars = build_operating_point!(sys, u0map, pmap, defs, cmap, dvs, ps) + add_observed_equations!(u0map, obs) if u0_constructor === identity && u0Type <: StaticArray u0_constructor = vals -> SymbolicUtils.Code.create_array( u0Type, eltype(vals), Val(1), Val(length(vals)), vals...) @@ -837,7 +841,7 @@ function process_SciMLProblem( op[iv] = t end - is_time_dependent(sys) && add_observed_equations!(op, obs) + add_observed_equations!(op, obs) add_parameter_dependencies!(sys, op) if warn_cyclic_dependency From bc0239cbfd92acae512944a6fd4181e5073c4ec9 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 18 Mar 2025 21:24:04 +0530 Subject: [PATCH 1182/1337] test: update NonlinearSystem tests with new `Initial` parameters --- test/nonlinearsystem.jl | 6 ++++-- test/parameter_dependencies.jl | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/test/nonlinearsystem.jl b/test/nonlinearsystem.jl index 420bdc088e..5cf3b4b1be 100644 --- a/test/nonlinearsystem.jl +++ b/test/nonlinearsystem.jl @@ -239,11 +239,13 @@ testdict = Dict([:test => 1]) prob_ = remake(prob, u0 = [1.0, 2.0, 3.0], p = [a => 1.1, b => 1.2, c => 1.3]) @test prob_.u0 == [1.0, 2.0, 3.0] - @test prob_.p == MTKParameters(sys, [a => 1.1, b => 1.2, c => 1.3]) + initials = unknowns(sys) .=> ones(3) + @test prob_.p == MTKParameters(sys, [a => 1.1, b => 1.2, c => 1.3, initials...]) prob_ = remake(prob, u0 = Dict(y => 2.0), p = Dict(a => 2.0)) @test prob_.u0 == [1.0, 2.0, 1.0] - @test prob_.p == MTKParameters(sys, [a => 2.0, b => 1.0, c => 1.0]) + initials = [x => 1.0, y => 2.0, z => 1.0] + @test prob_.p == MTKParameters(sys, [a => 2.0, b => 1.0, c => 1.0, initials...]) end @testset "Observed function generation without parameters" begin diff --git a/test/parameter_dependencies.jl b/test/parameter_dependencies.jl index f88f3f03b7..ad2131f458 100644 --- a/test/parameter_dependencies.jl +++ b/test/parameter_dependencies.jl @@ -326,7 +326,7 @@ end eqs = [0 ~ p1 * x * exp(x) + p2] @mtkbuild sys = NonlinearSystem(eqs; parameter_dependencies = [p2 => 2p1]) @test isequal(only(parameters(sys)), p1) - @test Set(full_parameters(sys)) == Set([p1, p2, Initial(p2)]) + @test Set(full_parameters(sys)) == Set([p1, p2, Initial(p2), Initial(x)]) prob = NonlinearProblem(sys, [x => 1.0]) @test prob.ps[p1] == 1.0 @test prob.ps[p2] == 2.0 From b5cfedb0bf8dfefda5d54f604c38e0364e660704 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 19 Mar 2025 22:56:36 +0530 Subject: [PATCH 1183/1337] test: make hack tests more consistent --- test/structural_transformation/utils.jl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/structural_transformation/utils.jl b/test/structural_transformation/utils.jl index 9bf239a574..6dfc107cc9 100644 --- a/test/structural_transformation/utils.jl +++ b/test/structural_transformation/utils.jl @@ -77,6 +77,7 @@ end @test length(equations(sys)) == 1 @test length(observed(sys)) == 3 prob = ODEProblem(sys, [x => 1.0], (0.0, 1.0), [foo => _tmp_fn2]) + val[] = 0 @test_nowarn prob.f(prob.u0, prob.p, 0.0) @test val[] == 1 @@ -97,6 +98,7 @@ end @test length(observed(sys)) == 2 prob = ODEProblem( sys, [y => ones(2), z => 2ones(2), x => 3.0], (0.0, 1.0), [foo => _tmp_fn2]) + val[] = 0 @test_nowarn prob.f(prob.u0, prob.p, 0.0) @test val[] == 2 From beb777a730b262cb59c205ee6758b1f140321e53 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 19 Mar 2025 22:56:59 +0530 Subject: [PATCH 1184/1337] fix: fix duplicated code running unconditionally --- src/systems/problem_utils.jl | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/systems/problem_utils.jl b/src/systems/problem_utils.jl index 67096dda4e..1d0c532cc5 100644 --- a/src/systems/problem_utils.jl +++ b/src/systems/problem_utils.jl @@ -706,10 +706,6 @@ function maybe_build_initialization_problem( empty!(missing_unknowns) end - for v in missing_unknowns - op[v] = zero_var(v) - end - empty!(missing_unknowns) return (; initialization_data = SciMLBase.OverrideInitData( initializeprob, update_initializeprob!, initializeprobmap, From 6dc358683c4f1934fcc846effd29926dc388a65d Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 19 Mar 2025 22:57:20 +0530 Subject: [PATCH 1185/1337] fix: only add observed equations to `u0map` for time-independent or initialization systems --- src/systems/problem_utils.jl | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/systems/problem_utils.jl b/src/systems/problem_utils.jl index 1d0c532cc5..d0ae0d174f 100644 --- a/src/systems/problem_utils.jl +++ b/src/systems/problem_utils.jl @@ -815,7 +815,9 @@ function process_SciMLProblem( op, missing_unknowns, missing_pars = build_operating_point!(sys, u0map, pmap, defs, cmap, dvs, ps) - add_observed_equations!(u0map, obs) + if !is_time_dependent(sys) || is_initializesystem(sys) + add_observed_equations!(u0map, obs) + end if u0_constructor === identity && u0Type <: StaticArray u0_constructor = vals -> SymbolicUtils.Code.create_array( u0Type, eltype(vals), Val(1), Val(length(vals)), vals...) From 9335a5ba8e18a73e1036900e9b7931699f65e6c7 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 19 Mar 2025 22:57:34 +0530 Subject: [PATCH 1186/1337] fix: fix `modelingtoolkitize(::NonlinearProblem)` --- src/systems/nonlinear/modelingtoolkitize.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/systems/nonlinear/modelingtoolkitize.jl b/src/systems/nonlinear/modelingtoolkitize.jl index ec35318e3e..2f12157884 100644 --- a/src/systems/nonlinear/modelingtoolkitize.jl +++ b/src/systems/nonlinear/modelingtoolkitize.jl @@ -45,7 +45,7 @@ function modelingtoolkitize( eqs = vcat([0.0 ~ rhs[i] for i in 1:length(prob.f.resid_prototype)]...) else rhs = ArrayInterface.restructure(prob.u0, similar(vars, Num)) - prob.f(rhs, vars, p isa MTKParameters ? (params,) : params) + prob.f(rhs, vars, params) eqs = vcat([0.0 ~ rhs[i] for i in 1:length(rhs)]...) end From f276d4c19a33b9cee811c5a9f7285803dbf73379 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 19 Mar 2025 22:57:47 +0530 Subject: [PATCH 1187/1337] fix: add `add_additional_parameters` kwarg to `complete` --- src/systems/abstractsystem.jl | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index dd2a834e1c..7675eef73b 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -741,7 +741,8 @@ the global structure of the system. One property to note is that if a system is complete, the system will no longer namespace its subsystems or variables, i.e. `isequal(complete(sys).v.i, v.i)`. """ -function complete(sys::AbstractSystem; split = true, flatten = true) +function complete( + sys::AbstractSystem; split = true, flatten = true, add_initial_parameters = true) newunknowns = OrderedSet() newparams = OrderedSet() iv = has_iv(sys) ? get_iv(sys) : nothing @@ -762,7 +763,9 @@ function complete(sys::AbstractSystem; split = true, flatten = true) @set! newsys.parent = complete(sys; split = false, flatten = false) end sys = newsys - sys = add_initialization_parameters(sys) + if add_initial_parameters + sys = add_initialization_parameters(sys) + end end if split && has_index_cache(sys) @set! sys.index_cache = IndexCache(sys) From ec2fbb4a8d4f3ba6ccd49327731de5329a77c3eb Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 19 Mar 2025 22:58:13 +0530 Subject: [PATCH 1188/1337] fix: don't add initial parameters in `ODEProblem(::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 34fc26bd44..60637ed6d1 100644 --- a/src/systems/jumps/jumpsystem.jl +++ b/src/systems/jumps/jumpsystem.jl @@ -507,7 +507,7 @@ function DiffEqBase.ODEProblem(sys::JumpSystem, u0map, tspan::Union{Tuple, Nothi systems = get_systems(sys), defaults = defaults(sys), guesses = guesses(sys), parameter_dependencies = parameter_dependencies(sys), metadata = get_metadata(sys), gui_metadata = get_gui_metadata(sys)) - osys = complete(osys) + osys = complete(osys; add_initial_parameters = false) return ODEProblem(osys, u0map, tspan, parammap; check_length = false, build_initializeprob = false, kwargs...) else From 77e4e704f825d43a4a52619df50b75a66673fe06 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 20 Mar 2025 12:29:26 +0530 Subject: [PATCH 1189/1337] fix: fix incorrect folding in `substitute` in `OptimizationSystem` --- .../optimization/optimizationsystem.jl | 6 ++--- test/optimizationsystem.jl | 23 ++++++++++++++++++- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/src/systems/optimization/optimizationsystem.jl b/src/systems/optimization/optimizationsystem.jl index 4d01bd31c9..a62580024e 100644 --- a/src/systems/optimization/optimizationsystem.jl +++ b/src/systems/optimization/optimizationsystem.jl @@ -114,8 +114,8 @@ function OptimizationSystem(op, unknowns, ps; unknowns′[i] = irrvar end end - op′ = substitute(op′, irreducible_subs) - constraints = substitute.(constraints, (irreducible_subs,)) + op′ = fast_substitute(op′, irreducible_subs) + constraints = fast_substitute.(constraints, (irreducible_subs,)) if !(isempty(default_u0) && isempty(default_p)) Base.depwarn( @@ -127,7 +127,7 @@ function OptimizationSystem(op, unknowns, ps; throw(ArgumentError("System names must be unique.")) end defaults = todict(defaults) - defaults = Dict(substitute(value(k), irreducible_subs) => substitute( + defaults = Dict(fast_substitute(value(k), irreducible_subs) => fast_substitute( value(v), irreducible_subs) for (k, v) in pairs(defaults) if value(v) !== nothing) diff --git a/test/optimizationsystem.jl b/test/optimizationsystem.jl index c20613441a..2ec9516721 100644 --- a/test/optimizationsystem.jl +++ b/test/optimizationsystem.jl @@ -1,5 +1,6 @@ using ModelingToolkit, SparseArrays, Test, Optimization, OptimizationOptimJL, - OptimizationMOI, Ipopt, AmplNLWriter, Ipopt_jll, SymbolicIndexingInterface + OptimizationMOI, Ipopt, AmplNLWriter, Ipopt_jll, SymbolicIndexingInterface, + LinearAlgebra using ModelingToolkit: get_metadata @testset "basic" begin @@ -388,3 +389,23 @@ end @test all(y -> any(x -> isequal(x, y), unknowns(sys2)), [x2, sys1.x1]) @test all(y -> any(x -> isequal(x, y), parameters(sys2)), [p2, sys1.p1]) end + +function myeigvals_1(A::AbstractMatrix) + eigvals(A)[1] +end + +@register_symbolic myeigvals_1(A::AbstractMatrix) + +@testset "Issue#3473: Registered array function in objective, no irreducible variables" begin + p_free = @variables begin + p1, [bounds = (0, 1)] + p2, [bounds = (0, 1)] + p3, [bounds = (0, 1)] + p4, [bounds = (0, 1)] + end + + m = diagm(p_free) + + obj = myeigvals_1(m) + @test_nowarn OptimizationSystem(obj, p_free, []; name = :osys) +end From dab80d944815f0c057d22fb2f0571563f437a0c2 Mon Sep 17 00:00:00 2001 From: Christopher Rackauckas Date: Thu, 20 Mar 2025 06:48:23 -0100 Subject: [PATCH 1190/1337] Update Debugging.md --- docs/src/basics/Debugging.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/basics/Debugging.md b/docs/src/basics/Debugging.md index b432810bd0..6e2d471461 100644 --- a/docs/src/basics/Debugging.md +++ b/docs/src/basics/Debugging.md @@ -65,7 +65,7 @@ logging in a system returned from `debug_system`, use `ModelingToolkit.ASSERTION ```@repl debug dprob[ModelingToolkit.ASSERTION_LOG_VARIABLE] = false; -solve(drob, Tsit5()); +solve(dprob, Tsit5()); ``` ```@docs From 0b8f4d5eae9987bf7541cfe791adbddec749250f Mon Sep 17 00:00:00 2001 From: Christopher Rackauckas Date: Thu, 20 Mar 2025 07:30:20 -0100 Subject: [PATCH 1191/1337] Update Project.toml --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 2b204c5ed7..cd3acc5279 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "ModelingToolkit" uuid = "961ee093-0014-501f-94e3-6117800e7a78" authors = ["Yingbo Ma ", "Chris Rackauckas and contributors"] -version = "9.66.0" +version = "9.67.0" [deps] AbstractTrees = "1520ce14-60c1-5f80-bbc7-55ef81b5835c" From a2d2855c095f9ea1073795d193617295c0ecd380 Mon Sep 17 00:00:00 2001 From: Christopher Rackauckas Date: Thu, 20 Mar 2025 07:53:02 -0100 Subject: [PATCH 1192/1337] Remove undefined exports --- src/ModelingToolkit.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ModelingToolkit.jl b/src/ModelingToolkit.jl index e671b869ae..55dbf69ab9 100644 --- a/src/ModelingToolkit.jl +++ b/src/ModelingToolkit.jl @@ -331,7 +331,7 @@ export debug_system #export ContinuousClock, Discrete, sampletime, input_timedomain, output_timedomain #export has_discrete_domain, has_continuous_domain #export is_discrete_domain, is_continuous_domain, is_hybrid_domain -export Sample, Hold, Shift, ShiftIndex, sampletime, SampleTime, Next, Prev +export Sample, Hold, Shift, ShiftIndex, sampletime, SampleTime export Clock, SolverStepClock, TimeDomain export MTKParameters, reorder_dimension_by_tunables!, reorder_dimension_by_tunables From 9a0dd4b0e86492ed26978598cb7bc124153c222f Mon Sep 17 00:00:00 2001 From: Christopher Rackauckas Date: Thu, 20 Mar 2025 08:20:59 -0100 Subject: [PATCH 1193/1337] Update Project.toml --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 9129523f5e..f4fb6a342c 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "ModelingToolkit" uuid = "961ee093-0014-501f-94e3-6117800e7a78" authors = ["Yingbo Ma ", "Chris Rackauckas and contributors"] -version = "9.67.0" +version = "9.68.0" [deps] AbstractTrees = "1520ce14-60c1-5f80-bbc7-55ef81b5835c" From 52bb5b47cbad0897e418daa29722a830f05542d1 Mon Sep 17 00:00:00 2001 From: Christopher Rackauckas Date: Thu, 20 Mar 2025 09:09:01 -0100 Subject: [PATCH 1194/1337] Update Downstream.yml --- .github/workflows/Downstream.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/Downstream.yml b/.github/workflows/Downstream.yml index 5026f798a3..1d1e4ce34e 100644 --- a/.github/workflows/Downstream.yml +++ b/.github/workflows/Downstream.yml @@ -31,7 +31,7 @@ jobs: - {user: SciML, repo: SBMLToolkit.jl, group: All} - {user: SciML, repo: NeuralPDE.jl, group: NNPDE} - {user: SciML, repo: DataDrivenDiffEq.jl, group: Downstream} - - {user: SciML, repo: StructuralIdentifiability.jl, group: All} + - {user: SciML, repo: StructuralIdentifiability.jl, group: Core} - {user: SciML, repo: ModelingToolkitStandardLibrary.jl, group: Core} - {user: SciML, repo: ModelOrderReduction.jl, group: All} - {user: SciML, repo: MethodOfLines.jl, group: Interface} From 8f6653be4bd40e9fe796df7e3f593df062f5cbe0 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 20 Mar 2025 18:28:21 +0530 Subject: [PATCH 1195/1337] test: make model parsing tests more generic --- test/model_parsing.jl | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/test/model_parsing.jl b/test/model_parsing.jl index 559992d7c5..62c19d2055 100644 --- a/test/model_parsing.jl +++ b/test/model_parsing.jl @@ -3,6 +3,7 @@ using ModelingToolkit: get_connector_type, get_defaults, get_gui_metadata, get_systems, get_ps, getdefault, getname, readable_code, scalarize, symtype, VariableDescription, RegularConnector, get_unit +using SymbolicIndexingInterface using URIs: URI using Distributions using DynamicQuantities, OrdinaryDiffEq @@ -819,15 +820,15 @@ end @named guess_model = GuessModel() j_guess = getguess(guess_model.j) - @test typeof(j_guess) == Num + @test symbolic_type(j_guess) == ScalarSymbolic() @test readable_code(j_guess) == "l(t) / i(t) + k(t)" i_guess = getguess(guess_model.i) - @test typeof(i_guess) == Num + @test symbolic_type(i_guess) == ScalarSymbolic() @test readable_code(i_guess) == "k(t)" l_guess = getguess(guess_model.l) - @test typeof(l_guess) == Num + @test symbolic_type(l_guess) == ScalarSymbolic() @test readable_code(l_guess) == "k(t)" end From 3f7397ba649c3bc4ccfea37d96de2141ef543bd5 Mon Sep 17 00:00:00 2001 From: Herman Sletmoen Date: Thu, 20 Mar 2025 21:54:07 +0100 Subject: [PATCH 1196/1337] Format tutorial --- .../tutorials/change_independent_variable.md | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/docs/src/tutorials/change_independent_variable.md b/docs/src/tutorials/change_independent_variable.md index 4b7ba037fe..d55639a669 100644 --- a/docs/src/tutorials/change_independent_variable.md +++ b/docs/src/tutorials/change_independent_variable.md @@ -4,9 +4,10 @@ Ordinary differential equations describe the rate of change of some dependent va For the modeler it is often most natural to write down the equations with a particular independent variable, say time $t$. However, in many cases there are good reasons for changing the independent variable: - 1. One may want $y(x)$ as a function of $x$ instead of $(x(t), y(t))$ as a function of $t$ - 2. Some differential equations vary more nicely (e.g. less stiff) with respect to one independent variable than another. - 3. It can reduce the number of equations that must be solved (e.g. $y(x)$ is one equation, while $(x(t), y(t))$ are two). + 1. One may want $y(x)$ as a function of $x$ instead of $(x(t), y(t))$ as a function of $t$ + + 2. Some differential equations vary more nicely (e.g. less stiff) with respect to one independent variable than another. + 3. It can reduce the number of equations that must be solved (e.g. $y(x)$ is one equation, while $(x(t), y(t))$ are two). To manually change the independent variable of an ODE, one must rewrite all equations in terms of a new variable and transform differentials with the chain rule. This is mechanical and error-prone. @@ -34,9 +35,9 @@ It expresses the position $(x(t), y(t))$ as a function of time $t$. But suppose we want to determine whether the projectile hits a target 10 meters away. There are at least three ways of answering this: - - Solve the ODE for $(x(t), y(t))$ and use a callback to terminate when $x$ reaches 10 meters, and evaluate $y$ at the final time. - - Solve the ODE for $(x(t), y(t))$ and use root finding to find the time when $x$ reaches 10 meters, and evaluate $y$ at that time. - - Solve the ODE for $y(x)$ and evaluate it at 10 meters. + - Solve the ODE for $(x(t), y(t))$ and use a callback to terminate when $x$ reaches 10 meters, and evaluate $y$ at the final time. + - Solve the ODE for $(x(t), y(t))$ and use root finding to find the time when $x$ reaches 10 meters, and evaluate $y$ at that time. + - Solve the ODE for $y(x)$ and evaluate it at 10 meters. We will demonstrate the last method by changing the independent variable from $t$ to $x$. This transformation is well-defined for any non-zero horizontal velocity $v$, so $x$ and $t$ are one-to-one. @@ -55,7 +56,7 @@ M2s # display this # hide The derivatives are now with respect to the new independent variable $x$, which can be accessed with `M2.x`. -!!! warn +!!! info At this point `x`, `M1.x`, `M1s.x`, `M2.x`, `M2s.x` are *three* different variables. Meanwhile `y`, `M1.y`, `M1s.y`, `M2.y` and `M2s.y` are *four* different variables. @@ -66,7 +67,7 @@ It is straightforward to evolve the ODE for 10 meters and plot the resulting tra ```@example changeivar using OrdinaryDiffEq, Plots -prob = ODEProblem(M2s, [M2s.y => 0.0], [0.0, 10.0], [v => 8.0]) # throw 10 meters with x-velocity 8 m/s +prob = ODEProblem(M2s, [M2s.y => 0.0], [0.0, 10.0], [v => 8.0]) # throw 10 meters sol = solve(prob, Tsit5()) plot(sol; idxs = M2.y) # must index by M2.y = y(x); not M1.y = y(t)! ``` From 1c003264bc60872d4b8726b94d03a62a43d1e8c4 Mon Sep 17 00:00:00 2001 From: Christopher Rackauckas Date: Fri, 21 Mar 2025 09:58:16 -0100 Subject: [PATCH 1197/1337] Update comparison.md Fixes https://github.com/SciML/ModelingToolkit.jl/issues/3472 --- docs/src/comparison.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/src/comparison.md b/docs/src/comparison.md index fcf0c70afb..52d5ab2f70 100644 --- a/docs/src/comparison.md +++ b/docs/src/comparison.md @@ -23,9 +23,9 @@ - Modelica is an object-oriented single dispatch language. ModelingToolkit.jl, built on Julia, uses multiple dispatch extensively to simplify code. - Many Modelica compilers supply a GUI. ModelingToolkit.jl does not. - - Modelica can be used to simulate ODE and DAE systems. ModelingToolkit.jl - has a much more expansive set of system types, including nonlinear systems, - SDEs, PDEs, and more. + - Modelica is designed for simulating ODE and DAE systems (which can include nonlinear dynamics). + In contrast, ModelingToolkit.jl supports a much broader range of system types, including SDEs, + PDEs, time-independent nonlinear systems (e.g. various forms of optimization problems) and more. ## Comparison Against Simulink From 90a8dd6ddfbc3faad04e892d46e5c23096222fa8 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Sat, 22 Mar 2025 14:22:46 +0530 Subject: [PATCH 1198/1337] fix: fix jump system hack --- src/systems/jumps/jumpsystem.jl | 25 +++++++------------------ 1 file changed, 7 insertions(+), 18 deletions(-) diff --git a/src/systems/jumps/jumpsystem.jl b/src/systems/jumps/jumpsystem.jl index 60637ed6d1..1ad6d30f59 100644 --- a/src/systems/jumps/jumpsystem.jl +++ b/src/systems/jumps/jumpsystem.jl @@ -4,25 +4,14 @@ const JumpType = Union{VariableRateJump, ConstantRateJump, MassActionJump} # call reset_aggregated_jumps!(integrator). # assumes iip function _reset_aggregator!(expr, integrator) - if expr isa Symbol - error("Error, encountered a symbol. This should not happen.") + @assert Meta.isexpr(expr, :function) + body = expr.args[end] + body = quote + $body + $reset_aggregated_jumps!($integrator) end - if expr isa LineNumberNode - return - end - - if (expr.head == :function) - _reset_aggregator!(expr.args[end], integrator) - else - if expr.args[end] == :nothing - expr.args[end] = :(reset_aggregated_jumps!($integrator)) - push!(expr.args, :nothing) - else - _reset_aggregator!(expr.args[end], integrator) - end - end - - nothing + expr.args[end] = body + return nothing end """ From ac51f7396ccdd47b56164d7434e3da51be2e2487 Mon Sep 17 00:00:00 2001 From: CompatHelper Julia Date: Sun, 23 Mar 2025 00:46:24 +0000 Subject: [PATCH 1199/1337] CompatHelper: bump compat for DataInterpolations to 8 for package docs, (keep existing compat) --- docs/Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Project.toml b/docs/Project.toml index cedbcc9061..e1df906ca5 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -29,7 +29,7 @@ Unitful = "1986cc42-f94f-5a68-af5c-568840ba703d" [compat] BenchmarkTools = "1.3" BifurcationKit = "0.4" -DataInterpolations = "6.5" +DataInterpolations = "6.5, 8" Distributions = "0.25" Documenter = "1" DynamicQuantities = "^0.11.2, 0.12, 1" From f2d00b01daab6fc299ad7a498d37b418f8c928f4 Mon Sep 17 00:00:00 2001 From: Christopher Rackauckas Date: Sun, 23 Mar 2025 10:34:35 -0100 Subject: [PATCH 1200/1337] Update Project.toml --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index f4fb6a342c..404efdbbe3 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "ModelingToolkit" uuid = "961ee093-0014-501f-94e3-6117800e7a78" authors = ["Yingbo Ma ", "Chris Rackauckas and contributors"] -version = "9.68.0" +version = "9.68.1" [deps] AbstractTrees = "1520ce14-60c1-5f80-bbc7-55ef81b5835c" From 247160d89fd9fe560cf15179f96f1351d39c5582 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 20 Mar 2025 16:09:35 +0530 Subject: [PATCH 1201/1337] feat: use DI to calculate jacobians in `LinearizationFunction` --- Project.toml | 6 ++- src/ModelingToolkit.jl | 3 ++ src/linearization.jl | 118 ++++++++++++++++++++++++++++++++++------- 3 files changed, 107 insertions(+), 20 deletions(-) diff --git a/Project.toml b/Project.toml index 404efdbbe3..216ef0c73a 100644 --- a/Project.toml +++ b/Project.toml @@ -4,6 +4,7 @@ authors = ["Yingbo Ma ", "Chris Rackauckas h(xz, p, t) - end, u) - pf = SciMLBase.ParamJacobianWrapper(fun, t, u) - fg_u = jacobian_wrt_vars(pf, p, linfun.input_idxs, linfun.chunk) + fg_xz = linfun.uf_jac(u, DI.Constant(p), DI.Constant(t)) + h_xz = linfun.h_jac(u, DI.Constant(p), DI.Constant(t)) + fg_u = linfun.pf_jac([p[idx] for idx in linfun.input_idxs], DI.Constant(u), DI.Constant(p), DI.Constant(t)) else linfun.num_states == 0 || error("Number of unknown variables (0) does not match the number of input unknowns ($(length(u)))") fg_xz = zeros(0, 0) h_xz = fg_u = zeros(0, length(linfun.input_idxs)) end - hp = let u = u, t = t, h = linfun.h - _hp(p) = h(u, p, t) - _hp - end - h_u = jacobian_wrt_vars(hp, p, linfun.input_idxs, linfun.chunk) + h_u = linfun.hp_jac([p[idx] for idx in linfun.input_idxs], DI.Constant(u), DI.Constant(p), DI.Constant(t)) (f_x = fg_xz[linfun.diff_idxs, linfun.diff_idxs], f_z = fg_xz[linfun.diff_idxs, linfun.alge_idxs], g_x = fg_xz[linfun.alge_idxs, linfun.diff_idxs], From da84f0cd133c0c93749d1955b1fd178f43f1e7c4 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 20 Mar 2025 16:09:45 +0530 Subject: [PATCH 1202/1337] test: test alternative AD backends for linearization --- test/downstream/linearize.jl | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/test/downstream/linearize.jl b/test/downstream/linearize.jl index 0336709173..9918f8145d 100644 --- a/test/downstream/linearize.jl +++ b/test/downstream/linearize.jl @@ -1,4 +1,4 @@ -using ModelingToolkit, Test +using ModelingToolkit, ADTypes, Test using CommonSolve: solve # r is an input, and y is an output. @@ -17,11 +17,12 @@ eqs = [u ~ kp * (r - y) lsys, ssys = linearize(sys, [r], [y]) lprob = LinearizationProblem(sys, [r], [y]) lsys2 = solve(lprob) +lsys3, _ = linearize(sys, [r], [y]; autodiff = AutoFiniteDiff()) -@test lsys.A[] == lsys2.A[] == -2 -@test lsys.B[] == lsys2.B[] == 1 -@test lsys.C[] == lsys2.C[] == 1 -@test lsys.D[] == lsys2.D[] == 0 +@test lsys.A[] == lsys2.A[] == lsys3.A[] == -2 +@test lsys.B[] == lsys2.B[] == lsys3.B[] == 1 +@test lsys.C[] == lsys2.C[] == lsys3.C[] == 1 +@test lsys.D[] == lsys2.D[] == lsys3.D[] == 0 lsys, ssys = linearize(sys, [r], [r]) @@ -89,11 +90,13 @@ connections = [f.y ~ c.r # filtered reference to controller reference lsys0, ssys = linearize(cl, [f.u], [p.x]) desired_order = [f.x, p.x] lsys = ModelingToolkit.reorder_unknowns(lsys0, unknowns(ssys), desired_order) +lsys1, ssys = linearize(cl, [f.u], [p.x]; autodiff = AutoFiniteDiff()) +lsys2 = ModelingToolkit.reorder_unknowns(lsys1, unknowns(ssys), desired_order) -@test lsys.A == [-2 0; 1 -2] -@test lsys.B == reshape([1, 0], 2, 1) -@test lsys.C == [0 1] -@test lsys.D[] == 0 +@test lsys.A == lsys2.A == [-2 0; 1 -2] +@test lsys.B == lsys2.B == reshape([1, 0], 2, 1) +@test lsys.C == lsys2.C == [0 1] +@test lsys.D[] == lsys2.D[] == 0 ## Symbolic linearization lsyss, _ = ModelingToolkit.linearize_symbolic(cl, [f.u], [p.x]) From f9b0fd3e42b92f3fe7db1ac96dae1729b9422a2f Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 20 Mar 2025 16:09:52 +0530 Subject: [PATCH 1203/1337] refactor: remove `jacobian_wrt_vars` --- src/systems/parameter_buffer.jl | 29 ----------------------------- src/utils.jl | 21 --------------------- 2 files changed, 50 deletions(-) diff --git a/src/systems/parameter_buffer.jl b/src/systems/parameter_buffer.jl index 4f1bd1a234..a8ebeeb1e4 100644 --- a/src/systems/parameter_buffer.jl +++ b/src/systems/parameter_buffer.jl @@ -731,35 +731,6 @@ function Base.:(==)(a::MTKParameters, b::MTKParameters) end) end -# to support linearize/linearization_function -function jacobian_wrt_vars(pf::F, p::MTKParameters, input_idxs, chunk::C) where {F, C} - tunable, _, _ = SciMLStructures.canonicalize(SciMLStructures.Tunable(), p) - T = eltype(tunable) - tag = ForwardDiff.Tag(pf, T) - dualtype = ForwardDiff.Dual{typeof(tag), T, ForwardDiff.chunksize(chunk)} - p_big = SciMLStructures.replace(SciMLStructures.Tunable(), p, dualtype.(tunable)) - p_closure = let pf = pf, - input_idxs = input_idxs, - p_big = p_big - - function (p_small_inner) - for (i, val) in zip(input_idxs, p_small_inner) - set_parameter!(p_big, val, i) - end - return if pf isa SciMLBase.ParamJacobianWrapper - buffer = Array{dualtype}(undef, size(pf.u)) - pf(buffer, p_big) - buffer - else - pf(p_big) - end - end - end - p_small = parameter_values.((p,), input_idxs) - cfg = ForwardDiff.JacobianConfig(p_closure, p_small, chunk, tag) - ForwardDiff.jacobian(p_closure, p_small, cfg, Val(false)) -end - const MISSING_PARAMETERS_MESSAGE = """ Some parameters are missing from the variable map. Please provide a value or default for the following variables: diff --git a/src/utils.jl b/src/utils.jl index 78c665ffca..55f3d2cec5 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -963,27 +963,6 @@ function Base.iterate(it::StatefulBFS, queue = (eltype(it)[(0, it.t)])) return (lv, t), queue end -function jacobian_wrt_vars(pf::F, p, input_idxs, chunk::C) where {F, C} - E = eltype(p) - tag = ForwardDiff.Tag(pf, E) - T = typeof(tag) - dualtype = ForwardDiff.Dual{T, E, ForwardDiff.chunksize(chunk)} - p_big = similar(p, dualtype) - copyto!(p_big, p) - p_closure = let pf = pf, - input_idxs = input_idxs, - p_big = p_big - - function (p_small_inner) - p_big[input_idxs] .= p_small_inner - pf(p_big) - end - end - p_small = p[input_idxs] - cfg = ForwardDiff.JacobianConfig(p_closure, p_small, chunk, tag) - ForwardDiff.jacobian(p_closure, p_small, cfg, Val(false)) -end - function fold_constants(ex) if iscall(ex) maketerm(typeof(ex), operation(ex), map(fold_constants, arguments(ex)), From c1430957df0b7421680682439ac50e4f61730c8a Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 20 Mar 2025 17:07:30 +0530 Subject: [PATCH 1204/1337] Update src/linearization.jl Co-authored-by: Fredrik Bagge Carlson --- src/linearization.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/linearization.jl b/src/linearization.jl index eb6de1e899..1b8f03596a 100644 --- a/src/linearization.jl +++ b/src/linearization.jl @@ -25,7 +25,7 @@ The `simplified_sys` has undergone [`structural_simplify`](@ref) and had any occ - `simplify`: Apply simplification in tearing. - `initialize`: If true, a check is performed to ensure that the operating point is consistent (satisfies algebraic equations). If the op is not consistent, initialization is performed. - `initialization_solver_alg`: A NonlinearSolve algorithm to use for solving for a feasible set of state and algebraic variables that satisfies the specified operating point. - - `autodiff`: An `ADType` supported by DifferentiationInterface.jl to use for calculating the necessary jacobians. + - `autodiff`: An `ADType` supported by DifferentiationInterface.jl to use for calculating the necessary jacobians. Defaults to using `AutoForwardDiff()` - `kwargs`: Are passed on to `find_solvables!` See also [`linearize`](@ref) which provides a higher-level interface. From 5c5a6769083cf3422f79bfb17217bbec43c8a192 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 20 Mar 2025 18:38:15 +0530 Subject: [PATCH 1205/1337] fix: handle case when `prob.u0 === nothing` in `linearization_function` --- src/linearization.jl | 59 ++++++++++++++++++++++++++++---------------- 1 file changed, 38 insertions(+), 21 deletions(-) diff --git a/src/linearization.jl b/src/linearization.jl index 1b8f03596a..2da4fc5145 100644 --- a/src/linearization.jl +++ b/src/linearization.jl @@ -89,21 +89,6 @@ function linearization_function(sys::AbstractSystem, inputs, t0 = current_time(prob) inputvals = [p[idx] for idx in input_idxs] - uf_fun = let fun = prob.f - function uff(du, u, p, t) - SciMLBase.UJacobianWrapper(fun, t, p)(du, u) - end - end - uf_jac = PreparedJacobian{true}(uf_fun, similar(prob.u0), autodiff, prob.u0, DI.Constant(p), DI.Constant(t0)) - # observed function is a `GeneratedFunctionWrapper` with iip component - h_jac = PreparedJacobian{true}(h, similar(prob.u0, size(outputs)), autodiff, prob.u0, DI.Constant(p), DI.Constant(t0)) - pf_fun = let fun = prob.f, setter = setp_oop(sys, input_idxs) - function pff(du, input, u, p, t) - p = setter(p, input) - SciMLBase.ParamJacobianWrapper(fun, t, u)(du, p) - end - end - pf_jac = PreparedJacobian{true}(pf_fun, similar(prob.u0), autodiff, inputvals, DI.Constant(prob.u0), DI.Constant(p), DI.Constant(t0)) hp_fun = let fun = h, setter = setp_oop(sys, input_idxs) function hpf(du, input, u, p, t) p = setter(p, input) @@ -111,8 +96,36 @@ function linearization_function(sys::AbstractSystem, inputs, return du end end - hp_jac = PreparedJacobian{true}(hp_fun, similar(prob.u0, size(outputs)), autodiff, inputvals, DI.Constant(prob.u0), DI.Constant(p), DI.Constant(t0)) - + if u0 === nothing + uf_jac = h_jac = pf_jac = nothing + T = p isa MTKParameters ? eltype(p.tunable) : eltype(p) + hp_jac = PreparedJacobian{true}( + hp_fun, zeros(T, size(outputs)), autodiff, inputvals, + DI.Constant(prob.u0), DI.Constant(p), DI.Constant(t0)) + else + uf_fun = let fun = prob.f + function uff(du, u, p, t) + SciMLBase.UJacobianWrapper(fun, t, p)(du, u) + end + end + uf_jac = PreparedJacobian{true}( + uf_fun, similar(prob.u0), autodiff, prob.u0, DI.Constant(p), DI.Constant(t0)) + # observed function is a `GeneratedFunctionWrapper` with iip component + h_jac = PreparedJacobian{true}(h, similar(prob.u0, size(outputs)), autodiff, + prob.u0, DI.Constant(p), DI.Constant(t0)) + pf_fun = let fun = prob.f, setter = setp_oop(ssimilarys, input_idxs) + function pff(du, input, u, p, t) + p = setter(p, input) + SciMLBase.ParamJacobianWrapper(fun, t, u)(du, p) + end + end + pf_jac = PreparedJacobian{true}(pf_fun, similar(prob.u0), autodiff, inputvals, + DI.Constant(prob.u0), DI.Constant(p), DI.Constant(t0)) + hp_jac = PreparedJacobian{true}( + hp_fun, similar(prob.u0, size(outputs)), autodiff, inputvals, + DI.Constant(prob.u0), DI.Constant(p), DI.Constant(t0)) + end + lin_fun = LinearizationFunction( diff_idxs, alge_idxs, input_idxs, length(unknowns(sys)), prob, h, u0 === nothing ? nothing : similar(u0), uf_jac, h_jac, pf_jac, @@ -151,12 +164,14 @@ end function PreparedJacobian{true}(f, buf, autodiff, args...) prep = DI.prepare_jacobian(f, buf, autodiff, args...) - return PreparedJacobian{true, typeof(prep), typeof(f), typeof(buf), typeof(autodiff)}(prep, f, buf, autodiff) + return PreparedJacobian{true, typeof(prep), typeof(f), typeof(buf), typeof(autodiff)}( + prep, f, buf, autodiff) end function PreparedJacobian{false}(f, autodiff, args...) prep = DI.prepare_jacobian(f, autodiff, args...) - return PreparedJacobian{true, typeof(prep), typeof(f), Nothing, typeof(autodiff)}(prep, f, nothing) + return PreparedJacobian{true, typeof(prep), typeof(f), Nothing, typeof(autodiff)}( + prep, f, nothing) end function (pj::PreparedJacobian{true})(args...) @@ -279,14 +294,16 @@ function (linfun::LinearizationFunction)(u, p, t) end fg_xz = linfun.uf_jac(u, DI.Constant(p), DI.Constant(t)) h_xz = linfun.h_jac(u, DI.Constant(p), DI.Constant(t)) - fg_u = linfun.pf_jac([p[idx] for idx in linfun.input_idxs], DI.Constant(u), DI.Constant(p), DI.Constant(t)) + fg_u = linfun.pf_jac([p[idx] for idx in linfun.input_idxs], + DI.Constant(u), DI.Constant(p), DI.Constant(t)) else linfun.num_states == 0 || error("Number of unknown variables (0) does not match the number of input unknowns ($(length(u)))") fg_xz = zeros(0, 0) h_xz = fg_u = zeros(0, length(linfun.input_idxs)) end - h_u = linfun.hp_jac([p[idx] for idx in linfun.input_idxs], DI.Constant(u), DI.Constant(p), DI.Constant(t)) + h_u = linfun.hp_jac([p[idx] for idx in linfun.input_idxs], + DI.Constant(u), DI.Constant(p), DI.Constant(t)) (f_x = fg_xz[linfun.diff_idxs, linfun.diff_idxs], f_z = fg_xz[linfun.diff_idxs, linfun.alge_idxs], g_x = fg_xz[linfun.alge_idxs, linfun.diff_idxs], From d448fc50a0b2fbd5b698235366a8a00b21c80d55 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 21 Mar 2025 11:35:52 +0530 Subject: [PATCH 1206/1337] fix: fix typo --- src/linearization.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/linearization.jl b/src/linearization.jl index 2da4fc5145..57b171e874 100644 --- a/src/linearization.jl +++ b/src/linearization.jl @@ -113,7 +113,7 @@ function linearization_function(sys::AbstractSystem, inputs, # observed function is a `GeneratedFunctionWrapper` with iip component h_jac = PreparedJacobian{true}(h, similar(prob.u0, size(outputs)), autodiff, prob.u0, DI.Constant(p), DI.Constant(t0)) - pf_fun = let fun = prob.f, setter = setp_oop(ssimilarys, input_idxs) + pf_fun = let fun = prob.f, setter = setp_oop(sys, input_idxs) function pff(du, input, u, p, t) p = setter(p, input) SciMLBase.ParamJacobianWrapper(fun, t, u)(du, p) From 1dedcdbc4375406c4d0a8b8e5bb47b646efd2447 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 24 Mar 2025 11:48:01 +0530 Subject: [PATCH 1207/1337] fix: fix callable parameters in `remake_buffer` --- src/systems/parameter_buffer.jl | 3 +++ test/initial_values.jl | 17 +++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/src/systems/parameter_buffer.jl b/src/systems/parameter_buffer.jl index 4f1bd1a234..23fb2e11a7 100644 --- a/src/systems/parameter_buffer.jl +++ b/src/systems/parameter_buffer.jl @@ -444,6 +444,9 @@ end function validate_parameter_type(ic::IndexCache, stype, sz, sym, index, val) (; portion) = index + if stype <: FnType + stype = fntype_to_function_type(stype) + end # Nonnumeric parameters have to match the type if portion === NONNUMERIC_PORTION val isa stype && return nothing diff --git a/test/initial_values.jl b/test/initial_values.jl index f413c1a018..dbe08962cd 100644 --- a/test/initial_values.jl +++ b/test/initial_values.jl @@ -1,6 +1,7 @@ using ModelingToolkit using ModelingToolkit: t_nounits as t, D_nounits as D, get_u0 using OrdinaryDiffEq +using DataInterpolations using SymbolicIndexingInterface: getu @variables x(t)[1:3]=[1.0, 2.0, 3.0] y(t) z(t)[1:2] @@ -219,3 +220,19 @@ end @test_throws ["a(t)", "c(t)"] ODEProblem( sys, [e => 2, a => b, b => a + 1, c => d, d => c + 1], (0, 1)) end + +@testset "Issue#3490: `remake` works with callable parameters" begin + ts = collect(0.0:0.1:10.0) + spline = LinearInterpolation(ts .^ 2, ts) + Tspline = typeof(spline) + @variables x(t) + @parameters (interp::Tspline)(..) + + @mtkbuild sys = ODESystem(D(x) ~ interp(t), t) + + prob = ODEProblem(sys, [x => 0.0], (0.0, 1.0), [interp => spline]) + spline2 = LinearInterpolation(ts .^ 2, ts .^ 2) + p_new = [interp => spline2] + prob2 = remake(prob; p = p_new) + @test prob2.ps[interp] == spline2 +end From 0ce6ec054d2fed0a75e04474847f46956425ecbf Mon Sep 17 00:00:00 2001 From: Sam Isaacson Date: Tue, 25 Mar 2025 12:42:01 -0400 Subject: [PATCH 1208/1337] don't run initialization on variable rate jump problems --- 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 1ad6d30f59..7aa7c6909b 100644 --- a/src/systems/jumps/jumpsystem.jl +++ b/src/systems/jumps/jumpsystem.jl @@ -502,7 +502,7 @@ function DiffEqBase.ODEProblem(sys::JumpSystem, u0map, tspan::Union{Tuple, Nothi else _, u0, p = process_SciMLProblem(EmptySciMLFunction, sys, u0map, parammap; t = tspan === nothing ? nothing : tspan[1], tofloat = false, - check_length = false) + check_length = false, build_initializeprob = false) f = (du, u, p, t) -> (du .= 0; nothing) observedfun = ObservedFunctionCache(sys; eval_expression, eval_module, checkbounds = get(kwargs, :checkbounds, false)) From 349e9d53d503022971e949584b98282491ef9466 Mon Sep 17 00:00:00 2001 From: Christopher Rackauckas Date: Wed, 26 Mar 2025 11:45:10 -0400 Subject: [PATCH 1209/1337] Update Project.toml --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 216ef0c73a..92d8f98ab4 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "ModelingToolkit" uuid = "961ee093-0014-501f-94e3-6117800e7a78" authors = ["Yingbo Ma ", "Chris Rackauckas and contributors"] -version = "9.68.1" +version = "9.69.0" [deps] ADTypes = "47edcb42-4c32-4615-8424-f2b9edc5f35b" From cd60c91dcdfe4df200088003a3add708febaa9fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Miclu=C8=9Ba-C=C3=A2mpeanu?= Date: Wed, 26 Mar 2025 21:44:54 +0200 Subject: [PATCH 1210/1337] fix: fix depwarn due to wrapping `Vararg` directly in `UnionAll` --- src/systems/connectors.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/systems/connectors.jl b/src/systems/connectors.jl index ff4eb3e501..bb6c5ea838 100644 --- a/src/systems/connectors.jl +++ b/src/systems/connectors.jl @@ -448,7 +448,7 @@ ignored. All analysis points in `ignored_system_aps` must contain systems (conne as their input/outputs. """ function systems_to_ignore(ignored_system_aps::Vector{HierarchyAnalysisPointT}, - systems::Union{Vector{<:AbstractSystem}, Tuple{Vararg{<:AbstractSystem}}}) + systems::Union{Vector{S}, Tuple{Vararg{S}}}) where S <: AbstractSystem to_ignore = HierarchySystemT[] for ap in ignored_system_aps # if `systems` contains the input of the AP, ignore any outputs of the AP present in it. @@ -476,7 +476,7 @@ points to be ignored. All analysis points in `ignored_system_aps` must contain v as their input/outputs. """ function variables_to_ignore(ignored_variable_aps::Vector{HierarchyAnalysisPointT}, - systems::Union{Vector{<:AbstractSystem}, Tuple{Vararg{<:AbstractSystem}}}) + systems::Union{Vector{S}, Tuple{Vararg{S}}}) where S <: AbstractSystem to_ignore = HierarchyVariableT[] for ap in ignored_variable_aps ivar_hierarchy = HierarchyVariableT([ap[1].input; @view ap[2:end]]) @@ -503,7 +503,7 @@ be ignored. All analysis points in `ignored_system_aps` must contain variables a input/outputs. """ function variables_to_ignore(ignored_variable_aps::Vector{HierarchyAnalysisPointT}, - vars::Union{Vector{<:BasicSymbolic}, Tuple{Vararg{<:BasicSymbolic}}}) + vars::Union{Vector{S}, Tuple{Vararg{S}}}) where S <: BasicSymbolic to_ignore = eltype(vars)[] for ap in ignored_variable_aps ivar_hierarchy = HierarchyVariableT([ap[1].input; @view ap[2:end]]) From cca1ea10efc32a1699475af486ccb4184db6583a Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 27 Mar 2025 12:25:27 +0530 Subject: [PATCH 1211/1337] refactor: format --- src/systems/connectors.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/systems/connectors.jl b/src/systems/connectors.jl index bb6c5ea838..6b0600fbb7 100644 --- a/src/systems/connectors.jl +++ b/src/systems/connectors.jl @@ -448,7 +448,7 @@ ignored. All analysis points in `ignored_system_aps` must contain systems (conne as their input/outputs. """ function systems_to_ignore(ignored_system_aps::Vector{HierarchyAnalysisPointT}, - systems::Union{Vector{S}, Tuple{Vararg{S}}}) where S <: AbstractSystem + systems::Union{Vector{S}, Tuple{Vararg{S}}}) where {S <: AbstractSystem} to_ignore = HierarchySystemT[] for ap in ignored_system_aps # if `systems` contains the input of the AP, ignore any outputs of the AP present in it. @@ -476,7 +476,7 @@ points to be ignored. All analysis points in `ignored_system_aps` must contain v as their input/outputs. """ function variables_to_ignore(ignored_variable_aps::Vector{HierarchyAnalysisPointT}, - systems::Union{Vector{S}, Tuple{Vararg{S}}}) where S <: AbstractSystem + systems::Union{Vector{S}, Tuple{Vararg{S}}}) where {S <: AbstractSystem} to_ignore = HierarchyVariableT[] for ap in ignored_variable_aps ivar_hierarchy = HierarchyVariableT([ap[1].input; @view ap[2:end]]) @@ -503,7 +503,7 @@ be ignored. All analysis points in `ignored_system_aps` must contain variables a input/outputs. """ function variables_to_ignore(ignored_variable_aps::Vector{HierarchyAnalysisPointT}, - vars::Union{Vector{S}, Tuple{Vararg{S}}}) where S <: BasicSymbolic + vars::Union{Vector{S}, Tuple{Vararg{S}}}) where {S <: BasicSymbolic} to_ignore = eltype(vars)[] for ap in ignored_variable_aps ivar_hierarchy = HierarchyVariableT([ap[1].input; @view ap[2:end]]) From 28a6ed2b52dec6b7581f3f50981ffc8c17628c6f Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 25 Feb 2025 16:31:33 +0530 Subject: [PATCH 1212/1337] feat: throw error when differentiating registered function with no derivative in `structural_simplify` --- src/structural_transformation/StructuralTransformations.jl | 1 + src/structural_transformation/symbolics_tearing.jl | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/structural_transformation/StructuralTransformations.jl b/src/structural_transformation/StructuralTransformations.jl index 2e600e86e4..4adc817ef8 100644 --- a/src/structural_transformation/StructuralTransformations.jl +++ b/src/structural_transformation/StructuralTransformations.jl @@ -4,6 +4,7 @@ using Setfield: @set!, @set using UnPack: @unpack using Symbolics: unwrap, linear_expansion, fast_substitute +import Symbolics using SymbolicUtils using SymbolicUtils.Code using SymbolicUtils.Rewriters diff --git a/src/structural_transformation/symbolics_tearing.jl b/src/structural_transformation/symbolics_tearing.jl index f1cdd7ce9c..6845351008 100644 --- a/src/structural_transformation/symbolics_tearing.jl +++ b/src/structural_transformation/symbolics_tearing.jl @@ -65,7 +65,7 @@ function eq_derivative!(ts::TearingState{ODESystem}, ieq::Int; kwargs...) sys = ts.sys eq = equations(ts)[ieq] - eq = 0 ~ ModelingToolkit.derivative(eq.rhs - eq.lhs, get_iv(sys)) + eq = 0 ~ Symbolics.derivative(eq.rhs - eq.lhs, get_iv(sys); throw_no_derivative = true) push!(equations(ts), eq) # Analyze the new equation and update the graph/solvable_graph # First, copy the previous incidence and add the derivative terms. From 5aceb9e9add2b8549f79489e74ba3b242fc6a5ba Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 26 Feb 2025 21:43:53 +0530 Subject: [PATCH 1213/1337] test: add required derivative, fix initialization in split parameters test --- test/split_parameters.jl | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/test/split_parameters.jl b/test/split_parameters.jl index 74f8d41d73..1052f4ad27 100644 --- a/test/split_parameters.jl +++ b/test/split_parameters.jl @@ -51,8 +51,8 @@ end get_value(interp::Interpolator, t) = interp(t) @register_symbolic get_value(interp::Interpolator, t) -# get_value(data, t, dt) = data[round(Int, t / dt + 1)] -# @register_symbolic get_value(data::Vector, t, dt) + +Symbolics.derivative(::typeof(get_value), args::NTuple{2, Any}, ::Val{2}) = 0 function Sampled(; name, interp = Interpolator(Float64[], 0.0)) pars = @parameters begin @@ -68,11 +68,10 @@ function Sampled(; name, interp = Interpolator(Float64[], 0.0)) output.u ~ get_value(interpolator, t) ] - return ODESystem(eqs, t, vars, [interpolator]; name, systems, - defaults = [output.u => interp.data[1]]) + return ODESystem(eqs, t, vars, [interpolator]; name, systems) end -vars = @variables y(t)=1 dy(t)=0 ddy(t)=0 +vars = @variables y(t) dy(t) ddy(t) @named src = Sampled(; interp = Interpolator(x, dt)) @named int = Integrator() @@ -84,11 +83,9 @@ eqs = [y ~ src.output.u @named sys = ODESystem(eqs, t, vars, []; systems = [int, src]) s = complete(sys) sys = structural_simplify(sys) -@test_broken ODEProblem( - sys, [], (0.0, t_end), [s.src.interpolator => Interpolator(x, dt)]; tofloat = false) prob = ODEProblem( sys, [], (0.0, t_end), [s.src.interpolator => Interpolator(x, dt)]; - tofloat = false, build_initializeprob = false) + tofloat = false) sol = solve(prob, ImplicitEuler()); @test sol.retcode == ReturnCode.Success @test sol[y][end] == x[end] From 8c43ac5dd9bde84a35619bd8e4817d6e5d7ffaf4 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 3 Mar 2025 16:52:54 +0530 Subject: [PATCH 1214/1337] build: bump Symbolics compat --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 92d8f98ab4..a9290b037a 100644 --- a/Project.toml +++ b/Project.toml @@ -151,7 +151,7 @@ StochasticDelayDiffEq = "1.8.1" StochasticDiffEq = "6.72.1" SymbolicIndexingInterface = "0.3.37" SymbolicUtils = "3.14" -Symbolics = "6.29.1" +Symbolics = "6.29.2" URIs = "1" UnPack = "0.1, 1.0" Unitful = "1.1" From 6e5be63ef1b321d59cb77918a72c41869c48fde1 Mon Sep 17 00:00:00 2001 From: Herman Sletmoen Date: Thu, 27 Mar 2025 12:33:40 +0100 Subject: [PATCH 1215/1337] Show system name in function hints also for extended fields --- src/systems/diffeqs/odesystem.jl | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/systems/diffeqs/odesystem.jl b/src/systems/diffeqs/odesystem.jl index 7be3b0c08a..ac08cbb5a7 100644 --- a/src/systems/diffeqs/odesystem.jl +++ b/src/systems/diffeqs/odesystem.jl @@ -717,10 +717,12 @@ function Base.show(io::IO, mime::MIME"text/plain", sys::ODESystem; hint = true, invoke(Base.show, Tuple{typeof(io), typeof(mime), AbstractSystem}, io, mime, sys; hint, bold) + name = nameof(sys) + # Print initialization equations (unique to ODESystems) nini = length(initialization_equations(sys)) nini > 0 && printstyled(io, "\nInitialization equations ($nini):"; bold) - nini > 0 && hint && print(io, " see initialization_equations(sys)") + nini > 0 && hint && print(io, " see initialization_equations($name)") return nothing end From 1d389bd47b343e53e0a734226a218c1283382a15 Mon Sep 17 00:00:00 2001 From: Herman Sletmoen Date: Thu, 27 Mar 2025 12:37:36 +0100 Subject: [PATCH 1216/1337] Show system parameter dependencies --- src/systems/abstractsystem.jl | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index 3da5201039..50440bcaae 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -2045,6 +2045,11 @@ function Base.show( limited && printstyled(io, "\n ⋮") # too many variables to print end + # Print parameter dependencies + npdeps = has_parameter_dependencies(sys) ? length(parameter_dependencies(sys)) : 0 + npdeps > 0 && printstyled(io, "\nParameter dependencies ($npdeps):"; bold) + npdeps > 0 && hint && print(io, " see parameter_dependencies($name)") + # Print observed nobs = has_observed(sys) ? length(observed(sys)) : 0 nobs > 0 && printstyled(io, "\nObserved ($nobs):"; bold) From 18435687c95f88c76bd8065e4a65dc53fb685ef4 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 25 Mar 2025 23:06:56 +0530 Subject: [PATCH 1217/1337] feat: allow toggling namespacing independently of `complete` --- src/ModelingToolkit.jl | 2 +- src/systems/abstractsystem.jl | 38 ++++++++++++++++++- src/systems/analysis_points.jl | 8 +++- src/systems/diffeqs/odesystem.jl | 14 +++++-- src/systems/diffeqs/sdesystem.jl | 19 ++++++---- .../discrete_system/discrete_system.jl | 12 ++++-- .../implicit_discrete_system.jl | 12 ++++-- src/systems/jumps/jumpsystem.jl | 11 ++++-- src/systems/nonlinear/nonlinearsystem.jl | 11 ++++-- .../optimization/constraints_system.jl | 13 +++++-- .../optimization/optimizationsystem.jl | 15 +++++--- src/utils.jl | 14 +++++++ 12 files changed, 132 insertions(+), 37 deletions(-) diff --git a/src/ModelingToolkit.jl b/src/ModelingToolkit.jl index 1fdb946a93..b53d3ec098 100644 --- a/src/ModelingToolkit.jl +++ b/src/ModelingToolkit.jl @@ -328,7 +328,7 @@ export alg_equations, diff_equations, has_alg_equations, has_diff_equations export get_alg_eqs, get_diff_eqs, has_alg_eqs, has_diff_eqs export @variables, @parameters, @independent_variables, @constants, @brownian -export @named, @nonamespace, @namespace, extend, compose, complete +export @named, @nonamespace, @namespace, extend, compose, complete, toggle_namespacing export debug_system #export ContinuousClock, Discrete, sampletime, input_timedomain, output_timedomain diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index 50440bcaae..89ff0e510b 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -590,7 +590,24 @@ function SymbolicIndexingInterface.all_symbols(sys::AbstractSystem) return syms end +""" + $(TYPEDSIGNATURES) + +Check whether a system is marked as `complete`. +""" iscomplete(sys::AbstractSystem) = isdefined(sys, :complete) && getfield(sys, :complete) +""" + $(TYPEDSIGNATURES) + +Check whether a system performs namespacing. +""" +function does_namespacing(sys::AbstractSystem) + if isdefined(sys, :namespacing) + getfield(sys, :namespacing) + else + iscomplete(sys) + end +end """ $(TYPEDSIGNATURES) @@ -798,9 +815,25 @@ function complete( if isdefined(sys, :initializesystem) && get_initializesystem(sys) !== nothing @set! sys.initializesystem = complete(get_initializesystem(sys); split) end + sys = toggle_namespacing(sys, false; safe = true) isdefined(sys, :complete) ? (@set! sys.complete = true) : sys end +""" + $(TYPEDSIGNATURES) + +Return a new `sys` with namespacing enabled or disabled, depending on `value`. The +keyword argument `safe` denotes whether systems that do not support such a toggle +should error or be ignored. +""" +function toggle_namespacing(sys::AbstractSystem, value::Bool; safe = false) + if !isdefined(sys, :namespacing) + safe && return sys + throw(ArgumentError("The system must define the `namespacing` flag to toggle namespacing")) + end + @set sys.namespacing = value +end + """ $(TYPEDSIGNATURES) @@ -985,13 +1018,14 @@ function Base.propertynames(sys::AbstractSystem; private = false) end end -function Base.getproperty(sys::AbstractSystem, name::Symbol; namespace = !iscomplete(sys)) +function Base.getproperty( + sys::AbstractSystem, name::Symbol; namespace = does_namespacing(sys)) if has_parent(sys) && (parent = get_parent(sys); parent !== nothing) return getproperty(parent, name; namespace) end wrap(getvar(sys, name; namespace = namespace)) end -function getvar(sys::AbstractSystem, name::Symbol; namespace = !iscomplete(sys)) +function getvar(sys::AbstractSystem, name::Symbol; namespace = does_namespacing(sys)) systems = get_systems(sys) if isdefined(sys, name) Base.depwarn( diff --git a/src/systems/analysis_points.jl b/src/systems/analysis_points.jl index eb43bc40ef..104c26cbfe 100644 --- a/src/systems/analysis_points.jl +++ b/src/systems/analysis_points.jl @@ -768,7 +768,13 @@ already an `AnalysisPoint` or `Symbol`) is simply wrapped in an array. `Symbol` `AnalysisPoint`s are namespaced with `sys`. """ canonicalize_ap(sys::AbstractSystem, ap::Symbol) = [AnalysisPoint(renamespace(sys, ap))] -canonicalize_ap(sys::AbstractSystem, ap::AnalysisPoint) = [ap] +function canonicalize_ap(sys::AbstractSystem, ap::AnalysisPoint) + if does_namespacing(sys) + return [ap] + else + return [renamespace(sys, ap)] + end +end canonicalize_ap(sys::AbstractSystem, ap) = [ap] function canonicalize_ap(sys::AbstractSystem, aps::Vector) mapreduce(Base.Fix1(canonicalize_ap, sys), vcat, aps; init = []) diff --git a/src/systems/diffeqs/odesystem.jl b/src/systems/diffeqs/odesystem.jl index ac08cbb5a7..bd0a58e10a 100644 --- a/src/systems/diffeqs/odesystem.jl +++ b/src/systems/diffeqs/odesystem.jl @@ -169,7 +169,11 @@ struct ODESystem <: AbstractODESystem """ substitutions::Any """ - If a model `sys` is complete, then `sys.x` no longer performs namespacing. + If false, then `sys.x` no longer performs namespacing. + """ + namespacing::Bool + """ + If true, denotes the model will not be modified any further. """ complete::Bool """ @@ -207,8 +211,8 @@ struct ODESystem <: AbstractODESystem connector_type, preface, cevents, devents, parameter_dependencies, assertions = Dict{BasicSymbolic, String}(), metadata = nothing, gui_metadata = nothing, is_dde = false, - tstops = [], tearing_state = nothing, - substitutions = nothing, complete = false, index_cache = nothing, + tstops = [], tearing_state = nothing, substitutions = nothing, + namespacing = true, complete = false, index_cache = nothing, discrete_subsystems = nothing, solved_unknowns = nothing, split_idxs = nothing, ignored_connections = nothing, parent = nothing; checks::Union{Bool, Int} = true) @@ -218,6 +222,7 @@ struct ODESystem <: AbstractODESystem check_parameters(ps, iv) check_equations(deqs, iv) check_equations(equations(cevents), iv) + check_subsystems(systems) end if checks == true || (checks & CheckUnits) > 0 u = __get_unit_type(dvs, ps, iv) @@ -228,7 +233,8 @@ struct ODESystem <: AbstractODESystem ctrl_jac, Wfact, Wfact_t, name, description, systems, defaults, guesses, torn_matching, initializesystem, initialization_eqs, schedule, connector_type, preface, cevents, devents, parameter_dependencies, assertions, metadata, - gui_metadata, is_dde, tstops, tearing_state, substitutions, complete, index_cache, + gui_metadata, is_dde, tstops, tearing_state, substitutions, namespacing, + complete, index_cache, discrete_subsystems, solved_unknowns, split_idxs, ignored_connections, parent) end end diff --git a/src/systems/diffeqs/sdesystem.jl b/src/systems/diffeqs/sdesystem.jl index d8ac52dd1a..f51529a559 100644 --- a/src/systems/diffeqs/sdesystem.jl +++ b/src/systems/diffeqs/sdesystem.jl @@ -139,7 +139,11 @@ struct SDESystem <: AbstractODESystem """ gui_metadata::Union{Nothing, GUIMetadata} """ - If a model `sys` is complete, then `sys.x` no longer performs namespacing. + If false, then `sys.x` no longer performs namespacing. + """ + namespacing::Bool + """ + If true, denotes the model will not be modified any further. """ complete::Bool """ @@ -166,7 +170,7 @@ struct SDESystem <: AbstractODESystem guesses, initializesystem, initialization_eqs, connector_type, cevents, devents, parameter_dependencies, assertions = Dict{ BasicSymbolic, Nothing}, - metadata = nothing, gui_metadata = nothing, + metadata = nothing, gui_metadata = nothing, namespacing = true, complete = false, index_cache = nothing, parent = nothing, is_scalar_noise = false, is_dde = false, isscheduled = false; @@ -184,6 +188,7 @@ struct SDESystem <: AbstractODESystem if is_scalar_noise && neqs isa AbstractMatrix throw(ArgumentError("Noise equations ill-formed. Received a matrix of noise equations of size $(size(neqs)), but `is_scalar_noise` was set to `true`. Scalar noise is only compatible with an `AbstractVector` of noise equations.")) end + check_subsystems(systems) end if checks == true || (checks & CheckUnits) > 0 u = __get_unit_type(dvs, ps, iv) @@ -192,8 +197,8 @@ struct SDESystem <: AbstractODESystem new(tag, deqs, neqs, iv, dvs, ps, tspan, var_to_name, ctrls, observed, tgrad, jac, ctrl_jac, Wfact, Wfact_t, name, description, systems, defaults, guesses, initializesystem, initialization_eqs, connector_type, cevents, - devents, parameter_dependencies, assertions, metadata, gui_metadata, complete, - index_cache, parent, is_scalar_noise, is_dde, isscheduled) + devents, parameter_dependencies, assertions, metadata, gui_metadata, namespacing, + complete, index_cache, parent, is_scalar_noise, is_dde, isscheduled) end end @@ -218,7 +223,6 @@ function SDESystem(deqs::AbstractVector{<:Equation}, neqs::AbstractArray, iv, dv assertions = Dict{BasicSymbolic, String}(), metadata = nothing, gui_metadata = nothing, - complete = false, index_cache = nothing, parent = nothing, is_scalar_noise = false, @@ -274,7 +278,7 @@ function SDESystem(deqs::AbstractVector{<:Equation}, neqs::AbstractArray, iv, dv ctrl_jac, Wfact, Wfact_t, name, description, systems, defaults, guesses, initializesystem, initialization_eqs, connector_type, cont_callbacks, disc_callbacks, parameter_dependencies, assertions, metadata, gui_metadata, - complete, index_cache, parent, is_scalar_noise, is_dde; checks = checks) + true, false, index_cache, parent, is_scalar_noise, is_dde; checks = checks) end function SDESystem(sys::ODESystem, neqs; kwargs...) @@ -321,9 +325,8 @@ function SDESystem(eqs::Vector{Equation}, noiseeqs::AbstractArray, iv; kwargs... throw(ArgumentError("Variable $dv in noise equations is not an unknown of the system.")) end algevars = setdiff(allunknowns, diffvars) - return SDESystem(eqs, noiseeqs, iv, Iterators.flatten((diffvars, algevars)), - [ps; collect(noiseps)]; kwargs...) + [collect(ps); collect(noiseps)]; kwargs...) end function SDESystem(eq::Equation, noiseeqs::AbstractArray, args...; kwargs...) diff --git a/src/systems/discrete_system/discrete_system.jl b/src/systems/discrete_system/discrete_system.jl index f51ba638a4..40f01769ee 100644 --- a/src/systems/discrete_system/discrete_system.jl +++ b/src/systems/discrete_system/discrete_system.jl @@ -97,7 +97,11 @@ struct DiscreteSystem <: AbstractDiscreteSystem """ substitutions::Any """ - If a model `sys` is complete, then `sys.x` no longer performs namespacing. + If false, then `sys.x` no longer performs namespacing. + """ + namespacing::Bool + """ + If true, denotes the model will not be modified any further. """ complete::Bool """ @@ -114,7 +118,7 @@ struct DiscreteSystem <: AbstractDiscreteSystem observed, name, description, systems, defaults, guesses, initializesystem, initialization_eqs, preface, connector_type, parameter_dependencies = Equation[], metadata = nothing, gui_metadata = nothing, - tearing_state = nothing, substitutions = nothing, + tearing_state = nothing, substitutions = nothing, namespacing = true, complete = false, index_cache = nothing, parent = nothing, isscheduled = false; checks::Union{Bool, Int} = true) @@ -122,6 +126,7 @@ struct DiscreteSystem <: AbstractDiscreteSystem check_independent_variables([iv]) check_variables(dvs, iv) check_parameters(ps, iv) + check_subsystems(systems) end if checks == true || (checks & CheckUnits) > 0 u = __get_unit_type(dvs, ps, iv) @@ -130,7 +135,8 @@ struct DiscreteSystem <: AbstractDiscreteSystem new(tag, discreteEqs, iv, dvs, ps, tspan, var_to_name, observed, name, description, systems, defaults, guesses, initializesystem, initialization_eqs, preface, connector_type, parameter_dependencies, metadata, gui_metadata, - tearing_state, substitutions, complete, index_cache, parent, isscheduled) + tearing_state, substitutions, namespacing, complete, index_cache, parent, + isscheduled) end end diff --git a/src/systems/discrete_system/implicit_discrete_system.jl b/src/systems/discrete_system/implicit_discrete_system.jl index 514576e927..ebae78384a 100644 --- a/src/systems/discrete_system/implicit_discrete_system.jl +++ b/src/systems/discrete_system/implicit_discrete_system.jl @@ -96,7 +96,11 @@ struct ImplicitDiscreteSystem <: AbstractDiscreteSystem """ substitutions::Any """ - If a model `sys` is complete, then `sys.x` no longer performs namespacing. + If false, then `sys.x` no longer performs namespacing. + """ + namespacing::Bool + """ + If true, denotes the model will not be modified any further. """ complete::Bool """ @@ -113,7 +117,7 @@ struct ImplicitDiscreteSystem <: AbstractDiscreteSystem observed, name, description, systems, defaults, guesses, initializesystem, initialization_eqs, preface, connector_type, parameter_dependencies = Equation[], metadata = nothing, gui_metadata = nothing, - tearing_state = nothing, substitutions = nothing, + tearing_state = nothing, substitutions = nothing, namespacing = true, complete = false, index_cache = nothing, parent = nothing, isscheduled = false; checks::Union{Bool, Int} = true) @@ -121,6 +125,7 @@ struct ImplicitDiscreteSystem <: AbstractDiscreteSystem check_independent_variables([iv]) check_variables(dvs, iv) check_parameters(ps, iv) + check_subsystems(systems) end if checks == true || (checks & CheckUnits) > 0 u = __get_unit_type(dvs, ps, iv) @@ -129,7 +134,8 @@ struct ImplicitDiscreteSystem <: AbstractDiscreteSystem new(tag, discreteEqs, iv, dvs, ps, tspan, var_to_name, observed, name, description, systems, defaults, guesses, initializesystem, initialization_eqs, preface, connector_type, parameter_dependencies, metadata, gui_metadata, - tearing_state, substitutions, complete, index_cache, parent, isscheduled) + tearing_state, substitutions, namespacing, complete, index_cache, parent, + isscheduled) end end diff --git a/src/systems/jumps/jumpsystem.jl b/src/systems/jumps/jumpsystem.jl index 7aa7c6909b..9da32a4305 100644 --- a/src/systems/jumps/jumpsystem.jl +++ b/src/systems/jumps/jumpsystem.jl @@ -116,7 +116,11 @@ struct JumpSystem{U <: ArrayPartition} <: AbstractTimeDependentSystem """ gui_metadata::Union{Nothing, GUIMetadata} """ - If a model `sys` is complete, then `sys.x` no longer performs namespacing. + If false, then `sys.x` no longer performs namespacing. + """ + namespacing::Bool + """ + If true, denotes the model will not be modified any further. """ complete::Bool """ @@ -130,12 +134,13 @@ struct JumpSystem{U <: ArrayPartition} <: AbstractTimeDependentSystem systems, defaults, guesses, initializesystem, initialization_eqs, connector_type, cevents, devents, parameter_dependencies, metadata = nothing, gui_metadata = nothing, - complete = false, index_cache = nothing, isscheduled = false; + namespacing = true, complete = false, index_cache = nothing, isscheduled = false; checks::Union{Bool, Int} = true) where {U <: ArrayPartition} if checks == true || (checks & CheckComponents) > 0 check_independent_variables([iv]) check_variables(unknowns, iv) check_parameters(ps, iv) + check_subsystems(systems) end if checks == true || (checks & CheckUnits) > 0 u = __get_unit_type(unknowns, ps, iv) @@ -145,7 +150,7 @@ struct JumpSystem{U <: ArrayPartition} <: AbstractTimeDependentSystem observed, name, description, systems, defaults, guesses, initializesystem, initialization_eqs, connector_type, cevents, devents, parameter_dependencies, metadata, - gui_metadata, complete, index_cache, isscheduled) + gui_metadata, namespacing, complete, index_cache, isscheduled) end end function JumpSystem(tag, ap, iv, states, ps, var_to_name, args...; kwargs...) diff --git a/src/systems/nonlinear/nonlinearsystem.jl b/src/systems/nonlinear/nonlinearsystem.jl index ea3730fdb6..77215be5eb 100644 --- a/src/systems/nonlinear/nonlinearsystem.jl +++ b/src/systems/nonlinear/nonlinearsystem.jl @@ -95,7 +95,11 @@ struct NonlinearSystem <: AbstractTimeIndependentSystem """ substitutions::Any """ - If a model `sys` is complete, then `sys.x` no longer performs namespacing. + If false, then `sys.x` no longer performs namespacing. + """ + namespacing::Bool + """ + If true, denotes the model will not be modified any further. """ complete::Bool """ @@ -112,17 +116,18 @@ struct NonlinearSystem <: AbstractTimeIndependentSystem tag, eqs, unknowns, ps, var_to_name, observed, jac, name, description, systems, defaults, guesses, initializesystem, initialization_eqs, connector_type, parameter_dependencies = Equation[], metadata = nothing, gui_metadata = nothing, - tearing_state = nothing, substitutions = nothing, + tearing_state = nothing, substitutions = nothing, namespacing = true, complete = false, index_cache = nothing, parent = nothing, isscheduled = false; checks::Union{Bool, Int} = true) if checks == true || (checks & CheckUnits) > 0 u = __get_unit_type(unknowns, ps) check_units(u, eqs) + check_subsystems(systems) end new(tag, eqs, unknowns, ps, var_to_name, observed, jac, name, description, systems, defaults, guesses, initializesystem, initialization_eqs, connector_type, parameter_dependencies, metadata, gui_metadata, tearing_state, - substitutions, complete, index_cache, parent, isscheduled) + substitutions, namespacing, complete, index_cache, parent, isscheduled) end end diff --git a/src/systems/optimization/constraints_system.jl b/src/systems/optimization/constraints_system.jl index 8a71c58043..0f69e6d0b9 100644 --- a/src/systems/optimization/constraints_system.jl +++ b/src/systems/optimization/constraints_system.jl @@ -74,7 +74,11 @@ struct ConstraintsSystem <: AbstractTimeIndependentSystem """ substitutions::Any """ - If a model `sys` is complete, then `sys.x` no longer performs namespacing. + If false, then `sys.x` no longer performs namespacing. + """ + namespacing::Bool + """ + If true, denotes the model will not be modified any further. """ complete::Bool """ @@ -86,17 +90,18 @@ struct ConstraintsSystem <: AbstractTimeIndependentSystem name, description, systems, defaults, connector_type, metadata = nothing, - tearing_state = nothing, substitutions = nothing, + tearing_state = nothing, substitutions = nothing, namespacing = true, complete = false, index_cache = nothing; checks::Union{Bool, Int} = true) if checks == true || (checks & CheckUnits) > 0 u = __get_unit_type(unknowns, ps) check_units(u, constraints) + check_subsystems(systems) end new(tag, constraints, unknowns, ps, var_to_name, observed, jac, name, description, systems, - defaults, - connector_type, metadata, tearing_state, substitutions, complete, index_cache) + defaults, connector_type, metadata, tearing_state, substitutions, + namespacing, complete, index_cache) end end diff --git a/src/systems/optimization/optimizationsystem.jl b/src/systems/optimization/optimizationsystem.jl index bd0f0aa679..e55f4b7871 100644 --- a/src/systems/optimization/optimizationsystem.jl +++ b/src/systems/optimization/optimizationsystem.jl @@ -55,7 +55,11 @@ struct OptimizationSystem <: AbstractOptimizationSystem """ gui_metadata::Union{Nothing, GUIMetadata} """ - If a model `sys` is complete, then `sys.x` no longer performs namespacing. + If false, then `sys.x` no longer performs namespacing. + """ + namespacing::Bool + """ + If true, denotes the model will not be modified any further. """ complete::Bool """ @@ -70,18 +74,19 @@ struct OptimizationSystem <: AbstractOptimizationSystem function OptimizationSystem(tag, op, unknowns, ps, var_to_name, observed, constraints, name, description, systems, defaults, metadata = nothing, - gui_metadata = nothing, complete = false, index_cache = nothing, parent = nothing, - isscheduled = false; + gui_metadata = nothing, namespacing = true, complete = false, + index_cache = nothing, parent = nothing, isscheduled = false; checks::Union{Bool, Int} = true) if checks == true || (checks & CheckUnits) > 0 u = __get_unit_type(unknowns, ps) unwrap(op) isa Symbolic && check_units(u, op) check_units(u, observed) check_units(u, constraints) + check_subsystems(systems) end new(tag, op, unknowns, ps, var_to_name, observed, - constraints, name, description, systems, defaults, metadata, gui_metadata, complete, - index_cache, parent, isscheduled) + constraints, name, description, systems, defaults, metadata, gui_metadata, + namespacing, complete, index_cache, parent, isscheduled) end end diff --git a/src/utils.jl b/src/utils.jl index 55f3d2cec5..726a2ce25f 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -202,6 +202,20 @@ function check_equations(eqs, iv) throw(ArgumentError("Differential w.r.t. variable ($single_iv) other than the independent variable ($iv) are not allowed.")) end end + +""" + $(TYPEDSIGNATURES) + +Assert that the subsystems have the appropriate namespacing behavior. +""" +function check_subsystems(systems) + idxs = findall(!does_namespacing, systems) + if !isempty(idxs) + names = join(" " .* string.(nameof.(systems[idxs])), "\n") + throw(ArgumentError("All subsystems have namespacing enabled. The following subsystems do not perform namespacing:\n$(names)")) + end +end + """ Get all the independent variables with respect to which differentials are taken. """ From f9bbdc5c0d2e85dc838914f3f7dc9c27334a8a32 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 25 Mar 2025 23:07:03 +0530 Subject: [PATCH 1218/1337] docs: document `toggle_namespacing` --- docs/src/basics/AbstractSystem.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/src/basics/AbstractSystem.md b/docs/src/basics/AbstractSystem.md index 3aa43e6d25..d1707f822f 100644 --- a/docs/src/basics/AbstractSystem.md +++ b/docs/src/basics/AbstractSystem.md @@ -148,3 +148,15 @@ The `AbstractSystem` types allow for specifying default values, for example into the value maps, where for any repeats the value maps override the default. In addition, defaults of a higher level in the system override the defaults of a lower level in the system. + +## Namespacing + +By default, unsimplified systems will namespace variables accessed via `getproperty`. +Systems created via `@mtkbuild`, or ones passed through `structural_simplify` or +`complete` will not perform this namespacing. However, all of these processes modify +the system in a variety of ways. To toggle namespacing without transforming any other +property of the system, use `toggle_namespacing`. + +```@docs +toggle_namespacing +``` From e0ae7a9131ef5f32ffae362a5fbc8e027648a620 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 25 Mar 2025 23:10:46 +0530 Subject: [PATCH 1219/1337] test: test new namespacing functionality --- test/analysis_points.jl | 9 +- test/namespacing.jl | 186 ++++++++++++++++++++++++++++++++++++++++ test/runtests.jl | 1 + 3 files changed, 194 insertions(+), 2 deletions(-) create mode 100644 test/namespacing.jl diff --git a/test/analysis_points.jl b/test/analysis_points.jl index 9e8e3fb455..e36afc02f7 100644 --- a/test/analysis_points.jl +++ b/test/analysis_points.jl @@ -47,8 +47,11 @@ end @named sys = ODESystem(Equation[], t; systems = [sys_ap]) ap3 = @test_nowarn sys.hej.plant_input @test nameof(ap3) == Symbol(join(["sys", "hej", "plant_input"], NAMESPACE_SEPARATOR)) - sys = complete(sys) - ap4 = sys.hej.plant_input + csys = complete(sys) + ap4 = csys.hej.plant_input + @test nameof(ap4) == Symbol(join(["hej", "plant_input"], NAMESPACE_SEPARATOR)) + nsys = toggle_namespacing(sys, false) + ap5 = nsys.hej.plant_input @test nameof(ap4) == Symbol(join(["hej", "plant_input"], NAMESPACE_SEPARATOR)) end @@ -61,6 +64,7 @@ ap = AnalysisPoint(:plant_input) eqs = [connect(P.output, C.input), connect(C.output, ap, P.input)] sys = ODESystem(eqs, t, systems = [P, C], name = :hej) @named nested_sys = ODESystem(Equation[], t; systems = [sys]) +nonamespace_sys = toggle_namespacing(nested_sys, false) @testset "simplifies and solves" begin ssys = structural_simplify(sys) @@ -73,6 +77,7 @@ end test_cases = [ ("inner", sys, sys.plant_input), ("nested", nested_sys, nested_sys.hej.plant_input), + ("nonamespace", nonamespace_sys, nonamespace_sys.hej.plant_input), ("inner - Symbol", sys, :plant_input), ("nested - Symbol", nested_sys, nameof(sys.plant_input)) ] diff --git a/test/namespacing.jl b/test/namespacing.jl new file mode 100644 index 0000000000..b6305a1776 --- /dev/null +++ b/test/namespacing.jl @@ -0,0 +1,186 @@ +using ModelingToolkit +using ModelingToolkit: t_nounits as t, D_nounits as D, iscomplete, does_namespacing + +@testset "ODESystem" begin + @variables x(t) + @parameters p + sys = ODESystem(D(x) ~ p * x, t; name = :inner) + @test !iscomplete(sys) + @test does_namespacing(sys) + + csys = complete(sys) + @test iscomplete(csys) + @test !does_namespacing(csys) + + nsys = toggle_namespacing(sys, false) + @test !iscomplete(nsys) + @test !does_namespacing(nsys) + + @test isequal(x, csys.x) + @test isequal(x, nsys.x) + @test !isequal(x, sys.x) + @test isequal(p, csys.p) + @test isequal(p, nsys.p) + @test !isequal(p, sys.p) + + @test_throws ["namespacing", "inner"] ODESystem( + Equation[], t; systems = [nsys], name = :a) +end + +@testset "SDESystem" begin + @variables x(t) + @parameters p + sys = SDESystem([D(x) ~ p * x], [x], t, [x], [p]; name = :inner) + @test !iscomplete(sys) + @test does_namespacing(sys) + + csys = complete(sys) + @test iscomplete(csys) + @test !does_namespacing(csys) + + nsys = toggle_namespacing(sys, false) + @test !iscomplete(nsys) + @test !does_namespacing(nsys) + + @test isequal(x, csys.x) + @test isequal(x, nsys.x) + @test !isequal(x, sys.x) + @test isequal(p, csys.p) + @test isequal(p, nsys.p) + @test !isequal(p, sys.p) + + @test_throws ["namespacing", "inner"] SDESystem( + Equation[], [], t; systems = [nsys], name = :a) +end + +@testset "DiscreteSystem" begin + @variables x(t) + @parameters p + k = ShiftIndex(t) + sys = DiscreteSystem([x(k) ~ p * x(k - 1)], t; name = :inner) + @test !iscomplete(sys) + @test does_namespacing(sys) + + csys = complete(sys) + @test iscomplete(csys) + @test !does_namespacing(csys) + + nsys = toggle_namespacing(sys, false) + @test !iscomplete(nsys) + @test !does_namespacing(nsys) + + @test isequal(x, csys.x) + @test isequal(x, nsys.x) + @test !isequal(x, sys.x) + @test isequal(p, csys.p) + @test isequal(p, nsys.p) + @test !isequal(p, sys.p) + + @test_throws ["namespacing", "inner"] DiscreteSystem( + Equation[], t; systems = [nsys], name = :a) +end + +@testset "ImplicitDiscreteSystem" begin + @variables x(t) + @parameters p + k = ShiftIndex(t) + sys = ImplicitDiscreteSystem([x(k) ~ p + x(k - 1) * x(k)], t; name = :inner) + @test !iscomplete(sys) + @test does_namespacing(sys) + + csys = complete(sys) + @test iscomplete(csys) + @test !does_namespacing(csys) + + nsys = toggle_namespacing(sys, false) + @test !iscomplete(nsys) + @test !does_namespacing(nsys) + + @test isequal(x, csys.x) + @test isequal(x, nsys.x) + @test !isequal(x, sys.x) + @test isequal(p, csys.p) + @test isequal(p, nsys.p) + @test !isequal(p, sys.p) + + @test_throws ["namespacing", "inner"] ImplicitDiscreteSystem( + Equation[], t; systems = [nsys], name = :a) +end + +@testset "NonlinearSystem" begin + @variables x + @parameters p + sys = NonlinearSystem([x ~ p * x^2 + 1]; name = :inner) + @test !iscomplete(sys) + @test does_namespacing(sys) + + csys = complete(sys) + @test iscomplete(csys) + @test !does_namespacing(csys) + + nsys = toggle_namespacing(sys, false) + @test !iscomplete(nsys) + @test !does_namespacing(nsys) + + @test isequal(x, csys.x) + @test isequal(x, nsys.x) + @test !isequal(x, sys.x) + @test isequal(p, csys.p) + @test isequal(p, nsys.p) + @test !isequal(p, sys.p) + + @test_throws ["namespacing", "inner"] NonlinearSystem( + Equation[]; systems = [nsys], name = :a) +end + +@testset "OptimizationSystem" begin + @variables x + @parameters p + sys = OptimizationSystem(p * x^2 + 1; name = :inner) + @test !iscomplete(sys) + @test does_namespacing(sys) + + csys = complete(sys) + @test iscomplete(csys) + @test !does_namespacing(csys) + + nsys = toggle_namespacing(sys, false) + @test !iscomplete(nsys) + @test !does_namespacing(nsys) + + @test isequal(x, csys.x) + @test isequal(x, nsys.x) + @test !isequal(x, sys.x) + @test isequal(p, csys.p) + @test isequal(p, nsys.p) + @test !isequal(p, sys.p) + + @test_throws ["namespacing", "inner"] OptimizationSystem( + []; systems = [nsys], name = :a) +end + +@testset "ConstraintsSystem" begin + @variables x + @parameters p + sys = ConstraintsSystem([x^2 + p ~ 0], [x], [p]; name = :inner) + @test !iscomplete(sys) + @test does_namespacing(sys) + + csys = complete(sys) + @test iscomplete(csys) + @test !does_namespacing(csys) + + nsys = toggle_namespacing(sys, false) + @test !iscomplete(nsys) + @test !does_namespacing(nsys) + + @test isequal(x, csys.x) + @test isequal(x, nsys.x) + @test !isequal(x, sys.x) + @test isequal(p, csys.p) + @test isequal(p, nsys.p) + @test !isequal(p, sys.p) + + @test_throws ["namespacing", "inner"] ConstraintsSystem( + [], [], []; systems = [nsys], name = :a) +end diff --git a/test/runtests.jl b/test/runtests.jl index a03b0f1977..301134219e 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -97,6 +97,7 @@ end @safetestset "Analysis Points Test" include("analysis_points.jl") @safetestset "Causal Variables Connection Test" include("causal_variables_connection.jl") @safetestset "Debugging Test" include("debugging.jl") + @safetestset "Namespacing test" include("namespacing.jl") end end From d62da8fc2c1e8900024a69b8d11100235e0b85e2 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 26 Mar 2025 11:56:33 +0530 Subject: [PATCH 1220/1337] refactor: improve error message for subsystem namespacing --- src/utils.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils.jl b/src/utils.jl index 726a2ce25f..2a0009b644 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -212,7 +212,7 @@ function check_subsystems(systems) idxs = findall(!does_namespacing, systems) if !isempty(idxs) names = join(" " .* string.(nameof.(systems[idxs])), "\n") - throw(ArgumentError("All subsystems have namespacing enabled. The following subsystems do not perform namespacing:\n$(names)")) + throw(ArgumentError("All subsystems must have namespacing enabled. The following subsystems do not perform namespacing:\n$(names)")) end end From 1e455e7257fe01dff8d074e88f07833975f03e7d Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 26 Mar 2025 11:56:43 +0530 Subject: [PATCH 1221/1337] test: fix usage of completed system as subsystem --- test/parameter_dependencies.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/parameter_dependencies.jl b/test/parameter_dependencies.jl index ad2131f458..31881e1ca8 100644 --- a/test/parameter_dependencies.jl +++ b/test/parameter_dependencies.jl @@ -121,7 +121,7 @@ end @parameters p1=1.0 p2=2.0 @variables x(t) = 0 - @mtkbuild sys1 = ODESystem( + @named sys1 = ODESystem( [D(x) ~ p1 * t + p2], t ) From be6156164b26692d09deab757a8ac0cc1438a5ad Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 28 Mar 2025 14:31:34 +0530 Subject: [PATCH 1222/1337] fix: fix `does_namespacing` fallback to `iscomplete` --- src/systems/abstractsystem.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index 89ff0e510b..fb692b4028 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -605,7 +605,7 @@ function does_namespacing(sys::AbstractSystem) if isdefined(sys, :namespacing) getfield(sys, :namespacing) else - iscomplete(sys) + !iscomplete(sys) end end From b54b56f5c9655aac6c7e5dfe8698b91906e44a7b Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 28 Mar 2025 16:25:31 +0530 Subject: [PATCH 1223/1337] test: fix rootfinding tests --- test/symbolic_events.jl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/symbolic_events.jl b/test/symbolic_events.jl index 3fb66081a2..804432408b 100644 --- a/test/symbolic_events.jl +++ b/test/symbolic_events.jl @@ -422,7 +422,7 @@ cond.rf_ip(out, [0, 1], p0, t0) # this should return 0 cond.rf_ip(out, [0, 2], p0, t0) @test out[2] ≈ 1 # signature is u,p,t -sol = solve(prob, Tsit5()) +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 @@ -430,7 +430,7 @@ sol = solve(prob, Tsit5()) sys = complete(sys) prob = ODEProblem(sys, Pair[], (0.0, 3.0)) @test get_callback(prob) isa ModelingToolkit.DiffEqCallbacks.VectorContinuousCallback -sol = solve(prob, Tsit5()) +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 @@ -540,8 +540,8 @@ ev = [sin(20pi * t) ~ 0.0] => [vmeasured ~ v] 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 +@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 ## https://github.com/SciML/ModelingToolkit.jl/issues/1528 Dₜ = D From 8335980c42728ccfb66867b5090936c568b16db7 Mon Sep 17 00:00:00 2001 From: George Datseris Date: Sun, 30 Mar 2025 10:25:44 +0100 Subject: [PATCH 1224/1337] Add contribution of multi/nonlocal stability by Attractors.jl We have discussed this on Slack some time ago! --- docs/src/tutorials/attractors.md | 218 +++++++++++++++++++++++++++++++ 1 file changed, 218 insertions(+) create mode 100644 docs/src/tutorials/attractors.md diff --git a/docs/src/tutorials/attractors.md b/docs/src/tutorials/attractors.md new file mode 100644 index 0000000000..56e6026427 --- /dev/null +++ b/docs/src/tutorials/attractors.md @@ -0,0 +1,218 @@ +# [Multi- and Nonlocal- Continuation](@id attractors) + +In the tutorial on [Bifurcation Diagrams](@ref bifurcation_diagrams) we saw how one can create them by integrating ModelingToolkit.jl with BifurcationKit.jl. +This approach is also often called _continuation_ in the broader literature, +because in essence we are "continuing" the location of individual un/stable fixed points or limit cycles in a dynamical system across a parameter axis. + +Recently, an alternative continuation framework was proposed that takes a fundamentally different approach to continuation that is particularly suitable for complex systems. This framework is implemented in [Attractors.jl](https://juliadynamics.github.io/DynamicalSystemsDocs.jl/attractors/stable/) as part of the DynamicalSystems.jl software library. +This new continuation is called _global_ continuation, while the one of BifurcationKit.jl is called _local_ continuation. + +Instead of continuing an individual fixed point or limit cycle, the global continuation finds all attractors of the dynamical system and continues all of them, in parallel, in a single continuation. It distinguishes and labels automatically the different attractors. +Hence "multi-" for multiple attractors. +Another key difference is that instead of estimating the local (or linear, or Jacobian) stability of the attractors, it estimates various measures of _nonlocal_ stability (e.g, related with the size of the basins of attraction, or the size of a perturbation that would make the dynamical system state converge to an alternative attractor). +Hence the "nonlocal-" component. +More differences and pros & cons are discussed in the documentation of Attractors.jl. + +!!! note "Attractors and basins" + This tutorial assumes that you have some familiarity with dynamical systems, + specifically what are attractors and basins of attraction. If you don't have + this yet, we recommend Chapter 1 of the textbook + [Nonlinear Dynamics](https://link.springer.com/book/10.1007/978-3-030-91032-7). + +## Creating the `DynamicalSystem` via MTK + +Let's showcase this framework by modelling a chaotic bistable dynamical system that we define via ModelingToolkit.jl, which will the be casted into a `DynamicalSystem` type for the DynamicalSystems.jl library. The equations of our system are + +```@example Attractors +using ModelingToolkit +using ModelingToolkit: t_nounits as t, D_nounits as D + +@variables x(t) = -4.0 y(t) = 5.0 z(t) = 0.0 +@parameters a = 5.0 b = 0.1 + +eqs = [ + D(x) ~ y - x, + D(y) ~ - x*z + b*abs(z), + D(z) ~ x*y - a, +] +``` + +Because our dynamical system is super simple, we will directly make an `ODESystem` and cast it in an `ODEProblem` as in the [`ODESystems` tutorial](@ref programmatically). Since all state variables and parameters have a default value we can immediately write + +```@example Attractors +@named modlorenz = ODESystem(eqs, t) +ssys = structural_simplify(modlorenz) +# The timespan given to the problem is irrelevant for DynamicalSystems.jl +prob = ODEProblem(ssys, [], (0.0, 1.0), []) +``` + +This `prob` can be turned to a dynamical system as simply as + +```@example Attractors +using Attractors # or `DynamicalSystems` +ds = CoupledODEs(prob) +``` + +DynamicalSystems.jl integrates fully with ModelingToolkit.jl out of the box and understands whether a given problem has been created via ModelingToolkit.jl. For example you can use the symbolic variables, or their `Symbol` representations, to access a system state or parameter + +```@example Attractors +observe_state(ds, x) +``` + +```@example Attractors +current_parameter(ds, :a) # or `a` directly +``` + +## Finding all attractors in the state space + +Attractors.jl provides an extensive interface for finding all (within a state space region and numerical accuracy) attractors of a dynamical system. +This interface is structured around the type `AttractorMapper` and is discussed in the Attractors.jl documentation in detail. Here we will briefly mention one of the possible approaches, the recurrences-based algorithm. It finds attractors by finding locations in the state space where the trajectory returns again and again. + +To use this technique, we first need to create a tessellation of the state space + +```@example Attractors +grid = ( + range(-15.0, 15.0; length = 150), # x + range(-20.0, 20.0; length = 150), # y + range(-20.0, 20.0; length = 150), # z +) +``` + +which we then give as input to the `AttractorsViaRecurrences` mapper along with the dynamical system + +```@example Attractors +mapper = AttractorsViaRecurrences(ds, grid; + consecutive_recurrences = 1000, + consecutive_lost_steps = 100, +) +``` +to learn about the metaparameters of the algorithm visit the documentation of Attractors.jl. + +This `mapper` object is incredibly powerful! It can be used to map initial conditions to attractor they converge to, while ensuring that initial conditions that converge to the same attractor are given the same label. +For example, if we use the `mapper` as a function and give it an initial condition we get + +```@example Attractors +mapper([-4.0, 5, 0]) +``` + +```@example Attractors +mapper([4.0, 2, 0]) +``` + +```@example Attractors +mapper([1.0, 3, 2]) +``` + +The numbers returned are simply the unique identifiers of the attractors the initial conditions converged to. + +DynamicalSystems.jl library is the only dynamical systems software (in any language) that provides such an infrastructure for mapping initial conditions of any arbitrary dynamical system to its unique attractors. And this is only the tip of this iceberg! The rest of the functionality of Attractors.jl is all full of brand new cutting edge progress in dynamical systems research. + +The found attractors are stored in the mapper internally, to obtain them we +use the function + +```@example Attractors +attractors = extract_attractors(mapper) +``` + +This is a dictionary that maps attractor IDs to the attractor sets themselves. +`StateSpaceSet` is a wrapper of a vector of points and behaves exactly like a vector of points. We can plot them easily like + +```@example Attractors +using CairoMakie +fig = Figure() +ax = Axis(fig[1,1]) +colors = ["#7143E0", "#191E44"] +for (id, A) in attractors + scatter!(ax, A[:, [1, 3]]; color = colors[id]) +end +fig +``` + +## Basins of attraction + +Estimating the basins of attraction of these attractor is a matter of a couple lines of code. First we define the state space are to estimate the basins for. Here we can re-use the `grid` we defined above. Then we only have to call + +```julia +basins = basins_of_attraction(mapper, grid) +``` + +We won't run this in this tutorial because it is a length computation (150×150×150). +We will however estimate a slice of the 3D basins of attraction. +DynamicalSystems.jl allows for a rather straightforward setting of initial conditions: +```@example Attractors +ics = [Dict(:x => x, :y => 0, :z=>z) for x in grid[1] for z in grid[3]] +``` + +now we can estimate the basins of attraction on a slice on the x-z grid + +```@example Attractors +fs, labels = basins_fractions(mapper, ics) +labels = reshape(labels, (length(grid[1]), length(grid[3]))) +``` + +and visualize them + +```@example Attractors +heatmap(grid[1], grid[3], labels; colormap = colors) +``` + +## Global continuation + +We've already outlined the principles of the global continuation, so let's just do it here! +We first have to define a global continuation algorithm, which for this tutorial, +it is just a wrapper of the existing `mapper` + +```@example Attractors +ascm = AttractorSeedContinueMatch(mapper); +``` + +we need two more ingredients to perform the global continuation. +One is a sampler of initial conditions in the state space. +Here we'll uniformly sample initial conditions within this grid we have already defined + +```@example Attractors +sampler, = statespace_sampler(grid); +``` + +the last ingredient is what parameter(s) to perform the continuation over. +In contrast to local continuation, where we can only specify a parameter range, in global continuation one can specify an exact parameter curve to continue over. +This curve can span any-dimensional parameter space, in contrast to the 1D or 2D parameter spaces supported in local continuation. +Here we will use the curve + +```@example Attractors +params(θ) = [:a => 5 + 0.5cos(θ), :b => 0.1 + 0.01sin(θ)] +θs = range(0, 2π; length = 101) +pcurve = params.(θs) +``` +which makes an ellipsis over the parameter space. + +We put these three ingredients together to call the global continuation + +```@example Attractors +fractions_cont, attractors_cont = global_continuation(ascm, pcurve, sampler); +``` + +The output of the continuation is how the attractors and their basins fractions change over this parameter curve. We can visualize this directly using a convenience function + +```@example Attractors +fig = plot_basins_attractors_curves( + fractions_cont, attractors_cont, A -> minimum(A[:, 1]), θs, +) +``` + +The top panel shows the relative basins of attractions of the attractors and the bottom panel shows their minimum x-position. The colors correspond to unique attractors. Perhaps making a video is easier to understand: + +```@example Attractors +animate_attractors_continuation( + ds, attractors_cont, fractions_cont, pcurve; + savename = "curvecont.mp4" +); +``` + +```@raw html + +``` + +To learn more about this global continuation and its various options, and more details about how it compares with local continuation, visit the documentation of [Attractors.jl](https://juliadynamics.github.io/DynamicalSystemsDocs.jl/attractors/stable/). From 007ffd144908b257439caf0db04bc29d98bb7034 Mon Sep 17 00:00:00 2001 From: George Datseris Date: Sun, 30 Mar 2025 10:28:40 +0100 Subject: [PATCH 1225/1337] Add new page to pages.jl --- docs/pages.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/pages.jl b/docs/pages.jl index 70a10c436f..97de7d8f76 100644 --- a/docs/pages.jl +++ b/docs/pages.jl @@ -12,6 +12,7 @@ pages = [ "tutorials/parameter_identifiability.md", "tutorials/change_independent_variable.md", "tutorials/bifurcation_diagram_computation.md", + "tutorials/attractors.md", "tutorials/SampledData.md", "tutorials/domain_connections.md", "tutorials/callable_params.md", From b864c4ba9de666f1083779c3e595d71c394d6991 Mon Sep 17 00:00:00 2001 From: George Datseris Date: Sun, 30 Mar 2025 10:29:46 +0100 Subject: [PATCH 1226/1337] Add CairoMakie as a docs dependency --- docs/Project.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/Project.toml b/docs/Project.toml index e1df906ca5..0dcaf23262 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -1,6 +1,7 @@ [deps] BenchmarkTools = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf" BifurcationKit = "0f109fa4-8a5d-4b75-95aa-f515264e7665" +CairoMakie = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0" ControlSystemsBase = "aaaaaaaa-a6ca-5380-bf3e-84a91bcd477e" DataInterpolations = "82cc6244-b520-54b8-b5a6-8a565e85f1d0" Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f" @@ -29,6 +30,7 @@ Unitful = "1986cc42-f94f-5a68-af5c-568840ba703d" [compat] BenchmarkTools = "1.3" BifurcationKit = "0.4" +CairoMakie = "0.13" DataInterpolations = "6.5, 8" Distributions = "0.25" Documenter = "1" From 83c3a45e386ae79997329b1735071009a0dc62ab Mon Sep 17 00:00:00 2001 From: George Datseris Date: Sun, 30 Mar 2025 10:32:40 +0100 Subject: [PATCH 1227/1337] Update docs/src/tutorials/attractors.md --- docs/src/tutorials/attractors.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/src/tutorials/attractors.md b/docs/src/tutorials/attractors.md index 56e6026427..ac4a643dbe 100644 --- a/docs/src/tutorials/attractors.md +++ b/docs/src/tutorials/attractors.md @@ -130,7 +130,9 @@ fig ## Basins of attraction -Estimating the basins of attraction of these attractor is a matter of a couple lines of code. First we define the state space are to estimate the basins for. Here we can re-use the `grid` we defined above. Then we only have to call +Estimating the basins of attraction of these attractors is a matter of a couple lines of code. +First we define the state space are to estimate the basins for. +Here we can re-use the `grid` we defined above. Then we only have to call ```julia basins = basins_of_attraction(mapper, grid) From 92344eccee9df29cf5840008f85b541d92d3e02e Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 1 Apr 2025 12:11:50 +0530 Subject: [PATCH 1228/1337] test: make homotopy continuation test more robust --- test/extensions/homotopy_continuation.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/extensions/homotopy_continuation.jl b/test/extensions/homotopy_continuation.jl index ed630ea8e3..ee13b89992 100644 --- a/test/extensions/homotopy_continuation.jl +++ b/test/extensions/homotopy_continuation.jl @@ -151,7 +151,7 @@ end @variables x=0.25 y=0.125 a = sin(x^2 - 4x + 1) b = cos(3log(y) + 4) - @mtkbuild sys = NonlinearSystem([(a^2 - 4a * b + 4b^2) / (a - 0.25) ~ 0 + @mtkbuild sys = NonlinearSystem([(a^2 - 5a * b + 6b^2) / (a - 0.25) ~ 0 (a^2 - 0.75a + 0.125) ~ 0]) prob = HomotopyContinuationProblem(sys, []) @test prob[x] ≈ 0.25 @@ -159,7 +159,7 @@ end sol = solve(prob, allrootsalg).u[1] @test SciMLBase.successful_retcode(sol) @test sol[a]≈0.5 atol=1e-6 - @test sol[b]≈0.25 atol=1e-6 + @test isapprox(sol[b], 0.25; atol = 1e-6) || isapprox(sol[b], 0.5 / 3; atol = 1e-6) end @testset "Rational functions" begin From 6a5a885eaeae8078eee120b9098dac20322c8d89 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 1 Apr 2025 12:53:45 +0530 Subject: [PATCH 1229/1337] test: make homotopy continuation test more robust --- test/extensions/homotopy_continuation.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/extensions/homotopy_continuation.jl b/test/extensions/homotopy_continuation.jl index ee13b89992..65f7d765a4 100644 --- a/test/extensions/homotopy_continuation.jl +++ b/test/extensions/homotopy_continuation.jl @@ -164,9 +164,9 @@ end @testset "Rational functions" begin @variables x=2.0 y=2.0 - @parameters n = 4 + @parameters n = 5 @mtkbuild sys = NonlinearSystem([ - 0 ~ (x^2 - n * x + n) * (x - 1) / (x - 2) / (x - 3) + 0 ~ (x^2 - n * x + 6) * (x - 1) / (x - 2) / (x - 3) ]) prob = HomotopyContinuationProblem(sys, []) sol = solve_allroots_closest(prob) @@ -184,7 +184,7 @@ end 0 ~ ((y - 3) / (y - 4)) * (n / (y - 5)) + ((x - 1.5) / (x - 5.5))^2 ], [x, y], - [n]) + [n]; defaults = [n => 4]) sys = complete(sys) prob = HomotopyContinuationProblem(sys, []) sol = solve(prob, singlerootalg) From 9ef40dc3cce0f2d26555b6478f04b45b90bc5b11 Mon Sep 17 00:00:00 2001 From: Christopher Rackauckas Date: Tue, 1 Apr 2025 03:44:33 -0400 Subject: [PATCH 1230/1337] Delete .github/workflows/Invalidations.yml --- .github/workflows/Invalidations.yml | 17 ----------------- 1 file changed, 17 deletions(-) delete mode 100644 .github/workflows/Invalidations.yml diff --git a/.github/workflows/Invalidations.yml b/.github/workflows/Invalidations.yml deleted file mode 100644 index 1e5662eb28..0000000000 --- a/.github/workflows/Invalidations.yml +++ /dev/null @@ -1,17 +0,0 @@ -name: "Invalidations" - -on: - pull_request: - paths-ignore: - - 'docs/**' - -concurrency: - # Skip intermediate builds: always. - # Cancel intermediate builds: always. - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - evaluate-invalidations: - name: "Evaluate Invalidations" - uses: "SciML/.github/.github/workflows/invalidations.yml@v1" From a46788281f45c9d1f7b14b4cdb91f2547c766fae Mon Sep 17 00:00:00 2001 From: George Datseris Date: Tue, 1 Apr 2025 08:50:21 +0100 Subject: [PATCH 1231/1337] add Attractors.jl to Project.toml --- docs/Project.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/Project.toml b/docs/Project.toml index 0dcaf23262..2f7a3c1317 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -1,4 +1,5 @@ [deps] +Attractors = "f3fd9213-ca85-4dba-9dfd-7fc91308fec7" BenchmarkTools = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf" BifurcationKit = "0f109fa4-8a5d-4b75-95aa-f515264e7665" CairoMakie = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0" @@ -28,6 +29,7 @@ Symbolics = "0c5d862f-8b57-4792-8d23-62f2024744c7" Unitful = "1986cc42-f94f-5a68-af5c-568840ba703d" [compat] +Attractors = "1.24" BenchmarkTools = "1.3" BifurcationKit = "0.4" CairoMakie = "0.13" From 9781a5ee7d71dfd00312695b3af138ba4fc8512f Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 1 Apr 2025 15:24:53 +0530 Subject: [PATCH 1232/1337] build: bump minor version --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index a9290b037a..6e01126270 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "ModelingToolkit" uuid = "961ee093-0014-501f-94e3-6117800e7a78" authors = ["Yingbo Ma ", "Chris Rackauckas and contributors"] -version = "9.69.0" +version = "9.70.0" [deps] ADTypes = "47edcb42-4c32-4615-8424-f2b9edc5f35b" From cb9475156066c3396e8fc0d5eb89b357d2dd083f Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 1 Apr 2025 16:32:49 +0530 Subject: [PATCH 1233/1337] refactor: don't mark array variables as irreducible in `TearingState` --- src/systems/systemstructure.jl | 1 - 1 file changed, 1 deletion(-) diff --git a/src/systems/systemstructure.jl b/src/systems/systemstructure.jl index c0c4a5ff4d..d27e5c93a1 100644 --- a/src/systems/systemstructure.jl +++ b/src/systems/systemstructure.jl @@ -299,7 +299,6 @@ function TearingState(sys; quick_cancel = false, check = true) end v = scalarize(v) if v isa AbstractArray - v = setmetadata.(v, VariableIrreducible, true) append!(varsvec, v) else push!(varsvec, v) From bd95c856e18830835ff259ae996e463ae48dab02 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 1 Apr 2025 15:20:22 +0530 Subject: [PATCH 1234/1337] feat: add latexify recipe for `AnalysisPoint` --- src/systems/analysis_points.jl | 13 +++++++++++++ test/latexify.jl | 11 +++++++++++ test/latexify/50.tex | 19 +++++++++++++++++++ 3 files changed, 43 insertions(+) create mode 100644 test/latexify/50.tex diff --git a/src/systems/analysis_points.jl b/src/systems/analysis_points.jl index 104c26cbfe..0d1a2830cf 100644 --- a/src/systems/analysis_points.jl +++ b/src/systems/analysis_points.jl @@ -140,6 +140,19 @@ function Base.show(io::IO, ::MIME"text/plain", ap::AnalysisPoint) end end +@latexrecipe function f(ap::AnalysisPoint) + index --> :subscript + snakecase --> true + ap.input === nothing && return 0 + outs = Expr(:vect) + append!(outs.args, ap_var.(ap.outputs)) + return Expr(:call, :AnalysisPoint, ap_var(ap.input), ap.name, outs) +end + +function Base.show(io::IO, ::MIME"text/latex", ap::AnalysisPoint) + print(io, latexify(ap)) +end + """ $(TYPEDSIGNATURES) diff --git a/test/latexify.jl b/test/latexify.jl index eabaeacdd8..105a17ca6e 100644 --- a/test/latexify.jl +++ b/test/latexify.jl @@ -3,6 +3,7 @@ using Latexify using ModelingToolkit using ReferenceTests using ModelingToolkit: t_nounits as t, D_nounits as D +using ModelingToolkitStandardLibrary.Blocks ### Tips for generating latex tests: ### Latexify has an unexported macro: @@ -47,3 +48,13 @@ eqs = [D(u[1]) ~ p[3] * (u[2] - u[1]), eqs = [D(x) ~ (1 + cos(t)) / (1 + 2 * x)] @test_reference "latexify/40.tex" latexify(eqs) + +@named P = FirstOrder(k = 1, T = 1) +@named C = Gain(; k = -1) + +ap = AnalysisPoint(:plant_input) +eqs = [connect(P.output, C.input) + connect(C.output, ap, P.input)] +sys_ap = ODESystem(eqs, t, systems = [P, C], name = :hej) + +@test_reference "latexify/50.tex" latexify(sys_ap) diff --git a/test/latexify/50.tex b/test/latexify/50.tex new file mode 100644 index 0000000000..b1e6a1fda4 --- /dev/null +++ b/test/latexify/50.tex @@ -0,0 +1,19 @@ +\begin{equation} +\left[ +\begin{array}{c} +\mathrm{connect}\left( P_{+}output, C_{+}input \right) \\ +AnalysisPoint\left( \mathtt{C.output.u}\left( t \right), plant\_input, \left[ +\begin{array}{c} +\mathtt{P.input.u}\left( t \right) \\ +\end{array} +\right] \right) \\ +\mathtt{P.u}\left( t \right) = \mathtt{P.input.u}\left( t \right) \\ +\mathtt{P.y}\left( t \right) = \mathtt{P.output.u}\left( t \right) \\ +\mathtt{P.y}\left( t \right) = \mathtt{P.x}\left( t \right) \\ +\frac{\mathrm{d} \mathtt{P.x}\left( t \right)}{\mathrm{d}t} = \frac{ - \mathtt{P.x}\left( t \right) + \mathtt{P.k} \mathtt{P.u}\left( t \right)}{\mathtt{P.T}} \\ +\mathtt{C.u}\left( t \right) = \mathtt{C.input.u}\left( t \right) \\ +\mathtt{C.y}\left( t \right) = \mathtt{C.output.u}\left( t \right) \\ +\mathtt{C.y}\left( t \right) = \mathtt{C.k} \mathtt{C.u}\left( t \right) \\ +\end{array} +\right] +\end{equation} From 28447a161f7490afc3687591358a0e46aa16e380 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 1 Apr 2025 23:37:12 +0530 Subject: [PATCH 1235/1337] build: bump Symbolics compat --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 6e01126270..5b4f97e815 100644 --- a/Project.toml +++ b/Project.toml @@ -151,7 +151,7 @@ StochasticDelayDiffEq = "1.8.1" StochasticDiffEq = "6.72.1" SymbolicIndexingInterface = "0.3.37" SymbolicUtils = "3.14" -Symbolics = "6.29.2" +Symbolics = "6.36" URIs = "1" UnPack = "0.1, 1.0" Unitful = "1.1" From b96c611e79db24e6635dbd1c9f80e136ae89b2e5 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 2 Apr 2025 11:46:36 +0530 Subject: [PATCH 1236/1337] refactor: format --- docs/src/tutorials/attractors.md | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/docs/src/tutorials/attractors.md b/docs/src/tutorials/attractors.md index ac4a643dbe..317384b01a 100644 --- a/docs/src/tutorials/attractors.md +++ b/docs/src/tutorials/attractors.md @@ -14,6 +14,7 @@ Hence the "nonlocal-" component. More differences and pros & cons are discussed in the documentation of Attractors.jl. !!! note "Attractors and basins" + This tutorial assumes that you have some familiarity with dynamical systems, specifically what are attractors and basins of attraction. If you don't have this yet, we recommend Chapter 1 of the textbook @@ -27,13 +28,13 @@ Let's showcase this framework by modelling a chaotic bistable dynamical system t using ModelingToolkit using ModelingToolkit: t_nounits as t, D_nounits as D -@variables x(t) = -4.0 y(t) = 5.0 z(t) = 0.0 -@parameters a = 5.0 b = 0.1 +@variables x(t)=-4.0 y(t)=5.0 z(t)=0.0 +@parameters a=5.0 b=0.1 eqs = [ D(x) ~ y - x, - D(y) ~ - x*z + b*abs(z), - D(z) ~ x*y - a, + D(y) ~ -x * z + b * abs(z), + D(z) ~ x * y - a ] ``` @@ -74,7 +75,7 @@ To use this technique, we first need to create a tessellation of the state space grid = ( range(-15.0, 15.0; length = 150), # x range(-20.0, 20.0; length = 150), # y - range(-20.0, 20.0; length = 150), # z + range(-20.0, 20.0; length = 150) # z ) ``` @@ -83,9 +84,10 @@ which we then give as input to the `AttractorsViaRecurrences` mapper along with ```@example Attractors mapper = AttractorsViaRecurrences(ds, grid; consecutive_recurrences = 1000, - consecutive_lost_steps = 100, + consecutive_lost_steps = 100 ) ``` + to learn about the metaparameters of the algorithm visit the documentation of Attractors.jl. This `mapper` object is incredibly powerful! It can be used to map initial conditions to attractor they converge to, while ensuring that initial conditions that converge to the same attractor are given the same label. @@ -120,7 +122,7 @@ This is a dictionary that maps attractor IDs to the attractor sets themselves. ```@example Attractors using CairoMakie fig = Figure() -ax = Axis(fig[1,1]) +ax = Axis(fig[1, 1]) colors = ["#7143E0", "#191E44"] for (id, A) in attractors scatter!(ax, A[:, [1, 3]]; color = colors[id]) @@ -130,8 +132,8 @@ fig ## Basins of attraction -Estimating the basins of attraction of these attractors is a matter of a couple lines of code. -First we define the state space are to estimate the basins for. +Estimating the basins of attraction of these attractors is a matter of a couple lines of code. +First we define the state space are to estimate the basins for. Here we can re-use the `grid` we defined above. Then we only have to call ```julia @@ -141,8 +143,9 @@ basins = basins_of_attraction(mapper, grid) We won't run this in this tutorial because it is a length computation (150×150×150). We will however estimate a slice of the 3D basins of attraction. DynamicalSystems.jl allows for a rather straightforward setting of initial conditions: + ```@example Attractors -ics = [Dict(:x => x, :y => 0, :z=>z) for x in grid[1] for z in grid[3]] +ics = [Dict(:x => x, :y => 0, :z => z) for x in grid[1] for z in grid[3]] ``` now we can estimate the basins of attraction on a slice on the x-z grid @@ -186,6 +189,7 @@ params(θ) = [:a => 5 + 0.5cos(θ), :b => 0.1 + 0.01sin(θ)] θs = range(0, 2π; length = 101) pcurve = params.(θs) ``` + which makes an ellipsis over the parameter space. We put these three ingredients together to call the global continuation @@ -198,7 +202,7 @@ The output of the continuation is how the attractors and their basins fractions ```@example Attractors fig = plot_basins_attractors_curves( - fractions_cont, attractors_cont, A -> minimum(A[:, 1]), θs, + fractions_cont, attractors_cont, A -> minimum(A[:, 1]), θs ) ``` From 908c32b4a72f4cc43868730eeefa0c2a0e192315 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 26 Mar 2025 16:25:03 +0530 Subject: [PATCH 1237/1337] feat: add `substitute_component` --- src/ModelingToolkit.jl | 2 +- src/systems/abstractsystem.jl | 177 ++++++++++++++++++++++ test/runtests.jl | 1 + test/substitute_component.jl | 269 ++++++++++++++++++++++++++++++++++ 4 files changed, 448 insertions(+), 1 deletion(-) create mode 100644 test/substitute_component.jl diff --git a/src/ModelingToolkit.jl b/src/ModelingToolkit.jl index b53d3ec098..99d38c7b33 100644 --- a/src/ModelingToolkit.jl +++ b/src/ModelingToolkit.jl @@ -291,7 +291,7 @@ export isinput, isoutput, getbounds, hasbounds, getguess, hasguess, isdisturbanc hasunit, getunit, hasconnect, getconnect, hasmisc, getmisc, state_priority export ode_order_lowering, dae_order_lowering, liouville_transform, - change_independent_variable + change_independent_variable, substitute_component export PDESystem export Differential, expand_derivatives, @derivatives export Equation, ConstrainedEquation diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index fb692b4028..0372858b3a 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -3181,3 +3181,180 @@ has_diff_eqs(osys21) # returns `false`. ``` """ has_diff_eqs(sys::AbstractSystem) = any(is_diff_equation, get_eqs(sys)) + +""" + $(TYPEDSIGNATURES) + +Validate the rules for replacement of subcomponents as defined in `substitute_component`. +""" +function validate_replacement_rule( + rule::Pair{T, T}; namespace = []) where {T <: AbstractSystem} + lhs, rhs = rule + + iscomplete(lhs) && throw(ArgumentError("LHS of replacement rule cannot be completed.")) + iscomplete(rhs) && throw(ArgumentError("RHS of replacement rule cannot be completed.")) + + rhs_h = namespace_hierarchy(nameof(rhs)) + if length(rhs_h) != 1 + throw(ArgumentError("RHS of replacement rule must not be namespaced.")) + end + rhs_h[1] == namespace_hierarchy(nameof(lhs))[end] || + throw(ArgumentError("LHS and RHS must have the same name.")) + + if !isequal(get_iv(lhs), get_iv(rhs)) + throw(ArgumentError("LHS and RHS of replacement rule must have the same independent variable.")) + end + + lhs_u = get_unknowns(lhs) + rhs_u = Dict(get_unknowns(rhs) .=> nothing) + for u in lhs_u + if !haskey(rhs_u, u) + if isempty(namespace) + throw(ArgumentError("RHS of replacement rule does not contain unknown $u.")) + else + throw(ArgumentError("Subsystem $(join([namespace; nameof(lhs)], NAMESPACE_SEPARATOR)) of RHS does not contain unknown $u.")) + end + end + ru = getkey(rhs_u, u, nothing) + name = join([namespace; nameof(lhs); (hasname(u) ? getname(u) : Symbol(u))], + NAMESPACE_SEPARATOR) + l_connect = something(getconnect(u), Equality) + r_connect = something(getconnect(ru), Equality) + if l_connect != r_connect + throw(ArgumentError("Variable $(name) should have connection metadata $(l_connect),")) + end + + l_input = isinput(u) + r_input = isinput(ru) + if l_input != r_input + throw(ArgumentError("Variable $name has differing causality. Marked as `input = $l_input` in LHS and `input = $r_input` in RHS.")) + end + l_output = isoutput(u) + r_output = isoutput(ru) + if l_output != r_output + throw(ArgumentError("Variable $name has differing causality. Marked as `output = $l_output` in LHS and `output = $r_output` in RHS.")) + end + end + + lhs_p = get_ps(lhs) + rhs_p = Set(get_ps(rhs)) + for p in lhs_p + if !(p in rhs_p) + if isempty(namespace) + throw(ArgumentError("RHS of replacement rule does not contain parameter $p")) + else + throw(ArgumentError("Subsystem $(join([namespace; nameof(lhs)], NAMESPACE_SEPARATOR)) of RHS does not contain parameter $p.")) + end + end + end + + lhs_s = get_systems(lhs) + rhs_s = Dict(nameof(s) => s for s in get_systems(rhs)) + + for s in lhs_s + if haskey(rhs_s, nameof(s)) + rs = rhs_s[nameof(s)] + if isconnector(s) + name = join([namespace; nameof(lhs); nameof(s)], NAMESPACE_SEPARATOR) + if !isconnector(rs) + throw(ArgumentError("Subsystem $name of RHS is not a connector.")) + end + if (lct = get_connector_type(s)) !== (rct = get_connector_type(rs)) + throw(ArgumentError("Subsystem $name of RHS has connection type $rct but LHS has $lct.")) + end + end + validate_replacement_rule(s => rs; namespace = [namespace; nameof(rhs)]) + continue + end + name1 = join([namespace; nameof(lhs)], NAMESPACE_SEPARATOR) + throw(ArgumentError("$name1 of replacement rule does not contain subsystem $(nameof(s)).")) + end +end + +""" + $(TYPEDSIGNATURES) + +Chain `getproperty` calls on `root` in the order given in `hierarchy`. + +# Keyword Arguments + +- `skip_namespace_first`: Whether to avoid namespacing in the first `getproperty` call. +""" +function recursive_getproperty( + root::AbstractSystem, hierarchy::Vector{Symbol}; skip_namespace_first = true) + cur = root + for (i, name) in enumerate(hierarchy) + cur = getproperty(cur, name; namespace = i > 1 || !skip_namespace_first) + end + return cur +end + +""" + $(TYPEDSIGNATURES) + +Recursively descend through `sys`, finding all connection equations and re-creating them +using the names of the involved variables/systems and finding the required variables/ +systems in the hierarchy. +""" +function recreate_connections(sys::AbstractSystem) + eqs = map(get_eqs(sys)) do eq + eq.lhs isa Union{Connection, AnalysisPoint} || return eq + if eq.lhs isa Connection + oldargs = get_systems(eq.rhs) + else + ap::AnalysisPoint = eq.rhs + oldargs = [ap.input; ap.outputs] + end + newargs = map(get_systems(eq.rhs)) do arg + name = arg isa AbstractSystem ? nameof(arg) : getname(arg) + hierarchy = namespace_hierarchy(name) + return recursive_getproperty(sys, hierarchy) + end + if eq.lhs isa Connection + return eq.lhs ~ Connection(newargs) + else + return eq.lhs ~ AnalysisPoint(newargs[1], eq.rhs.name, newargs[2:end]) + end + end + @set! sys.eqs = eqs + @set! sys.systems = map(recreate_connections, get_systems(sys)) + return sys +end + +""" + $(TYPEDSIGNATURES) + +Given a hierarchical system `sys` and a rule `lhs => rhs`, replace the subsystem `lhs` in +`sys` by `rhs`. The `lhs` must be the namespaced version of a subsystem of `sys` (e.g. +obtained via `sys.inner.component`). The `rhs` must be valid as per the following +conditions: + +1. `rhs` must not be namespaced. +2. The name of `rhs` must be the same as the unnamespaced name of `lhs`. +3. Neither one of `lhs` or `rhs` can be marked as complete. +4. Both `lhs` and `rhs` must share the same independent variable. +5. `rhs` must contain at least all of the unknowns and parameters present in + `lhs`. +6. Corresponding unknowns in `rhs` must share the same connection and causality + (input/output) metadata as their counterparts in `lhs`. +7. For each subsystem of `lhs`, there must be an identically named subsystem of `rhs`. + These two corresponding subsystems must satisfy conditions 3, 4, 5, 6, 7. If the + subsystem of `lhs` is a connector, the corresponding subsystem of `rhs` must also + be a connector of the same type. + +`sys` also cannot be marked as complete. +""" +function substitute_component(sys::T, rule::Pair{T, T}) where {T <: AbstractSystem} + iscomplete(sys) && + throw(ArgumentError("Cannot replace subsystems of completed systems")) + + validate_replacement_rule(rule) + + lhs, rhs = rule + hierarchy = namespace_hierarchy(nameof(lhs)) + + newsys, _ = modify_nested_subsystem(sys, hierarchy) do inner + return rhs, () + end + return recreate_connections(newsys) +end diff --git a/test/runtests.jl b/test/runtests.jl index 301134219e..04d99cc8b9 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -98,6 +98,7 @@ end @safetestset "Causal Variables Connection Test" include("causal_variables_connection.jl") @safetestset "Debugging Test" include("debugging.jl") @safetestset "Namespacing test" include("namespacing.jl") + @safetestset "Subsystem replacement" include("substitute_component.jl") end end diff --git a/test/substitute_component.jl b/test/substitute_component.jl new file mode 100644 index 0000000000..f9dd8580c7 --- /dev/null +++ b/test/substitute_component.jl @@ -0,0 +1,269 @@ +using ModelingToolkit, ModelingToolkitStandardLibrary, Test +using ModelingToolkitStandardLibrary.Blocks +using ModelingToolkitStandardLibrary.Electrical +using OrdinaryDiffEq +using ModelingToolkit: t_nounits as t, D_nounits as D, renamespace, + NAMESPACE_SEPARATOR as NS + +@mtkmodel TwoComponent begin + @parameters begin + V = 1.0 + end + @components begin + component1 = OnePort() + component2 = OnePort() + source = Voltage() + constant = Constant(k = V) + ground = Ground() + end + @equations begin + connect(constant.output, source.V) + connect(source.p, component1.p) + connect(component1.n, component2.p) + connect(component2.n, source.n, ground.g) + end +end + +@mtkmodel RC begin + @parameters begin + R = 1.0 + C = 1.0 + V = 1.0 + end + @components begin + component1 = Resistor(R = R) + component2 = Capacitor(C = C, v = 0.0) + source = Voltage() + constant = Constant(k = V) + ground = Ground() + end + @equations begin + connect(constant.output, source.V) + connect(source.p, component1.p) + connect(component1.n, component2.p) + connect(component2.n, source.n, ground.g) + end +end + +@testset "Replacement with connections works" begin + @named templated = TwoComponent() + @named component1 = Resistor(R = 1.0) + @named component2 = Capacitor(C = 1.0, v = 0.0) + rsys = substitute_component(templated, templated.component1 => component1) + rcsys = substitute_component(rsys, rsys.component2 => component2) + + @named reference = RC() + + sys1 = structural_simplify(rcsys) + sys2 = structural_simplify(reference) + @test isequal(unknowns(sys1), unknowns(sys2)) + @test isequal(observed(sys1), observed(sys2)) + @test isequal(equations(sys1), equations(sys2)) + + prob1 = ODEProblem(sys1, [], (0.0, 10.0)) + prob2 = ODEProblem(sys2, [], (0.0, 10.0)) + + sol1 = solve(prob1, Tsit5()) + sol2 = solve(prob2, Tsit5(); saveat = sol1.t) + @test sol1.u≈sol2.u atol=1e-8 +end + +@mtkmodel BadOnePort1 begin + @components begin + p = Pin() + n = Pin() + end + @variables begin + i(t) + end + @equations begin + 0 ~ p.i + n.i + i ~ p.i + end +end + +@connector BadPin1 begin + v(t) +end + +@mtkmodel BadOnePort2 begin + @components begin + p = BadPin1() + n = Pin() + end + @variables begin + v(t) + i(t) + end + @equations begin + 0 ~ p.v + n.v + v ~ p.v + end +end + +@connector BadPin2 begin + v(t) + i(t) +end + +@mtkmodel BadOnePort3 begin + @components begin + p = BadPin2() + n = Pin() + end + @variables begin + v(t) + i(t) + end + @equations begin + 0 ~ p.v + n.v + v ~ p.v + end +end + +@connector BadPin3 begin + v(t), [input = true] + i(t), [connect = Flow] +end + +@mtkmodel BadOnePort4 begin + @components begin + p = BadPin3() + n = Pin() + end + @variables begin + v(t) + i(t) + end + @equations begin + 0 ~ p.v + n.v + v ~ p.v + end +end + +@connector BadPin4 begin + v(t), [output = true] + i(t), [connect = Flow] +end + +@mtkmodel BadOnePort5 begin + @components begin + p = BadPin4() + n = Pin() + end + @variables begin + v(t) + i(t) + end + @equations begin + 0 ~ p.v + n.v + v ~ p.v + end +end + +@mtkmodel BadPin5 begin + @variables begin + v(t) + i(t), [connect = Flow] + end +end + +@mtkmodel BadOnePort6 begin + @components begin + p = BadPin5() + n = Pin() + end + @variables begin + v(t) + i(t) + end + @equations begin + 0 ~ p.v + n.v + v ~ p.v + end +end + +@connector BadPin6 begin + i(t), [connect = Flow] +end + +@mtkmodel BadOnePort7 begin + @components begin + p = BadPin6() + n = Pin() + end + @variables begin + v(t) + i(t) + end + @equations begin + 0 ~ p.i + n.i + i ~ p.i + end +end + +@mtkmodel BadOnePort8 begin + @components begin + n = Pin() + end + @variables begin + v(t) + i(t) + end +end + +@testset "Error checking" begin + @named templated = TwoComponent() + @named component1 = Resistor(R = 1.0) + @named component2 = Capacitor(C = 1.0, v = 0.0) + @test_throws ["LHS", "cannot be completed"] substitute_component( + templated, complete(templated.component1) => component1) + @test_throws ["RHS", "cannot be completed"] substitute_component( + templated, templated.component1 => complete(component1)) + @test_throws ["RHS", "not be namespaced"] substitute_component( + templated, templated.component1 => renamespace(templated, component1)) + @named resistor = Resistor(R = 1.0) + @test_throws ["RHS", "same name"] substitute_component( + templated, templated.component1 => resistor) + + @testset "Different indepvar" begin + @independent_variables tt + @named empty = ODESystem(Equation[], t) + @named outer = ODESystem(Equation[], t; systems = [empty]) + @named empty = ODESystem(Equation[], tt) + @test_throws ["independent variable"] substitute_component( + outer, outer.empty => empty) + end + + @named component1 = BadOnePort1() + @test_throws ["RHS", "unknown", "v(t)"] substitute_component( + templated, templated.component1 => component1) + + @named component1 = BadOnePort2() + @test_throws ["component1$(NS)p", "i(t)"] substitute_component( + templated, templated.component1 => component1) + + @named component1 = BadOnePort3() + @test_throws ["component1$(NS)p$(NS)i", "Flow"] substitute_component( + templated, templated.component1 => component1) + + @named component1 = BadOnePort4() + @test_throws ["component1$(NS)p$(NS)v", "differing causality", "input"] substitute_component( + templated, templated.component1 => component1) + + @named component1 = BadOnePort5() + @test_throws ["component1$(NS)p$(NS)v", "differing causality", "output"] substitute_component( + templated, templated.component1 => component1) + + @named component1 = BadOnePort6() + @test_throws ["templated$(NS)component1$(NS)p", "not a connector"] substitute_component( + templated, templated.component1 => component1) + + @named component1 = BadOnePort7() + @test_throws ["templated$(NS)component1$(NS)p", "DomainConnector", "RegularConnector"] substitute_component( + templated, templated.component1 => component1) + + @named component1 = BadOnePort8() + @test_throws ["templated$(NS)component1", "subsystem p"] substitute_component( + templated, templated.component1 => component1) +end From e747405e6bf2ff73c480c3cf3f6b17351f50559c Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 28 Mar 2025 17:54:57 +0530 Subject: [PATCH 1238/1337] fix: test and handle scalar connections in `substitute_component` --- src/systems/abstractsystem.jl | 12 ++++++++++-- test/substitute_component.jl | 16 ++++++++++------ 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index 0372858b3a..299990951a 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -3286,7 +3286,7 @@ function recursive_getproperty( for (i, name) in enumerate(hierarchy) cur = getproperty(cur, name; namespace = i > 1 || !skip_namespace_first) end - return cur + return unwrap(cur) end """ @@ -3306,9 +3306,17 @@ function recreate_connections(sys::AbstractSystem) oldargs = [ap.input; ap.outputs] end newargs = map(get_systems(eq.rhs)) do arg + rewrap_nameof = arg isa SymbolicWithNameof + if rewrap_nameof + arg = arg.var + end name = arg isa AbstractSystem ? nameof(arg) : getname(arg) hierarchy = namespace_hierarchy(name) - return recursive_getproperty(sys, hierarchy) + newarg = recursive_getproperty(sys, hierarchy) + if rewrap_nameof + newarg = SymbolicWithNameof(newarg) + end + return newarg end if eq.lhs isa Connection return eq.lhs ~ Connection(newargs) diff --git a/test/substitute_component.jl b/test/substitute_component.jl index f9dd8580c7..9fb254136b 100644 --- a/test/substitute_component.jl +++ b/test/substitute_component.jl @@ -5,19 +5,22 @@ using OrdinaryDiffEq using ModelingToolkit: t_nounits as t, D_nounits as D, renamespace, NAMESPACE_SEPARATOR as NS -@mtkmodel TwoComponent begin - @parameters begin - V = 1.0 +@mtkmodel SignalInterface begin + @components begin + output = RealOutput() end +end + +@mtkmodel TwoComponent begin @components begin component1 = OnePort() component2 = OnePort() source = Voltage() - constant = Constant(k = V) + signal = SignalInterface() ground = Ground() end @equations begin - connect(constant.output, source.V) + connect(signal.output.u, source.V.u) connect(source.p, component1.p) connect(component1.n, component2.p) connect(component2.n, source.n, ground.g) @@ -49,15 +52,16 @@ end @named templated = TwoComponent() @named component1 = Resistor(R = 1.0) @named component2 = Capacitor(C = 1.0, v = 0.0) + @named signal = Constant(k = 1.0) rsys = substitute_component(templated, templated.component1 => component1) rcsys = substitute_component(rsys, rsys.component2 => component2) + rcsys = substitute_component(rcsys, rcsys.signal => signal) @named reference = RC() sys1 = structural_simplify(rcsys) sys2 = structural_simplify(reference) @test isequal(unknowns(sys1), unknowns(sys2)) - @test isequal(observed(sys1), observed(sys2)) @test isequal(equations(sys1), equations(sys2)) prob1 = ODEProblem(sys1, [], (0.0, 10.0)) From 2b8c0f645651508594e8d35c3c7e8779d8e0c5cc Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Sun, 30 Mar 2025 16:12:48 +0530 Subject: [PATCH 1239/1337] feat: enable CSE in `build_function_wrapper` --- src/systems/codegen_utils.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/systems/codegen_utils.jl b/src/systems/codegen_utils.jl index 1eeb7e026b..a3fe53b95d 100644 --- a/src/systems/codegen_utils.jl +++ b/src/systems/codegen_utils.jl @@ -132,7 +132,7 @@ function build_function_wrapper(sys::AbstractSystem, expr, args...; p_start = 2, wrap_delays = is_dde(sys), wrap_code = identity, add_observed = true, filter_observed = Returns(true), create_bindings = false, output_type = nothing, mkarray = nothing, - wrap_mtkparameters = true, extra_assignments = Assignment[], kwargs...) + wrap_mtkparameters = true, extra_assignments = Assignment[], cse = true, kwargs...) isscalar = !(expr isa AbstractArray || symbolic_type(expr) == ArraySymbolic()) # filter observed equations obs = filter(filter_observed, observed(sys)) @@ -234,7 +234,7 @@ function build_function_wrapper(sys::AbstractSystem, expr, args...; p_start = 2, if wrap_code isa Tuple && symbolic_type(expr) == ScalarSymbolic() wrap_code = wrap_code[1] end - return build_function(expr, args...; wrap_code, similarto, kwargs...) + return build_function(expr, args...; wrap_code, similarto, cse, kwargs...) end """ From d45b2dbc8397c0f676dc4acb09f9f2ec8556d6e5 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 1 Apr 2025 11:23:50 +0530 Subject: [PATCH 1240/1337] feat: proagate `cse` kwarg from problem constructors --- src/systems/abstractsystem.jl | 15 ++++--- src/systems/diffeqs/abstractodesystem.jl | 39 +++++++++++-------- src/systems/diffeqs/odesystem.jl | 7 ++-- src/systems/diffeqs/sdesystem.jl | 14 +++---- .../discrete_system/discrete_system.jl | 6 +-- .../implicit_discrete_system.jl | 6 +-- src/systems/jumps/jumpsystem.jl | 11 +++--- .../nonlinear/homotopy_continuation.jl | 6 +-- src/systems/nonlinear/nonlinearsystem.jl | 23 +++++------ .../optimization/optimizationsystem.jl | 16 ++++---- 10 files changed, 78 insertions(+), 65 deletions(-) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index fb692b4028..df07985f5a 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -535,7 +535,8 @@ end SymbolicIndexingInterface.supports_tuple_observed(::AbstractSystem) = true function SymbolicIndexingInterface.observed( - sys::AbstractSystem, sym; eval_expression = false, eval_module = @__MODULE__, checkbounds = true) + sys::AbstractSystem, sym; eval_expression = false, eval_module = @__MODULE__, + checkbounds = true, cse = true) if has_index_cache(sys) && (ic = get_index_cache(sys)) !== nothing if sym isa Symbol _sym = get(ic.symbol_to_variable, sym, nothing) @@ -559,7 +560,7 @@ function SymbolicIndexingInterface.observed( end end return build_explicit_observed_function( - sys, sym; eval_expression, eval_module, checkbounds) + sys, sym; eval_expression, eval_module, checkbounds, cse) end function SymbolicIndexingInterface.default_values(sys::AbstractSystem) @@ -1774,13 +1775,14 @@ struct ObservedFunctionCache{S} eval_expression::Bool eval_module::Module checkbounds::Bool + cse::Bool end function ObservedFunctionCache( sys; steady_state = false, eval_expression = false, - eval_module = @__MODULE__, checkbounds = true) + eval_module = @__MODULE__, checkbounds = true, cse = true) return ObservedFunctionCache( - sys, Dict(), steady_state, eval_expression, eval_module, checkbounds) + sys, Dict(), steady_state, eval_expression, eval_module, checkbounds, cse) end # This is hit because ensemble problems do a deepcopy @@ -1791,8 +1793,9 @@ function Base.deepcopy_internal(ofc::ObservedFunctionCache, stackdict::IdDict) eval_expression = ofc.eval_expression eval_module = ofc.eval_module checkbounds = ofc.checkbounds + cse = ofc.cse newofc = ObservedFunctionCache( - sys, dict, steady_state, eval_expression, eval_module, checkbounds) + sys, dict, steady_state, eval_expression, eval_module, checkbounds, cse) stackdict[ofc] = newofc return newofc end @@ -1801,7 +1804,7 @@ function (ofc::ObservedFunctionCache)(obsvar, args...) obs = get!(ofc.dict, value(obsvar)) do SymbolicIndexingInterface.observed( ofc.sys, obsvar; eval_expression = ofc.eval_expression, - eval_module = ofc.eval_module, checkbounds = ofc.checkbounds) + eval_module = ofc.eval_module, checkbounds = ofc.checkbounds, cse = ofc.cse) end if ofc.steady_state obs = let fn = obs diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index 306aa59fc1..23f20b00ec 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -312,12 +312,13 @@ function DiffEqBase.ODEFunction{iip, specialize}(sys::AbstractODESystem, analytic = nothing, split_idxs = nothing, initialization_data = nothing, + cse = true, kwargs...) where {iip, specialize} if !iscomplete(sys) error("A completed system is required. Call `complete` or `structural_simplify` on the system before creating an `ODEFunction`") end f_gen = generate_function(sys, dvs, ps; expression = Val{true}, - expression_module = eval_module, checkbounds = checkbounds, + expression_module = eval_module, checkbounds = checkbounds, cse, kwargs...) f_oop, f_iip = eval_or_rgf.(f_gen; eval_expression, eval_module) f = GeneratedFunctionWrapper{(2, 3, is_split(sys))}(f_oop, f_iip) @@ -333,7 +334,7 @@ function DiffEqBase.ODEFunction{iip, specialize}(sys::AbstractODESystem, tgrad_gen = generate_tgrad(sys, dvs, ps; simplify = simplify, expression = Val{true}, - expression_module = eval_module, + expression_module = eval_module, cse, checkbounds = checkbounds, kwargs...) tgrad_oop, tgrad_iip = eval_or_rgf.(tgrad_gen; eval_expression, eval_module) _tgrad = GeneratedFunctionWrapper{(2, 3, is_split(sys))}(tgrad_oop, tgrad_iip) @@ -345,7 +346,7 @@ function DiffEqBase.ODEFunction{iip, specialize}(sys::AbstractODESystem, jac_gen = generate_jacobian(sys, dvs, ps; simplify = simplify, sparse = sparse, expression = Val{true}, - expression_module = eval_module, + expression_module = eval_module, cse, checkbounds = checkbounds, kwargs...) jac_oop, jac_iip = eval_or_rgf.(jac_gen; eval_expression, eval_module) @@ -365,7 +366,7 @@ function DiffEqBase.ODEFunction{iip, specialize}(sys::AbstractODESystem, end observedfun = ObservedFunctionCache( - sys; steady_state, eval_expression, eval_module, checkbounds) + sys; steady_state, eval_expression, eval_module, checkbounds, cse) jac_prototype = if sparse uElType = u0 === nothing ? Float64 : eltype(u0) @@ -420,12 +421,13 @@ function DiffEqBase.DAEFunction{iip}(sys::AbstractODESystem, dvs = unknowns(sys) eval_module = @__MODULE__, checkbounds = false, initialization_data = nothing, + cse = true, kwargs...) where {iip} if !iscomplete(sys) error("A completed system is required. Call `complete` or `structural_simplify` on the system before creating a `DAEFunction`") end f_gen = generate_function(sys, dvs, ps; implicit_dae = true, - expression = Val{true}, + expression = Val{true}, cse, expression_module = eval_module, checkbounds = checkbounds, kwargs...) f_oop, f_iip = eval_or_rgf.(f_gen; eval_expression, eval_module) @@ -435,7 +437,7 @@ function DiffEqBase.DAEFunction{iip}(sys::AbstractODESystem, dvs = unknowns(sys) jac_gen = generate_dae_jacobian(sys, dvs, ps; simplify = simplify, sparse = sparse, expression = Val{true}, - expression_module = eval_module, + expression_module = eval_module, cse, checkbounds = checkbounds, kwargs...) jac_oop, jac_iip = eval_or_rgf.(jac_gen; eval_expression, eval_module) @@ -445,7 +447,7 @@ function DiffEqBase.DAEFunction{iip}(sys::AbstractODESystem, dvs = unknowns(sys) end observedfun = ObservedFunctionCache( - sys; eval_expression, eval_module, checkbounds = get(kwargs, :checkbounds, false)) + sys; eval_expression, eval_module, checkbounds = get(kwargs, :checkbounds, false), cse) jac_prototype = if sparse uElType = u0 === nothing ? Float64 : eltype(u0) @@ -479,6 +481,7 @@ function DiffEqBase.DDEFunction{iip}(sys::AbstractODESystem, dvs = unknowns(sys) eval_module = @__MODULE__, checkbounds = false, initialization_data = nothing, + cse = true, kwargs...) where {iip} if !iscomplete(sys) error("A completed system is required. Call `complete` or `structural_simplify` on the system before creating an `DDEFunction`") @@ -486,7 +489,7 @@ function DiffEqBase.DDEFunction{iip}(sys::AbstractODESystem, dvs = unknowns(sys) f_gen = generate_function(sys, dvs, ps; isdde = true, expression = Val{true}, expression_module = eval_module, checkbounds = checkbounds, - kwargs...) + cse, kwargs...) f_oop, f_iip = eval_or_rgf.(f_gen; eval_expression, eval_module) f = GeneratedFunctionWrapper{(3, 4, is_split(sys))}(f_oop, f_iip) @@ -503,6 +506,7 @@ function DiffEqBase.SDDEFunction{iip}(sys::AbstractODESystem, dvs = unknowns(sys eval_module = @__MODULE__, checkbounds = false, initialization_data = nothing, + cse = true, kwargs...) where {iip} if !iscomplete(sys) error("A completed system is required. Call `complete` or `structural_simplify` on the system before creating an `SDDEFunction`") @@ -510,12 +514,12 @@ function DiffEqBase.SDDEFunction{iip}(sys::AbstractODESystem, dvs = unknowns(sys f_gen = generate_function(sys, dvs, ps; isdde = true, expression = Val{true}, expression_module = eval_module, checkbounds = checkbounds, - kwargs...) + cse, kwargs...) f_oop, f_iip = eval_or_rgf.(f_gen; eval_expression, eval_module) f = GeneratedFunctionWrapper{(3, 4, is_split(sys))}(f_oop, f_iip) g_gen = generate_diffusion_function(sys, dvs, ps; expression = Val{true}, - isdde = true, kwargs...) + isdde = true, cse, kwargs...) g_oop, g_iip = eval_or_rgf.(g_gen; eval_expression, eval_module) g = GeneratedFunctionWrapper{(3, 4, is_split(sys))}(g_oop, g_iip) @@ -841,6 +845,7 @@ function SciMLBase.BVProblem{iip, specialize}(sys::AbstractODESystem, u0map = [] warn_initialize_determined = true, eval_expression = false, eval_module = @__MODULE__, + cse = true, kwargs...) where {iip, specialize} if !iscomplete(sys) error("A completed system is required. Call `complete` or `structural_simplify` on the system before creating an `BVProblem`") @@ -864,12 +869,12 @@ function SciMLBase.BVProblem{iip, specialize}(sys::AbstractODESystem, u0map = [] _u0map = has_alg_eqs(sys) ? u0map : merge(Dict(u0map), Dict(guesses)) f, u0, p = process_SciMLProblem(ODEFunction{iip, specialize}, sys, _u0map, parammap; t = tspan !== nothing ? tspan[1] : tspan, guesses, - check_length, warn_initialize_determined, eval_expression, eval_module, kwargs...) + check_length, warn_initialize_determined, eval_expression, eval_module, cse, kwargs...) stidxmap = Dict([v => i for (i, v) in enumerate(sts)]) u0_idxs = has_alg_eqs(sys) ? collect(1:length(sts)) : [stidxmap[k] for (k, v) in u0map] - fns = generate_function_bc(sys, u0, u0_idxs, tspan) + fns = generate_function_bc(sys, u0, u0_idxs, tspan; cse) bc_oop, bc_iip = eval_or_rgf.(fns; eval_expression, eval_module) bc(sol, p, t) = bc_oop(sol, p, t) bc(resid, u, p, t) = bc_iip(resid, u, p, t) @@ -988,15 +993,16 @@ function DiffEqBase.DDEProblem{iip}(sys::AbstractODESystem, u0map = [], eval_expression = false, eval_module = @__MODULE__, u0_constructor = identity, + cse = true, kwargs...) where {iip} if !iscomplete(sys) error("A completed system is required. Call `complete` or `structural_simplify` on the system before creating a `DDEProblem`") end f, u0, p = process_SciMLProblem(DDEFunction{iip}, sys, u0map, parammap; t = tspan !== nothing ? tspan[1] : tspan, - symbolic_u0 = true, u0_constructor, + symbolic_u0 = true, u0_constructor, cse, check_length, eval_expression, eval_module, kwargs...) - h_gen = generate_history(sys, u0; expression = Val{true}) + h_gen = generate_history(sys, u0; expression = Val{true}, cse) h_oop, h_iip = eval_or_rgf.(h_gen; eval_expression, eval_module) h = h_oop u0 = float.(h(p, tspan[1])) @@ -1027,6 +1033,7 @@ function DiffEqBase.SDDEProblem{iip}(sys::AbstractODESystem, u0map = [], eval_expression = false, eval_module = @__MODULE__, u0_constructor = identity, + cse = true, kwargs...) where {iip} if !iscomplete(sys) error("A completed system is required. Call `complete` or `structural_simplify` on the system before creating a `SDDEProblem`") @@ -1034,8 +1041,8 @@ function DiffEqBase.SDDEProblem{iip}(sys::AbstractODESystem, u0map = [], f, u0, p = process_SciMLProblem(SDDEFunction{iip}, sys, u0map, parammap; t = tspan !== nothing ? tspan[1] : tspan, symbolic_u0 = true, eval_expression, eval_module, u0_constructor, - check_length, kwargs...) - h_gen = generate_history(sys, u0; expression = Val{true}) + check_length, cse, kwargs...) + h_gen = generate_history(sys, u0; expression = Val{true}, cse) h_oop, h_iip = eval_or_rgf.(h_gen; eval_expression, eval_module) h = h_oop u0 = h(p, tspan[1]) diff --git a/src/systems/diffeqs/odesystem.jl b/src/systems/diffeqs/odesystem.jl index bd0a58e10a..57d71b18c8 100644 --- a/src/systems/diffeqs/odesystem.jl +++ b/src/systems/diffeqs/odesystem.jl @@ -454,8 +454,8 @@ Generates a function that computes the observed value(s) `ts` in the system `sys - `checkbounds = true` checks bounds if true when destructuring parameters - `op = Operator` sets the recursion terminator for the walk done by `vars` to identify the variables that appear in `ts`. See the documentation for `vars` for more detail. - `throw = true` if true, throw an error when generating a function for `ts` that reference variables that do not exist. -- `mkarray`; only used if the output is an array (that is, `!isscalar(ts)` and `ts` is not a tuple, in which case the result will always be a tuple). Called as `mkarray(ts, output_type)` where `ts` are the expressions to put in -the array and `output_type` is the argument of the same name passed to build_explicit_observed_function. +- `mkarray`: only used if the output is an array (that is, `!isscalar(ts)` and `ts` is not a tuple, in which case the result will always be a tuple). Called as `mkarray(ts, output_type)` where `ts` are the expressions to put in the array and `output_type` is the argument of the same name passed to build_explicit_observed_function. +- `cse = true`: Whether to use Common Subexpression Elimination (CSE) to generate a more efficient function. ## Returns @@ -493,6 +493,7 @@ function build_explicit_observed_function(sys, ts; param_only = false, op = Operator, throw = true, + cse = true, mkarray = nothing) is_tuple = ts isa Tuple if is_tuple @@ -579,7 +580,7 @@ function build_explicit_observed_function(sys, ts; p_end = length(dvs) + length(inputs) + length(ps) fns = build_function_wrapper( sys, ts, args...; p_start, p_end, filter_observed = obsfilter, - output_type, mkarray, try_namespaced = true, expression = Val{true}) + output_type, mkarray, try_namespaced = true, expression = Val{true}, cse) if fns isa Tuple if expression return return_inplace ? fns : fns[1] diff --git a/src/systems/diffeqs/sdesystem.jl b/src/systems/diffeqs/sdesystem.jl index f51529a559..66044427eb 100644 --- a/src/systems/diffeqs/sdesystem.jl +++ b/src/systems/diffeqs/sdesystem.jl @@ -595,23 +595,23 @@ function DiffEqBase.SDEFunction{iip, specialize}(sys::SDESystem, dvs = unknowns( jac = false, Wfact = false, eval_expression = false, eval_module = @__MODULE__, checkbounds = false, initialization_data = nothing, - kwargs...) where {iip, specialize} + cse = true, kwargs...) where {iip, specialize} if !iscomplete(sys) error("A completed `SDESystem` is required. Call `complete` or `structural_simplify` on the system before creating an `SDEFunction`") end dvs = scalarize.(dvs) - f_gen = generate_function(sys, dvs, ps; expression = Val{true}, kwargs...) + f_gen = generate_function(sys, dvs, ps; expression = Val{true}, cse, kwargs...) f_oop, f_iip = eval_or_rgf.(f_gen; eval_expression, eval_module) g_gen = generate_diffusion_function(sys, dvs, ps; expression = Val{true}, - kwargs...) + cse, kwargs...) g_oop, g_iip = eval_or_rgf.(g_gen; eval_expression, eval_module) f = GeneratedFunctionWrapper{(2, 3, is_split(sys))}(f_oop, f_iip) g = GeneratedFunctionWrapper{(2, 3, is_split(sys))}(g_oop, g_iip) if tgrad - tgrad_gen = generate_tgrad(sys, dvs, ps; expression = Val{true}, + tgrad_gen = generate_tgrad(sys, dvs, ps; expression = Val{true}, cse, kwargs...) tgrad_oop, tgrad_iip = eval_or_rgf.(tgrad_gen; eval_expression, eval_module) _tgrad = GeneratedFunctionWrapper{(2, 3, is_split(sys))}(tgrad_oop, tgrad_iip) @@ -621,7 +621,7 @@ function DiffEqBase.SDEFunction{iip, specialize}(sys::SDESystem, dvs = unknowns( if jac jac_gen = generate_jacobian(sys, dvs, ps; expression = Val{true}, - sparse = sparse, kwargs...) + sparse = sparse, cse, kwargs...) jac_oop, jac_iip = eval_or_rgf.(jac_gen; eval_expression, eval_module) _jac = GeneratedFunctionWrapper{(2, 3, is_split(sys))}(jac_oop, jac_iip) @@ -631,7 +631,7 @@ function DiffEqBase.SDEFunction{iip, specialize}(sys::SDESystem, dvs = unknowns( if Wfact tmp_Wfact, tmp_Wfact_t = generate_factorized_W(sys, dvs, ps, true; - expression = Val{true}, kwargs...) + expression = Val{true}, cse, kwargs...) Wfact_oop, Wfact_iip = eval_or_rgf.(tmp_Wfact; eval_expression, eval_module) Wfact_oop_t, Wfact_iip_t = eval_or_rgf.(tmp_Wfact_t; eval_expression, eval_module) @@ -645,7 +645,7 @@ function DiffEqBase.SDEFunction{iip, specialize}(sys::SDESystem, dvs = unknowns( _M = (u0 === nothing || M == I) ? M : ArrayInterface.restructure(u0 .* u0', M) observedfun = ObservedFunctionCache( - sys; eval_expression, eval_module, checkbounds = get(kwargs, :checkbounds, false)) + sys; eval_expression, eval_module, checkbounds = get(kwargs, :checkbounds, false), cse) SDEFunction{iip, specialize}(f, g; sys = sys, diff --git a/src/systems/discrete_system/discrete_system.jl b/src/systems/discrete_system/discrete_system.jl index 40f01769ee..5f7c986659 100644 --- a/src/systems/discrete_system/discrete_system.jl +++ b/src/systems/discrete_system/discrete_system.jl @@ -360,13 +360,13 @@ function SciMLBase.DiscreteFunction{iip, specialize}( t = nothing, eval_expression = false, eval_module = @__MODULE__, - analytic = nothing, + analytic = nothing, cse = true, kwargs...) where {iip, specialize} if !iscomplete(sys) error("A completed `DiscreteSystem` is required. Call `complete` or `structural_simplify` on the system before creating a `DiscreteProblem`") end f_gen = generate_function(sys, dvs, ps; expression = Val{true}, - expression_module = eval_module, kwargs...) + expression_module = eval_module, cse, kwargs...) f_oop, f_iip = eval_or_rgf.(f_gen; eval_expression, eval_module) f = GeneratedFunctionWrapper{(2, 3, is_split(sys))}(f_oop, f_iip) @@ -378,7 +378,7 @@ function SciMLBase.DiscreteFunction{iip, specialize}( end observedfun = ObservedFunctionCache( - sys; eval_expression, eval_module, checkbounds = get(kwargs, :checkbounds, false)) + sys; eval_expression, eval_module, checkbounds = get(kwargs, :checkbounds, false), cse) DiscreteFunction{iip, specialize}(f; sys = sys, diff --git a/src/systems/discrete_system/implicit_discrete_system.jl b/src/systems/discrete_system/implicit_discrete_system.jl index ebae78384a..3956c089d4 100644 --- a/src/systems/discrete_system/implicit_discrete_system.jl +++ b/src/systems/discrete_system/implicit_discrete_system.jl @@ -369,13 +369,13 @@ function SciMLBase.ImplicitDiscreteFunction{iip, specialize}( t = nothing, eval_expression = false, eval_module = @__MODULE__, - analytic = nothing, + analytic = nothing, cse = true, kwargs...) where {iip, specialize} if !iscomplete(sys) error("A completed `ImplicitDiscreteSystem` is required. Call `complete` or `structural_simplify` on the system before creating a `ImplicitDiscreteProblem`") end f_gen = generate_function(sys, dvs, ps; expression = Val{true}, - expression_module = eval_module, kwargs...) + expression_module = eval_module, cse, kwargs...) f_oop, f_iip = eval_or_rgf.(f_gen; eval_expression, eval_module) 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) @@ -388,7 +388,7 @@ function SciMLBase.ImplicitDiscreteFunction{iip, specialize}( end observedfun = ObservedFunctionCache( - sys; eval_expression, eval_module, checkbounds = get(kwargs, :checkbounds, false)) + sys; eval_expression, eval_module, checkbounds = get(kwargs, :checkbounds, false), cse) ImplicitDiscreteFunction{iip, specialize}(f; sys = sys, diff --git a/src/systems/jumps/jumpsystem.jl b/src/systems/jumps/jumpsystem.jl index 9da32a4305..57a3aee7df 100644 --- a/src/systems/jumps/jumpsystem.jl +++ b/src/systems/jumps/jumpsystem.jl @@ -398,6 +398,7 @@ function DiffEqBase.DiscreteProblem(sys::JumpSystem, u0map, tspan::Union{Tuple, parammap = DiffEqBase.NullParameters(); eval_expression = false, eval_module = @__MODULE__, + cse = true, kwargs...) if !iscomplete(sys) error("A completed `JumpSystem` is required. Call `complete` or `structural_simplify` on the system before creating a `DiscreteProblem`") @@ -408,11 +409,11 @@ function DiffEqBase.DiscreteProblem(sys::JumpSystem, u0map, tspan::Union{Tuple, end _f, u0, p = process_SciMLProblem(EmptySciMLFunction, sys, u0map, parammap; - t = tspan === nothing ? nothing : tspan[1], tofloat = false, check_length = false, build_initializeprob = false) + t = tspan === nothing ? nothing : tspan[1], tofloat = false, check_length = false, build_initializeprob = false, cse) f = DiffEqBase.DISCRETE_INPLACE_DEFAULT observedfun = ObservedFunctionCache( - sys; eval_expression, eval_module, checkbounds = get(kwargs, :checkbounds, false)) + sys; eval_expression, eval_module, checkbounds = get(kwargs, :checkbounds, false), cse) df = DiscreteFunction{true, true}(f; sys = sys, observed = observedfun, initialization_data = get(_f.kwargs, :initialization_data, nothing)) @@ -488,7 +489,7 @@ oprob = ODEProblem(complete(js), u₀map, tspan, parammap) function DiffEqBase.ODEProblem(sys::JumpSystem, u0map, tspan::Union{Tuple, Nothing}, parammap = DiffEqBase.NullParameters(); eval_expression = false, - eval_module = @__MODULE__, + eval_module = @__MODULE__, cse = true, kwargs...) if !iscomplete(sys) error("A completed `JumpSystem` is required. Call `complete` or `structural_simplify` on the system before creating a `DiscreteProblem`") @@ -507,10 +508,10 @@ function DiffEqBase.ODEProblem(sys::JumpSystem, u0map, tspan::Union{Tuple, Nothi else _, u0, p = process_SciMLProblem(EmptySciMLFunction, sys, u0map, parammap; t = tspan === nothing ? nothing : tspan[1], tofloat = false, - check_length = false, build_initializeprob = false) + check_length = false, build_initializeprob = false, cse) f = (du, u, p, t) -> (du .= 0; nothing) observedfun = ObservedFunctionCache(sys; eval_expression, eval_module, - checkbounds = get(kwargs, :checkbounds, false)) + checkbounds = get(kwargs, :checkbounds, false), cse) df = ODEFunction(f; sys, observed = observedfun) return ODEProblem(df, u0, tspan, p; kwargs...) end diff --git a/src/systems/nonlinear/homotopy_continuation.jl b/src/systems/nonlinear/homotopy_continuation.jl index 474224eacd..9a77779103 100644 --- a/src/systems/nonlinear/homotopy_continuation.jl +++ b/src/systems/nonlinear/homotopy_continuation.jl @@ -489,7 +489,7 @@ end function SciMLBase.HomotopyNonlinearFunction{iip, specialize}( sys::NonlinearSystem, args...; eval_expression = false, eval_module = @__MODULE__, - p = nothing, fraction_cancel_fn = SymbolicUtils.simplify_fractions, + p = nothing, fraction_cancel_fn = SymbolicUtils.simplify_fractions, cse = true, kwargs...) where {iip, specialize} if !iscomplete(sys) error("A completed `NonlinearSystem` is required. Call `complete` or `structural_simplify` on the system before creating a `HomotopyContinuationFunction`") @@ -511,9 +511,9 @@ function SciMLBase.HomotopyNonlinearFunction{iip, specialize}( # we want to create f, jac etc. according to `sys2` since that will do the solving # but the `sys` inside for symbolic indexing should be the non-polynomial system - fn = NonlinearFunction{iip}(sys2; eval_expression, eval_module, kwargs...) + fn = NonlinearFunction{iip}(sys2; eval_expression, eval_module, cse, kwargs...) obsfn = ObservedFunctionCache( - sys; eval_expression, eval_module, checkbounds = get(kwargs, :checkbounds, false)) + sys; eval_expression, eval_module, checkbounds = get(kwargs, :checkbounds, false), cse) fn = remake(fn; sys = sys, observed = obsfn) denominator = build_explicit_observed_function(sys2, denoms) diff --git a/src/systems/nonlinear/nonlinearsystem.jl b/src/systems/nonlinear/nonlinearsystem.jl index 77215be5eb..856822492b 100644 --- a/src/systems/nonlinear/nonlinearsystem.jl +++ b/src/systems/nonlinear/nonlinearsystem.jl @@ -373,19 +373,19 @@ function SciMLBase.NonlinearFunction{iip}(sys::NonlinearSystem, dvs = unknowns(s eval_expression = false, eval_module = @__MODULE__, sparse = false, simplify = false, - initialization_data = nothing, + initialization_data = nothing, cse = true, kwargs...) where {iip} if !iscomplete(sys) error("A completed `NonlinearSystem` is required. Call `complete` or `structural_simplify` on the system before creating a `NonlinearFunction`") end - f_gen = generate_function(sys, dvs, ps; expression = Val{true}, kwargs...) + f_gen = generate_function(sys, dvs, ps; expression = Val{true}, cse, kwargs...) f_oop, f_iip = eval_or_rgf.(f_gen; eval_expression, eval_module) f = GeneratedFunctionWrapper{(2, 2, is_split(sys))}(f_oop, f_iip) if jac jac_gen = generate_jacobian(sys, dvs, ps; simplify = simplify, sparse = sparse, - expression = Val{true}, kwargs...) + expression = Val{true}, cse, kwargs...) jac_oop, jac_iip = eval_or_rgf.(jac_gen; eval_expression, eval_module) _jac = GeneratedFunctionWrapper{(2, 2, is_split(sys))}(jac_oop, jac_iip) else @@ -393,7 +393,7 @@ function SciMLBase.NonlinearFunction{iip}(sys::NonlinearSystem, dvs = unknowns(s end observedfun = ObservedFunctionCache( - sys; eval_expression, eval_module, checkbounds = get(kwargs, :checkbounds, false)) + sys; eval_expression, eval_module, checkbounds = get(kwargs, :checkbounds, false), cse) if length(dvs) == length(equations(sys)) resid_prototype = nothing @@ -606,7 +606,7 @@ end function CacheWriter(sys::AbstractSystem, buffer_types::Vector{TypeT}, exprs::Dict{TypeT, Vector{Any}}, solsyms, obseqs::Vector{Equation}; - eval_expression = false, eval_module = @__MODULE__) + eval_expression = false, eval_module = @__MODULE__, cse = true) ps = parameters(sys; initial_parameters = true) rps = reorder_parameters(sys, ps) obs_assigns = [eq.lhs ← eq.rhs for eq in obseqs] @@ -625,7 +625,7 @@ function CacheWriter(sys::AbstractSystem, buffer_types::Vector{TypeT}, sys, nothing, :out, DestructuredArgs(DestructuredArgs.(solsyms), generated_argument_name(1)), rps...; p_start = 3, p_end = length(rps) + 2, - expression = Val{true}, add_observed = false, + expression = Val{true}, add_observed = false, cse, extra_assignments = [array_assignments; obs_assigns; body]) fn = eval_or_rgf(fn; eval_expression, eval_module) fn = GeneratedFunctionWrapper{(3, 3, is_split(sys))}(fn, nothing) @@ -636,7 +636,7 @@ struct SCCNonlinearFunction{iip} end function SCCNonlinearFunction{iip}( sys::NonlinearSystem, _eqs, _dvs, _obs, cachesyms; eval_expression = false, - eval_module = @__MODULE__, kwargs...) where {iip} + eval_module = @__MODULE__, cse = true, kwargs...) where {iip} ps = parameters(sys; initial_parameters = true) rps = reorder_parameters(sys, ps) @@ -646,7 +646,7 @@ function SCCNonlinearFunction{iip}( f_gen = build_function_wrapper(sys, rhss, _dvs, rps..., cachesyms...; p_start = 2, p_end = length(rps) + length(cachesyms) + 1, add_observed = false, - extra_assignments = obs_assignments, expression = Val{true}) + extra_assignments = obs_assignments, expression = Val{true}, cse) f_oop, f_iip = eval_or_rgf.(f_gen; eval_expression, eval_module) f = GeneratedFunctionWrapper{(2, 2, is_split(sys))}(f_oop, f_iip) @@ -666,7 +666,8 @@ function SciMLBase.SCCNonlinearProblem(sys::NonlinearSystem, args...; kwargs...) end function SciMLBase.SCCNonlinearProblem{iip}(sys::NonlinearSystem, u0map, - parammap = SciMLBase.NullParameters(); eval_expression = false, eval_module = @__MODULE__, kwargs...) where {iip} + parammap = SciMLBase.NullParameters(); eval_expression = false, eval_module = @__MODULE__, + cse = true, kwargs...) where {iip} if !iscomplete(sys) || get_tearing_state(sys) === nothing error("A simplified `NonlinearSystem` is required. Call `structural_simplify` on the system before creating an `SCCNonlinearProblem`.") end @@ -801,14 +802,14 @@ function SciMLBase.SCCNonlinearProblem{iip}(sys::NonlinearSystem, u0map, solsyms = getindex.((dvs,), view(var_sccs, 1:(i - 1))) push!(explicitfuns, CacheWriter(sys, cachetypes, cacheexprs, solsyms, obs[_prevobsidxs]; - eval_expression, eval_module)) + eval_expression, eval_module, cse)) end cachebufsyms = Tuple(map(cachetypes) do T get(cachevars, T, []) end) f = SCCNonlinearFunction{iip}( - sys, _eqs, _dvs, _obs, cachebufsyms; eval_expression, eval_module, kwargs...) + sys, _eqs, _dvs, _obs, cachebufsyms; eval_expression, eval_module, cse, kwargs...) push!(nlfuns, f) end diff --git a/src/systems/optimization/optimizationsystem.jl b/src/systems/optimization/optimizationsystem.jl index e55f4b7871..fe238ee489 100644 --- a/src/systems/optimization/optimizationsystem.jl +++ b/src/systems/optimization/optimizationsystem.jl @@ -300,7 +300,7 @@ function DiffEqBase.OptimizationProblem{iip}(sys::OptimizationSystem, u0map, cons_sparse = false, checkbounds = false, linenumbers = true, parallel = SerialForm(), eval_expression = false, eval_module = @__MODULE__, - checks = true, + checks = true, cse = true, kwargs...) where {iip} if !iscomplete(sys) error("A completed `OptimizationSystem` is required. Call `complete` or `structural_simplify` on the system before creating a `OptimizationProblem`") @@ -355,7 +355,7 @@ function DiffEqBase.OptimizationProblem{iip}(sys::OptimizationSystem, u0map, f = let _f = eval_or_rgf( generate_function( sys, checkbounds = checkbounds, linenumbers = linenumbers, - expression = Val{true}, wrap_mtkparameters = false); + expression = Val{true}, wrap_mtkparameters = false, cse); eval_expression, eval_module) __f(u, p) = _f(u, p) @@ -370,7 +370,7 @@ function DiffEqBase.OptimizationProblem{iip}(sys::OptimizationSystem, u0map, sys, checkbounds = checkbounds, linenumbers = linenumbers, parallel = parallel, expression = Val{true}, - wrap_mtkparameters = false); + wrap_mtkparameters = false, cse); eval_expression, eval_module) _grad(u, p) = grad_oop(u, p) @@ -389,7 +389,7 @@ function DiffEqBase.OptimizationProblem{iip}(sys::OptimizationSystem, u0map, sys, checkbounds = checkbounds, linenumbers = linenumbers, sparse = sparse, parallel = parallel, - expression = Val{true}, wrap_mtkparameters = false); + expression = Val{true}, wrap_mtkparameters = false, cse); eval_expression, eval_module) _hess(u, p) = hess_oop(u, p) @@ -408,14 +408,14 @@ function DiffEqBase.OptimizationProblem{iip}(sys::OptimizationSystem, u0map, hess_prototype = nothing end - observedfun = ObservedFunctionCache(sys; eval_expression, eval_module, checkbounds) + observedfun = ObservedFunctionCache(sys; eval_expression, eval_module, checkbounds, cse) if length(cstr) > 0 @named cons_sys = ConstraintsSystem(cstr, dvs, ps; checks) cons_sys = complete(cons_sys) cons, lcons_, ucons_ = generate_function(cons_sys, checkbounds = checkbounds, linenumbers = linenumbers, - expression = Val{true}; wrap_mtkparameters = false) + expression = Val{true}; wrap_mtkparameters = false, cse) cons = let (cons_oop, cons_iip) = eval_or_rgf.(cons; eval_expression, eval_module) _cons(u, p) = cons_oop(u, p) _cons(resid, u, p) = cons_iip(resid, u, p) @@ -428,7 +428,7 @@ function DiffEqBase.OptimizationProblem{iip}(sys::OptimizationSystem, u0map, checkbounds = checkbounds, linenumbers = linenumbers, parallel = parallel, expression = Val{true}, - sparse = cons_sparse, wrap_mtkparameters = false); + sparse = cons_sparse, wrap_mtkparameters = false, cse); eval_expression, eval_module) _cons_j(u, p) = cons_jac_oop(u, p) @@ -446,7 +446,7 @@ function DiffEqBase.OptimizationProblem{iip}(sys::OptimizationSystem, u0map, cons_sys, checkbounds = checkbounds, linenumbers = linenumbers, sparse = cons_sparse, parallel = parallel, - expression = Val{true}, wrap_mtkparameters = false); + expression = Val{true}, wrap_mtkparameters = false, cse); eval_expression, eval_module) _cons_h(u, p) = cons_hess_oop(u, p) From b1a4b3607a1439bef8dc6137a230e36ad8f55c81 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 1 Apr 2025 16:06:33 +0530 Subject: [PATCH 1241/1337] fix: fix OptimizationProblem codegen with CSE --- src/systems/optimization/optimizationsystem.jl | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/systems/optimization/optimizationsystem.jl b/src/systems/optimization/optimizationsystem.jl index fe238ee489..be4567aee5 100644 --- a/src/systems/optimization/optimizationsystem.jl +++ b/src/systems/optimization/optimizationsystem.jl @@ -354,7 +354,7 @@ function DiffEqBase.OptimizationProblem{iip}(sys::OptimizationSystem, u0map, f = let _f = eval_or_rgf( generate_function( - sys, checkbounds = checkbounds, linenumbers = linenumbers, + sys; checkbounds = checkbounds, linenumbers = linenumbers, expression = Val{true}, wrap_mtkparameters = false, cse); eval_expression, eval_module) @@ -367,7 +367,7 @@ function DiffEqBase.OptimizationProblem{iip}(sys::OptimizationSystem, u0map, if grad _grad = let (grad_oop, grad_iip) = eval_or_rgf.( generate_gradient( - sys, checkbounds = checkbounds, + sys; checkbounds = checkbounds, linenumbers = linenumbers, parallel = parallel, expression = Val{true}, wrap_mtkparameters = false, cse); @@ -386,7 +386,7 @@ function DiffEqBase.OptimizationProblem{iip}(sys::OptimizationSystem, u0map, if hess _hess = let (hess_oop, hess_iip) = eval_or_rgf.( generate_hessian( - sys, checkbounds = checkbounds, + sys; checkbounds = checkbounds, linenumbers = linenumbers, sparse = sparse, parallel = parallel, expression = Val{true}, wrap_mtkparameters = false, cse); @@ -413,9 +413,9 @@ function DiffEqBase.OptimizationProblem{iip}(sys::OptimizationSystem, u0map, if length(cstr) > 0 @named cons_sys = ConstraintsSystem(cstr, dvs, ps; checks) cons_sys = complete(cons_sys) - cons, lcons_, ucons_ = generate_function(cons_sys, checkbounds = checkbounds, + cons, lcons_, ucons_ = generate_function(cons_sys; checkbounds = checkbounds, linenumbers = linenumbers, - expression = Val{true}; wrap_mtkparameters = false, cse) + expression = Val{true}, wrap_mtkparameters = false, cse) cons = let (cons_oop, cons_iip) = eval_or_rgf.(cons; eval_expression, eval_module) _cons(u, p) = cons_oop(u, p) _cons(resid, u, p) = cons_iip(resid, u, p) @@ -443,7 +443,7 @@ function DiffEqBase.OptimizationProblem{iip}(sys::OptimizationSystem, u0map, if cons_h _cons_h = let (cons_hess_oop, cons_hess_iip) = eval_or_rgf.( generate_hessian( - cons_sys, checkbounds = checkbounds, + cons_sys; checkbounds = checkbounds, linenumbers = linenumbers, sparse = cons_sparse, parallel = parallel, expression = Val{true}, wrap_mtkparameters = false, cse); From 53d0c9f822fba5710bfe5a71fa6c1aa3eb036760 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 1 Apr 2025 16:06:38 +0530 Subject: [PATCH 1242/1337] fix: disable CSE in callback codegen --- src/systems/callbacks.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/systems/callbacks.jl b/src/systems/callbacks.jl index 937264d083..07809bf611 100644 --- a/src/systems/callbacks.jl +++ b/src/systems/callbacks.jl @@ -699,7 +699,7 @@ function compile_affect(eqs::Vector{Equation}, cb, sys, dvs, ps; outputidxs = no add_integrator_header(sys, integ, outvar), outputidxs = update_inds, create_bindings = false, - kwargs...) + kwargs..., cse = false) # applied user-provided function to the generated expression if postprocess_affect_expr! !== nothing postprocess_affect_expr!(rf_ip, integ) @@ -729,7 +729,7 @@ function generate_single_rootfinding_callback( end rf_oop, rf_ip = generate_custom_function( - sys, [eq.rhs], dvs, ps; expression = Val{false}, kwargs...) + 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) @@ -780,7 +780,7 @@ function generate_vector_rootfinding_callback( rhss = map(x -> x.rhs, eqs) _, rf_ip = generate_custom_function( - sys, rhss, dvs, ps; expression = Val{false}, kwargs...) + sys, rhss, dvs, ps; expression = Val{false}, kwargs..., cse = false) affect_functions = @NamedTuple{ affect::Function, From f7466d034d7dd516e6589882d66dcca967a1ffcb Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 2 Apr 2025 11:43:54 +0530 Subject: [PATCH 1243/1337] build: bump Symbolics, SymbolicUtils compat --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 5b4f97e815..b41ff59b22 100644 --- a/Project.toml +++ b/Project.toml @@ -150,7 +150,7 @@ StaticArrays = "0.10, 0.11, 0.12, 1.0" StochasticDelayDiffEq = "1.8.1" StochasticDiffEq = "6.72.1" SymbolicIndexingInterface = "0.3.37" -SymbolicUtils = "3.14" +SymbolicUtils = "3.25.1" Symbolics = "6.36" URIs = "1" UnPack = "0.1, 1.0" From 3e36c47938e5476a4f145dee1b0be616662a320d Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 2 Apr 2025 16:42:48 +0530 Subject: [PATCH 1244/1337] test: make input output tests less sensitive to FP error --- test/input_output_handling.jl | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/input_output_handling.jl b/test/input_output_handling.jl index 84539f0b37..7f4a3247ad 100644 --- a/test/input_output_handling.jl +++ b/test/input_output_handling.jl @@ -173,7 +173,7 @@ end p = [rand()] x = [rand()] u = [rand()] - @test f[1](x, u, p, 1) == -x + u + @test f[1](x, u, p, 1) ≈ -x + u # With disturbance inputs @variables x(t)=0 u(t)=0 [input = true] d(t)=0 @@ -191,7 +191,7 @@ end p = [rand()] x = [rand()] u = [rand()] - @test f[1](x, u, p, 1) == -x + u + @test f[1](x, u, p, 1) ≈ -x + u ## With added d argument @variables x(t)=0 u(t)=0 [input = true] d(t)=0 @@ -210,7 +210,7 @@ end x = [rand()] u = [rand()] d = [rand()] - @test f[1](x, u, p, t, d) == -x + u + [d[]^2] + @test f[1](x, u, p, t, d) ≈ -x + u + [d[]^2] end end @@ -434,7 +434,7 @@ matrices, ssys = linearize(augmented_sys, (; io_sys,) = ModelingToolkit.generate_control_function(sys, simplify = true) obsfn = ModelingToolkit.build_explicit_observed_function( io_sys, [x + u * t]; inputs = [u]) - @test obsfn([1.0], [2.0], MTKParameters(io_sys, []), 3.0) == [7.0] + @test obsfn([1.0], [2.0], MTKParameters(io_sys, []), 3.0) ≈ [7.0] end # https://github.com/SciML/ModelingToolkit.jl/issues/2896 @@ -445,7 +445,7 @@ end @named sys = ODESystem(eqs, t, [x], []) f, dvs, ps, io_sys = ModelingToolkit.generate_control_function(sys, simplify = true) - @test f[1]([0.5], nothing, MTKParameters(io_sys, []), 0.0) == [1.0] + @test f[1]([0.5], nothing, MTKParameters(io_sys, []), 0.0) ≈ [1.0] end @testset "With callable symbolic" begin From 5b81a118e35c662f04b8aead8b401957a0637fec Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 3 Apr 2025 01:00:35 +0530 Subject: [PATCH 1245/1337] fix: update initials with non-symbolic `u0` in `remake` --- src/systems/nonlinear/initializesystem.jl | 12 +++++++++++- test/initializationsystem.jl | 11 +++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/systems/nonlinear/initializesystem.jl b/src/systems/nonlinear/initializesystem.jl index c03524c61b..23573673b0 100644 --- a/src/systems/nonlinear/initializesystem.jl +++ b/src/systems/nonlinear/initializesystem.jl @@ -655,7 +655,17 @@ end function SciMLBase.late_binding_update_u0_p( prob, sys::AbstractSystem, u0, p, t0, newu0, newp) u0 === missing && return newu0, (p === missing ? copy(newp) : newp) - eltype(u0) <: Pair || return newu0, (p === missing ? copy(newp) : newp) + if !(eltype(u0) <: Pair) + p === missing || return newu0, newp + newu0 === nothing && return newu0, newp + newp = p === missing ? copy(newp) : newp + initials, repack, alias = SciMLStructures.canonicalize( + SciMLStructures.Initials(), newp) + initials = DiffEqBase.promote_u0(initials, newu0, t0) + newp = repack(initials) + setp(sys, Initial.(unknowns(sys)))(newp, newu0) + return newu0, newp + end newp = p === missing ? copy(newp) : newp newu0 = DiffEqBase.promote_u0(newu0, newp, t0) diff --git a/test/initializationsystem.jl b/test/initializationsystem.jl index 54b847c64a..1120394b01 100644 --- a/test/initializationsystem.jl +++ b/test/initializationsystem.jl @@ -1476,3 +1476,14 @@ end @test sol.ps[Γ[1]] ≈ 5.0 end end + +@testset "Issue#3504: Update initials when `remake` called with non-symbolic `u0`" begin + @variables x(t) y(t) + @parameters c1 c2 + @mtkbuild sys = ODESystem([D(x) ~ -c1 * x + c2 * y, D(y) ~ c1 * x - c2 * y], t) + prob1 = ODEProblem(sys, [1.0, 2.0], (0.0, 1.0), [c1 => 1.0, c2 => 2.0]) + prob2 = remake(prob1, u0 = [2.0, 3.0]) + integ1 = init(prob1, Tsit5()) + integ2 = init(prob2, Tsit5()) + @test integ2.u ≈ [2.0, 3.0] +end From 68e06054b83f63af76ed4f0a6b9ea3f56cb559dd Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 3 Apr 2025 01:10:43 +0530 Subject: [PATCH 1246/1337] fix: update initials in `remake` with non-symbolic `u0` and symbolic `p` --- src/systems/nonlinear/initializesystem.jl | 4 +++- test/initializationsystem.jl | 4 ++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/systems/nonlinear/initializesystem.jl b/src/systems/nonlinear/initializesystem.jl index 23573673b0..daeef1def6 100644 --- a/src/systems/nonlinear/initializesystem.jl +++ b/src/systems/nonlinear/initializesystem.jl @@ -655,8 +655,10 @@ end function SciMLBase.late_binding_update_u0_p( prob, sys::AbstractSystem, u0, p, t0, newu0, newp) u0 === missing && return newu0, (p === missing ? copy(newp) : newp) + # non-symbolic u0 updates initials... if !(eltype(u0) <: Pair) - p === missing || return newu0, newp + # if `p` is not provided or is symbolic + p === missing || eltype(p) <: Pair || return newu0, newp newu0 === nothing && return newu0, newp newp = p === missing ? copy(newp) : newp initials, repack, alias = SciMLStructures.canonicalize( diff --git a/test/initializationsystem.jl b/test/initializationsystem.jl index 1120394b01..94a1f4c14f 100644 --- a/test/initializationsystem.jl +++ b/test/initializationsystem.jl @@ -1483,7 +1483,11 @@ end @mtkbuild sys = ODESystem([D(x) ~ -c1 * x + c2 * y, D(y) ~ c1 * x - c2 * y], t) prob1 = ODEProblem(sys, [1.0, 2.0], (0.0, 1.0), [c1 => 1.0, c2 => 2.0]) prob2 = remake(prob1, u0 = [2.0, 3.0]) + prob3 = remake(prob1, u0 = [2.0, 3.0], p = [c1 => 2.0]) integ1 = init(prob1, Tsit5()) integ2 = init(prob2, Tsit5()) + integ3 = init(prob3, Tsit5()) @test integ2.u ≈ [2.0, 3.0] + @test integ3.u ≈ [2.0, 3.0] + @test integ3.ps[c1] ≈ 2.0 end From c70f1015cd4be94c858904fce407424edecffe6c Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 3 Apr 2025 11:42:25 +0530 Subject: [PATCH 1247/1337] fix: early exit `late_binding_update_u0_p` if system doesn't support initialization --- src/systems/nonlinear/initializesystem.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/src/systems/nonlinear/initializesystem.jl b/src/systems/nonlinear/initializesystem.jl index daeef1def6..52eb886bf9 100644 --- a/src/systems/nonlinear/initializesystem.jl +++ b/src/systems/nonlinear/initializesystem.jl @@ -654,6 +654,7 @@ end function SciMLBase.late_binding_update_u0_p( prob, sys::AbstractSystem, u0, p, t0, newu0, newp) + supports_initialization(sys) || return newu0, newp u0 === missing && return newu0, (p === missing ? copy(newp) : newp) # non-symbolic u0 updates initials... if !(eltype(u0) <: Pair) From 69509276a2954665fab4a40c9496e5b64c196ef8 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 3 Apr 2025 11:42:39 +0530 Subject: [PATCH 1248/1337] test: update test to account for new new initial propogation --- test/nonlinearsystem.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/nonlinearsystem.jl b/test/nonlinearsystem.jl index 5cf3b4b1be..9475f24006 100644 --- a/test/nonlinearsystem.jl +++ b/test/nonlinearsystem.jl @@ -239,7 +239,7 @@ testdict = Dict([:test => 1]) prob_ = remake(prob, u0 = [1.0, 2.0, 3.0], p = [a => 1.1, b => 1.2, c => 1.3]) @test prob_.u0 == [1.0, 2.0, 3.0] - initials = unknowns(sys) .=> ones(3) + initials = unknowns(sys) .=> [1.0, 2.0, 3.0] @test prob_.p == MTKParameters(sys, [a => 1.1, b => 1.2, c => 1.3, initials...]) prob_ = remake(prob, u0 = Dict(y => 2.0), p = Dict(a => 2.0)) From a9ca3e7dca3cfab4e38ec2805a3647ee3b491682 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 3 Apr 2025 12:17:23 +0530 Subject: [PATCH 1249/1337] fix: remove unnecessary copies in `late_binding_update_u0_p` --- src/systems/nonlinear/initializesystem.jl | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/systems/nonlinear/initializesystem.jl b/src/systems/nonlinear/initializesystem.jl index 52eb886bf9..99fc02e5e7 100644 --- a/src/systems/nonlinear/initializesystem.jl +++ b/src/systems/nonlinear/initializesystem.jl @@ -664,8 +664,10 @@ function SciMLBase.late_binding_update_u0_p( newp = p === missing ? copy(newp) : newp initials, repack, alias = SciMLStructures.canonicalize( SciMLStructures.Initials(), newp) - initials = DiffEqBase.promote_u0(initials, newu0, t0) - newp = repack(initials) + if eltype(initials) != eltype(newu0) + initials = DiffEqBase.promote_u0(initials, newu0, t0) + newp = repack(initials) + end setp(sys, Initial.(unknowns(sys)))(newp, newu0) return newu0, newp end @@ -673,11 +675,15 @@ function SciMLBase.late_binding_update_u0_p( newp = p === missing ? copy(newp) : newp newu0 = DiffEqBase.promote_u0(newu0, newp, t0) tunables, repack, alias = SciMLStructures.canonicalize(SciMLStructures.Tunable(), newp) - tunables = DiffEqBase.promote_u0(tunables, newu0, t0) - newp = repack(tunables) + if eltype(tunables) != eltype(newu0) + tunables = DiffEqBase.promote_u0(tunables, newu0, t0) + newp = repack(tunables) + end initials, repack, alias = SciMLStructures.canonicalize(SciMLStructures.Initials(), newp) - initials = DiffEqBase.promote_u0(initials, newu0, t0) - newp = repack(initials) + if eltype(initials) != eltype(newu0) + initials = DiffEqBase.promote_u0(initials, newu0, t0) + newp = repack(initials) + end allsyms = all_symbols(sys) for (k, v) in u0 From 216aa73e5c0ae76e4642f29aa7de22b1ff75d92a Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 3 Apr 2025 16:03:16 +0530 Subject: [PATCH 1250/1337] fix: handle edge case in `late_binding_update_u0_p` when `Initial` parameters don't exist --- src/systems/nonlinear/initializesystem.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/src/systems/nonlinear/initializesystem.jl b/src/systems/nonlinear/initializesystem.jl index 99fc02e5e7..2283e7a1d4 100644 --- a/src/systems/nonlinear/initializesystem.jl +++ b/src/systems/nonlinear/initializesystem.jl @@ -661,6 +661,7 @@ function SciMLBase.late_binding_update_u0_p( # if `p` is not provided or is symbolic p === missing || eltype(p) <: Pair || return newu0, newp newu0 === nothing && return newu0, newp + all(is_parameter(sys, Initial(x)) for x in unknowns(sys)) || return newu0, newp newp = p === missing ? copy(newp) : newp initials, repack, alias = SciMLStructures.canonicalize( SciMLStructures.Initials(), newp) From 01a43bec2f4bf1e1f2af0444dcc7abfdedaa6d77 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 3 Apr 2025 17:41:43 +0530 Subject: [PATCH 1251/1337] feat: error in `late_binding_update_u0_p` if `newu0` is of incorrect length --- src/systems/nonlinear/initializesystem.jl | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/systems/nonlinear/initializesystem.jl b/src/systems/nonlinear/initializesystem.jl index 2283e7a1d4..9c85b9b324 100644 --- a/src/systems/nonlinear/initializesystem.jl +++ b/src/systems/nonlinear/initializesystem.jl @@ -669,6 +669,9 @@ function SciMLBase.late_binding_update_u0_p( initials = DiffEqBase.promote_u0(initials, newu0, t0) newp = repack(initials) end + if length(newu0) != length(unknowns(sys)) + throw(ArgumentError("Expected `newu0` to be of same length as unknowns ($(length(unknowns(sys)))). Got $(typeof(newu0)) of length $(length(newu0))")) + end setp(sys, Initial.(unknowns(sys)))(newp, newu0) return newu0, newp end From 61a64f9f6b39ce2846e19761ebc9520e11b4ce0f Mon Sep 17 00:00:00 2001 From: Christopher Rackauckas Date: Thu, 3 Apr 2025 08:47:59 -0400 Subject: [PATCH 1252/1337] Update Project.toml --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index b41ff59b22..8766db8eee 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "ModelingToolkit" uuid = "961ee093-0014-501f-94e3-6117800e7a78" authors = ["Yingbo Ma ", "Chris Rackauckas and contributors"] -version = "9.70.0" +version = "9.71.0" [deps] ADTypes = "47edcb42-4c32-4615-8424-f2b9edc5f35b" From d690f389b8c21177de70c5eaa28186e1d1ea033e Mon Sep 17 00:00:00 2001 From: vyudu Date: Thu, 3 Apr 2025 10:48:39 -0400 Subject: [PATCH 1253/1337] init: add jac_prototype --- src/systems/diffeqs/sdesystem.jl | 33 ++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/src/systems/diffeqs/sdesystem.jl b/src/systems/diffeqs/sdesystem.jl index 66044427eb..98c8aa6b25 100644 --- a/src/systems/diffeqs/sdesystem.jl +++ b/src/systems/diffeqs/sdesystem.jl @@ -593,6 +593,7 @@ function DiffEqBase.SDEFunction{iip, specialize}(sys::SDESystem, dvs = unknowns( u0 = nothing; version = nothing, tgrad = false, sparse = false, jac = false, Wfact = false, eval_expression = false, + sparsity = false, analytic = nothing, eval_module = @__MODULE__, checkbounds = false, initialization_data = nothing, cse = true, kwargs...) where {iip, specialize} @@ -641,6 +642,17 @@ function DiffEqBase.SDEFunction{iip, specialize}(sys::SDESystem, dvs = unknowns( _Wfact, _Wfact_t = nothing, nothing end + jac_prototype = if sparse + uElType = u0 === nothing ? Float64 : eltype(u0) + if jac + similar(calculate_jacobian(sys, sparse = sparse), uElType) + else + similar(jacobian_sparsity(sys), uElType) + end + else + nothing + end + M = calculate_massmatrix(sys) _M = (u0 === nothing || M == I) ? M : ArrayInterface.restructure(u0 .* u0', M) @@ -651,10 +663,14 @@ function DiffEqBase.SDEFunction{iip, specialize}(sys::SDESystem, dvs = unknowns( sys = sys, jac = _jac === nothing ? nothing : _jac, tgrad = _tgrad === nothing ? nothing : _tgrad, + mass_matrix = _M + jac_prototype = jac_prototype, + observed = observedfun, + sparsity = sparsity ? jacobian_sparsity(sys) : nothing, + analytic = analytic, Wfact = _Wfact === nothing ? nothing : _Wfact, Wfact_t = _Wfact_t === nothing ? nothing : _Wfact_t, - mass_matrix = _M, initialization_data, - observed = observedfun) + initialization_data) end """ @@ -724,6 +740,17 @@ function SDEFunctionExpr{iip}(sys::SDESystem, dvs = unknowns(sys), _jac = :nothing end + jac_prototype = if sparse + uElType = u0 === nothing ? Float64 : eltype(u0) + if jac + similar(calculate_jacobian(sys, sparse = sparse), uElType) + else + similar(jacobian_sparsity(sys), uElType) + end + else + nothing + end + if Wfact tmp_Wfact, tmp_Wfact_t = generate_factorized_W( sys, dvs, ps; expression = Val{true}, @@ -743,11 +770,13 @@ function SDEFunctionExpr{iip}(sys::SDESystem, dvs = unknowns(sys), g = $g tgrad = $_tgrad jac = $_jac + jac_prototype = $jac_prototype Wfact = $_Wfact Wfact_t = $_Wfact_t M = $_M SDEFunction{$iip}(f, g, jac = jac, + jac_prototype = jac_prototype, tgrad = tgrad, Wfact = Wfact, Wfact_t = Wfact_t, From a71ba77435549da2ee8cf6dfd80c7879fae856de Mon Sep 17 00:00:00 2001 From: vyudu Date: Thu, 3 Apr 2025 10:54:59 -0400 Subject: [PATCH 1254/1337] typo: comma --- src/systems/diffeqs/sdesystem.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/systems/diffeqs/sdesystem.jl b/src/systems/diffeqs/sdesystem.jl index 98c8aa6b25..806dbdf38a 100644 --- a/src/systems/diffeqs/sdesystem.jl +++ b/src/systems/diffeqs/sdesystem.jl @@ -663,7 +663,7 @@ function DiffEqBase.SDEFunction{iip, specialize}(sys::SDESystem, dvs = unknowns( sys = sys, jac = _jac === nothing ? nothing : _jac, tgrad = _tgrad === nothing ? nothing : _tgrad, - mass_matrix = _M + mass_matrix = _M, jac_prototype = jac_prototype, observed = observedfun, sparsity = sparsity ? jacobian_sparsity(sys) : nothing, From bd8913b23db9e9fe6320bb05b3708998dc2fbdfd Mon Sep 17 00:00:00 2001 From: vyudu Date: Thu, 3 Apr 2025 12:50:12 -0400 Subject: [PATCH 1255/1337] feat: add W sparsity/generation functions --- src/systems/diffeqs/abstractodesystem.jl | 46 +++++++++++++++++++++--- src/systems/diffeqs/sdesystem.jl | 13 ++++--- 2 files changed, 49 insertions(+), 10 deletions(-) diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index 23f20b00ec..a231db040c 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -126,6 +126,33 @@ function generate_jacobian(sys::AbstractODESystem, dvs = unknowns(sys), dvs, p..., get_iv(sys); + wrap_code = sparse ? assert_jac_length_header(sys) : (identity, identity), + kwargs...) +end + +function assert_jac_length_header(sys) + W = W_sparsity(sys) + + identity, expr -> Func([expr.args...], [], LiteralExpr(quote + @assert nnz($(expr.args[1])) == nnz(W) + expr.body + end)) +end + +function generate_W(sys::AbstractODESystem, γ = 1., dvs = unknowns(sys), + ps = parameters(sys; initial_parameters = true); + simplify = false, sparse = false, kwargs...) + @variables ˍ₋gamma + M = calculate_massmatrix(sys; simplify) + J = calculate_jacobian(sys; simplify, sparse, dvs) + W = ˍ₋gamma*M + J + + p = reorder_parameters(sys, ps) + return build_function_wrapper(sys, W, + dvs, + p..., + ˍ₋gamma, + get_iv(sys); kwargs...) end @@ -264,6 +291,12 @@ function jacobian_dae_sparsity(sys::AbstractODESystem) J1 + J2 end +function W_sparsity(sys::AbstractODESystem) + jac_sparsity = jacobian_sparsity(sys) + M_sparsity = sparse(iszero.(calculate_massmatrix(sys))) + jac_sparsity .|| M_sparsity +end + function isautonomous(sys::AbstractODESystem) tgrad = calculate_tgrad(sys; simplify = true) all(iszero, tgrad) @@ -368,15 +401,17 @@ function DiffEqBase.ODEFunction{iip, specialize}(sys::AbstractODESystem, observedfun = ObservedFunctionCache( sys; steady_state, eval_expression, eval_module, checkbounds, cse) - jac_prototype = if sparse + if sparse uElType = u0 === nothing ? Float64 : eltype(u0) if jac - similar(calculate_jacobian(sys, sparse = sparse), uElType) + jac_prototype = similar(calculate_jacobian(sys; sparse), uElType) else - similar(jacobian_sparsity(sys), uElType) + jac_prototype = similar(jacobian_sparsity(sys), uElType) end + W_prototype = similar(W_sparsity(sys), uElType) else - nothing + jac_prototype = nothing + W_prototype = nothing end @set! sys.split_idxs = split_idxs @@ -386,7 +421,8 @@ function DiffEqBase.ODEFunction{iip, specialize}(sys::AbstractODESystem, jac = _jac === nothing ? nothing : _jac, tgrad = _tgrad === nothing ? nothing : _tgrad, mass_matrix = _M, - jac_prototype = jac_prototype, + jac_prototype = W_prototype, + W_prototype = W_prototype, observed = observedfun, sparsity = sparsity ? jacobian_sparsity(sys) : nothing, analytic = analytic, diff --git a/src/systems/diffeqs/sdesystem.jl b/src/systems/diffeqs/sdesystem.jl index 806dbdf38a..2f84b70dcd 100644 --- a/src/systems/diffeqs/sdesystem.jl +++ b/src/systems/diffeqs/sdesystem.jl @@ -642,15 +642,17 @@ function DiffEqBase.SDEFunction{iip, specialize}(sys::SDESystem, dvs = unknowns( _Wfact, _Wfact_t = nothing, nothing end - jac_prototype = if sparse + if sparse uElType = u0 === nothing ? Float64 : eltype(u0) if jac - similar(calculate_jacobian(sys, sparse = sparse), uElType) + jac_prototype = similar(calculate_jacobian(sys; sparse), uElType) else - similar(jacobian_sparsity(sys), uElType) + jac_prototype = similar(jacobian_sparsity(sys), uElType) end + W_prototype = similar(W_sparsity(sys), uElType) else - nothing + jac_prototype = nothing + W_prototype = nothing end M = calculate_massmatrix(sys) @@ -664,7 +666,8 @@ function DiffEqBase.SDEFunction{iip, specialize}(sys::SDESystem, dvs = unknowns( jac = _jac === nothing ? nothing : _jac, tgrad = _tgrad === nothing ? nothing : _tgrad, mass_matrix = _M, - jac_prototype = jac_prototype, + jac_prototype = W_prototype, + W_prototype = W_prototype, observed = observedfun, sparsity = sparsity ? jacobian_sparsity(sys) : nothing, analytic = analytic, From 1e6b29606960b4a7cb8c11b3c33cdd63188e903e Mon Sep 17 00:00:00 2001 From: vyudu Date: Thu, 3 Apr 2025 13:08:24 -0400 Subject: [PATCH 1256/1337] fix: check actual sparsity pattern --- src/systems/diffeqs/abstractodesystem.jl | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index a231db040c..63aa97ac20 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -132,9 +132,10 @@ end function assert_jac_length_header(sys) W = W_sparsity(sys) - + (W_Is, W_Js, W_Vs) = findnz(W) identity, expr -> Func([expr.args...], [], LiteralExpr(quote - @assert nnz($(expr.args[1])) == nnz(W) + J_Is, J_Js, J_Vs = $(findnz)($(expr.args[1])) + @assert (J_Is, J_Js) == ($W_Is, $W_Js) expr.body end)) end From 9c05aa4999826fedd3d99e1798cab1be31a7f951 Mon Sep 17 00:00:00 2001 From: vyudu Date: Thu, 3 Apr 2025 13:28:22 -0400 Subject: [PATCH 1257/1337] test: test W_sparsity --- src/systems/diffeqs/abstractodesystem.jl | 6 ++---- test/jacobiansparsity.jl | 25 +++++++++++++++++++++++- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index 63aa97ac20..911762c765 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -132,10 +132,8 @@ end function assert_jac_length_header(sys) W = W_sparsity(sys) - (W_Is, W_Js, W_Vs) = findnz(W) identity, expr -> Func([expr.args...], [], LiteralExpr(quote - J_Is, J_Js, J_Vs = $(findnz)($(expr.args[1])) - @assert (J_Is, J_Js) == ($W_Is, $W_Js) + @assert $(findnz)($(expr.args[1]))[1:2] == $(findnz)($W)[1:2] expr.body end)) end @@ -294,7 +292,7 @@ end function W_sparsity(sys::AbstractODESystem) jac_sparsity = jacobian_sparsity(sys) - M_sparsity = sparse(iszero.(calculate_massmatrix(sys))) + M_sparsity = sparse((!iszero).(calculate_massmatrix(sys))) jac_sparsity .|| M_sparsity end diff --git a/test/jacobiansparsity.jl b/test/jacobiansparsity.jl index 24d47236b3..ef4a38c14f 100644 --- a/test/jacobiansparsity.jl +++ b/test/jacobiansparsity.jl @@ -1,4 +1,4 @@ -using OrdinaryDiffEq, ModelingToolkit, Test, SparseArrays +using OrdinaryDiffEq, ModelingToolkit, SparseArrays N = 3 xyd_brusselator = range(0, stop = 1, length = N) @@ -82,3 +82,26 @@ prob = ODEProblem(sys, u0, (0, 11.5), sparse = true, jac = false) prob = ODEProblem(sys, u0, (0, 11.5), sparse = true, jac = true) @test eltype(prob.f.jac_prototype) == Float32 + +@testset "W matrix sparsity" begin + @parameters g + @variables x(t) y(t) [state_priority = 10] λ(t) + eqs = [D(D(x)) ~ λ * x + D(D(y)) ~ λ * y - g + x^2 + y^2 ~ 1] + @mtkbuild pend = ODESystem(eqs, t) + + u0 = [x => 1, y => 0] + prob = ODEProblem(pend, u0, (0, 11.5), [g => 1], guesses = [λ => 1], sparse = true, jac = true) + jac, jac! = generate_jacobian(pend; expression = Val{false}, sparse = true) + jac_prototype = ModelingToolkit.jacobian_sparsity(pend) + W_prototype = ModelingToolkit.W_sparsity(pend) + @test nnz(W_prototype) == nnz(jac_prototype) + 2 + + @test findnz(prob.f.jac_prototype)[1:2] == findnz(W_prototype)[1:2] + + u = zeros(5) + p = prob.p + t = 0.0 + @test_throws AssertionError jac!(similar(jac_prototype, Float64), u, p, t) +end From 47c1418d0bcb7b95bd1540db77743f61aaa0ea1f Mon Sep 17 00:00:00 2001 From: vyudu Date: Thu, 3 Apr 2025 13:37:50 -0400 Subject: [PATCH 1258/1337] fix tests --- src/systems/diffeqs/abstractodesystem.jl | 7 +++++-- test/jacobiansparsity.jl | 7 ++++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index 911762c765..199c1de9bb 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -134,7 +134,7 @@ function assert_jac_length_header(sys) W = W_sparsity(sys) identity, expr -> Func([expr.args...], [], LiteralExpr(quote @assert $(findnz)($(expr.args[1]))[1:2] == $(findnz)($W)[1:2] - expr.body + $(expr.body) end)) end @@ -292,7 +292,10 @@ end function W_sparsity(sys::AbstractODESystem) jac_sparsity = jacobian_sparsity(sys) - M_sparsity = sparse((!iszero).(calculate_massmatrix(sys))) + (n, n) = size(jac_sparsity) + + M = calculate_massmatrix(sys) + M_sparsity = M isa UniformScaling ? sparse(I(n)) : sparse((!iszero).(M)) jac_sparsity .|| M_sparsity end diff --git a/test/jacobiansparsity.jl b/test/jacobiansparsity.jl index ef4a38c14f..28f8c66dc0 100644 --- a/test/jacobiansparsity.jl +++ b/test/jacobiansparsity.jl @@ -3,15 +3,15 @@ using OrdinaryDiffEq, ModelingToolkit, SparseArrays N = 3 xyd_brusselator = range(0, stop = 1, length = N) brusselator_f(x, y, t) = (((x - 0.3)^2 + (y - 0.6)^2) <= 0.1^2) * (t >= 1.1) * 5.0 -limit(a, N) = ModelingToolkit.ifelse(a == N + 1, 1, ModelingToolkit.ifelse(a == 0, N, a)) +lim(a, N) = ModelingToolkit.ifelse(a == N + 1, 1, ModelingToolkit.ifelse(a == 0, N, a)) function brusselator_2d_loop(du, u, p, t) A, B, alpha, dx = p alpha = alpha / dx^2 @inbounds for I in CartesianIndices((N, N)) i, j = Tuple(I) x, y = xyd_brusselator[I[1]], xyd_brusselator[I[2]] - ip1, im1, jp1, jm1 = limit(i + 1, N), limit(i - 1, N), limit(j + 1, N), - limit(j - 1, N) + ip1, im1, jp1, jm1 = lim(i + 1, N), lim(i - 1, N), lim(j + 1, N), + lim(j - 1, N) du[i, j, 1] = alpha * (u[im1, j, 1] + u[ip1, j, 1] + u[i, jp1, 1] + u[i, jm1, 1] - 4u[i, j, 1]) + B + u[i, j, 1]^2 * u[i, j, 2] - (A + 1) * u[i, j, 1] + @@ -84,6 +84,7 @@ prob = ODEProblem(sys, u0, (0, 11.5), sparse = true, jac = true) @test eltype(prob.f.jac_prototype) == Float32 @testset "W matrix sparsity" begin + t = ModelingToolkit.t_nounits @parameters g @variables x(t) y(t) [state_priority = 10] λ(t) eqs = [D(D(x)) ~ λ * x From 92c1cb90e84b7b07c4091dea4b4906b860e96d5d Mon Sep 17 00:00:00 2001 From: vyudu Date: Thu, 3 Apr 2025 16:24:35 -0400 Subject: [PATCH 1259/1337] fix: fix generate_W --- src/ModelingToolkit.jl | 2 +- src/structural_transformation/symbolics_tearing.jl | 5 +++++ src/systems/diffeqs/abstractodesystem.jl | 5 +++++ test/jacobiansparsity.jl | 8 +++++++- 4 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/ModelingToolkit.jl b/src/ModelingToolkit.jl index b53d3ec098..bd77abfbb9 100644 --- a/src/ModelingToolkit.jl +++ b/src/ModelingToolkit.jl @@ -303,7 +303,7 @@ export structural_simplify, expand_connections, linearize, linearization_functio LinearizationProblem export solve -export calculate_jacobian, generate_jacobian, generate_function, generate_custom_function +export calculate_jacobian, generate_jacobian, generate_function, generate_custom_function, generate_W export calculate_control_jacobian, generate_control_jacobian export calculate_tgrad, generate_tgrad export calculate_gradient, generate_gradient diff --git a/src/structural_transformation/symbolics_tearing.jl b/src/structural_transformation/symbolics_tearing.jl index 6845351008..ea17b0d005 100644 --- a/src/structural_transformation/symbolics_tearing.jl +++ b/src/structural_transformation/symbolics_tearing.jl @@ -378,6 +378,7 @@ function generate_derivative_variables!( # For variable x, make dummy derivative x_t if the # derivative is in the system for v in 1:length(var_to_diff) + @show graph dv = var_to_diff[v] dv isa Int || continue solved = var_eq_matching[dv] isa Int @@ -403,6 +404,10 @@ function generate_derivative_variables!( v_t = add_dd_variable!(structure, fullvars, x_t, dv) # Add `D(x) - x_t ~ 0` to the graph dummy_eq = add_dd_equation!(structure, neweqs, 0 ~ dx - x_t, dv, v_t) + # Update graph to say, all the equations featuring D(x) also feature x_t + for e in 𝑑neighbors(graph, dv) + add_edge!(graph, e, v_t) + end # Update matching push!(var_eq_matching, unassigned) diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index 199c1de9bb..df3eb6cf92 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -143,15 +143,20 @@ function generate_W(sys::AbstractODESystem, γ = 1., dvs = unknowns(sys), simplify = false, sparse = false, kwargs...) @variables ˍ₋gamma M = calculate_massmatrix(sys; simplify) + sparse && (M = SparseArrays.sparse(M)) J = calculate_jacobian(sys; simplify, sparse, dvs) W = ˍ₋gamma*M + J + @show W p = reorder_parameters(sys, ps) + @show length(p) return build_function_wrapper(sys, W, dvs, p..., ˍ₋gamma, get_iv(sys); + wrap_code = sparse ? assert_jac_length_header(sys) : (identity, identity), + p_end = 1 + length(p), kwargs...) end diff --git a/test/jacobiansparsity.jl b/test/jacobiansparsity.jl index 28f8c66dc0..ced723507b 100644 --- a/test/jacobiansparsity.jl +++ b/test/jacobiansparsity.jl @@ -86,7 +86,7 @@ prob = ODEProblem(sys, u0, (0, 11.5), sparse = true, jac = true) @testset "W matrix sparsity" begin t = ModelingToolkit.t_nounits @parameters g - @variables x(t) y(t) [state_priority = 10] λ(t) + @variables x(t) y(t) λ(t) eqs = [D(D(x)) ~ λ * x D(D(y)) ~ λ * y - g x^2 + y^2 ~ 1] @@ -105,4 +105,10 @@ prob = ODEProblem(sys, u0, (0, 11.5), sparse = true, jac = true) p = prob.p t = 0.0 @test_throws AssertionError jac!(similar(jac_prototype, Float64), u, p, t) + + W, W! = generate_W(pend; expression = Val{false}, sparse = true) + γ = .1 + M = sparse(calculate_massmatrix(pend)) + @test_throws AssertionError W!(similar(jac_prototype, Float64), u, p, γ, t) + @test W!(similar(W_prototype, Float64), u, p, γ, t) == 0.1 * M + jac!(similar(W_prototype, Float64), u, p, t) end From 1d3da35f166ef46dfdbe9c1c23e2efa688cdd612 Mon Sep 17 00:00:00 2001 From: vyudu Date: Thu, 3 Apr 2025 17:01:10 -0400 Subject: [PATCH 1260/1337] fix --- src/linearization.jl | 1 - src/structural_transformation/symbolics_tearing.jl | 1 - src/systems/diffeqs/abstractodesystem.jl | 2 -- src/systems/diffeqs/odesystem.jl | 1 - test/jacobiansparsity.jl | 7 ++++--- 5 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/linearization.jl b/src/linearization.jl index 57b171e874..77f4422b63 100644 --- a/src/linearization.jl +++ b/src/linearization.jl @@ -535,7 +535,6 @@ function linearize_symbolic(sys::AbstractSystem, inputs, if !iszero(Bs) if !allow_input_derivatives der_inds = findall(vec(any(!iszero, Bs, dims = 1))) - @show typeof(der_inds) error("Input derivatives appeared in expressions (-g_z\\g_u != 0), the following inputs appeared differentiated: $(ModelingToolkit.inputs(sys)[der_inds]). Call `linearize_symbolic` with keyword argument `allow_input_derivatives = true` to allow this and have the returned `B` matrix be of double width ($(2nu)), where the last $nu inputs are the derivatives of the first $nu inputs.") end B = [B [zeros(nx, nu); Bs]] diff --git a/src/structural_transformation/symbolics_tearing.jl b/src/structural_transformation/symbolics_tearing.jl index ea17b0d005..552c6d13c3 100644 --- a/src/structural_transformation/symbolics_tearing.jl +++ b/src/structural_transformation/symbolics_tearing.jl @@ -378,7 +378,6 @@ function generate_derivative_variables!( # For variable x, make dummy derivative x_t if the # derivative is in the system for v in 1:length(var_to_diff) - @show graph dv = var_to_diff[v] dv isa Int || continue solved = var_eq_matching[dv] isa Int diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index df3eb6cf92..d006b8934b 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -146,10 +146,8 @@ function generate_W(sys::AbstractODESystem, γ = 1., dvs = unknowns(sys), sparse && (M = SparseArrays.sparse(M)) J = calculate_jacobian(sys; simplify, sparse, dvs) W = ˍ₋gamma*M + J - @show W p = reorder_parameters(sys, ps) - @show length(p) return build_function_wrapper(sys, W, dvs, p..., diff --git a/src/systems/diffeqs/odesystem.jl b/src/systems/diffeqs/odesystem.jl index 57d71b18c8..599ea06b7b 100644 --- a/src/systems/diffeqs/odesystem.jl +++ b/src/systems/diffeqs/odesystem.jl @@ -323,7 +323,6 @@ function ODESystem(deqs::AbstractVector{<:Equation}, iv, dvs, ps; cons = get_constraintsystem(sys) cons !== nothing && push!(conssystems, cons) end - @show conssystems @set! constraintsystem.systems = conssystems end diff --git a/test/jacobiansparsity.jl b/test/jacobiansparsity.jl index ced723507b..1668788375 100644 --- a/test/jacobiansparsity.jl +++ b/test/jacobiansparsity.jl @@ -1,4 +1,4 @@ -using OrdinaryDiffEq, ModelingToolkit, SparseArrays +using ModelingToolkit, SparseArrays#, OrdinaryDiffEq N = 3 xyd_brusselator = range(0, stop = 1, length = N) @@ -51,7 +51,7 @@ JP = prob.f.jac_prototype # test sparse jacobian prob = ODEProblem(sys, u0, (0, 11.5), sparse = true, jac = true) -@test_nowarn solve(prob, Rosenbrock23()) +#@test_nowarn solve(prob, Rosenbrock23()) @test findnz(calculate_jacobian(sys, sparse = true))[1:2] == findnz(prob.f.jac_prototype)[1:2] @@ -74,7 +74,7 @@ f = DiffEqBase.ODEFunction(sys, u0 = nothing, sparse = true, jac = false) # test when u0 is not Float64 u0 = similar(init_brusselator_2d(xyd_brusselator), Float32) prob_ode_brusselator_2d = ODEProblem(brusselator_2d_loop, - u0, (0.0, 11.5), p) + u0, (0.0, 11.5), p) sys = complete(modelingtoolkitize(prob_ode_brusselator_2d)) prob = ODEProblem(sys, u0, (0, 11.5), sparse = true, jac = false) @@ -85,6 +85,7 @@ prob = ODEProblem(sys, u0, (0, 11.5), sparse = true, jac = true) @testset "W matrix sparsity" begin t = ModelingToolkit.t_nounits + D = ModelingToolkit.D_nounits @parameters g @variables x(t) y(t) λ(t) eqs = [D(D(x)) ~ λ * x From d2e952f703a1ab75df2cee68ff9452864ee35ea7 Mon Sep 17 00:00:00 2001 From: vyudu Date: Thu, 3 Apr 2025 18:26:06 -0400 Subject: [PATCH 1261/1337] fix: sdesystems don't have W_prototype --- src/systems/diffeqs/sdesystem.jl | 1 - 1 file changed, 1 deletion(-) diff --git a/src/systems/diffeqs/sdesystem.jl b/src/systems/diffeqs/sdesystem.jl index 2f84b70dcd..78b7a02195 100644 --- a/src/systems/diffeqs/sdesystem.jl +++ b/src/systems/diffeqs/sdesystem.jl @@ -667,7 +667,6 @@ function DiffEqBase.SDEFunction{iip, specialize}(sys::SDESystem, dvs = unknowns( tgrad = _tgrad === nothing ? nothing : _tgrad, mass_matrix = _M, jac_prototype = W_prototype, - W_prototype = W_prototype, observed = observedfun, sparsity = sparsity ? jacobian_sparsity(sys) : nothing, analytic = analytic, From 7f37ea3179920bd2a8b0791ad5615ae2e6532447 Mon Sep 17 00:00:00 2001 From: vyudu Date: Fri, 4 Apr 2025 13:24:12 -0400 Subject: [PATCH 1262/1337] fix: SDE has no tearing state --- src/systems/diffeqs/sdesystem.jl | 25 +++++++++---------------- test/jacobiansparsity.jl | 3 ++- test/nonlinearsystem.jl | 2 +- 3 files changed, 12 insertions(+), 18 deletions(-) diff --git a/src/systems/diffeqs/sdesystem.jl b/src/systems/diffeqs/sdesystem.jl index 78b7a02195..605e340aa5 100644 --- a/src/systems/diffeqs/sdesystem.jl +++ b/src/systems/diffeqs/sdesystem.jl @@ -642,20 +642,16 @@ function DiffEqBase.SDEFunction{iip, specialize}(sys::SDESystem, dvs = unknowns( _Wfact, _Wfact_t = nothing, nothing end + M = calculate_massmatrix(sys) if sparse uElType = u0 === nothing ? Float64 : eltype(u0) - if jac - jac_prototype = similar(calculate_jacobian(sys; sparse), uElType) - else - jac_prototype = similar(jacobian_sparsity(sys), uElType) - end - W_prototype = similar(W_sparsity(sys), uElType) + jac_prototype = similar(calculate_jacobian(sys; sparse), uElType) + W_prototype = similar(jac_prototype .+ M, uElType) else jac_prototype = nothing W_prototype = nothing end - M = calculate_massmatrix(sys) _M = (u0 === nothing || M == I) ? M : ArrayInterface.restructure(u0 .* u0', M) observedfun = ObservedFunctionCache( @@ -742,15 +738,14 @@ function SDEFunctionExpr{iip}(sys::SDESystem, dvs = unknowns(sys), _jac = :nothing end - jac_prototype = if sparse + M = calculate_massmatrix(sys) + if sparse uElType = u0 === nothing ? Float64 : eltype(u0) - if jac - similar(calculate_jacobian(sys, sparse = sparse), uElType) - else - similar(jacobian_sparsity(sys), uElType) - end + jac_prototype = similar(calculate_jacobian(sys; sparse), uElType) + W_prototype = similar(jac_prototype .+ M, uElType) else - nothing + jac_prototype = nothing + W_prototype = nothing end if Wfact @@ -763,8 +758,6 @@ function SDEFunctionExpr{iip}(sys::SDESystem, dvs = unknowns(sys), _Wfact, _Wfact_t = :nothing, :nothing end - M = calculate_massmatrix(sys) - _M = (u0 === nothing || M == I) ? M : ArrayInterface.restructure(u0 .* u0', M) ex = quote diff --git a/test/jacobiansparsity.jl b/test/jacobiansparsity.jl index 1668788375..f6f9853137 100644 --- a/test/jacobiansparsity.jl +++ b/test/jacobiansparsity.jl @@ -1,4 +1,4 @@ -using ModelingToolkit, SparseArrays#, OrdinaryDiffEq +using ModelingToolkit, SparseArrays, OrdinaryDiffEq N = 3 xyd_brusselator = range(0, stop = 1, length = N) @@ -100,6 +100,7 @@ prob = ODEProblem(sys, u0, (0, 11.5), sparse = true, jac = true) W_prototype = ModelingToolkit.W_sparsity(pend) @test nnz(W_prototype) == nnz(jac_prototype) + 2 + # jac_prototype should be the same as W_prototype @test findnz(prob.f.jac_prototype)[1:2] == findnz(W_prototype)[1:2] u = zeros(5) diff --git a/test/nonlinearsystem.jl b/test/nonlinearsystem.jl index 9475f24006..a315371141 100644 --- a/test/nonlinearsystem.jl +++ b/test/nonlinearsystem.jl @@ -30,7 +30,7 @@ eqs = [0 ~ σ * (y - x) * h, @test eval(toexpr(ns)) == ns test_nlsys_inference("standard", ns, (x, y, z), (σ, ρ, β)) @test begin - f = eval(generate_function(ns, [x, y, z], [σ, ρ, β])[2]) + f = generate_function(ns, [x, y, z], [σ, ρ, β], expression = Val{false})[2] du = [0.0, 0.0, 0.0] f(du, [1, 2, 3], [1, 2, 3]) du ≈ [1, -3, -7] From 15453aab9713bb87414c6ec96e72be405a5110c3 Mon Sep 17 00:00:00 2001 From: vyudu Date: Fri, 4 Apr 2025 13:31:37 -0400 Subject: [PATCH 1263/1337] fix: fix tests --- test/discrete_system.jl | 25 ++++++--------- test/nonlinearsystem.jl | 2 +- test/odesystem.jl | 70 +++++++++++++++++++---------------------- 3 files changed, 44 insertions(+), 53 deletions(-) diff --git a/test/discrete_system.jl b/test/discrete_system.jl index 874d045aa9..1afd43c417 100644 --- a/test/discrete_system.jl +++ b/test/discrete_system.jl @@ -34,21 +34,16 @@ eqs = [S ~ S(k - 1) - infection * h, syss = structural_simplify(sys) @test syss == syss -for df in [ - DiscreteFunction(syss), - eval(DiscreteFunctionExpr(syss)) -] - - # iip - du = zeros(3) - u = collect(1:3) - p = MTKParameters(syss, [c, nsteps, δt, β, γ] .=> collect(1:5)) - df.f(du, u, p, 0) - @test du ≈ [0.01831563888873422, 0.9816849729159067, 4.999999388195359] - - # oop - @test df.f(u, p, 0) ≈ [0.01831563888873422, 0.9816849729159067, 4.999999388195359] -end +df = DiscreteFunction(ssys) +# iip +du = zeros(3) +u = collect(1:3) +p = MTKParameters(syss, [c, nsteps, δt, β, γ] .=> collect(1:5)) +df.f(du, u, p, 0) +@test du ≈ [0.01831563888873422, 0.9816849729159067, 4.999999388195359] + +# oop +@test df.f(u, p, 0) ≈ [0.01831563888873422, 0.9816849729159067, 4.999999388195359] # Problem u0 = [S(k - 1) => 990.0, I(k - 1) => 10.0, R(k - 1) => 0.0] diff --git a/test/nonlinearsystem.jl b/test/nonlinearsystem.jl index 9475f24006..a315371141 100644 --- a/test/nonlinearsystem.jl +++ b/test/nonlinearsystem.jl @@ -30,7 +30,7 @@ eqs = [0 ~ σ * (y - x) * h, @test eval(toexpr(ns)) == ns test_nlsys_inference("standard", ns, (x, y, z), (σ, ρ, β)) @test begin - f = eval(generate_function(ns, [x, y, z], [σ, ρ, β])[2]) + f = generate_function(ns, [x, y, z], [σ, ρ, β], expression = Val{false})[2] du = [0.0, 0.0, 0.0] f(du, [1, 2, 3], [1, 2, 3]) du ≈ [1, -3, -7] diff --git a/test/odesystem.jl b/test/odesystem.jl index 78218c5107..e6945891de 100644 --- a/test/odesystem.jl +++ b/test/odesystem.jl @@ -54,42 +54,38 @@ jac = calculate_jacobian(de) jacfun = eval(jac_expr[2]) de = complete(de) -for f in [ - ODEFunction(de, [x, y, z], [σ, ρ, β], tgrad = true, jac = true), - eval(ODEFunctionExpr(de, [x, y, z], [σ, ρ, β], tgrad = true, jac = true)) -] - # system - @test f.sys === de - - # iip - du = zeros(3) - u = collect(1:3) - p = ModelingToolkit.MTKParameters(de, [σ, ρ, β] .=> 4.0:6.0) - f.f(du, u, p, 0.1) - @test du == [4, 0, -16] - - # oop - du = @SArray zeros(3) - u = SVector(1:3...) - p = ModelingToolkit.MTKParameters(de, SVector{3}([σ, ρ, β] .=> 4.0:6.0)) - @test f.f(u, p, 0.1) === @SArray [4.0, 0.0, -16.0] - - # iip vs oop - du = zeros(3) - g = similar(du) - J = zeros(3, 3) - u = collect(1:3) - p = ModelingToolkit.MTKParameters(de, [σ, ρ, β] .=> 4.0:6.0) - f.f(du, u, p, 0.1) - @test du == f(u, p, 0.1) - f.tgrad(g, u, p, t) - @test g == f.tgrad(u, p, t) - f.jac(J, u, p, t) - @test J == f.jac(u, p, t) -end +f = ODEFunction(de, [x, y, z], [σ, ρ, β], tgrad = true, jac = true), +# system +@test f.sys === de + +# iip +du = zeros(3) +u = collect(1:3) +p = ModelingToolkit.MTKParameters(de, [σ, ρ, β] .=> 4.0:6.0) +f.f(du, u, p, 0.1) +@test du == [4, 0, -16] + +# oop +du = @SArray zeros(3) +u = SVector(1:3...) +p = ModelingToolkit.MTKParameters(de, SVector{3}([σ, ρ, β] .=> 4.0:6.0)) +@test f.f(u, p, 0.1) === @SArray [4.0, 0.0, -16.0] + +# iip vs oop +du = zeros(3) +g = similar(du) +J = zeros(3, 3) +u = collect(1:3) +p = ModelingToolkit.MTKParameters(de, [σ, ρ, β] .=> 4.0:6.0) +f.f(du, u, p, 0.1) +@test du == f(u, p, 0.1) +f.tgrad(g, u, p, t) +@test g == f.tgrad(u, p, t) +f.jac(J, u, p, t) +@test J == f.jac(u, p, t) #check iip_config -f = eval(ODEFunctionExpr(de, [x, y, z], [σ, ρ, β], iip_config = (false, true))) +f = ODEFunction(de, [x, y, z], [σ, ρ, β], iip_config = (false, true)) du = zeros(3) u = collect(1:3) p = ModelingToolkit.MTKParameters(de, [σ, ρ, β] .=> 4.0:6.0) @@ -146,7 +142,7 @@ eqs = [D(x) ~ σ′ * (y - x), @named de = ODESystem(eqs, t) test_diffeq_inference("global iv-varying", de, t, (x, y, z), (σ′, ρ, β)) -f = eval(generate_function(de, [x, y, z], [σ′, ρ, β])[2]) +f = generate_function(de, [x, y, z], [σ′, ρ, β], expression = Val{false})[2] du = [0.0, 0.0, 0.0] f(du, [1.0, 2.0, 3.0], [x -> x + 7, 2, 3], 5.0) @test du ≈ [11, -3, -7] @@ -157,7 +153,7 @@ eqs = [D(x) ~ σ(t - 1) * (y - x), D(z) ~ x * y - β * z * κ] @named de = ODESystem(eqs, t) test_diffeq_inference("single internal iv-varying", de, t, (x, y, z), (σ, ρ, β)) -f = eval(generate_function(de, [x, y, z], [σ, ρ, β])[2]) +f = generate_function(de, [x, y, z], [σ, ρ, β], expression = Val{false})[2] du = [0.0, 0.0, 0.0] f(du, [1.0, 2.0, 3.0], [x -> x + 7, 2, 3], 5.0) @test du ≈ [11, -3, -7] @@ -165,7 +161,7 @@ f(du, [1.0, 2.0, 3.0], [x -> x + 7, 2, 3], 5.0) eqs = [D(x) ~ x + 10σ(t - 1) + 100σ(t - 2) + 1000σ(t^2)] @named de = ODESystem(eqs, t) test_diffeq_inference("many internal iv-varying", de, t, (x,), (σ,)) -f = eval(generate_function(de, [x], [σ])[2]) +f = generate_function(de, [x], [σ], expression = Val{false})[2] du = [0.0] f(du, [1.0], [t -> t + 2], 5.0) @test du ≈ [27561] From db030e46bf8b59f67e499ef9c1ea2dbce765ce62 Mon Sep 17 00:00:00 2001 From: vyudu Date: Fri, 4 Apr 2025 17:00:32 -0400 Subject: [PATCH 1264/1337] fix: comma --- test/odesystem.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/odesystem.jl b/test/odesystem.jl index e6945891de..be5fb32b7e 100644 --- a/test/odesystem.jl +++ b/test/odesystem.jl @@ -54,7 +54,7 @@ jac = calculate_jacobian(de) jacfun = eval(jac_expr[2]) de = complete(de) -f = ODEFunction(de, [x, y, z], [σ, ρ, β], tgrad = true, jac = true), +f = ODEFunction(de, [x, y, z], [σ, ρ, β], tgrad = true, jac = true) # system @test f.sys === de From 400dbb43d3bf79b71030a07741b73b40f526df6a Mon Sep 17 00:00:00 2001 From: vyudu Date: Fri, 4 Apr 2025 17:04:32 -0400 Subject: [PATCH 1265/1337] fix: remove bad broadcast --- src/systems/diffeqs/sdesystem.jl | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/systems/diffeqs/sdesystem.jl b/src/systems/diffeqs/sdesystem.jl index 605e340aa5..1d7dd321bc 100644 --- a/src/systems/diffeqs/sdesystem.jl +++ b/src/systems/diffeqs/sdesystem.jl @@ -739,10 +739,12 @@ function SDEFunctionExpr{iip}(sys::SDESystem, dvs = unknowns(sys), end M = calculate_massmatrix(sys) + _M = (u0 === nothing || M == I) ? M : ArrayInterface.restructure(u0 .* u0', M) + if sparse uElType = u0 === nothing ? Float64 : eltype(u0) jac_prototype = similar(calculate_jacobian(sys; sparse), uElType) - W_prototype = similar(jac_prototype .+ M, uElType) + W_prototype = similar(jac_prototype + M, uElType) else jac_prototype = nothing W_prototype = nothing @@ -758,7 +760,6 @@ function SDEFunctionExpr{iip}(sys::SDESystem, dvs = unknowns(sys), _Wfact, _Wfact_t = :nothing, :nothing end - _M = (u0 === nothing || M == I) ? M : ArrayInterface.restructure(u0 .* u0', M) ex = quote f = $f From e21ac76764b7f6d3b662dce39f4d0b71c6f50d1c Mon Sep 17 00:00:00 2001 From: vyudu Date: Fri, 4 Apr 2025 17:05:14 -0400 Subject: [PATCH 1266/1337] up --- src/systems/diffeqs/sdesystem.jl | 1 - 1 file changed, 1 deletion(-) diff --git a/src/systems/diffeqs/sdesystem.jl b/src/systems/diffeqs/sdesystem.jl index 1d7dd321bc..af4273fc22 100644 --- a/src/systems/diffeqs/sdesystem.jl +++ b/src/systems/diffeqs/sdesystem.jl @@ -760,7 +760,6 @@ function SDEFunctionExpr{iip}(sys::SDESystem, dvs = unknowns(sys), _Wfact, _Wfact_t = :nothing, :nothing end - ex = quote f = $f g = $g From 483be933cb8f5b75786bc76cd78c72e530593ba7 Mon Sep 17 00:00:00 2001 From: Christopher Rackauckas Date: Fri, 4 Apr 2025 21:21:59 -0400 Subject: [PATCH 1267/1337] Update test/discrete_system.jl --- test/discrete_system.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/discrete_system.jl b/test/discrete_system.jl index 1afd43c417..0d215052d8 100644 --- a/test/discrete_system.jl +++ b/test/discrete_system.jl @@ -34,7 +34,7 @@ eqs = [S ~ S(k - 1) - infection * h, syss = structural_simplify(sys) @test syss == syss -df = DiscreteFunction(ssys) +df = DiscreteFunction(syss) # iip du = zeros(3) u = collect(1:3) From baf7de67daee654c68bc986960681a39685b7539 Mon Sep 17 00:00:00 2001 From: vyudu Date: Fri, 4 Apr 2025 23:02:32 -0400 Subject: [PATCH 1268/1337] fix a couple mroe cases --- test/odesystem.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/odesystem.jl b/test/odesystem.jl index be5fb32b7e..a45ed99f35 100644 --- a/test/odesystem.jl +++ b/test/odesystem.jl @@ -214,11 +214,11 @@ eqs = [D(x) ~ -A * x, @named de = ODESystem(eqs, t) @test begin local f, du - f = eval(generate_function(de, [x, y], [A, B, C])[2]) + f = generate_function(de, [x, y], [A, B, C], expression = Val{false})[2] du = [0.0, 0.0] f(du, [1.0, 2.0], [1, 2, 3], 0.0) du ≈ [-1, -1 / 3] - f = eval(generate_function(de, [x, y], [A, B, C])[1]) + f = generate_function(de, [x, y], [A, B, C], expression = Val{false})[1] du ≈ f([1.0, 2.0], [1, 2, 3], 0.0) end From be7b8d8d49bc93b3a5e1ce2bc177a1cc91c0b091 Mon Sep 17 00:00:00 2001 From: Christopher Rackauckas Date: Sat, 5 Apr 2025 08:40:04 -0400 Subject: [PATCH 1269/1337] Update steadystatesystems.jl --- test/steadystatesystems.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/steadystatesystems.jl b/test/steadystatesystems.jl index d29c809325..4f1b5ed063 100644 --- a/test/steadystatesystems.jl +++ b/test/steadystatesystems.jl @@ -17,7 +17,7 @@ for factor in [1e-1, 1e0, 1e10], ss_prob = SteadyStateProblem(de, u0, p) sol = solve(ss_prob, SSRootfind()).u[1] @test abs(sol^2 - factor * u0_p[2]) < 1e-8 - ss_prob = SteadyStateProblemExpr(de, u0, p) - sol_expr = solve(eval(ss_prob), SSRootfind()).u[1] + ss_prob = SteadyStateProblem(de, u0, p) + sol_expr = solve(ss_prob, SSRootfind()).u[1] @test all(x -> x == 0, sol - sol_expr) end From 8251a012f12966d9548f8684795fa990a3143020 Mon Sep 17 00:00:00 2001 From: vyudu Date: Sat, 5 Apr 2025 12:07:17 -0400 Subject: [PATCH 1270/1337] useW-sparsity --- src/systems/diffeqs/abstractodesystem.jl | 17 +++++++---------- src/systems/diffeqs/sdesystem.jl | 6 ++---- 2 files changed, 9 insertions(+), 14 deletions(-) diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index d006b8934b..ca9681cd8b 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -294,9 +294,13 @@ function jacobian_dae_sparsity(sys::AbstractODESystem) end function W_sparsity(sys::AbstractODESystem) - jac_sparsity = jacobian_sparsity(sys) + if has_tearing_state(sys) + jac_sparsity = jacobian_sparsity(sys) + else + jac = calculate_jacobian(sys; sparse = true) + jac_sparsity = sparse((!iszero).(jac)) + end (n, n) = size(jac_sparsity) - M = calculate_massmatrix(sys) M_sparsity = M isa UniformScaling ? sparse(I(n)) : sparse((!iszero).(M)) jac_sparsity .|| M_sparsity @@ -408,14 +412,8 @@ function DiffEqBase.ODEFunction{iip, specialize}(sys::AbstractODESystem, if sparse uElType = u0 === nothing ? Float64 : eltype(u0) - if jac - jac_prototype = similar(calculate_jacobian(sys; sparse), uElType) - else - jac_prototype = similar(jacobian_sparsity(sys), uElType) - end W_prototype = similar(W_sparsity(sys), uElType) else - jac_prototype = nothing W_prototype = nothing end @@ -427,9 +425,8 @@ function DiffEqBase.ODEFunction{iip, specialize}(sys::AbstractODESystem, tgrad = _tgrad === nothing ? nothing : _tgrad, mass_matrix = _M, jac_prototype = W_prototype, - W_prototype = W_prototype, observed = observedfun, - sparsity = sparsity ? jacobian_sparsity(sys) : nothing, + sparsity = sparsity ? W_sparsity(sys) : nothing, analytic = analytic, initialization_data) end diff --git a/src/systems/diffeqs/sdesystem.jl b/src/systems/diffeqs/sdesystem.jl index af4273fc22..e496b89cfa 100644 --- a/src/systems/diffeqs/sdesystem.jl +++ b/src/systems/diffeqs/sdesystem.jl @@ -645,10 +645,8 @@ function DiffEqBase.SDEFunction{iip, specialize}(sys::SDESystem, dvs = unknowns( M = calculate_massmatrix(sys) if sparse uElType = u0 === nothing ? Float64 : eltype(u0) - jac_prototype = similar(calculate_jacobian(sys; sparse), uElType) - W_prototype = similar(jac_prototype .+ M, uElType) + W_prototype = similar(W_sparsity(sys), uElType) else - jac_prototype = nothing W_prototype = nothing end @@ -664,7 +662,7 @@ function DiffEqBase.SDEFunction{iip, specialize}(sys::SDESystem, dvs = unknowns( mass_matrix = _M, jac_prototype = W_prototype, observed = observedfun, - sparsity = sparsity ? jacobian_sparsity(sys) : nothing, + sparsity = sparsity ? W_sparsity(sys) : nothing, analytic = analytic, Wfact = _Wfact === nothing ? nothing : _Wfact, Wfact_t = _Wfact_t === nothing ? nothing : _Wfact_t, From ade11d39ffa3fefc4bdc2a2eeacd6dd6a9343b8d Mon Sep 17 00:00:00 2001 From: vyudu Date: Sun, 6 Apr 2025 12:38:47 -0400 Subject: [PATCH 1271/1337] use W_sparsity in the SDEFunctionExpr --- src/systems/diffeqs/sdesystem.jl | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/systems/diffeqs/sdesystem.jl b/src/systems/diffeqs/sdesystem.jl index e496b89cfa..2fc9749309 100644 --- a/src/systems/diffeqs/sdesystem.jl +++ b/src/systems/diffeqs/sdesystem.jl @@ -741,10 +741,8 @@ function SDEFunctionExpr{iip}(sys::SDESystem, dvs = unknowns(sys), if sparse uElType = u0 === nothing ? Float64 : eltype(u0) - jac_prototype = similar(calculate_jacobian(sys; sparse), uElType) - W_prototype = similar(jac_prototype + M, uElType) + W_prototype = similar(W_sparsity(sys), uElType) else - jac_prototype = nothing W_prototype = nothing end @@ -763,13 +761,13 @@ function SDEFunctionExpr{iip}(sys::SDESystem, dvs = unknowns(sys), g = $g tgrad = $_tgrad jac = $_jac - jac_prototype = $jac_prototype + W_prototype = $W_prototype Wfact = $_Wfact Wfact_t = $_Wfact_t M = $_M SDEFunction{$iip}(f, g, jac = jac, - jac_prototype = jac_prototype, + jac_prototype = W_prototype, tgrad = tgrad, Wfact = Wfact, Wfact_t = Wfact_t, From 254cd330f28b5c0c4128d4914be2f817e9d10d62 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 2 Apr 2025 16:16:50 +0530 Subject: [PATCH 1272/1337] docs: document `SymScope` variants --- src/systems/abstractsystem.jl | 73 +++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index 1987cb9a60..9bbe05dbeb 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -1114,9 +1114,25 @@ function _apply_to_variables(f::F, ex) where {F} metadata(ex)) end +""" +Variable metadata key which contains information about scoping/namespacing of the +variable in a hierarchical system. +""" abstract type SymScope end +""" + $(TYPEDEF) + +The default scope of a variable. It belongs to the system whose equations it is involved +in and is namespaced by every level of the hierarchy. +""" struct LocalScope <: SymScope end + +""" + $(TYPEDSIGNATURES) + +Apply `LocalScope` to `sym`. +""" function LocalScope(sym::Union{Num, Symbolic, Symbolics.Arr{Num}}) apply_to_variables(sym) do sym if iscall(sym) && operation(sym) === getindex @@ -1130,9 +1146,25 @@ function LocalScope(sym::Union{Num, Symbolic, Symbolics.Arr{Num}}) end end +""" + $(TYPEDEF) + +Denotes that the variable does not belong to the system whose equations it is involved +in. It is not namespaced by this system. In the immediate parent of this system, the +scope of this variable is given by `parent`. + +# Fields + +$(TYPEDFIELDS) +""" struct ParentScope <: SymScope parent::SymScope end +""" + $(TYPEDSIGNATURES) + +Apply `ParentScope` to `sym`, with `parent` being `LocalScope`. +""" function ParentScope(sym::Union{Num, Symbolic, Symbolics.Arr{Num}}) apply_to_variables(sym) do sym if iscall(sym) && operation(sym) === getindex @@ -1148,10 +1180,31 @@ function ParentScope(sym::Union{Num, Symbolic, Symbolics.Arr{Num}}) end end +""" + $(TYPEDEF) + +Denotes that a variable belongs to a system that is at least `N + 1` levels up in the +hierarchy from the system whose equations it is involved in. It is namespaced by the +first `N` parents and not namespaced by the `N+1`th parent in the hierarchy. The scope +of the variable after this point is given by `parent`. + +In other words, this scope delays applying `ParentScope` by `N` levels, and applies +`LocalScope` in the meantime. + +# Fields + +$(TYPEDFIELDS) +""" struct DelayParentScope <: SymScope parent::SymScope N::Int end + +""" + $(TYPEDSIGNATURES) + +Apply `DelayParentScope` to `sym`, with a delay of `N` and `parent` being `LocalScope`. +""" function DelayParentScope(sym::Union{Num, Symbolic, Symbolics.Arr{Num}}, N) apply_to_variables(sym) do sym if iscall(sym) && operation(sym) == getindex @@ -1166,9 +1219,29 @@ function DelayParentScope(sym::Union{Num, Symbolic, Symbolics.Arr{Num}}, N) end end end + +""" + $(TYPEDSIGNATURES) + +Apply `DelayParentScope` to `sym`, with a delay of `1` and `parent` being `LocalScope`. +""" DelayParentScope(sym::Union{Num, Symbolic, Symbolics.Arr{Num}}) = DelayParentScope(sym, 1) +""" + $(TYPEDEF) + +Denotes that a variable belongs to the root system in the hierarchy, regardless of which +equations of subsystems in the hierarchy it is involved in. Variables with this scope +are never namespaced and only added to the unknowns/parameters of a system when calling +`complete` or `structural_simplify`. +""" struct GlobalScope <: SymScope end + +""" + $(TYPEDSIGNATURES) + +Apply `GlobalScope` to `sym`. +""" function GlobalScope(sym::Union{Num, Symbolic, Symbolics.Arr{Num}}) apply_to_variables(sym) do sym if iscall(sym) && operation(sym) == getindex From 96285da14a0118c387ebc52fbe3737a562de2b08 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 2 Apr 2025 16:17:29 +0530 Subject: [PATCH 1273/1337] fix: fix incorrect namespacing of `DelayParentScope` variables in `collect_scoped_vars!` --- src/utils.jl | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/utils.jl b/src/utils.jl index 2a0009b644..1884a91c19 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -535,7 +535,7 @@ recursively searches through all subsystems of `sys`, increasing the depth if it """ function collect_scoped_vars!(unknowns, parameters, sys, iv; depth = 1, op = Differential) if has_eqs(sys) - for eq in get_eqs(sys) + for eq in equations(sys) eqtype_supports_collect_vars(eq) || continue if eq isa Equation eq.lhs isa Union{Symbolic, Number} || continue @@ -544,7 +544,7 @@ function collect_scoped_vars!(unknowns, parameters, sys, iv; depth = 1, op = Dif end end if has_parameter_dependencies(sys) - for eq in get_parameter_dependencies(sys) + for eq in parameter_dependencies(sys) if eq isa Pair collect_vars!(unknowns, parameters, eq, iv; depth, op) else @@ -553,17 +553,13 @@ function collect_scoped_vars!(unknowns, parameters, sys, iv; depth = 1, op = Dif end end if has_constraints(sys) - for eq in get_constraints(sys) + for eq in constraints(sys) eqtype_supports_collect_vars(eq) || continue collect_vars!(unknowns, parameters, eq, iv; depth, op) end end if has_op(sys) - collect_vars!(unknowns, parameters, get_op(sys), iv; depth, op) - end - newdepth = depth == -1 ? depth : depth + 1 - for ssys in get_systems(sys) - collect_scoped_vars!(unknowns, parameters, ssys, iv; depth = newdepth, op) + collect_vars!(unknowns, parameters, objective(sys), iv; depth, op) end end @@ -608,6 +604,7 @@ end function collect_var!(unknowns, parameters, var, iv; depth = 0) isequal(var, iv) && return nothing check_scope_depth(getmetadata(var, SymScope, LocalScope()), depth) || return nothing + var = setmetadata(var, SymScope, LocalScope()) if iscalledparameter(var) callable = getcalledparameter(var) push!(parameters, callable) @@ -636,7 +633,7 @@ function check_scope_depth(scope, depth) elseif scope isa ParentScope return depth > 0 && check_scope_depth(scope.parent, depth - 1) elseif scope isa DelayParentScope - return depth >= scope.N && check_scope_depth(scope.parent, depth - scope.N) + return depth >= scope.N && check_scope_depth(scope.parent, depth - scope.N - 1) elseif scope isa GlobalScope return depth == -1 end From 520ac393263c8e52a54732d873537647c6d7d154 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 2 Apr 2025 16:17:46 +0530 Subject: [PATCH 1274/1337] test: test proper scoping of `DelayParentScope` during variable discovery --- test/variable_scope.jl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/variable_scope.jl b/test/variable_scope.jl index e90f7e6fbf..bd1d3cb0cf 100644 --- a/test/variable_scope.jl +++ b/test/variable_scope.jl @@ -106,12 +106,12 @@ defs = ModelingToolkit.defaults(bar) @variables x1(t) x2(t) x3(t) x4(t) x5(t) x2 = ParentScope(x2) x3 = ParentScope(ParentScope(x3)) -x4 = DelayParentScope(x4, 2) +x4 = DelayParentScope(x4) x5 = GlobalScope(x5) @parameters p1 p2 p3 p4 p5 p2 = ParentScope(p2) p3 = ParentScope(ParentScope(p3)) -p4 = DelayParentScope(p4, 2) +p4 = DelayParentScope(p4) p5 = GlobalScope(p5) @named sys1 = ODESystem([D(x1) ~ p1, D(x2) ~ p2, D(x3) ~ p3, D(x4) ~ p4, D(x5) ~ p5], t) @@ -126,10 +126,10 @@ p5 = GlobalScope(p5) sys3 = sys3 ∘ sys2 @test length(unknowns(sys3)) == 4 @test any(isequal(x3), unknowns(sys3)) -@test any(isequal(x4), unknowns(sys3)) +@test any(isequal(ModelingToolkit.renamespace(sys1, x4)), unknowns(sys3)) @test length(parameters(sys3)) == 4 @test any(isequal(p3), parameters(sys3)) -@test any(isequal(p4), parameters(sys3)) +@test any(isequal(ModelingToolkit.renamespace(sys1, p4)), parameters(sys3)) sys4 = complete(sys3) @test length(unknowns(sys3)) == 4 @test length(parameters(sys4)) == 5 From fd64e1962a4b1493371214565e984ac93c498b2c Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 3 Apr 2025 18:27:51 +0530 Subject: [PATCH 1275/1337] build: bump Symbolics compat --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 8766db8eee..06b20c4552 100644 --- a/Project.toml +++ b/Project.toml @@ -151,7 +151,7 @@ StochasticDelayDiffEq = "1.8.1" StochasticDiffEq = "6.72.1" SymbolicIndexingInterface = "0.3.37" SymbolicUtils = "3.25.1" -Symbolics = "6.36" +Symbolics = "6.37" URIs = "1" UnPack = "0.1, 1.0" Unitful = "1.1" From 115c793c6f141adca30c919993b521c09fff1f11 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 4 Apr 2025 12:17:13 +0530 Subject: [PATCH 1276/1337] test: fix usage of `DelayParentScope` in `JumpSystem` tests --- test/jumpsystem.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/jumpsystem.jl b/test/jumpsystem.jl index d77e37f516..9568990e73 100644 --- a/test/jumpsystem.jl +++ b/test/jumpsystem.jl @@ -381,12 +381,12 @@ let @variables x1(t) x2(t) x3(t) x4(t) x5(t) x2 = ParentScope(x2) x3 = ParentScope(ParentScope(x3)) - x4 = DelayParentScope(x4, 2) + x4 = DelayParentScope(x4) x5 = GlobalScope(x5) @parameters p1 p2 p3 p4 p5 p2 = ParentScope(p2) p3 = ParentScope(ParentScope(p3)) - p4 = DelayParentScope(p4, 2) + p4 = DelayParentScope(p4) p5 = GlobalScope(p5) j1 = ConstantRateJump(p1, [x1 ~ x1 + 1]) From 9890b26489ab81dfbca2a648cd96d9cb2e2e3062 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 7 Apr 2025 13:11:40 +0530 Subject: [PATCH 1277/1337] fix: filter equations in `getvar` --- src/systems/abstractsystem.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index 9bbe05dbeb..e19e576d5d 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -1077,6 +1077,7 @@ function getvar(sys::AbstractSystem, name::Symbol; namespace = does_namespacing( if has_eqs(sys) for eq in get_eqs(sys) + eq isa Equation || continue if eq.lhs isa AnalysisPoint && nameof(eq.rhs) == name return namespace ? renamespace(sys, eq.rhs) : eq.rhs end From 5c494c2bfcd4ac39a776be1532da218cea8e777f Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 7 Apr 2025 13:19:03 +0530 Subject: [PATCH 1278/1337] refactor: add depwarn for `DelayParentScope` --- src/systems/abstractsystem.jl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index e19e576d5d..dfd0c94a5b 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -1207,6 +1207,8 @@ end Apply `DelayParentScope` to `sym`, with a delay of `N` and `parent` being `LocalScope`. """ function DelayParentScope(sym::Union{Num, Symbolic, Symbolics.Arr{Num}}, N) + Base.depwarn( + "`DelayParentScope` is deprecated and will be removed soon", :DelayParentScope) apply_to_variables(sym) do sym if iscall(sym) && operation(sym) == getindex args = arguments(sym) From 5e439edff890b0646e9841c3f93971cd2e09be4f Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 2 Apr 2025 12:06:18 +0530 Subject: [PATCH 1279/1337] fix: don't substitute inside `Initial` in `build_operating_point!` --- src/systems/problem_utils.jl | 4 ++-- test/initial_values.jl | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/systems/problem_utils.jl b/src/systems/problem_utils.jl index d0ae0d174f..1db153781a 100644 --- a/src/systems/problem_utils.jl +++ b/src/systems/problem_utils.jl @@ -623,12 +623,12 @@ function build_operating_point!(sys::AbstractSystem, end for k in keys(u0map) - v = fixpoint_sub(u0map[k], neithermap) + v = fixpoint_sub(u0map[k], neithermap; operator = Symbolics.Operator) isequal(k, v) && continue u0map[k] = v end for k in keys(pmap) - v = fixpoint_sub(pmap[k], neithermap) + v = fixpoint_sub(pmap[k], neithermap; operator = Symbolics.Operator) isequal(k, v) && continue pmap[k] = v end diff --git a/test/initial_values.jl b/test/initial_values.jl index dbe08962cd..f63333cb2f 100644 --- a/test/initial_values.jl +++ b/test/initial_values.jl @@ -236,3 +236,19 @@ end prob2 = remake(prob; p = p_new) @test prob2.ps[interp] == spline2 end + +@testset "Issue#3523: don't substitute inside initial in `build_operating_point!`" begin + @variables (X(t))[1:2] + @parameters p[1:2] + eqs = [ + 0 ~ p[1] - X[1], + 0 ~ p[2] - X[2] + ] + @named nlsys = NonlinearSystem(eqs) + nlsys = complete(nlsys) + + # Creates the `NonlinearProblem`. + u0 = [X => [1.0, 2.0]] + ps = [p => [4.0, 5.0]] + @test_nowarn NonlinearProblem(nlsys, u0, ps) +end From 4626b5662c0c6dc11ab31dc914dfac4ee517423c Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 2 Apr 2025 16:36:13 +0530 Subject: [PATCH 1280/1337] fix: add `Initial` parameters for derivatives of all continuous variables Used for steady-state initial conditions --- src/systems/abstractsystem.jl | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index 1987cb9a60..a46d2c26a9 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -717,6 +717,11 @@ function add_initialization_parameters(sys::AbstractSystem) push!(all_initialvars, x) end end + + # add derivatives of all variables for steady-state initial conditions + if is_time_dependent(sys) && !(sys isa AbstractDiscreteSystem) + union!(all_initialvars, Differential(get_iv(sys)).(all_initialvars)) + end for eq in parameter_dependencies(sys) is_variable_floatingpoint(eq.lhs) || continue push!(all_initialvars, eq.lhs) From f5f1e6a342fb305fc43835e4a842a9871d1c78d4 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 2 Apr 2025 16:38:00 +0530 Subject: [PATCH 1281/1337] test: test present of steady-state `Initial` parameters --- test/initializationsystem.jl | 41 ++++++++++++++++++++---------------- 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/test/initializationsystem.jl b/test/initializationsystem.jl index 94a1f4c14f..0f40d4eaf1 100644 --- a/test/initializationsystem.jl +++ b/test/initializationsystem.jl @@ -406,30 +406,35 @@ sol = solve(prob, Tsit5()) @test sol.u[1] == [1.0] # Steady state initialization +@testset "Steady state initialization" begin + @parameters σ ρ β + @variables x(t) y(t) z(t) -@parameters σ ρ β -@variables x(t) y(t) z(t) + eqs = [D(D(x)) ~ σ * (y - x), + D(y) ~ x * (ρ - z) - y, + D(z) ~ x * y - β * z] -eqs = [D(D(x)) ~ σ * (y - x), - D(y) ~ x * (ρ - z) - y, - D(z) ~ x * y - β * z] + @named sys = ODESystem(eqs, t) + sys = structural_simplify(sys) -@named sys = ODESystem(eqs, t) -sys = structural_simplify(sys) + u0 = [D(x) => 2.0, + x => 1.0, + D(y) => 0.0, + z => 0.0] -u0 = [D(x) => 2.0, - x => 1.0, - D(y) => 0.0, - z => 0.0] + p = [σ => 28.0, + ρ => 10.0, + β => 8 / 3] -p = [σ => 28.0, - ρ => 10.0, - β => 8 / 3] + tspan = (0.0, 0.2) + prob_mtk = ODEProblem(sys, u0, tspan, p) + sol = solve(prob_mtk, Tsit5()) + @test sol[x * (ρ - z) - y][1] == 0.0 -tspan = (0.0, 0.2) -prob_mtk = ODEProblem(sys, u0, tspan, p) -sol = solve(prob_mtk, Tsit5()) -@test sol[x * (ρ - z) - y][1] == 0.0 + prob_mtk.ps[Initial(D(y))] = 1.0 + sol = solve(prob_mtk, Tsit5()) + @test sol[x * (ρ - z) - y][1] == 1.0 +end @variables x(t) y(t) z(t) @parameters α=1.5 β=1.0 γ=3.0 δ=1.0 From 50aaaaa732f393750fc250e007526e47fc816788 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 3 Apr 2025 18:30:59 +0530 Subject: [PATCH 1282/1337] test: update test to account for new `Initial` parameters --- test/modelingtoolkitize.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/modelingtoolkitize.jl b/test/modelingtoolkitize.jl index 28ce7b5e01..db99cc91a4 100644 --- a/test/modelingtoolkitize.jl +++ b/test/modelingtoolkitize.jl @@ -460,7 +460,7 @@ sys = modelingtoolkitize(prob) p = [10.0, 28.0, 2.66] sprob = SDEProblem(sdef!, sdeg!, u0, tspan, p) sys = complete(modelingtoolkitize(sprob)) - @test length(ModelingToolkit.defaults(sys)) == 2length(u0) + length(p) + @test length(ModelingToolkit.defaults(sys)) == 3length(u0) + length(p) sprob2 = SDEProblem(sys, [], tspan) truevals = similar(u0) From 6d07e11489fa81f105daa62783004f72d6e1ba4a Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 3 Apr 2025 18:31:33 +0530 Subject: [PATCH 1283/1337] fix: only add `Initial(D(x))` if `iscall(x)` --- src/systems/abstractsystem.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index a46d2c26a9..4ba2a70022 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -720,7 +720,8 @@ function add_initialization_parameters(sys::AbstractSystem) # add derivatives of all variables for steady-state initial conditions if is_time_dependent(sys) && !(sys isa AbstractDiscreteSystem) - union!(all_initialvars, Differential(get_iv(sys)).(all_initialvars)) + D = Differential(get_iv(sys)) + union!(all_initialvars, [D(v) for v in all_initialvars if iscall(v)]) end for eq in parameter_dependencies(sys) is_variable_floatingpoint(eq.lhs) || continue From e9286b96fc0c88e6a4b290d6a5a654ed13e782d3 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 4 Apr 2025 13:49:00 +0530 Subject: [PATCH 1284/1337] test: update test to account for new `Initial` parameters --- test/symbolic_indexing_interface.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/symbolic_indexing_interface.jl b/test/symbolic_indexing_interface.jl index 5979cd71ac..8b3da5fd72 100644 --- a/test/symbolic_indexing_interface.jl +++ b/test/symbolic_indexing_interface.jl @@ -24,7 +24,8 @@ using SciMLStructures: Tunable (odesys,), [x, y, t, ParameterIndex(Tunable(), 1), :x, :y]) == [nothing, nothing, nothing, ParameterIndex(Tunable(), 1), nothing, nothing] @test isequal( - Set(parameter_symbols(odesys)), Set([a, b, Initial(x), Initial(y), Initial(xy)])) + Set(parameter_symbols(odesys)), Set([a, b, Initial(x), Initial(y), Initial(xy), + Initial(D(x)), Initial(D(y)), Initial(D(xy))])) @test all(is_independent_variable.((odesys,), [t, :t])) @test all(.!is_independent_variable.((odesys,), [x, y, a, :x, :y, :a])) @test isequal(independent_variable_symbols(odesys), [t]) From 1bc3e04ae4ee5db0f8f5aaf8a6539b514050de5d Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 4 Apr 2025 13:49:17 +0530 Subject: [PATCH 1285/1337] fix: do not include `Initial` parameters in `toexpr(::AbstractSystem)` --- src/systems/abstractsystem.jl | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index 4ba2a70022..eaadef9be9 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -1930,7 +1930,9 @@ function toexpr(sys::AbstractSystem) end eqs_name = push_eqs!(stmt, full_equations(sys), var2name) - defs_name = push_defaults!(stmt, defaults(sys), var2name) + filtered_defs = filter( + kvp -> !(iscall(kvp[1]) && operation(kvp[1]) isa Initial), defaults(sys)) + defs_name = push_defaults!(stmt, filtered_defs, var2name) obs_name = push_eqs!(stmt, obs, var2name) if sys isa ODESystem From e74e754d51eda3f6f4d5dc8926e549fd007078a8 Mon Sep 17 00:00:00 2001 From: vyudu Date: Mon, 7 Apr 2025 10:03:50 -0400 Subject: [PATCH 1286/1337] fix type instability --- src/systems/diffeqs/abstractodesystem.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index ca9681cd8b..986aa27508 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -298,12 +298,12 @@ function W_sparsity(sys::AbstractODESystem) jac_sparsity = jacobian_sparsity(sys) else jac = calculate_jacobian(sys; sparse = true) - jac_sparsity = sparse((!iszero).(jac)) + jac_sparsity = SparseMatrixCSC{Bool, Int64}((!iszero).(jac)) end (n, n) = size(jac_sparsity) M = calculate_massmatrix(sys) - M_sparsity = M isa UniformScaling ? sparse(I(n)) : sparse((!iszero).(M)) - jac_sparsity .|| M_sparsity + M_sparsity = M isa UniformScaling ? sparse(I(n)) : SparseMatrixCSC{Bool, Int64}((!iszero).(M)) + jac_sparsity .| M_sparsity end function isautonomous(sys::AbstractODESystem) From 5ce7dac0157a8a17df043960a99c2144ff5cc147 Mon Sep 17 00:00:00 2001 From: vyudu Date: Tue, 25 Mar 2025 10:19:42 -0400 Subject: [PATCH 1287/1337] docs: document DAEProblem constructor --- docs/src/systems/ODESystem.md | 1 + src/systems/diffeqs/abstractodesystem.jl | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/src/systems/ODESystem.md b/docs/src/systems/ODESystem.md index 4be0f6e263..24e2952fc5 100644 --- a/docs/src/systems/ODESystem.md +++ b/docs/src/systems/ODESystem.md @@ -62,6 +62,7 @@ jacobian_sparsity ODEFunction(sys::ModelingToolkit.AbstractODESystem, args...) ODEProblem(sys::ModelingToolkit.AbstractODESystem, args...) SteadyStateProblem(sys::ModelingToolkit.AbstractODESystem, args...) +DAEProblem(sys::ModelingToolkit.AbstractODESystem, args...) ``` ## Expression Constructors diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index 23f20b00ec..05aa3fce94 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -941,6 +941,8 @@ DiffEqBase.DAEProblem{iip}(sys::AbstractODESystem, du0map, u0map, tspan, Generates a DAEProblem from an ODESystem and allows for automatically symbolically calculating numerical enhancements. + +Note: Solvers for DAEProblems like DFBDF, DImplicitEuler, DABDF2 are generally slower than the ones for ODEProblems. If possible, it is recommended to formulate your problem in terms of an ODEProblem and use the corresponding ODE Solvers. """ function DiffEqBase.DAEProblem(sys::AbstractODESystem, args...; kwargs...) DAEProblem{true}(sys, args...; kwargs...) @@ -951,7 +953,7 @@ function DiffEqBase.DAEProblem{iip}(sys::AbstractODESystem, du0map, u0map, tspan warn_initialize_determined = true, check_length = true, eval_expression = false, eval_module = @__MODULE__, kwargs...) where {iip} if !iscomplete(sys) - error("A completed system is required. Call `complete` or `structural_simplify` on the system before creating a `DAEProblem`") + error("A completed system is required. Call `complete` or `structural_simplify` on the system before creating a `DAEProblem`.") end f, du0, u0, p = process_SciMLProblem(DAEFunction{iip}, sys, u0map, parammap; implicit_dae = true, du0map = du0map, check_length, From c1bca7445f92eb7ffb4c6d2c0ef3ee4f5c16ee0a Mon Sep 17 00:00:00 2001 From: vyudu Date: Tue, 25 Mar 2025 10:20:25 -0400 Subject: [PATCH 1288/1337] fix lines --- src/systems/diffeqs/abstractodesystem.jl | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index 05aa3fce94..63cbf572ce 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -942,7 +942,10 @@ DiffEqBase.DAEProblem{iip}(sys::AbstractODESystem, du0map, u0map, tspan, Generates a DAEProblem from an ODESystem and allows for automatically symbolically calculating numerical enhancements. -Note: Solvers for DAEProblems like DFBDF, DImplicitEuler, DABDF2 are generally slower than the ones for ODEProblems. If possible, it is recommended to formulate your problem in terms of an ODEProblem and use the corresponding ODE Solvers. +Note: Solvers for DAEProblems like DFBDF, DImplicitEuler, DABDF2 are +generally slower than the ones for ODEProblems. If possible, it is +recommended to formulate your problem in terms of an ODEProblem and +use the corresponding ODE Solvers. """ function DiffEqBase.DAEProblem(sys::AbstractODESystem, args...; kwargs...) DAEProblem{true}(sys, args...; kwargs...) From 0f1c669fbbc1cf850c845b7f479584485b4e0309 Mon Sep 17 00:00:00 2001 From: vyudu Date: Mon, 7 Apr 2025 10:09:32 -0400 Subject: [PATCH 1289/1337] adjust docstring --- src/systems/diffeqs/abstractodesystem.jl | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index 63cbf572ce..f098e437f7 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -943,9 +943,8 @@ Generates a DAEProblem from an ODESystem and allows for automatically symbolically calculating numerical enhancements. Note: Solvers for DAEProblems like DFBDF, DImplicitEuler, DABDF2 are -generally slower than the ones for ODEProblems. If possible, it is -recommended to formulate your problem in terms of an ODEProblem and -use the corresponding ODE Solvers. +generally slower than the ones for ODEProblems. We recommend trying +ODEProblem and its solvers for your problem first. """ function DiffEqBase.DAEProblem(sys::AbstractODESystem, args...; kwargs...) DAEProblem{true}(sys, args...; kwargs...) From 64fb10ef625fd5a0cad52d5dd5317645ed3e4e8b Mon Sep 17 00:00:00 2001 From: vyudu Date: Tue, 1 Apr 2025 17:37:13 -0400 Subject: [PATCH 1290/1337] init: add cost and coalesce --- src/systems/diffeqs/odesystem.jl | 63 +++++++++++++++++++++++++++----- 1 file changed, 54 insertions(+), 9 deletions(-) diff --git a/src/systems/diffeqs/odesystem.jl b/src/systems/diffeqs/odesystem.jl index 57d71b18c8..35f132f12f 100644 --- a/src/systems/diffeqs/odesystem.jl +++ b/src/systems/diffeqs/odesystem.jl @@ -51,6 +51,10 @@ struct ODESystem <: AbstractODESystem observed::Vector{Equation} """System of constraints that must be satisfied by the solution to the system.""" constraintsystem::Union{Nothing, ConstraintsSystem} + """A set of expressions defining the costs of the system for optimal control.""" + costs::Vector + """Takes the cost vector and returns a scalar for optimization.""" + coalesce::Function """ Time-derivative matrix. Note: this field will not be defined until [`calculate_tgrad`](@ref) is called on the system. @@ -338,7 +342,7 @@ function ODESystem(deqs::AbstractVector{<:Equation}, iv, dvs, ps; metadata, gui_metadata, is_dde, tstops, checks = checks) end -function ODESystem(eqs, iv; constraints = Equation[], kwargs...) +function ODESystem(eqs, iv; constraints = Equation[], costs = Equation[], kwargs...) diffvars, allunknowns, ps, eqs = process_equations(eqs, iv) for eq in get(kwargs, :parameter_dependencies, Equation[]) @@ -384,6 +388,13 @@ function ODESystem(eqs, iv; constraints = Equation[], kwargs...) end end + if !isempty(costs) + coststs, costps = process_costs(costs, allunknowns, new_ps, iv) + for p in costps + !in(p, new_ps) && push!(new_ps, p) + end + end + return ODESystem(eqs, iv, collect(Iterators.flatten((diffvars, algevars, consvars))), collect(new_ps); constraintsystem, kwargs...) end @@ -734,22 +745,52 @@ function Base.show(io::IO, mime::MIME"text/plain", sys::ODESystem; hint = true, return nothing end -# Validate that all the variables in the BVP constraints are well-formed states or parameters. -# - Callable/delay variables (e.g. of the form x(0.6) should be unknowns of the system (and have one arg, etc.) -# - Callable/delay parameters should be parameters of the system (and have one arg, etc.) +""" +Build the constraint system for the ODESystem. +""" function process_constraint_system( constraints::Vector{Equation}, sts, ps, iv; consname = :cons) isempty(constraints) && return nothing constraintsts = OrderedSet() constraintps = OrderedSet() - for cons in constraints collect_vars!(constraintsts, constraintps, cons, iv) end # Validate the states. - for var in constraintsts + validate_vars_and_find_ps!(coststs, costps, sts, iv) + + ConstraintsSystem( + constraints, collect(constraintsts), collect(constraintps); name = consname) +end + +""" +Process the costs for the constraint system. +""" +function process_costs(costs::Vector{Equation}, sts, ps, iv) + coststs = OrderedSet() + costps = OrderedSet() + for cost in costs + collect_vars!(coststs, costps, cost, iv) + end + + validate_vars_and_find_ps!(coststs, costps, sts, iv) +end + +""" +Validate that all the variables in an auxiliary system of the ODESystem (constraint or costs) are +well-formed states or parameters. + - Callable/delay variables (e.g. of the form x(0.6) should be unknowns of the system (and have one arg, etc.) + - Callable/delay parameters should be parameters of the system + +Return the set of additional parameters found in the system, e.g. in x(p) ~ 3 then p should be added as a +parameter of the system. +""" +function validate_vars_and_find_ps!(auxvars, auxps, sysvars, iv) + sts = sysvars + + for var in auxvars if !iscall(var) occursin(iv, var) && (var ∈ sts || throw(ArgumentError("Time-dependent variable $var is not an unknown of the system."))) @@ -764,13 +805,17 @@ function process_constraint_system( arg isa AbstractFloat || throw(ArgumentError("Invalid argument specified for variable $var. The argument of the variable should be either $iv, a parameter, or a value specifying the time that the constraint holds.")) - isparameter(arg) && push!(constraintps, arg) + isparameter(arg) && push!(auxps, arg) else var ∈ sts && @warn "Variable $var has no argument. It will be interpreted as $var($iv), and the constraint will apply to the entire interval." end end +end - ConstraintsSystem( - constraints, collect(constraintsts), collect(constraintps); name = consname) +function generate_cost_function(sys::ODESystem) + costs = get_costs(sys) + coalesce = get_coalesce(sys) + cost_fn = build_function_wrapper() + return (u, p, t) -> coalesce(cost_fn(u, p, t)) end From 246464ca98dc58f6a10f9218588b1093690588d7 Mon Sep 17 00:00:00 2001 From: vyudu Date: Wed, 2 Apr 2025 17:36:23 -0400 Subject: [PATCH 1291/1337] feat: add cost and coalesce to ODESystem --- src/systems/abstractsystem.jl | 4 +- src/systems/diffeqs/abstractodesystem.jl | 2 +- src/systems/diffeqs/odesystem.jl | 59 +++++++++++++++++++----- test/bvproblem.jl | 31 +++++++++++-- 4 files changed, 78 insertions(+), 18 deletions(-) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index 1987cb9a60..1e086eaaff 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -920,7 +920,9 @@ for prop in [:eqs :tstops :index_cache :is_scalar_noise - :isscheduled] + :isscheduled + :costs + :coalesce] fname_get = Symbol(:get_, prop) fname_has = Symbol(:has_, prop) @eval begin diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index 23f20b00ec..4e7adcc5de 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -924,7 +924,7 @@ function generate_function_bc(sys::ODESystem, u0, u0_idxs, tspan; kwargs...) exprs = vcat(init_conds, cons) _p = reorder_parameters(sys, ps) - build_function_wrapper(sys, exprs, sol, _p..., t; output_type = Array, kwargs...) + build_function_wrapper(sys, exprs, sol, _p..., iv; output_type = Array, kwargs...) end """ diff --git a/src/systems/diffeqs/odesystem.jl b/src/systems/diffeqs/odesystem.jl index 35f132f12f..cc63e1ec7f 100644 --- a/src/systems/diffeqs/odesystem.jl +++ b/src/systems/diffeqs/odesystem.jl @@ -54,7 +54,7 @@ struct ODESystem <: AbstractODESystem """A set of expressions defining the costs of the system for optimal control.""" costs::Vector """Takes the cost vector and returns a scalar for optimization.""" - coalesce::Function + coalesce::Union{Nothing, Function} """ Time-derivative matrix. Note: this field will not be defined until [`calculate_tgrad`](@ref) is called on the system. @@ -209,7 +209,7 @@ struct ODESystem <: AbstractODESystem parent::Any function ODESystem( - tag, deqs, iv, dvs, ps, tspan, var_to_name, ctrls, observed, constraints, tgrad, + tag, deqs, iv, dvs, ps, tspan, var_to_name, ctrls, observed, constraints, costs, coalesce, tgrad, jac, ctrl_jac, Wfact, Wfact_t, name, description, systems, defaults, guesses, torn_matching, initializesystem, initialization_eqs, schedule, connector_type, preface, cevents, @@ -233,7 +233,7 @@ struct ODESystem <: AbstractODESystem check_units(u, deqs) end new(tag, deqs, iv, dvs, ps, tspan, var_to_name, - ctrls, observed, constraints, tgrad, jac, + ctrls, observed, constraints, costs, coalesce, tgrad, jac, ctrl_jac, Wfact, Wfact_t, name, description, systems, defaults, guesses, torn_matching, initializesystem, initialization_eqs, schedule, connector_type, preface, cevents, devents, parameter_dependencies, assertions, metadata, @@ -247,6 +247,8 @@ function ODESystem(deqs::AbstractVector{<:Equation}, iv, dvs, ps; controls = Num[], observed = Equation[], constraintsystem = nothing, + costs = Num[], + coalesce = nothing, systems = ODESystem[], tspan = nothing, name = nothing, @@ -327,14 +329,18 @@ function ODESystem(deqs::AbstractVector{<:Equation}, iv, dvs, ps; cons = get_constraintsystem(sys) cons !== nothing && push!(conssystems, cons) end - @show conssystems @set! constraintsystem.systems = conssystems end + costs = wrap.(costs) + + if length(costs) > 1 && isnothing(coalesce) + error("Must specify a coalesce function for the costs vector.") + end assertions = Dict{BasicSymbolic, Any}(unwrap(k) => v for (k, v) in assertions) ODESystem(Threads.atomic_add!(SYSTEM_COUNT, UInt(1)), - deqs, iv′, dvs′, ps′, tspan, var_to_name, ctrl′, observed, constraintsystem, tgrad, jac, + deqs, iv′, dvs′, ps′, tspan, var_to_name, ctrl′, observed, constraintsystem, costs, coalesce, tgrad, jac, ctrl_jac, Wfact, Wfact_t, name, description, systems, defaults, guesses, nothing, initializesystem, initialization_eqs, schedule, connector_type, preface, cont_callbacks, @@ -342,7 +348,7 @@ function ODESystem(deqs::AbstractVector{<:Equation}, iv, dvs, ps; metadata, gui_metadata, is_dde, tstops, checks = checks) end -function ODESystem(eqs, iv; constraints = Equation[], costs = Equation[], kwargs...) +function ODESystem(eqs, iv; constraints = Equation[], costs = Num[], kwargs...) diffvars, allunknowns, ps, eqs = process_equations(eqs, iv) for eq in get(kwargs, :parameter_dependencies, Equation[]) @@ -394,9 +400,10 @@ function ODESystem(eqs, iv; constraints = Equation[], costs = Equation[], kwargs !in(p, new_ps) && push!(new_ps, p) end end + costs = wrap.(costs) return ODESystem(eqs, iv, collect(Iterators.flatten((diffvars, algevars, consvars))), - collect(new_ps); constraintsystem, kwargs...) + collect(new_ps); constraintsystem, costs, kwargs...) end # NOTE: equality does not check cached Jacobian @@ -411,7 +418,9 @@ function Base.:(==)(sys1::ODESystem, sys2::ODESystem) _eq_unordered(get_ps(sys1), get_ps(sys2)) && _eq_unordered(continuous_events(sys1), continuous_events(sys2)) && _eq_unordered(discrete_events(sys1), discrete_events(sys2)) && - all(s1 == s2 for (s1, s2) in zip(get_systems(sys1), get_systems(sys2))) + all(s1 == s2 for (s1, s2) in zip(get_systems(sys1), get_systems(sys2))) && + isequal(get_constraintsystem(sys1), get_constraintssystem(sys2)) && + _eq_unordered(get_costs(sys1), get_costs(sys2)) end function flatten(sys::ODESystem, noeqs = false) @@ -768,7 +777,7 @@ end """ Process the costs for the constraint system. """ -function process_costs(costs::Vector{Equation}, sts, ps, iv) +function process_costs(costs::Vector, sts, ps, iv) coststs = OrderedSet() costps = OrderedSet() for cost in costs @@ -776,6 +785,7 @@ function process_costs(costs::Vector{Equation}, sts, ps, iv) end validate_vars_and_find_ps!(coststs, costps, sts, iv) + coststs, costps end """ @@ -813,9 +823,34 @@ function validate_vars_and_find_ps!(auxvars, auxps, sysvars, iv) end end -function generate_cost_function(sys::ODESystem) +""" +Generate a function that takes a solution object and computes the cost function obtained by coalescing the costs vector. +""" +function generate_cost_function(sys::ODESystem, kwargs...) costs = get_costs(sys) coalesce = get_coalesce(sys) - cost_fn = build_function_wrapper() - return (u, p, t) -> coalesce(cost_fn(u, p, t)) + iv = get_iv(sys) + + ps = parameters(sys; initial_parameters = false) + sts = unknowns(sys) + np = length(ps) + ns = length(sts) + stidxmap = Dict([v => i for (i, v) in enumerate(sts)]) + pidxmap = Dict([v => i for (i, v) in enumerate(ps)]) + + @variables sol(..)[1:ns] + for st in vars(costs) + x = operation(st) + t = only(arguments(st)) + idx = stidxmap[x(iv)] + + costs = map(c -> Symbolics.fast_substitute(c, Dict(x(t) => sol(t)[idx])), costs) + end + + _p = reorder_parameters(sys, ps) + fs = build_function_wrapper(sys, costs, sol, _p..., t; output_type = Array, kwargs...) + vc_oop, vc_iip = eval_or_rgf.(fs) + + cost(sol, p, t) = coalesce(vc_oop(sol, p, t)) + return cost end diff --git a/test/bvproblem.jl b/test/bvproblem.jl index c5451f681b..de698eaa8f 100644 --- a/test/bvproblem.jl +++ b/test/bvproblem.jl @@ -11,7 +11,7 @@ using ModelingToolkit: t_nounits as t, D_nounits as D solvers = [MIRK4] daesolvers = [Ascher2, Ascher4, Ascher6] -let +@testset "Lotka-Volterra" begin @parameters α=7.5 β=4.0 γ=8.0 δ=5.0 @variables x(t)=1.0 y(t)=2.0 @@ -47,7 +47,7 @@ let end ### Testing on pendulum -let +@testset "Pendulum" begin @parameters g=9.81 L=1.0 @variables θ(t)=π / 2 θ_t(t) @@ -86,7 +86,7 @@ end ################################################################## # Test generation of boundary condition function using `generate_function_bc`. Compare solutions to manually written boundary conditions -let +@testset "Boundary Condition Compilation" begin @parameters α=1.5 β=1.0 γ=3.0 δ=1.0 @variables x(..) y(..) eqs = [D(x(t)) ~ α * x(t) - β * x(t) * y(t), @@ -168,7 +168,7 @@ function test_solvers( end # Simple ODESystem with BVP constraints. -let +@testset "ODE with constraints" begin @parameters α=1.5 β=1.0 γ=3.0 δ=1.0 @variables x(..) y(..) @@ -274,3 +274,26 @@ end # bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(pend, u0map, tspan, parammap; guesses, check_length = false) # test_solvers(daesolvers, bvp, u0map, constr, get_alg_eqs(pend)) # end + +@testset "Cost function compilation" begin + @parameters α=1.5 β=1.0 γ=3.0 δ=1.0 + @variables x(..) y(..) + + eqs = [D(x(t)) ~ α * x(t) - β * x(t) * y(t), + D(y(t)) ~ -γ * y(t) + δ * x(t) * y(t)] + + tspan = (0.0, 1.0) + u0map = [x(t) => 4.0, y(t) => 2.0] + parammap = [α => 7.5, β => 4, γ => 8.0, δ => 5.0] + costs = [x(0.6), x(0.3)^2] + consolidate(u) = (u[1] + 3)^2 + u[2] + @mtkbuild lksys = ODESystem(eqs, t; costs, coalesce = consolidate) + @test_throws ErrorException @mtkbuild lksys2 = ODESystem(eqs, t; costs) + + prob = ODEProblem(lksys, u0map, tspan, parammap) + sol = solve(prob, Tsit5()) + costfn = ModelingToolkit.generate_cost_function(lksys) + p = prob.p + t = tspan[2] + @test costfn(sol, p, t) ≈ (sol(0.6)[1] + 3)^2 + sol(0.3)[1]^2 +end From bf9181662d690484ed2e0b0e4eb4d39a44af6d59 Mon Sep 17 00:00:00 2001 From: vyudu Date: Wed, 2 Apr 2025 17:37:09 -0400 Subject: [PATCH 1292/1337] rename bvproblem --- test/{bvproblem.jl => optimal_control.jl} | 0 test/runtests.jl | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename test/{bvproblem.jl => optimal_control.jl} (100%) diff --git a/test/bvproblem.jl b/test/optimal_control.jl similarity index 100% rename from test/bvproblem.jl rename to test/optimal_control.jl diff --git a/test/runtests.jl b/test/runtests.jl index 04d99cc8b9..37c738eec9 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -90,7 +90,7 @@ end @safetestset "SCCNonlinearProblem Test" include("scc_nonlinear_problem.jl") @safetestset "PDE Construction Test" include("pdesystem.jl") @safetestset "JumpSystem Test" include("jumpsystem.jl") - @safetestset "BVProblem Test" include("bvproblem.jl") + @safetestset "Optimal Control + Constraints Tests" include("optimal_control.jl") @safetestset "print_tree" include("print_tree.jl") @safetestset "Constraints Test" include("constraints.jl") @safetestset "IfLifting Test" include("if_lifting.jl") From d69dae7ac40855de0de7bdfe50bfadc2b057c77e Mon Sep 17 00:00:00 2001 From: vyudu Date: Wed, 2 Apr 2025 17:39:06 -0400 Subject: [PATCH 1293/1337] format --- src/systems/diffeqs/odesystem.jl | 12 +++++++----- test/optimal_control.jl | 2 +- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/systems/diffeqs/odesystem.jl b/src/systems/diffeqs/odesystem.jl index cc63e1ec7f..23350ff7cc 100644 --- a/src/systems/diffeqs/odesystem.jl +++ b/src/systems/diffeqs/odesystem.jl @@ -209,7 +209,8 @@ struct ODESystem <: AbstractODESystem parent::Any function ODESystem( - tag, deqs, iv, dvs, ps, tspan, var_to_name, ctrls, observed, constraints, costs, coalesce, tgrad, + tag, deqs, iv, dvs, ps, tspan, var_to_name, ctrls, + observed, constraints, costs, coalesce, tgrad, jac, ctrl_jac, Wfact, Wfact_t, name, description, systems, defaults, guesses, torn_matching, initializesystem, initialization_eqs, schedule, connector_type, preface, cevents, @@ -332,7 +333,7 @@ function ODESystem(deqs::AbstractVector{<:Equation}, iv, dvs, ps; @set! constraintsystem.systems = conssystems end costs = wrap.(costs) - + if length(costs) > 1 && isnothing(coalesce) error("Must specify a coalesce function for the costs vector.") end @@ -340,7 +341,8 @@ function ODESystem(deqs::AbstractVector{<:Equation}, iv, dvs, ps; assertions = Dict{BasicSymbolic, Any}(unwrap(k) => v for (k, v) in assertions) ODESystem(Threads.atomic_add!(SYSTEM_COUNT, UInt(1)), - deqs, iv′, dvs′, ps′, tspan, var_to_name, ctrl′, observed, constraintsystem, costs, coalesce, tgrad, jac, + deqs, iv′, dvs′, ps′, tspan, var_to_name, ctrl′, observed, + constraintsystem, costs, coalesce, tgrad, jac, ctrl_jac, Wfact, Wfact_t, name, description, systems, defaults, guesses, nothing, initializesystem, initialization_eqs, schedule, connector_type, preface, cont_callbacks, @@ -403,7 +405,7 @@ function ODESystem(eqs, iv; constraints = Equation[], costs = Num[], kwargs...) costs = wrap.(costs) return ODESystem(eqs, iv, collect(Iterators.flatten((diffvars, algevars, consvars))), - collect(new_ps); constraintsystem, costs, kwargs...) + collect(new_ps); constraintsystem, costs, kwargs...) end # NOTE: equality does not check cached Jacobian @@ -800,7 +802,7 @@ parameter of the system. function validate_vars_and_find_ps!(auxvars, auxps, sysvars, iv) sts = sysvars - for var in auxvars + for var in auxvars if !iscall(var) occursin(iv, var) && (var ∈ sts || throw(ArgumentError("Time-dependent variable $var is not an unknown of the system."))) diff --git a/test/optimal_control.jl b/test/optimal_control.jl index de698eaa8f..0bd7f57323 100644 --- a/test/optimal_control.jl +++ b/test/optimal_control.jl @@ -293,7 +293,7 @@ end prob = ODEProblem(lksys, u0map, tspan, parammap) sol = solve(prob, Tsit5()) costfn = ModelingToolkit.generate_cost_function(lksys) - p = prob.p + p = prob.p t = tspan[2] @test costfn(sol, p, t) ≈ (sol(0.6)[1] + 3)^2 + sol(0.3)[1]^2 end From 2ba3da788a659bc714edef43ad2e59a5d4bbc46a Mon Sep 17 00:00:00 2001 From: vyudu Date: Thu, 3 Apr 2025 10:13:56 -0400 Subject: [PATCH 1294/1337] fix: fix tests, add parameter test --- src/systems/abstractsystem.jl | 2 +- src/systems/diffeqs/odesystem.jl | 22 +++++++++++----------- test/optimal_control.jl | 21 ++++++++++++++++----- 3 files changed, 28 insertions(+), 17 deletions(-) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index 1e086eaaff..109fa9712c 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -922,7 +922,7 @@ for prop in [:eqs :is_scalar_noise :isscheduled :costs - :coalesce] + :consolidate] fname_get = Symbol(:get_, prop) fname_has = Symbol(:has_, prop) @eval begin diff --git a/src/systems/diffeqs/odesystem.jl b/src/systems/diffeqs/odesystem.jl index 23350ff7cc..cbce569a9b 100644 --- a/src/systems/diffeqs/odesystem.jl +++ b/src/systems/diffeqs/odesystem.jl @@ -54,7 +54,7 @@ struct ODESystem <: AbstractODESystem """A set of expressions defining the costs of the system for optimal control.""" costs::Vector """Takes the cost vector and returns a scalar for optimization.""" - coalesce::Union{Nothing, Function} + consolidate::Union{Nothing, Function} """ Time-derivative matrix. Note: this field will not be defined until [`calculate_tgrad`](@ref) is called on the system. @@ -210,7 +210,7 @@ struct ODESystem <: AbstractODESystem function ODESystem( tag, deqs, iv, dvs, ps, tspan, var_to_name, ctrls, - observed, constraints, costs, coalesce, tgrad, + observed, constraints, costs, consolidate, tgrad, jac, ctrl_jac, Wfact, Wfact_t, name, description, systems, defaults, guesses, torn_matching, initializesystem, initialization_eqs, schedule, connector_type, preface, cevents, @@ -234,7 +234,7 @@ struct ODESystem <: AbstractODESystem check_units(u, deqs) end new(tag, deqs, iv, dvs, ps, tspan, var_to_name, - ctrls, observed, constraints, costs, coalesce, tgrad, jac, + ctrls, observed, constraints, costs, consolidate, tgrad, jac, ctrl_jac, Wfact, Wfact_t, name, description, systems, defaults, guesses, torn_matching, initializesystem, initialization_eqs, schedule, connector_type, preface, cevents, devents, parameter_dependencies, assertions, metadata, @@ -249,7 +249,7 @@ function ODESystem(deqs::AbstractVector{<:Equation}, iv, dvs, ps; observed = Equation[], constraintsystem = nothing, costs = Num[], - coalesce = nothing, + consolidate = nothing, systems = ODESystem[], tspan = nothing, name = nothing, @@ -334,15 +334,15 @@ function ODESystem(deqs::AbstractVector{<:Equation}, iv, dvs, ps; end costs = wrap.(costs) - if length(costs) > 1 && isnothing(coalesce) - error("Must specify a coalesce function for the costs vector.") + if length(costs) > 1 && isnothing(consolidate) + error("Must specify a consolidation function for the costs vector.") end assertions = Dict{BasicSymbolic, Any}(unwrap(k) => v for (k, v) in assertions) ODESystem(Threads.atomic_add!(SYSTEM_COUNT, UInt(1)), deqs, iv′, dvs′, ps′, tspan, var_to_name, ctrl′, observed, - constraintsystem, costs, coalesce, tgrad, jac, + constraintsystem, costs, consolidate, tgrad, jac, ctrl_jac, Wfact, Wfact_t, name, description, systems, defaults, guesses, nothing, initializesystem, initialization_eqs, schedule, connector_type, preface, cont_callbacks, @@ -421,7 +421,7 @@ function Base.:(==)(sys1::ODESystem, sys2::ODESystem) _eq_unordered(continuous_events(sys1), continuous_events(sys2)) && _eq_unordered(discrete_events(sys1), discrete_events(sys2)) && all(s1 == s2 for (s1, s2) in zip(get_systems(sys1), get_systems(sys2))) && - isequal(get_constraintsystem(sys1), get_constraintssystem(sys2)) && + isequal(get_constraintsystem(sys1), get_constraintsystem(sys2)) && _eq_unordered(get_costs(sys1), get_costs(sys2)) end @@ -770,7 +770,7 @@ function process_constraint_system( end # Validate the states. - validate_vars_and_find_ps!(coststs, costps, sts, iv) + validate_vars_and_find_ps!(constraintsts, constraintps, sts, iv) ConstraintsSystem( constraints, collect(constraintsts), collect(constraintps); name = consname) @@ -830,7 +830,7 @@ Generate a function that takes a solution object and computes the cost function """ function generate_cost_function(sys::ODESystem, kwargs...) costs = get_costs(sys) - coalesce = get_coalesce(sys) + consolidate = get_consolidate(sys) iv = get_iv(sys) ps = parameters(sys; initial_parameters = false) @@ -853,6 +853,6 @@ function generate_cost_function(sys::ODESystem, kwargs...) fs = build_function_wrapper(sys, costs, sol, _p..., t; output_type = Array, kwargs...) vc_oop, vc_iip = eval_or_rgf.(fs) - cost(sol, p, t) = coalesce(vc_oop(sol, p, t)) + cost(sol, p, t) = consolidate(vc_oop(sol, p, t)) return cost end diff --git a/test/optimal_control.jl b/test/optimal_control.jl index 0bd7f57323..1c5d1dd7aa 100644 --- a/test/optimal_control.jl +++ b/test/optimal_control.jl @@ -1,5 +1,4 @@ ### TODO: update when BoundaryValueDiffEqAscher is updated to use the normal boundary condition conventions - using OrdinaryDiffEq using BoundaryValueDiffEqMIRK, BoundaryValueDiffEqAscher using BenchmarkTools @@ -278,6 +277,7 @@ end @testset "Cost function compilation" begin @parameters α=1.5 β=1.0 γ=3.0 δ=1.0 @variables x(..) y(..) + t = ModelingToolkit.t_nounits eqs = [D(x(t)) ~ α * x(t) - β * x(t) * y(t), D(y(t)) ~ -γ * y(t) + δ * x(t) * y(t)] @@ -287,13 +287,24 @@ end parammap = [α => 7.5, β => 4, γ => 8.0, δ => 5.0] costs = [x(0.6), x(0.3)^2] consolidate(u) = (u[1] + 3)^2 + u[2] - @mtkbuild lksys = ODESystem(eqs, t; costs, coalesce = consolidate) + @mtkbuild lksys = ODESystem(eqs, t; costs, consolidate) @test_throws ErrorException @mtkbuild lksys2 = ODESystem(eqs, t; costs) prob = ODEProblem(lksys, u0map, tspan, parammap) sol = solve(prob, Tsit5()) costfn = ModelingToolkit.generate_cost_function(lksys) - p = prob.p - t = tspan[2] - @test costfn(sol, p, t) ≈ (sol(0.6)[1] + 3)^2 + sol(0.3)[1]^2 + _t = tspan[2] + @test costfn(sol, prob.p, _t) ≈ (sol(0.6)[1] + 3)^2 + sol(0.3)[1]^2 + + ### With a parameter + @parameters t_c + costs = [y(t_c) + x(0.0), x(0.4)^2] + consolidate(u) = log(u[1]) - u[2] + @mtkbuild lksys = ODESystem(eqs, t; costs, consolidate) + @test t_c ∈ Set(parameters(lksys)) + push!(parammap, t_c => 0.56) + prob = ODEProblem(lksys, u0map, tspan, parammap) + sol = solve(prob, Tsit5()) + costfn = ModelingToolkit.generate_cost_function(lksys) + @test costfn(sol, prob.p, _t) ≈ log(sol(0.56)[2] + sol(0.)[1]) - sol(0.4)[1]^2 end From 6451788e2533d8ef27b10762843ea08084de36ce Mon Sep 17 00:00:00 2001 From: vyudu Date: Sat, 5 Apr 2025 12:14:22 -0400 Subject: [PATCH 1295/1337] add errors --- src/systems/diffeqs/abstractodesystem.jl | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index 4e7adcc5de..673d0a0dec 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -745,6 +745,11 @@ function DiffEqBase.ODEProblem{iip, specialize}(sys::AbstractODESystem, u0map = Consider a BVProblem instead.") end + if !isempty(get_costs(sys)) + error("An ODESystem with costs cannot be solved using a regular ODEProblem. + Solvers for optimal control problems are forthcoming.") + end + f, u0, p = process_SciMLProblem(ODEFunction{iip, specialize}, sys, u0map, parammap; t = tspan !== nothing ? tspan[1] : tspan, check_length, warn_initialize_determined, eval_expression, eval_module, kwargs...) @@ -852,6 +857,11 @@ function SciMLBase.BVProblem{iip, specialize}(sys::AbstractODESystem, u0map = [] end !isnothing(callback) && error("BVP solvers do not support callbacks.") + if !isempty(get_costs(sys)) + error("An ODESystem with costs cannot be solved using a regular DAEProblem. + Solvers for optimal control problems are forthcoming.") + end + has_alg_eqs(sys) && error("The BVProblem constructor currently does not support ODESystems with algebraic equations.") # Remove this when the BVDAE solvers get updated, the codegen should work when it does. @@ -953,6 +963,11 @@ function DiffEqBase.DAEProblem{iip}(sys::AbstractODESystem, du0map, u0map, tspan if !iscomplete(sys) error("A completed system is required. Call `complete` or `structural_simplify` on the system before creating a `DAEProblem`") end + + if !isempty(get_costs(sys)) + error("An ODESystem with costs cannot be solved using a regular DAEProblem. + Solvers for optimal control problems are forthcoming.") + end f, du0, u0, p = process_SciMLProblem(DAEFunction{iip}, sys, u0map, parammap; implicit_dae = true, du0map = du0map, check_length, t = tspan !== nothing ? tspan[1] : tspan, From 31b9954325d9cda32e40980ba9ba3dffb129fa66 Mon Sep 17 00:00:00 2001 From: vyudu Date: Mon, 7 Apr 2025 10:31:02 -0400 Subject: [PATCH 1296/1337] correct error messages --- src/systems/diffeqs/abstractodesystem.jl | 34 ++++++++++++++---------- test/optimal_control.jl | 5 ++-- 2 files changed, 23 insertions(+), 16 deletions(-) diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index 673d0a0dec..4c6e61e1f8 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -697,6 +697,7 @@ end ```julia DiffEqBase.ODEProblem{iip}(sys::AbstractODESystem, u0map, tspan, parammap = DiffEqBase.NullParameters(); + allow_cost = false, version = nothing, tgrad = false, jac = false, checkbounds = false, sparse = false, @@ -730,6 +731,7 @@ end function DiffEqBase.ODEProblem{iip, specialize}(sys::AbstractODESystem, u0map = [], tspan = get_tspan(sys), parammap = DiffEqBase.NullParameters(); + allow_cost = false, callback = nothing, check_length = true, warn_initialize_determined = true, @@ -745,9 +747,10 @@ function DiffEqBase.ODEProblem{iip, specialize}(sys::AbstractODESystem, u0map = Consider a BVProblem instead.") end - if !isempty(get_costs(sys)) - error("An ODESystem with costs cannot be solved using a regular ODEProblem. - Solvers for optimal control problems are forthcoming.") + if !isempty(get_costs(sys)) && !allow_cost + error("ODEProblem will not optimize solutions of ODESystems that have associated cost functions. + Solvers for optimal control problems are forthcoming. In order to bypass this error (e.g. + to check the cost of a regular solution), pass `allow_cost` = true into the constructor.") end f, u0, p = process_SciMLProblem(ODEFunction{iip, specialize}, sys, u0map, parammap; @@ -801,21 +804,19 @@ If an ODESystem without `constraints` is specified, it will be treated as an ini ```julia @parameters g t_c = 0.5 - @variables x(..) y(t) [state_priority = 10] λ(t) + @variables x(..) y(t) λ(t) eqs = [D(D(x(t))) ~ λ * x(t) D(D(y)) ~ λ * y - g x(t)^2 + y^2 ~ 1] cstr = [x(0.5) ~ 1] - @named cstrs = ConstraintsSystem(cstr, t) - @mtkbuild pend = ODESystem(eqs, t) + @mtkbuild pend = ODESystem(eqs, t; constraints = cstrs) tspan = (0.0, 1.5) u0map = [x(t) => 0.6, y => 0.8] parammap = [g => 1] guesses = [λ => 1] - constraints = [x(0.5) ~ 1] - bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(pend, u0map, tspan, parammap; constraints, guesses, check_length = false) + bvp = SciMLBase.BVProblem{true, SciMLBase.AutoSpecialize}(pend, u0map, tspan, parammap; guesses, check_length = false) ``` If the `ODESystem` has algebraic equations, like `x(t)^2 + y(t)^2`, the resulting @@ -844,6 +845,7 @@ function SciMLBase.BVProblem{iip, specialize}(sys::AbstractODESystem, u0map = [] tspan = get_tspan(sys), parammap = DiffEqBase.NullParameters(); guesses = Dict(), + allow_cost = false, version = nothing, tgrad = false, callback = nothing, check_length = true, @@ -857,9 +859,10 @@ function SciMLBase.BVProblem{iip, specialize}(sys::AbstractODESystem, u0map = [] end !isnothing(callback) && error("BVP solvers do not support callbacks.") - if !isempty(get_costs(sys)) - error("An ODESystem with costs cannot be solved using a regular DAEProblem. - Solvers for optimal control problems are forthcoming.") + if !isempty(get_costs(sys)) && !allow_cost + error("BVProblem will not optimize solutions of ODESystems that have associated cost functions. + Solvers for optimal control problems are forthcoming. In order to bypass this error (e.g. + to check the cost of a regular solution), pass `allow_cost` = true into the constructor.") end has_alg_eqs(sys) && @@ -958,16 +961,19 @@ end function DiffEqBase.DAEProblem{iip}(sys::AbstractODESystem, du0map, u0map, tspan, parammap = DiffEqBase.NullParameters(); + allow_cost = false, warn_initialize_determined = true, check_length = true, eval_expression = false, eval_module = @__MODULE__, kwargs...) where {iip} if !iscomplete(sys) error("A completed system is required. Call `complete` or `structural_simplify` on the system before creating a `DAEProblem`") end - if !isempty(get_costs(sys)) - error("An ODESystem with costs cannot be solved using a regular DAEProblem. - Solvers for optimal control problems are forthcoming.") + if !isempty(get_costs(sys)) && !allow_cost + error("DAEProblem will not optimize solutions of ODESystems that have associated cost functions. + Solvers for optimal control problems are forthcoming. In order to bypass this error (e.g. + to check the cost of a regular solution), pass `allow_cost` = true into the constructor.") end + f, du0, u0, p = process_SciMLProblem(DAEFunction{iip}, sys, u0map, parammap; implicit_dae = true, du0map = du0map, check_length, t = tspan !== nothing ? tspan[1] : tspan, diff --git a/test/optimal_control.jl b/test/optimal_control.jl index 1c5d1dd7aa..18b554c624 100644 --- a/test/optimal_control.jl +++ b/test/optimal_control.jl @@ -290,7 +290,8 @@ end @mtkbuild lksys = ODESystem(eqs, t; costs, consolidate) @test_throws ErrorException @mtkbuild lksys2 = ODESystem(eqs, t; costs) - prob = ODEProblem(lksys, u0map, tspan, parammap) + @test_throws ErrorException ODEProblem(lksys, u0map, tspan, parammap) + prob = ODEProblem(lksys, u0map, tspan, parammap; allow_cost = true) sol = solve(prob, Tsit5()) costfn = ModelingToolkit.generate_cost_function(lksys) _t = tspan[2] @@ -303,7 +304,7 @@ end @mtkbuild lksys = ODESystem(eqs, t; costs, consolidate) @test t_c ∈ Set(parameters(lksys)) push!(parammap, t_c => 0.56) - prob = ODEProblem(lksys, u0map, tspan, parammap) + prob = ODEProblem(lksys, u0map, tspan, parammap; allow_cost = true) sol = solve(prob, Tsit5()) costfn = ModelingToolkit.generate_cost_function(lksys) @test costfn(sol, prob.p, _t) ≈ log(sol(0.56)[2] + sol(0.)[1]) - sol(0.4)[1]^2 From 220db08b3c11b790325d2cca1e9ab6388fe3325b Mon Sep 17 00:00:00 2001 From: vyudu Date: Mon, 7 Apr 2025 10:31:51 -0400 Subject: [PATCH 1297/1337] formats --- test/optimal_control.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/optimal_control.jl b/test/optimal_control.jl index 18b554c624..f32ba471d8 100644 --- a/test/optimal_control.jl +++ b/test/optimal_control.jl @@ -307,5 +307,5 @@ end prob = ODEProblem(lksys, u0map, tspan, parammap; allow_cost = true) sol = solve(prob, Tsit5()) costfn = ModelingToolkit.generate_cost_function(lksys) - @test costfn(sol, prob.p, _t) ≈ log(sol(0.56)[2] + sol(0.)[1]) - sol(0.4)[1]^2 + @test costfn(sol, prob.p, _t) ≈ log(sol(0.56)[2] + sol(0.0)[1]) - sol(0.4)[1]^2 end From b85ce1dafb1d32c69c467441892a8292c7039a58 Mon Sep 17 00:00:00 2001 From: vyudu Date: Mon, 7 Apr 2025 13:22:51 -0400 Subject: [PATCH 1298/1337] fix: calculate_jacobian(sparse) should use W-sparsity) --- src/systems/diffeqs/abstractodesystem.jl | 16 ++++++++++------ src/systems/diffeqs/sdesystem.jl | 6 ++++-- src/systems/systems.jl | 2 +- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index 986aa27508..afe2c6d1b5 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -73,6 +73,15 @@ function calculate_jacobian(sys::AbstractODESystem; if sparse jac = sparsejacobian(rhs, dvs, simplify = simplify) + W_s = W_sparsity(sys) + (Is, Js, Vs) = findnz(W_s) + # Add nonzeros of W as non-structural zeros of the Jacobian (to ensure equal results for oop and iip Jacobian.) + for (i, j) in zip(Is, Js) + iszero(jac[i, j]) && begin + jac[i, j] = 1 + jac[i, j] = 0 + end + end else jac = jacobian(rhs, dvs, simplify = simplify) end @@ -294,12 +303,7 @@ function jacobian_dae_sparsity(sys::AbstractODESystem) end function W_sparsity(sys::AbstractODESystem) - if has_tearing_state(sys) - jac_sparsity = jacobian_sparsity(sys) - else - jac = calculate_jacobian(sys; sparse = true) - jac_sparsity = SparseMatrixCSC{Bool, Int64}((!iszero).(jac)) - end + jac_sparsity = jacobian_sparsity(sys) (n, n) = size(jac_sparsity) M = calculate_massmatrix(sys) M_sparsity = M isa UniformScaling ? sparse(I(n)) : SparseMatrixCSC{Bool, Int64}((!iszero).(M)) diff --git a/src/systems/diffeqs/sdesystem.jl b/src/systems/diffeqs/sdesystem.jl index 2fc9749309..3fa1302630 100644 --- a/src/systems/diffeqs/sdesystem.jl +++ b/src/systems/diffeqs/sdesystem.jl @@ -164,6 +164,7 @@ struct SDESystem <: AbstractODESystem """ is_dde::Bool isscheduled::Bool + tearing_state::Any function SDESystem(tag, deqs, neqs, iv, dvs, ps, tspan, var_to_name, ctrls, observed, tgrad, jac, ctrl_jac, Wfact, Wfact_t, name, description, systems, defaults, @@ -173,7 +174,8 @@ struct SDESystem <: AbstractODESystem metadata = nothing, gui_metadata = nothing, namespacing = true, complete = false, index_cache = nothing, parent = nothing, is_scalar_noise = false, is_dde = false, - isscheduled = false; + isscheduled = false, + tearing_state = nothing; checks::Union{Bool, Int} = true) if checks == true || (checks & CheckComponents) > 0 check_independent_variables([iv]) @@ -198,7 +200,7 @@ struct SDESystem <: AbstractODESystem ctrl_jac, Wfact, Wfact_t, name, description, systems, defaults, guesses, initializesystem, initialization_eqs, connector_type, cevents, devents, parameter_dependencies, assertions, metadata, gui_metadata, namespacing, - complete, index_cache, parent, is_scalar_noise, is_dde, isscheduled) + complete, index_cache, parent, is_scalar_noise, is_dde, isscheduled, tearing_state) end end diff --git a/src/systems/systems.jl b/src/systems/systems.jl index 367e1cd4cd..2652572bfe 100644 --- a/src/systems/systems.jl +++ b/src/systems/systems.jl @@ -155,7 +155,7 @@ function __structural_simplify(sys::AbstractSystem, io = nothing; simplify = fal get_iv(ode_sys), unknowns(ode_sys), parameters(ode_sys); name = nameof(ode_sys), is_scalar_noise, observed = observed(ode_sys), defaults = defaults(sys), parameter_dependencies = parameter_dependencies(sys), assertions = assertions(sys), - guesses = guesses(sys), initialization_eqs = initialization_equations(sys)) + guesses = guesses(sys), initialization_eqs = initialization_equations(sys), tearing_state = get_tearing_state(ode_sys)) end end From 66f5d594bb0d717133fae965cfeb63f7b80f55d5 Mon Sep 17 00:00:00 2001 From: vyudu Date: Mon, 7 Apr 2025 13:44:23 -0400 Subject: [PATCH 1299/1337] fix simplify --- src/systems/systems.jl | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/systems/systems.jl b/src/systems/systems.jl index 2652572bfe..0f8633f31f 100644 --- a/src/systems/systems.jl +++ b/src/systems/systems.jl @@ -151,11 +151,13 @@ function __structural_simplify(sys::AbstractSystem, io = nothing; simplify = fal end noise_eqs = StructuralTransformations.tearing_substitute_expr(ode_sys, noise_eqs) - return SDESystem(Vector{Equation}(full_equations(ode_sys)), noise_eqs, + ssys = SDESystem(Vector{Equation}(full_equations(ode_sys)), noise_eqs, 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), tearing_state = get_tearing_state(ode_sys)) + guesses = guesses(sys), initialization_eqs = initialization_equations(sys)) + @set! ssys.tearing_state = get_tearing_state(ode_sys) + return ssys end end From 7f071ee19fd8189e795e1f60b9cee47a57b775a9 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 9 Apr 2025 13:11:00 +0530 Subject: [PATCH 1300/1337] build: bump minor version --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 06b20c4552..63aef5b8da 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "ModelingToolkit" uuid = "961ee093-0014-501f-94e3-6117800e7a78" authors = ["Yingbo Ma ", "Chris Rackauckas and contributors"] -version = "9.71.0" +version = "9.72.0" [deps] ADTypes = "47edcb42-4c32-4615-8424-f2b9edc5f35b" From cc669c8cc9cf3d2f2d2a9cfd00b86430433cb55e Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 10 Apr 2025 17:31:28 +0530 Subject: [PATCH 1301/1337] fix: fix `assert_jac_length_header` --- src/systems/diffeqs/abstractodesystem.jl | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index b89dc79722..48c3e6e659 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -141,10 +141,12 @@ end function assert_jac_length_header(sys) W = W_sparsity(sys) - identity, expr -> Func([expr.args...], [], LiteralExpr(quote - @assert $(findnz)($(expr.args[1]))[1:2] == $(findnz)($W)[1:2] - $(expr.body) - end)) + identity, + function add_header(expr) + Func(expr.args, [], expr.body, + [:(@assert $(SymbolicUtils.Code.toexpr(term(findnz, expr.args[1])))[1:2] == + $(findnz(W)[1:2]))]) + end end function generate_W(sys::AbstractODESystem, γ = 1., dvs = unknowns(sys), From b6c67ba0062b233bebba72aee4758295c3716dd5 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 10 Apr 2025 17:31:33 +0530 Subject: [PATCH 1302/1337] refactor: format --- src/ModelingToolkit.jl | 3 ++- src/systems/diffeqs/abstractodesystem.jl | 13 +++++++------ test/jacobiansparsity.jl | 10 ++++++---- 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/src/ModelingToolkit.jl b/src/ModelingToolkit.jl index 76783d6be0..9f69458528 100644 --- a/src/ModelingToolkit.jl +++ b/src/ModelingToolkit.jl @@ -303,7 +303,8 @@ export structural_simplify, expand_connections, linearize, linearization_functio LinearizationProblem export solve -export calculate_jacobian, generate_jacobian, generate_function, generate_custom_function, generate_W +export calculate_jacobian, generate_jacobian, generate_function, generate_custom_function, + generate_W export calculate_control_jacobian, generate_control_jacobian export calculate_tgrad, generate_tgrad export calculate_gradient, generate_gradient diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index 48c3e6e659..196c692c67 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -149,17 +149,17 @@ function assert_jac_length_header(sys) end end -function generate_W(sys::AbstractODESystem, γ = 1., dvs = unknowns(sys), - ps = parameters(sys; initial_parameters = true); +function generate_W(sys::AbstractODESystem, γ = 1.0, dvs = unknowns(sys), + ps = parameters(sys; initial_parameters = true); simplify = false, sparse = false, kwargs...) @variables ˍ₋gamma M = calculate_massmatrix(sys; simplify) sparse && (M = SparseArrays.sparse(M)) J = calculate_jacobian(sys; simplify, sparse, dvs) - W = ˍ₋gamma*M + J + W = ˍ₋gamma * M + J p = reorder_parameters(sys, ps) - return build_function_wrapper(sys, W, + return build_function_wrapper(sys, W, dvs, p..., ˍ₋gamma, @@ -304,11 +304,12 @@ function jacobian_dae_sparsity(sys::AbstractODESystem) J1 + J2 end -function W_sparsity(sys::AbstractODESystem) +function W_sparsity(sys::AbstractODESystem) jac_sparsity = jacobian_sparsity(sys) (n, n) = size(jac_sparsity) M = calculate_massmatrix(sys) - M_sparsity = M isa UniformScaling ? sparse(I(n)) : SparseMatrixCSC{Bool, Int64}((!iszero).(M)) + M_sparsity = M isa UniformScaling ? sparse(I(n)) : + SparseMatrixCSC{Bool, Int64}((!iszero).(M)) jac_sparsity .| M_sparsity end diff --git a/test/jacobiansparsity.jl b/test/jacobiansparsity.jl index f6f9853137..efa1534979 100644 --- a/test/jacobiansparsity.jl +++ b/test/jacobiansparsity.jl @@ -74,7 +74,7 @@ f = DiffEqBase.ODEFunction(sys, u0 = nothing, sparse = true, jac = false) # test when u0 is not Float64 u0 = similar(init_brusselator_2d(xyd_brusselator), Float32) prob_ode_brusselator_2d = ODEProblem(brusselator_2d_loop, - u0, (0.0, 11.5), p) + u0, (0.0, 11.5), p) sys = complete(modelingtoolkitize(prob_ode_brusselator_2d)) prob = ODEProblem(sys, u0, (0, 11.5), sparse = true, jac = false) @@ -94,7 +94,8 @@ prob = ODEProblem(sys, u0, (0, 11.5), sparse = true, jac = true) @mtkbuild pend = ODESystem(eqs, t) u0 = [x => 1, y => 0] - prob = ODEProblem(pend, u0, (0, 11.5), [g => 1], guesses = [λ => 1], sparse = true, jac = true) + prob = ODEProblem( + pend, u0, (0, 11.5), [g => 1], guesses = [λ => 1], sparse = true, jac = true) jac, jac! = generate_jacobian(pend; expression = Val{false}, sparse = true) jac_prototype = ModelingToolkit.jacobian_sparsity(pend) W_prototype = ModelingToolkit.W_sparsity(pend) @@ -109,8 +110,9 @@ prob = ODEProblem(sys, u0, (0, 11.5), sparse = true, jac = true) @test_throws AssertionError jac!(similar(jac_prototype, Float64), u, p, t) W, W! = generate_W(pend; expression = Val{false}, sparse = true) - γ = .1 + γ = 0.1 M = sparse(calculate_massmatrix(pend)) @test_throws AssertionError W!(similar(jac_prototype, Float64), u, p, γ, t) - @test W!(similar(W_prototype, Float64), u, p, γ, t) == 0.1 * M + jac!(similar(W_prototype, Float64), u, p, t) + @test W!(similar(W_prototype, Float64), u, p, γ, t) == + 0.1 * M + jac!(similar(W_prototype, Float64), u, p, t) end From 875837b9010b70efbc1bd2d612bdd85cc3d21048 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 10 Apr 2025 18:32:43 +0530 Subject: [PATCH 1303/1337] test: test jacobian exactness --- test/jacobiansparsity.jl | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/test/jacobiansparsity.jl b/test/jacobiansparsity.jl index efa1534979..1f936bc878 100644 --- a/test/jacobiansparsity.jl +++ b/test/jacobiansparsity.jl @@ -116,3 +116,22 @@ prob = ODEProblem(sys, u0, (0, 11.5), sparse = true, jac = true) @test W!(similar(W_prototype, Float64), u, p, γ, t) == 0.1 * M + jac!(similar(W_prototype, Float64), u, p, t) end + +@testset "Issue#3556: Numerical accuracy" begin + t = ModelingToolkit.t_nounits + D = ModelingToolkit.D_nounits + @parameters g + @variables x(t) y(t) [state_priority = 10] λ(t) + eqs = [D(D(x)) ~ λ * x + D(D(y)) ~ λ * y - g + x^2 + y^2 ~ 1] + @mtkbuild pend = ODESystem(eqs, t) + prob = ODEProblem(pend, [x => 0.0, D(x) => 1.0], (0.0, 1.0), [g => 1.0]; + guesses = [y => 1.0, λ => 1.0], jac = true, sparse = true) + J = deepcopy(prob.f.jac_prototype) + prob.f.jac(J, prob.u0, prob.p, 1.0) + # this currently works but may not continue to do so + # see https://github.com/SciML/ModelingToolkit.jl/pull/3556#issuecomment-2792664039 + @test J == prob.f.jac(prob.u0, prob.p, 1.0) + @test J ≈ prob.f.jac(prob.u0, prob.p, 1.0) +end From 2cea8eb19585818c7e2f2e3f6f567377340457eb Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 14 Apr 2025 18:13:58 +0530 Subject: [PATCH 1304/1337] test: update reference tests --- test/latexify/20.tex | 6 +++--- test/latexify/30.tex | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/test/latexify/20.tex b/test/latexify/20.tex index ce6f5067c2..012243e981 100644 --- a/test/latexify/20.tex +++ b/test/latexify/20.tex @@ -1,5 +1,5 @@ \begin{align} -\frac{\mathrm{d} u\left( t \right)_{1}}{\mathrm{d}t} &= p_{3} \left( - u\left( t \right)_{1} + u\left( t \right)_{2} \right) \\ -0 &= - u\left( t \right)_{2} + \frac{1}{10} \left( p_{1} - u\left( t \right)_{1} \right) p_{2} p_{3} u\left( t \right)_{1} \\ -\frac{\mathrm{d} u\left( t \right)_{3}}{\mathrm{d}t} &= \left( u\left( t \right)_{2} \right)^{\frac{2}{3}} u\left( t \right)_{1} - p_{3} u\left( t \right)_{3} +\frac{\mathrm{d} u\_{1}\left( t \right)}{\mathrm{d}t} &= p_{3} \left( - u\_{1}\left( t \right) + u\_{2}\left( t \right) \right) \\ +0 &= - u\_{2}\left( t \right) + \frac{1}{10} \left( p_{1} - u\_{1}\left( t \right) \right) p_{2} p_{3} u\_{1}\left( t \right) \\ +\frac{\mathrm{d} u\_{3}\left( t \right)}{\mathrm{d}t} &= u\_{2}\left( t \right)^{\frac{2}{3}} u\_{1}\left( t \right) - p_{3} u\_{3}\left( t \right) \end{align} diff --git a/test/latexify/30.tex b/test/latexify/30.tex index d206c48d47..b51b73c34b 100644 --- a/test/latexify/30.tex +++ b/test/latexify/30.tex @@ -1,5 +1,5 @@ \begin{align} -\frac{\mathrm{d} u\left( t \right)_{1}}{\mathrm{d}t} &= p_{3} \left( - u\left( t \right)_{1} + u\left( t \right)_{2} \right) \\ -\frac{\mathrm{d} u\left( t \right)_{2}}{\mathrm{d}t} &= - u\left( t \right)_{2} + \frac{1}{10} \left( p_{1} - u\left( t \right)_{1} \right) p_{2} p_{3} u\left( t \right)_{1} \\ -\frac{\mathrm{d} u\left( t \right)_{3}}{\mathrm{d}t} &= \left( u\left( t \right)_{2} \right)^{\frac{2}{3}} u\left( t \right)_{1} - p_{3} u\left( t \right)_{3} +\frac{\mathrm{d} u\_{1}\left( t \right)}{\mathrm{d}t} &= p_{3} \left( - u\_{1}\left( t \right) + u\_{2}\left( t \right) \right) \\ +\frac{\mathrm{d} u\_{2}\left( t \right)}{\mathrm{d}t} &= - u\_{2}\left( t \right) + \frac{1}{10} \left( p_{1} - u\_{1}\left( t \right) \right) p_{2} p_{3} u\_{1}\left( t \right) \\ +\frac{\mathrm{d} u\_{3}\left( t \right)}{\mathrm{d}t} &= u\_{2}\left( t \right)^{\frac{2}{3}} u\_{1}\left( t \right) - p_{3} u\_{3}\left( t \right) \end{align} From 50f932fd0589da2f0ab0e1821abdc08c73c04f34 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 14 Apr 2025 17:43:56 +0530 Subject: [PATCH 1305/1337] fix: use `Float16` zero default for `Initial` parameters --- src/systems/abstractsystem.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index 67bcbc88c0..b28b2d5279 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -733,11 +733,11 @@ function add_initialization_parameters(sys::AbstractSystem) defs = copy(get_defaults(sys)) for ivar in initials if symbolic_type(ivar) == ScalarSymbolic() - defs[ivar] = zero_var(ivar) + defs[ivar] = zero(Float16) else defs[ivar] = collect(ivar) for scal_ivar in defs[ivar] - defs[scal_ivar] = zero_var(scal_ivar) + defs[scal_ivar] = zero(Float16) end end end From 0305c9ddba2cf12bb4683cb08202dc2041b2958c Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Mon, 14 Apr 2025 17:44:16 +0530 Subject: [PATCH 1306/1337] test: test `Float32` values retained in problem construction --- test/initial_values.jl | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/test/initial_values.jl b/test/initial_values.jl index f63333cb2f..01a053a9bf 100644 --- a/test/initial_values.jl +++ b/test/initial_values.jl @@ -252,3 +252,16 @@ end ps = [p => [4.0, 5.0]] @test_nowarn NonlinearProblem(nlsys, u0, ps) end + +@testset "Issue#3553: Retain `Float32` initial values" begin + @parameters p d + @variables X(t) + eqs = [D(X) ~ p - d * X] + @mtkbuild osys = ODESystem(eqs, t) + u0 = [X => 1.0f0] + ps = [p => 1.0f0, d => 2.0f0] + oprob = ODEProblem(osys, u0, (0.0f0, 1.0f0), ps) + sol = solve(oprob) + @test eltype(oprob.u0) == Float32 + @test eltype(eltype(sol.u)) == Float32 +end From bcaa86941e2b7c0bb2b1e36acc0bbce26a0c82c2 Mon Sep 17 00:00:00 2001 From: Christopher Rackauckas Date: Mon, 14 Apr 2025 09:08:48 -0400 Subject: [PATCH 1307/1337] Update src/systems/abstractsystem.jl --- src/systems/abstractsystem.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index b28b2d5279..7da0179676 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -733,7 +733,7 @@ function add_initialization_parameters(sys::AbstractSystem) defs = copy(get_defaults(sys)) for ivar in initials if symbolic_type(ivar) == ScalarSymbolic() - defs[ivar] = zero(Float16) + defs[ivar] = false else defs[ivar] = collect(ivar) for scal_ivar in defs[ivar] From 65a326db7a8cb80b4fb4b50fe958cf4569b5d940 Mon Sep 17 00:00:00 2001 From: Christopher Rackauckas Date: Mon, 14 Apr 2025 09:08:53 -0400 Subject: [PATCH 1308/1337] Update src/systems/abstractsystem.jl --- src/systems/abstractsystem.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index 7da0179676..72f3132889 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -737,7 +737,7 @@ function add_initialization_parameters(sys::AbstractSystem) else defs[ivar] = collect(ivar) for scal_ivar in defs[ivar] - defs[scal_ivar] = zero(Float16) + defs[scal_ivar] = false end end end From aa7f38b25c7282ae93f6c12c63388293c9baa93f Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 15 Apr 2025 11:43:14 +0530 Subject: [PATCH 1309/1337] fix: fix type promotion in `MTKParameters` constructor --- src/systems/parameter_buffer.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/systems/parameter_buffer.jl b/src/systems/parameter_buffer.jl index 36105c0de8..2cf87769a0 100644 --- a/src/systems/parameter_buffer.jl +++ b/src/systems/parameter_buffer.jl @@ -1,6 +1,6 @@ symconvert(::Type{Symbolics.Struct{T}}, x) where {T} = convert(T, x) symconvert(::Type{T}, x) where {T} = convert(T, x) -symconvert(::Type{Real}, x::Integer) = convert(Float64, x) +symconvert(::Type{Real}, x::Integer) = x symconvert(::Type{V}, x) where {V <: AbstractArray} = convert(V, symconvert.(eltype(V), x)) struct MTKParameters{T, I, D, C, N, H} From 227a0d8edf5623a4be727ab0b5b7601d4a5e5c3d Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 15 Apr 2025 11:43:21 +0530 Subject: [PATCH 1310/1337] fix: use `Bool(0)` for `default_dd_guess` --- src/systems/nonlinear/initializesystem.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/systems/nonlinear/initializesystem.jl b/src/systems/nonlinear/initializesystem.jl index 9c85b9b324..3b90bfc66a 100644 --- a/src/systems/nonlinear/initializesystem.jl +++ b/src/systems/nonlinear/initializesystem.jl @@ -8,7 +8,7 @@ function generate_initializesystem(sys::AbstractTimeDependentSystem; pmap = Dict(), initialization_eqs = [], guesses = Dict(), - default_dd_guess = 0.0, + default_dd_guess = Bool(0), algebraic_only = false, check_units = true, check_defguess = false, name = nameof(sys), extra_metadata = (;), kwargs...) From b228fd957bb9ad4413c982254df85fd16cdad92b Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 15 Apr 2025 13:02:37 +0530 Subject: [PATCH 1311/1337] fix: promote integer valued reals to `Float16` --- src/systems/parameter_buffer.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/systems/parameter_buffer.jl b/src/systems/parameter_buffer.jl index 2cf87769a0..6035605a02 100644 --- a/src/systems/parameter_buffer.jl +++ b/src/systems/parameter_buffer.jl @@ -1,6 +1,6 @@ symconvert(::Type{Symbolics.Struct{T}}, x) where {T} = convert(T, x) symconvert(::Type{T}, x) where {T} = convert(T, x) -symconvert(::Type{Real}, x::Integer) = x +symconvert(::Type{Real}, x::Integer) = convert(Float16, x) symconvert(::Type{V}, x) where {V <: AbstractArray} = convert(V, symconvert.(eltype(V), x)) struct MTKParameters{T, I, D, C, N, H} From c814779c365205629edba03cf653e4840d0310cb Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 15 Apr 2025 17:08:21 +0530 Subject: [PATCH 1312/1337] feat: allow specifying `eltype` of numeric buffer in `better_varmap_to_vars` --- src/systems/problem_utils.jl | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/systems/problem_utils.jl b/src/systems/problem_utils.jl index 1db153781a..236a3b31b8 100644 --- a/src/systems/problem_utils.jl +++ b/src/systems/problem_utils.jl @@ -362,7 +362,7 @@ Keyword arguments: - `is_initializeprob, guesses`: Used to determine whether the system is missing guesses. """ function better_varmap_to_vars(varmap::AbstractDict, vars::Vector; - tofloat = true, container_type = Array, + tofloat = true, container_type = Array, floatT = Float64, toterm = default_toterm, promotetoconcrete = nothing, check = true, allow_symbolic = false, is_initializeprob = false) isempty(vars) && return nothing @@ -385,6 +385,8 @@ function better_varmap_to_vars(varmap::AbstractDict, vars::Vector; is_initializeprob ? throw(MissingGuessError(missingsyms, missingvals)) : throw(UnexpectedSymbolicValueInVarmap(missingsyms[1], missingvals[1])) end + + vals = floatT.(vals) end if container_type <: Union{AbstractDict, Tuple, Nothing, SciMLBase.NullParameters} From a8d59d65d985938e0495ec97964bacf48d112f87 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 15 Apr 2025 17:08:38 +0530 Subject: [PATCH 1313/1337] feat: allow specifying floating point type in `get_temporary_value` --- src/systems/problem_utils.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/systems/problem_utils.jl b/src/systems/problem_utils.jl index 236a3b31b8..8ba5349011 100644 --- a/src/systems/problem_utils.jl +++ b/src/systems/problem_utils.jl @@ -535,12 +535,12 @@ function (f::UpdateInitializeprob)(initializeprob, prob) f.setvals(initializeprob, f.getvals(prob)) end -function get_temporary_value(p) +function get_temporary_value(p, floatT = Float64) stype = symtype(unwrap(p)) return if stype == Real - zero(Float64) + zero(floatT) elseif stype <: AbstractArray{Real} - zeros(Float64, size(p)) + zeros(floatT, size(p)) elseif stype <: Real zero(stype) elseif stype <: AbstractArray From 742146eac0f9fe085e648ff280297520d978b22d Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 15 Apr 2025 17:08:48 +0530 Subject: [PATCH 1314/1337] refactor: remove `zero_var` --- src/systems/problem_utils.jl | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/src/systems/problem_utils.jl b/src/systems/problem_utils.jl index 8ba5349011..78f2473f63 100644 --- a/src/systems/problem_utils.jl +++ b/src/systems/problem_utils.jl @@ -247,25 +247,6 @@ function recursive_unwrap(x::AbstractDict) return anydict(unwrap(k) => recursive_unwrap(v) for (k, v) in x) end -""" - $(TYPEDSIGNATURES) - -Return the appropriate zero value for a symbolic variable representing a number or array of -numbers. Sized array symbolics return a zero-filled array of matching size. Unsized array -symbolics return an empty array of the appropriate `eltype`. -""" -function zero_var(x::Symbolic{T}) where {V <: Number, T <: Union{V, AbstractArray{V}}} - if Symbolics.isarraysymbolic(x) - if is_sized_array_symbolic(x) - return zeros(eltype(T), size(x)) - else - return T[] - end - else - return zero(T) - end -end - """ $(TYPEDSIGNATURES) From fb1fb93e464d82af6490c77180180684623e9e7a Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 15 Apr 2025 17:09:10 +0530 Subject: [PATCH 1315/1337] refactor: determine floating point type earlier and propagate --- src/systems/problem_utils.jl | 31 +++++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/src/systems/problem_utils.jl b/src/systems/problem_utils.jl index 78f2473f63..877b844a32 100644 --- a/src/systems/problem_utils.jl +++ b/src/systems/problem_utils.jl @@ -631,11 +631,12 @@ All other keyword arguments are forwarded to `InitializationProblem`. """ function maybe_build_initialization_problem( sys::AbstractSystem, op::AbstractDict, u0map, pmap, t, defs, - guesses, missing_unknowns; implicit_dae = false, u0_constructor = identity, kwargs...) + guesses, missing_unknowns; implicit_dae = false, + u0_constructor = identity, floatT = Float64, kwargs...) guesses = merge(ModelingToolkit.guesses(sys), todict(guesses)) if t === nothing && is_time_dependent(sys) - t = 0.0 + t = zero(floatT) end initializeprob = ModelingToolkit.InitializationProblem{true, SciMLBase.FullSpecialize}( @@ -675,7 +676,7 @@ function maybe_build_initialization_problem( get(op, p, missing) === missing || continue p = unwrap(p) stype = symtype(p) - op[p] = get_temporary_value(p) + op[p] = get_temporary_value(p, floatT) if iscall(p) && operation(p) === getindex arrp = arguments(p)[1] op[arrp] = collect(arrp) @@ -684,7 +685,7 @@ function maybe_build_initialization_problem( if is_time_dependent(sys) for v in missing_unknowns - op[v] = zero_var(v) + op[v] = get_temporary_value(v, floatT) end empty!(missing_unknowns) end @@ -798,12 +799,26 @@ function process_SciMLProblem( op, missing_unknowns, missing_pars = build_operating_point!(sys, u0map, pmap, defs, cmap, dvs, ps) + floatT = Bool + for (k, v) in op + symbolic_type(v) == NotSymbolic() || continue + is_array_of_symbolics(v) && continue + + if v isa AbstractArray + isconcretetype(eltype(v)) || continue + floatT = promote_type(floatT, eltype(v)) + elseif v isa Real && isconcretetype(v) + floatT = promote_type(floatT, typeof(v)) + end + end + floatT = float(floatT) + if !is_time_dependent(sys) || is_initializesystem(sys) add_observed_equations!(u0map, obs) end if u0_constructor === identity && u0Type <: StaticArray u0_constructor = vals -> SymbolicUtils.Code.create_array( - u0Type, eltype(vals), Val(1), Val(length(vals)), vals...) + u0Type, floatT, Val(1), Val(length(vals)), vals...) end if build_initializeprob kws = maybe_build_initialization_problem( @@ -813,7 +828,7 @@ function process_SciMLProblem( warn_cyclic_dependency, check_units = check_initialization_units, circular_dependency_max_cycle_length, circular_dependency_max_cycles, use_scc, force_time_independent = force_initialization_time_independent, algebraic_only, allow_incomplete, - u0_constructor) + u0_constructor, floatT) kwargs = merge(kwargs, kws) end @@ -841,7 +856,7 @@ function process_SciMLProblem( evaluate_varmap!(op, dvs; limit = substitution_limit) u0 = better_varmap_to_vars( - op, dvs; tofloat, + op, dvs; tofloat, floatT, container_type = u0Type, allow_symbolic = symbolic_u0, is_initializeprob) if u0 !== nothing @@ -865,7 +880,7 @@ function process_SciMLProblem( end evaluate_varmap!(op, ps; limit = substitution_limit) if is_split(sys) - p = MTKParameters(sys, op) + p = MTKParameters(sys, op; floatT = floatT) else p = better_varmap_to_vars(op, ps; tofloat, container_type = pType) end From 59c6b31dd44b6d7194b49326270a182f8cf76f2d Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 15 Apr 2025 17:09:27 +0530 Subject: [PATCH 1316/1337] refactor: remove type-promotion hack in `InitializationProblem` --- src/systems/diffeqs/abstractodesystem.jl | 26 ------------------------ 1 file changed, 26 deletions(-) diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index 196c692c67..62ddd12a08 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -1541,32 +1541,6 @@ function InitializationProblem{iip, specialize}(sys::AbstractSystem, filter_missing_values!(parammap) u0map = merge(ModelingToolkit.guesses(sys), todict(guesses), u0map) - fullmap = merge(u0map, parammap) - u0T = Union{} - for sym in unknowns(isys) - val = fixpoint_sub(sym, fullmap) - symbolic_type(val) == NotSymbolic() || continue - u0T = promote_type(u0T, typeof(val)) - end - for eq in observed(isys) - # ignore HACK-ed observed equations - symbolic_type(eq.lhs) == ArraySymbolic() && continue - val = fixpoint_sub(eq.lhs, fullmap) - symbolic_type(val) == NotSymbolic() || continue - u0T = promote_type(u0T, typeof(val)) - end - if u0T != Union{} - u0T = eltype(u0T) - u0map = Dict(k => if v === nothing - nothing - elseif symbolic_type(v) == NotSymbolic() && !is_array_of_symbolics(v) - v isa AbstractArray ? u0T.(v) : u0T(v) - else - v - end - for (k, v) in u0map) - end - TProb = if neqs == nunknown && isempty(unassigned_vars) if use_scc && neqs > 0 if is_split(isys) From 27e7efbf73ccbe3ccb48c67e49c2ea8fa990830e Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 15 Apr 2025 17:09:56 +0530 Subject: [PATCH 1317/1337] fix: use `remake` to promote intialization problem --- src/systems/diffeqs/abstractodesystem.jl | 30 +++++++++++++++++------- src/systems/diffeqs/sdesystem.jl | 9 +++++-- src/systems/nonlinear/nonlinearsystem.jl | 9 +++++-- 3 files changed, 36 insertions(+), 12 deletions(-) diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index 62ddd12a08..c6b2eb8a87 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -817,7 +817,9 @@ function DiffEqBase.ODEProblem{iip, specialize}(sys::AbstractODESystem, u0map = end # Call `remake` so it runs initialization if it is trivial - return remake(ODEProblem{iip}(f, u0, tspan, p, pt; kwargs1..., kwargs...)) + # Pass `u0` and `p` to run `ReconstructInitializeprob` which will promote + # u0 and p of initializeprob + return remake(ODEProblem{iip}(f, u0, tspan, p, pt; kwargs1..., kwargs...); u0, p) end get_callback(prob::ODEProblem) = prob.kwargs[:callback] @@ -1040,9 +1042,14 @@ function DiffEqBase.DAEProblem{iip}(sys::AbstractODESystem, du0map, u0map, tspan end # Call `remake` so it runs initialization if it is trivial - return remake(DAEProblem{iip}( - f, du0, u0, tspan, p; differential_vars = differential_vars, - kwargs..., kwargs1...)) + # Pass `u0` and `p` to run `ReconstructInitializeprob` which will promote + # u0 and p of initializeprob + return remake( + DAEProblem{iip}( + f, du0, u0, tspan, p; differential_vars = differential_vars, + kwargs..., kwargs1...); + u0, + p) end function generate_history(sys::AbstractODESystem, u0; expression = Val{false}, kwargs...) @@ -1088,7 +1095,9 @@ function DiffEqBase.DDEProblem{iip}(sys::AbstractODESystem, u0map = [], kwargs1 = merge(kwargs1, (callback = cbs,)) end # Call `remake` so it runs initialization if it is trivial - return remake(DDEProblem{iip}(f, u0, h, tspan, p; kwargs1..., kwargs...)) + # Pass `u0` and `p` to run `ReconstructInitializeprob` which will promote + # u0 and p of initializeprob + return remake(DDEProblem{iip}(f, u0, h, tspan, p; kwargs1..., kwargs...); u0, p) end function DiffEqBase.SDDEProblem(sys::AbstractODESystem, args...; kwargs...) @@ -1139,9 +1148,14 @@ function DiffEqBase.SDDEProblem{iip}(sys::AbstractODESystem, u0map = [], noise_rate_prototype = zeros(eltype(u0), size(noiseeqs)) end # Call `remake` so it runs initialization if it is trivial - return remake(SDDEProblem{iip}(f, f.g, u0, h, tspan, p; - noise_rate_prototype = - noise_rate_prototype, kwargs1..., kwargs...)) + # Pass `u0` and `p` to run `ReconstructInitializeprob` which will promote + # u0 and p of initializeprob + return remake( + SDDEProblem{iip}(f, f.g, u0, h, tspan, p; + noise_rate_prototype = + noise_rate_prototype, kwargs1..., kwargs...); + u0, + p) end """ diff --git a/src/systems/diffeqs/sdesystem.jl b/src/systems/diffeqs/sdesystem.jl index 3fa1302630..f77b245b8a 100644 --- a/src/systems/diffeqs/sdesystem.jl +++ b/src/systems/diffeqs/sdesystem.jl @@ -818,8 +818,13 @@ function DiffEqBase.SDEProblem{iip, specialize}( kwargs = filter_kwargs(kwargs) # Call `remake` so it runs initialization if it is trivial - return remake(SDEProblem{iip}(f, u0, tspan, p; callback = cbs, noise, - noise_rate_prototype = noise_rate_prototype, kwargs...)) + # Pass `u0` and `p` to run `ReconstructInitializeprob` which will promote + # u0 and p of initializeprob + return remake( + SDEProblem{iip}(f, u0, tspan, p; callback = cbs, noise, + noise_rate_prototype = noise_rate_prototype, kwargs...); + u0, + p) end function DiffEqBase.SDEProblem(sys::ODESystem, args...; kwargs...) diff --git a/src/systems/nonlinear/nonlinearsystem.jl b/src/systems/nonlinear/nonlinearsystem.jl index 856822492b..c87d71abf5 100644 --- a/src/systems/nonlinear/nonlinearsystem.jl +++ b/src/systems/nonlinear/nonlinearsystem.jl @@ -557,7 +557,9 @@ function DiffEqBase.NonlinearProblem{iip}(sys::NonlinearSystem, u0map, check_length, kwargs...) pt = something(get_metadata(sys), StandardNonlinearProblem()) # Call `remake` so it runs initialization if it is trivial - return remake(NonlinearProblem{iip}(f, u0, p, pt; filter_kwargs(kwargs)...)) + # Pass `u0` and `p` to run `ReconstructInitializeprob` which will promote + # u0 and p of initializeprob + return remake(NonlinearProblem{iip}(f, u0, p, pt; filter_kwargs(kwargs)...); u0, p) end function DiffEqBase.NonlinearProblem(sys::AbstractODESystem, args...; kwargs...) @@ -591,7 +593,10 @@ function DiffEqBase.NonlinearLeastSquaresProblem{iip}(sys::NonlinearSystem, u0ma check_length, kwargs...) pt = something(get_metadata(sys), StandardNonlinearProblem()) # Call `remake` so it runs initialization if it is trivial - return remake(NonlinearLeastSquaresProblem{iip}(f, u0, p; filter_kwargs(kwargs)...)) + # Pass `u0` and `p` to run `ReconstructInitializeprob` which will promote + # u0 and p of initializeprob + return remake( + NonlinearLeastSquaresProblem{iip}(f, u0, p; filter_kwargs(kwargs)...); u0, p) end const TypeT = Union{DataType, UnionAll} From 8d2482c5b61ef18ee38a9c9da561d01ca6da7c57 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 15 Apr 2025 17:10:05 +0530 Subject: [PATCH 1318/1337] fix: fix `symconvert` --- src/systems/parameter_buffer.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/systems/parameter_buffer.jl b/src/systems/parameter_buffer.jl index 6035605a02..e1bb83238e 100644 --- a/src/systems/parameter_buffer.jl +++ b/src/systems/parameter_buffer.jl @@ -1,5 +1,5 @@ symconvert(::Type{Symbolics.Struct{T}}, x) where {T} = convert(T, x) -symconvert(::Type{T}, x) where {T} = convert(T, x) +symconvert(::Type{T}, x::V) where {T, V} = convert(promote_type(T, V), x) symconvert(::Type{Real}, x::Integer) = convert(Float16, x) symconvert(::Type{V}, x) where {V <: AbstractArray} = convert(V, symconvert.(eltype(V), x)) From 38724b2e38e180a8b8dae14cddf86a789dd376a5 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 15 Apr 2025 17:10:20 +0530 Subject: [PATCH 1319/1337] fix: allow specifying floating point type in `MTKParameters` --- src/systems/parameter_buffer.jl | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/systems/parameter_buffer.jl b/src/systems/parameter_buffer.jl index e1bb83238e..529be4eaf8 100644 --- a/src/systems/parameter_buffer.jl +++ b/src/systems/parameter_buffer.jl @@ -28,7 +28,7 @@ the default behavior). """ function MTKParameters( sys::AbstractSystem, p, u0 = Dict(); tofloat = false, - t0 = nothing, substitution_limit = 1000) + t0 = nothing, substitution_limit = 1000, floatT = nothing) ic = if has_index_cache(sys) && get_index_cache(sys) !== nothing get_index_cache(sys) else @@ -111,6 +111,9 @@ function MTKParameters( if ctype <: FnType ctype = fntype_to_function_type(ctype) end + if ctype == Real && floatT !== nothing + ctype = floatT + end val = symconvert(ctype, val) done = set_value(sym, val) if !done && Symbolics.isarraysymbolic(sym) From 57d7da10dc4fe8dd09ae690af84d21ba149c8b2e Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 15 Apr 2025 18:04:37 +0530 Subject: [PATCH 1320/1337] Revert "fix: use `remake` to promote intialization problem" This reverts commit 27e7efbf73ccbe3ccb48c67e49c2ea8fa990830e. --- src/systems/diffeqs/abstractodesystem.jl | 30 +++++++----------------- src/systems/diffeqs/sdesystem.jl | 9 ++----- src/systems/nonlinear/nonlinearsystem.jl | 9 ++----- 3 files changed, 12 insertions(+), 36 deletions(-) diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index c6b2eb8a87..62ddd12a08 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -817,9 +817,7 @@ function DiffEqBase.ODEProblem{iip, specialize}(sys::AbstractODESystem, u0map = end # Call `remake` so it runs initialization if it is trivial - # Pass `u0` and `p` to run `ReconstructInitializeprob` which will promote - # u0 and p of initializeprob - return remake(ODEProblem{iip}(f, u0, tspan, p, pt; kwargs1..., kwargs...); u0, p) + return remake(ODEProblem{iip}(f, u0, tspan, p, pt; kwargs1..., kwargs...)) end get_callback(prob::ODEProblem) = prob.kwargs[:callback] @@ -1042,14 +1040,9 @@ function DiffEqBase.DAEProblem{iip}(sys::AbstractODESystem, du0map, u0map, tspan end # Call `remake` so it runs initialization if it is trivial - # Pass `u0` and `p` to run `ReconstructInitializeprob` which will promote - # u0 and p of initializeprob - return remake( - DAEProblem{iip}( - f, du0, u0, tspan, p; differential_vars = differential_vars, - kwargs..., kwargs1...); - u0, - p) + return remake(DAEProblem{iip}( + f, du0, u0, tspan, p; differential_vars = differential_vars, + kwargs..., kwargs1...)) end function generate_history(sys::AbstractODESystem, u0; expression = Val{false}, kwargs...) @@ -1095,9 +1088,7 @@ function DiffEqBase.DDEProblem{iip}(sys::AbstractODESystem, u0map = [], kwargs1 = merge(kwargs1, (callback = cbs,)) end # Call `remake` so it runs initialization if it is trivial - # Pass `u0` and `p` to run `ReconstructInitializeprob` which will promote - # u0 and p of initializeprob - return remake(DDEProblem{iip}(f, u0, h, tspan, p; kwargs1..., kwargs...); u0, p) + return remake(DDEProblem{iip}(f, u0, h, tspan, p; kwargs1..., kwargs...)) end function DiffEqBase.SDDEProblem(sys::AbstractODESystem, args...; kwargs...) @@ -1148,14 +1139,9 @@ function DiffEqBase.SDDEProblem{iip}(sys::AbstractODESystem, u0map = [], noise_rate_prototype = zeros(eltype(u0), size(noiseeqs)) end # Call `remake` so it runs initialization if it is trivial - # Pass `u0` and `p` to run `ReconstructInitializeprob` which will promote - # u0 and p of initializeprob - return remake( - SDDEProblem{iip}(f, f.g, u0, h, tspan, p; - noise_rate_prototype = - noise_rate_prototype, kwargs1..., kwargs...); - u0, - p) + return remake(SDDEProblem{iip}(f, f.g, u0, h, tspan, p; + noise_rate_prototype = + noise_rate_prototype, kwargs1..., kwargs...)) end """ diff --git a/src/systems/diffeqs/sdesystem.jl b/src/systems/diffeqs/sdesystem.jl index f77b245b8a..3fa1302630 100644 --- a/src/systems/diffeqs/sdesystem.jl +++ b/src/systems/diffeqs/sdesystem.jl @@ -818,13 +818,8 @@ function DiffEqBase.SDEProblem{iip, specialize}( kwargs = filter_kwargs(kwargs) # Call `remake` so it runs initialization if it is trivial - # Pass `u0` and `p` to run `ReconstructInitializeprob` which will promote - # u0 and p of initializeprob - return remake( - SDEProblem{iip}(f, u0, tspan, p; callback = cbs, noise, - noise_rate_prototype = noise_rate_prototype, kwargs...); - u0, - p) + return remake(SDEProblem{iip}(f, u0, tspan, p; callback = cbs, noise, + noise_rate_prototype = noise_rate_prototype, kwargs...)) end function DiffEqBase.SDEProblem(sys::ODESystem, args...; kwargs...) diff --git a/src/systems/nonlinear/nonlinearsystem.jl b/src/systems/nonlinear/nonlinearsystem.jl index c87d71abf5..856822492b 100644 --- a/src/systems/nonlinear/nonlinearsystem.jl +++ b/src/systems/nonlinear/nonlinearsystem.jl @@ -557,9 +557,7 @@ function DiffEqBase.NonlinearProblem{iip}(sys::NonlinearSystem, u0map, check_length, kwargs...) pt = something(get_metadata(sys), StandardNonlinearProblem()) # Call `remake` so it runs initialization if it is trivial - # Pass `u0` and `p` to run `ReconstructInitializeprob` which will promote - # u0 and p of initializeprob - return remake(NonlinearProblem{iip}(f, u0, p, pt; filter_kwargs(kwargs)...); u0, p) + return remake(NonlinearProblem{iip}(f, u0, p, pt; filter_kwargs(kwargs)...)) end function DiffEqBase.NonlinearProblem(sys::AbstractODESystem, args...; kwargs...) @@ -593,10 +591,7 @@ function DiffEqBase.NonlinearLeastSquaresProblem{iip}(sys::NonlinearSystem, u0ma check_length, kwargs...) pt = something(get_metadata(sys), StandardNonlinearProblem()) # Call `remake` so it runs initialization if it is trivial - # Pass `u0` and `p` to run `ReconstructInitializeprob` which will promote - # u0 and p of initializeprob - return remake( - NonlinearLeastSquaresProblem{iip}(f, u0, p; filter_kwargs(kwargs)...); u0, p) + return remake(NonlinearLeastSquaresProblem{iip}(f, u0, p; filter_kwargs(kwargs)...)) end const TypeT = Union{DataType, UnionAll} From 9e6e8942ec8ab21548043a05801f2317bfc699af Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 15 Apr 2025 18:17:09 +0530 Subject: [PATCH 1321/1337] fix: use `remake_initialization_data` to promote the initialization problem --- src/systems/problem_utils.jl | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/systems/problem_utils.jl b/src/systems/problem_utils.jl index 877b844a32..8a343baf7b 100644 --- a/src/systems/problem_utils.jl +++ b/src/systems/problem_utils.jl @@ -896,6 +896,16 @@ function process_SciMLProblem( du0 = nothing end + if build_initializeprob + t0 = t + if is_time_dependent(sys) && t0 === nothing + t0 = zero(floatT) + end + initialization_data = SciMLBase.remake_initialization_data( + kwargs.initialization_data, kwargs, u0, t0, p, u0, p) + kwargs = merge(kwargs,) + end + f = constructor(sys, dvs, ps, u0; p = p, eval_expression = eval_expression, eval_module = eval_module, From 662217b28fc301b65d7e18a3a9a9f05e56ff22fa Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 15 Apr 2025 20:52:57 +0530 Subject: [PATCH 1322/1337] fix: promote initializeprob in `remake_initialization_data` --- src/systems/nonlinear/initializesystem.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/systems/nonlinear/initializesystem.jl b/src/systems/nonlinear/initializesystem.jl index 3b90bfc66a..4d08042533 100644 --- a/src/systems/nonlinear/initializesystem.jl +++ b/src/systems/nonlinear/initializesystem.jl @@ -649,7 +649,8 @@ function SciMLBase.remake_initialization_data( kws = maybe_build_initialization_problem( sys, op, u0map, pmap, t0, defs, guesses, missing_unknowns; use_scc, initialization_eqs, allow_incomplete = true) - return get(kws, :initialization_data, nothing) + + return SciMLBase.remake_initialization_data(sys, kws, newu0, t0, newp, newu0, newp) end function SciMLBase.late_binding_update_u0_p( From f9cdd1684e852f0a6cb181dfa9b2b06a56c42289 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 15 Apr 2025 20:53:18 +0530 Subject: [PATCH 1323/1337] fix: respect `floatT` in `maybe_build_initialization_problem` --- src/systems/problem_utils.jl | 38 +++++++++++++++++++++++++++--------- 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/src/systems/problem_utils.jl b/src/systems/problem_utils.jl index 8a343baf7b..4cc03b52ab 100644 --- a/src/systems/problem_utils.jl +++ b/src/systems/problem_utils.jl @@ -641,6 +641,22 @@ function maybe_build_initialization_problem( initializeprob = ModelingToolkit.InitializationProblem{true, SciMLBase.FullSpecialize}( sys, t, u0map, pmap; guesses, kwargs...) + if state_values(initializeprob) !== nothing + initializeprob = remake(initializeprob; u0 = floatT.(state_values(initializeprob))) + end + initp = parameter_values(initializeprob) + if is_split(sys) + buffer, repack, _ = SciMLStructures.canonicalize(SciMLStructures.Tunable(), initp) + initp = repack(floatT.(buffer)) + buffer, repack, _ = SciMLStructures.canonicalize(SciMLStructures.Initials(), initp) + initp = repack(floatT.(buffer)) + elseif initp isa AbstractArray + initp′ = similar(initp, floatT) + copyto!(initp′, initp) + initp = initp′ + end + initializeprob = remake(initializeprob; p = initp) + meta = get_metadata(initializeprob.f.sys) if is_time_dependent(sys) @@ -800,15 +816,19 @@ function process_SciMLProblem( u0map, pmap, defs, cmap, dvs, ps) floatT = Bool - for (k, v) in op - symbolic_type(v) == NotSymbolic() || continue - is_array_of_symbolics(v) && continue - - if v isa AbstractArray - isconcretetype(eltype(v)) || continue - floatT = promote_type(floatT, eltype(v)) - elseif v isa Real && isconcretetype(v) - floatT = promote_type(floatT, typeof(v)) + if u0Type <: AbstractArray && isconcretetype(eltype(u0Type)) && eltype(u0Type) <: Real + floatT = eltype(u0Type) + else + for (k, v) in op + symbolic_type(v) == NotSymbolic() || continue + is_array_of_symbolics(v) && continue + + if v isa AbstractArray + isconcretetype(eltype(v)) || continue + floatT = promote_type(floatT, eltype(v)) + elseif v isa Real && isconcretetype(v) + floatT = promote_type(floatT, typeof(v)) + end end end floatT = float(floatT) From 9fe75863d927ba29a556fc1c8bad2ad67cade42d Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 15 Apr 2025 21:05:00 +0530 Subject: [PATCH 1324/1337] fix: respect `tofloat` when using `floatT`, default to not promoting --- src/systems/problem_utils.jl | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/systems/problem_utils.jl b/src/systems/problem_utils.jl index 4cc03b52ab..96444bc31f 100644 --- a/src/systems/problem_utils.jl +++ b/src/systems/problem_utils.jl @@ -343,7 +343,7 @@ Keyword arguments: - `is_initializeprob, guesses`: Used to determine whether the system is missing guesses. """ function better_varmap_to_vars(varmap::AbstractDict, vars::Vector; - tofloat = true, container_type = Array, floatT = Float64, + tofloat = true, container_type = Array, floatT = Nothing, toterm = default_toterm, promotetoconcrete = nothing, check = true, allow_symbolic = false, is_initializeprob = false) isempty(vars) && return nothing @@ -366,8 +366,9 @@ function better_varmap_to_vars(varmap::AbstractDict, vars::Vector; is_initializeprob ? throw(MissingGuessError(missingsyms, missingvals)) : throw(UnexpectedSymbolicValueInVarmap(missingsyms[1], missingvals[1])) end - - vals = floatT.(vals) + if tofloat && !(floatT == Nothing) + vals = floatT.(vals) + end end if container_type <: Union{AbstractDict, Tuple, Nothing, SciMLBase.NullParameters} From a8e2495964d04cd7818c43b841aea434f7a1d5c4 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 15 Apr 2025 21:05:14 +0530 Subject: [PATCH 1325/1337] fix: do not require `isconcretetype` when discovering `floatT` --- src/systems/problem_utils.jl | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/systems/problem_utils.jl b/src/systems/problem_utils.jl index 96444bc31f..51c018bacc 100644 --- a/src/systems/problem_utils.jl +++ b/src/systems/problem_utils.jl @@ -817,7 +817,7 @@ function process_SciMLProblem( u0map, pmap, defs, cmap, dvs, ps) floatT = Bool - if u0Type <: AbstractArray && isconcretetype(eltype(u0Type)) && eltype(u0Type) <: Real + if u0Type <: AbstractArray && eltype(u0Type) <: Real floatT = eltype(u0Type) else for (k, v) in op @@ -825,9 +825,8 @@ function process_SciMLProblem( is_array_of_symbolics(v) && continue if v isa AbstractArray - isconcretetype(eltype(v)) || continue floatT = promote_type(floatT, eltype(v)) - elseif v isa Real && isconcretetype(v) + elseif v isa Real floatT = promote_type(floatT, typeof(v)) end end From 6023efbad37a8e0326ab7a5e8990feb3296959dd Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 16 Apr 2025 10:42:05 +0530 Subject: [PATCH 1326/1337] refactor: add `float_type_from_varmap` --- src/systems/problem_utils.jl | 34 ++++++++++++++++++++++------------ 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/src/systems/problem_utils.jl b/src/systems/problem_utils.jl index 51c018bacc..0750585905 100644 --- a/src/systems/problem_utils.jl +++ b/src/systems/problem_utils.jl @@ -713,6 +713,26 @@ function maybe_build_initialization_problem( initializeprobpmap)) end +""" + $(TYPEDSIGNATURES) + +Calculate the floating point type to use from the given `varmap` by looking at variables +with a constant value. +""" +function float_type_from_varmap(varmap, floatT = Bool) + for (k, v) in varmap + symbolic_type(v) == NotSymbolic() || continue + is_array_of_symbolics(v) && continue + + if v isa AbstractArray + floatT = promote_type(floatT, eltype(v)) + elseif v isa Real + floatT = promote_type(floatT, typeof(v)) + end + end + return float(floatT) +end + """ $(TYPEDSIGNATURES) @@ -818,20 +838,10 @@ function process_SciMLProblem( floatT = Bool if u0Type <: AbstractArray && eltype(u0Type) <: Real - floatT = eltype(u0Type) + floatT = float(eltype(u0Type)) else - for (k, v) in op - symbolic_type(v) == NotSymbolic() || continue - is_array_of_symbolics(v) && continue - - if v isa AbstractArray - floatT = promote_type(floatT, eltype(v)) - elseif v isa Real - floatT = promote_type(floatT, typeof(v)) - end - end + floatT = float_type_from_varmap(op, floatT) end - floatT = float(floatT) if !is_time_dependent(sys) || is_initializesystem(sys) add_observed_equations!(u0map, obs) From ead8682fd88b02ff019d6e244e1100c90fc72631 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 16 Apr 2025 10:42:16 +0530 Subject: [PATCH 1327/1337] fix: infer `floatT` in `MTKParameters` constructor if not provided --- src/systems/parameter_buffer.jl | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/systems/parameter_buffer.jl b/src/systems/parameter_buffer.jl index 529be4eaf8..5fb830290f 100644 --- a/src/systems/parameter_buffer.jl +++ b/src/systems/parameter_buffer.jl @@ -56,6 +56,10 @@ function MTKParameters( op[get_iv(sys)] = t0 end + if floatT === nothing + floatT = float(float_type_from_varmap(op)) + end + isempty(missing_pars) || throw(MissingParametersError(collect(missing_pars))) evaluate_varmap!(op, ps; limit = substitution_limit) From cd19c7edab02ec27ed1015230c217ac9cff3a04d Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 16 Apr 2025 10:42:26 +0530 Subject: [PATCH 1328/1337] fix: infer `floatT` in `remake_initialization_data` --- src/systems/nonlinear/initializesystem.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/systems/nonlinear/initializesystem.jl b/src/systems/nonlinear/initializesystem.jl index 4d08042533..ec25b9b660 100644 --- a/src/systems/nonlinear/initializesystem.jl +++ b/src/systems/nonlinear/initializesystem.jl @@ -646,9 +646,10 @@ function SciMLBase.remake_initialization_data( op, missing_unknowns, missing_pars = build_operating_point!(sys, u0map, pmap, defs, cmap, dvs, ps) + floatT = float_type_from_varmap(op) kws = maybe_build_initialization_problem( sys, op, u0map, pmap, t0, defs, guesses, missing_unknowns; - use_scc, initialization_eqs, allow_incomplete = true) + use_scc, initialization_eqs, floatT, allow_incomplete = true) return SciMLBase.remake_initialization_data(sys, kws, newu0, t0, newp, newu0, newp) end From b685ab5912dfee89682c7167f7c2c4e2afe2afb5 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 8 Apr 2025 17:22:25 +0530 Subject: [PATCH 1329/1337] feat: add `@generated` method for `_remake_buffer` for type-inference --- src/systems/parameter_buffer.jl | 66 +++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/src/systems/parameter_buffer.jl b/src/systems/parameter_buffer.jl index 36105c0de8..3122ace245 100644 --- a/src/systems/parameter_buffer.jl +++ b/src/systems/parameter_buffer.jl @@ -496,6 +496,72 @@ end function SymbolicIndexingInterface.remake_buffer(indp, oldbuf::MTKParameters, idxs, vals) _remake_buffer(indp, oldbuf, idxs, vals) end + +# For type-inference when using `SII.setp_oop` +@generated function _remake_buffer( + indp, oldbuf::MTKParameters{T, I, D, C, N, H}, idxs::Tuple{Vararg{ParameterIndex}}, + vals::Union{AbstractArray, Tuple}; validate = true) where {T, I, D, C, N, H} + valtype(i) = vals <: AbstractArray ? eltype(vals) : fieldtype(vals, i) + tunablesT = eltype(T) + for (i, idxT) in enumerate(fieldtypes(idxs)) + idxT <: ParameterIndex{SciMLStructures.Tunable} || continue + tunablesT = promote_type(tunablesT, valtype(i)) + end + initialsT = eltype(I) + for (i, idxT) in enumerate(fieldtypes(idxs)) + idxT <: ParameterIndex{SciMLStructures.Initials} || continue + initialsT = promote_type(initialsT, valtype(i)) + end + discretesT = ntuple(Val(fieldcount(D))) do i + bufT = eltype(fieldtype(D, i)) + for (j, idxT) in enumerate(fieldtypes(idxs)) + idxT <: ParameterIndex{SciMLStructures.Discrete, i} || continue + bufT = promote_type(bufT, valtype(i)) + end + bufT + end + constantsT = ntuple(Val(fieldcount(C))) do i + bufT = eltype(fieldtype(C, i)) + for (j, idxT) in enumerate(fieldtypes(idxs)) + idxT <: ParameterIndex{SciMLStructures.Constants, i} || continue + bufT = promote_type(bufT, valtype(i)) + end + bufT + end + nonnumericT = ntuple(Val(fieldcount(N))) do i + bufT = eltype(fieldtype(N, i)) + for (j, idxT) in enumerate(fieldtypes(idxs)) + idxT <: ParameterIndex{Nonnumeric, i} || continue + bufT = promote_type(bufT, valtype(i)) + end + bufT + end + + expr = quote + tunables = $similar(oldbuf.tunable, $tunablesT) + copyto!(tunables, oldbuf.tunable) + initials = $similar(oldbuf.initials, $initialsT) + copyto!(initials, oldbuf.initials) + discretes = $(Expr(:tuple, + (:($similar(oldbuf.discrete[$i], $(discretesT[i]))) for i in 1:length(discretesT))...)) + $((:($copyto!(discretes[$i], oldbuf.discrete[$i])) for i in 1:length(discretesT))...) + constants = $(Expr(:tuple, + (:($similar(oldbuf.constant[$i], $(constantsT[i]))) for i in 1:length(constantsT))...)) + $((:($copyto!(constants[$i], oldbuf.constant[$i])) for i in 1:length(constantsT))...) + nonnumerics = $(Expr(:tuple, + (:($similar(oldbuf.nonnumeric[$i], $(nonnumericT[i]))) for i in 1:length(nonnumericT))...)) + $((:($copyto!(nonnumerics[$i], oldbuf.nonnumeric[$i])) for i in 1:length(nonnumericT))...) + newbuf = MTKParameters( + tunables, initials, discretes, constants, nonnumerics, copy.(oldbuf.caches)) + end + for i in 1:fieldcount(idxs) + push!(expr.args, :($setindex!(newbuf, vals[$i], idxs[$i]))) + end + push!(expr.args, :(return newbuf)) + + return expr +end + function _remake_buffer(indp, oldbuf::MTKParameters, idxs, vals; validate = true) newbuf = @set oldbuf.tunable = similar(oldbuf.tunable, Any) @set! newbuf.initials = similar(oldbuf.initials, Any) From 0ba61201650b7e3a2d3adbb388e7a546a6bb8e63 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 15 Apr 2025 13:04:51 +0530 Subject: [PATCH 1330/1337] build: bump SII compat --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 63aef5b8da..693a81e148 100644 --- a/Project.toml +++ b/Project.toml @@ -149,7 +149,7 @@ SpecialFunctions = "0.7, 0.8, 0.9, 0.10, 1.0, 2" StaticArrays = "0.10, 0.11, 0.12, 1.0" StochasticDelayDiffEq = "1.8.1" StochasticDiffEq = "6.72.1" -SymbolicIndexingInterface = "0.3.37" +SymbolicIndexingInterface = "0.3.39" SymbolicUtils = "3.25.1" Symbolics = "6.37" URIs = "1" From 96694de0e47208da1e2d1e0f557a9c7f271e6fa2 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 16 Apr 2025 13:06:34 +0530 Subject: [PATCH 1331/1337] feat: handle more type-stable cases in `@generated _remake_buffer` --- src/systems/parameter_buffer.jl | 72 ++++++++++++++++++++++++++------- 1 file changed, 57 insertions(+), 15 deletions(-) diff --git a/src/systems/parameter_buffer.jl b/src/systems/parameter_buffer.jl index 3122ace245..0916bb1185 100644 --- a/src/systems/parameter_buffer.jl +++ b/src/systems/parameter_buffer.jl @@ -499,40 +499,76 @@ end # For type-inference when using `SII.setp_oop` @generated function _remake_buffer( - indp, oldbuf::MTKParameters{T, I, D, C, N, H}, idxs::Tuple{Vararg{ParameterIndex}}, - vals::Union{AbstractArray, Tuple}; validate = true) where {T, I, D, C, N, H} - valtype(i) = vals <: AbstractArray ? eltype(vals) : fieldtype(vals, i) + indp, oldbuf::MTKParameters{T, I, D, C, N, H}, + idxs::Union{Tuple{Vararg{ParameterIndex}}, AbstractArray{<:ParameterIndex{P}}}, + vals::Union{AbstractArray, Tuple}; validate = true) where {T, I, D, C, N, H, P} + + # fallback to non-generated method if values aren't type-stable + if vals <: AbstractArray && !isconcretetype(eltype(vals)) + return quote + $_remake_buffer(indp, oldbuf, collect(idxs), vals; validate) + end + end + + # given an index in idxs/vals and the current `eltype` of the buffer, + # return the promoted eltype of the buffer + function promote_valtype(i, valT) + # tuples have distinct types, arrays have a common eltype + valT′ = vals <: AbstractArray ? eltype(vals) : fieldtype(vals, i) + # if the buffer is a scalarized buffer but the variable is an array + # e.g. an array tunable, take the eltype + if valT′ <: AbstractArray && !(valT <: AbstractArray) + valT′ = eltype(valT′) + end + return promote_type(valT, valT′) + end + + # types of the idxs + idxtypes = if idxs <: AbstractArray + # if both are arrays, there is only one possible type to check + if vals <: AbstractArray + (eltype(idxs),) + else + # if `vals` is a tuple, we repeat `eltype(idxs)` to check against + # every possible type of the buffer + ntuple(Returns(eltype(idxs)), Val(fieldcount(vals))) + end + else + # `idxs` is a tuple, so we check against all buffers + fieldtypes(idxs) + end + # promote types tunablesT = eltype(T) - for (i, idxT) in enumerate(fieldtypes(idxs)) + for (i, idxT) in enumerate(idxtypes) idxT <: ParameterIndex{SciMLStructures.Tunable} || continue - tunablesT = promote_type(tunablesT, valtype(i)) + tunablesT = promote_valtype(i, tunablesT) end initialsT = eltype(I) - for (i, idxT) in enumerate(fieldtypes(idxs)) + for (i, idxT) in enumerate(idxtypes) idxT <: ParameterIndex{SciMLStructures.Initials} || continue - initialsT = promote_type(initialsT, valtype(i)) + initialsT = promote_valtype(i, initialsT) end discretesT = ntuple(Val(fieldcount(D))) do i bufT = eltype(fieldtype(D, i)) - for (j, idxT) in enumerate(fieldtypes(idxs)) + for (j, idxT) in enumerate(idxtypes) idxT <: ParameterIndex{SciMLStructures.Discrete, i} || continue - bufT = promote_type(bufT, valtype(i)) + bufT = promote_valtype(i, bufT) end bufT end constantsT = ntuple(Val(fieldcount(C))) do i bufT = eltype(fieldtype(C, i)) - for (j, idxT) in enumerate(fieldtypes(idxs)) + for (j, idxT) in enumerate(idxtypes) idxT <: ParameterIndex{SciMLStructures.Constants, i} || continue - bufT = promote_type(bufT, valtype(i)) + bufT = promote_valtype(i, bufT) end bufT end nonnumericT = ntuple(Val(fieldcount(N))) do i bufT = eltype(fieldtype(N, i)) - for (j, idxT) in enumerate(fieldtypes(idxs)) + for (j, idxT) in enumerate(idxtypes) idxT <: ParameterIndex{Nonnumeric, i} || continue - bufT = promote_type(bufT, valtype(i)) + bufT = promote_valtype(i, bufT) end bufT end @@ -554,8 +590,14 @@ end newbuf = MTKParameters( tunables, initials, discretes, constants, nonnumerics, copy.(oldbuf.caches)) end - for i in 1:fieldcount(idxs) - push!(expr.args, :($setindex!(newbuf, vals[$i], idxs[$i]))) + if idxs <: AbstractArray + push!(expr.args, :(for (idx, val) in zip(idxs, vals) + $setindex!(newbuf, val, idx) + end)) + else + for i in 1:fieldcount(idxs) + push!(expr.args, :($setindex!(newbuf, vals[$i], idxs[$i]))) + end end push!(expr.args, :(return newbuf)) From c5e8240458b503f2cfd8e92ad9c0c4b2e73456fb Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 16 Apr 2025 13:06:48 +0530 Subject: [PATCH 1332/1337] test: test type-stability of `remake_buffer` in supported cases --- test/mtkparameters.jl | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/test/mtkparameters.jl b/test/mtkparameters.jl index f9d71f00bc..22201b1988 100644 --- a/test/mtkparameters.jl +++ b/test/mtkparameters.jl @@ -91,6 +91,27 @@ end @test getp(sys, f)(newps) isa Matrix{UInt} @test getp(sys, g)(newps) isa Vector{Float32} +@testset "Type-stability of `remake_buffer`" begin + prob = ODEProblem(sys, [], (0.0, 1.0), ivs) + + idxs = (a, c, d, e, f, g, h) + vals = (1.0, 2.0, 3, ones(3), ones(Int, 3, 3), ones(2), "a") + + setter = setsym_oop(prob, idxs) + @test_nowarn @inferred setter(prob, vals) + @test_throws ErrorException @inferred setter(prob, collect(vals)) + + idxs = (a, c, e...) + vals = Float16[1.0, 2.0, 3.0, 4.0, 5.0] + setter = setsym_oop(prob, idxs) + @test_nowarn @inferred setter(prob, vals) + + idxs = [a, e] + vals = (Float16(1.0), ForwardDiff.Dual{Nothing, Float16, 0}[1.0, 2.0, 3.0]) + setter = setsym_oop(prob, idxs) + @test_nowarn @inferred setter(prob, vals) +end + ps = MTKParameters(sys, ivs) function loss(value, sys, ps) @test value isa ForwardDiff.Dual From 06a63df059223f6625e0a5b221102563f53825b4 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 16 Apr 2025 13:39:54 +0530 Subject: [PATCH 1333/1337] fix: fix stack overflow in `@generated _remake_buffer` fallback --- src/systems/parameter_buffer.jl | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/systems/parameter_buffer.jl b/src/systems/parameter_buffer.jl index 0916bb1185..672fb58795 100644 --- a/src/systems/parameter_buffer.jl +++ b/src/systems/parameter_buffer.jl @@ -506,7 +506,7 @@ end # fallback to non-generated method if values aren't type-stable if vals <: AbstractArray && !isconcretetype(eltype(vals)) return quote - $_remake_buffer(indp, oldbuf, collect(idxs), vals; validate) + $__remake_buffer(indp, oldbuf, collect(idxs), vals; validate) end end @@ -605,6 +605,10 @@ end end function _remake_buffer(indp, oldbuf::MTKParameters, idxs, vals; validate = true) + return __remake_buffer(indp, oldbuf, idxs, vals; validate) +end + +function __remake_buffer(indp, oldbuf::MTKParameters, idxs, vals; validate = true) newbuf = @set oldbuf.tunable = similar(oldbuf.tunable, Any) @set! newbuf.initials = similar(oldbuf.initials, Any) @set! newbuf.discrete = Tuple(similar(buf, Any) for buf in newbuf.discrete) From 0618a68c5ed0f7888bca2f439bc3c04e9c962e48 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 16 Apr 2025 13:36:40 +0530 Subject: [PATCH 1334/1337] fix: scalarize `Initial` parameters for `split = false` systems --- src/systems/abstractsystem.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index 72f3132889..4ad08bb1f4 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -696,7 +696,7 @@ end supports_initialization(sys::AbstractSystem) = true -function add_initialization_parameters(sys::AbstractSystem) +function add_initialization_parameters(sys::AbstractSystem; split = true) @assert !has_systems(sys) || isempty(get_systems(sys)) supports_initialization(sys) || return sys is_initializesystem(sys) && return sys @@ -711,7 +711,7 @@ function add_initialization_parameters(sys::AbstractSystem) obs, eqs = unhack_observed(observed(sys), eqs) for x in Iterators.flatten((unknowns(sys), Iterators.map(eq -> eq.lhs, obs))) x = unwrap(x) - if iscall(x) && operation(x) == getindex + if iscall(x) && operation(x) == getindex && split push!(all_initialvars, arguments(x)[1]) else push!(all_initialvars, x) @@ -788,7 +788,7 @@ function complete( end sys = newsys if add_initial_parameters - sys = add_initialization_parameters(sys) + sys = add_initialization_parameters(sys; split) end end if split && has_index_cache(sys) From 8147aabad454263c2a5279394c3141145f9d2002 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 16 Apr 2025 13:36:54 +0530 Subject: [PATCH 1335/1337] fix: recognize scalarized `Initial` parameters in `parameters` --- src/systems/abstractsystem.jl | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/systems/abstractsystem.jl b/src/systems/abstractsystem.jl index 4ad08bb1f4..29338d0722 100644 --- a/src/systems/abstractsystem.jl +++ b/src/systems/abstractsystem.jl @@ -1465,7 +1465,11 @@ function parameters(sys::AbstractSystem; initial_parameters = false) result = unique(isempty(systems) ? ps : [ps; reduce(vcat, namespace_parameters.(systems))]) if !initial_parameters && !is_initializesystem(sys) - filter!(x -> !iscall(x) || !isa(operation(x), Initial), result) + filter!(result) do sym + return !(isoperator(sym, Initial) || + iscall(sym) && operation(sym) == getindex && + isoperator(arguments(sym)[1], Initial)) + end end return result end From ec4fd42d6c31888f374d4852971683abecd33a54 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 16 Apr 2025 13:37:07 +0530 Subject: [PATCH 1336/1337] test: test mix of array initials and scalar parameters with `split = false` --- test/initial_values.jl | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/test/initial_values.jl b/test/initial_values.jl index 01a053a9bf..79b6b8e067 100644 --- a/test/initial_values.jl +++ b/test/initial_values.jl @@ -265,3 +265,19 @@ end @test eltype(oprob.u0) == Float32 @test eltype(eltype(sol.u)) == Float32 end + +@testset "Array initials and scalar parameters with `split = false`" begin + @variables x(t)[1:2] + @parameters p + @mtkbuild sys=ODESystem([D(x[1]) ~ x[1], D(x[2]) ~ x[2] + p], t) split=false + ps = Set(parameters(sys; initial_parameters = true)) + @test length(ps) == 5 + for i in 1:2 + @test Initial(x[i]) in ps + @test Initial(D(x[i])) in ps + end + @test p in ps + prob = ODEProblem(sys, [x => ones(2)], (0.0, 1.0), [p => 1.0]) + @test prob.p isa Vector{Float64} + @test length(prob.p) == 5 +end From 3dea97cd9430bc9ae75c81b96921587d4eec2650 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Wed, 16 Apr 2025 16:36:53 +0530 Subject: [PATCH 1337/1337] test: account for new scalarized initials in tests --- test/odesystem.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/odesystem.jl b/test/odesystem.jl index a45ed99f35..4b76da6e9d 100644 --- a/test/odesystem.jl +++ b/test/odesystem.jl @@ -1305,7 +1305,7 @@ end ps = zeros(8) setp(sys2, x)(ps, 2ones(2)) setp(sys2, p)(ps, 2ones(2, 2)) - @test_nowarn fn2(ones(4), 2ones(6), 4.0) + @test_nowarn fn2(ones(4), 2ones(14), 4.0) end # https://github.com/SciML/ModelingToolkit.jl/issues/2969 @@ -1416,7 +1416,7 @@ end obsfn = ModelingToolkit.build_explicit_observed_function( sys1, u + x + p[1:2]; inputs = [x...]) - @test obsfn(ones(2), 2ones(2), 3ones(4), 4.0) == 6ones(2) + @test obsfn(ones(2), 2ones(2), 3ones(12), 4.0) == 6ones(2) end @testset "Passing `nothing` to `u0`" begin

M;WbV4y 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 z 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 Date: Tue, 24 Dec 2024 20:26:00 +0530 Subject: [PATCH 0634/1337] feat: allow `ImperativeAffect` to accept callable structs --- src/systems/imperative_affect.jl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/systems/imperative_affect.jl b/src/systems/imperative_affect.jl index 2f489913ad..7580580add 100644 --- a/src/systems/imperative_affect.jl +++ b/src/systems/imperative_affect.jl @@ -38,7 +38,7 @@ in the returned tuple, in which case the associated field will not be updated. skip_checks::Bool end -function ImperativeAffect(f::Function; +function ImperativeAffect(f; observed::NamedTuple = NamedTuple{()}(()), modified::NamedTuple = NamedTuple{()}(()), ctx = nothing, @@ -48,18 +48,18 @@ function ImperativeAffect(f::Function; collect(values(modified)), collect(keys(modified)), ctx, skip_checks) end -function ImperativeAffect(f::Function, modified::NamedTuple; +function ImperativeAffect(f, modified::NamedTuple; observed::NamedTuple = NamedTuple{()}(()), ctx = nothing, skip_checks = false) ImperativeAffect( f, observed = observed, modified = modified, ctx = ctx, skip_checks = skip_checks) end function ImperativeAffect( - f::Function, modified::NamedTuple, observed::NamedTuple; ctx = nothing, skip_checks = false) + f, modified::NamedTuple, observed::NamedTuple; ctx = nothing, skip_checks = false) ImperativeAffect( f, observed = observed, modified = modified, ctx = ctx, skip_checks = skip_checks) end function ImperativeAffect( - f::Function, modified::NamedTuple, observed::NamedTuple, ctx; skip_checks = false) + f, modified::NamedTuple, observed::NamedTuple, ctx; skip_checks = false) ImperativeAffect( f, observed = observed, modified = modified, ctx = ctx, skip_checks = skip_checks) end From 81ff95a04a79cde91460c6d9440b4b9264450005 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 24 Dec 2024 20:26:23 +0530 Subject: [PATCH 0635/1337] feat: add `vars!` for callbacks and affects --- src/systems/callbacks.jl | 45 ++++++++++++++++++++++++++++++++ src/systems/imperative_affect.jl | 17 ++++++++++++ 2 files changed, 62 insertions(+) diff --git a/src/systems/callbacks.jl b/src/systems/callbacks.jl index eaf31a9c5f..492b5dac8c 100644 --- a/src/systems/callbacks.jl +++ b/src/systems/callbacks.jl @@ -77,6 +77,13 @@ 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 ##################################### const NULL_AFFECT = Equation[] @@ -333,6 +340,22 @@ function continuous_events(sys::AbstractSystem) filter(!isempty, cbs) 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 ##################################### struct SymbolicDiscreteCallback @@ -469,6 +492,28 @@ function discrete_events(sys::AbstractSystem) 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 + ################################# compilation functions #################################### # handles ensuring that affect! functions work with integrator arguments diff --git a/src/systems/imperative_affect.jl b/src/systems/imperative_affect.jl index 7580580add..aee95fd051 100644 --- a/src/systems/imperative_affect.jl +++ b/src/systems/imperative_affect.jl @@ -216,3 +216,20 @@ function compile_user_affect(affect::ImperativeAffect, cb, sys, dvs, ps; kwargs. end scalarize_affects(affects::ImperativeAffect) = affects + +function vars!(vars, aff::ImperativeAffect; op = Differential) + for var in Iterators.flatten((observed(aff), modified(aff))) + if symbolic_type(var) == NotSymbolic() + if var isa AbstractArray + for v in var + v = unwrap(v) + vars!(vars, v) + end + end + else + var = unwrap(var) + vars!(vars, var) + end + end + return vars +end From 363a30c13bd1e90417cb08a0f690cd4f73a3892f Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 24 Dec 2024 20:27:09 +0530 Subject: [PATCH 0636/1337] fix: fix removal of `missing` defaults for solved array parameters --- src/systems/problem_utils.jl | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/systems/problem_utils.jl b/src/systems/problem_utils.jl index bf3d72e1e5..81be9c3d9e 100644 --- a/src/systems/problem_utils.jl +++ b/src/systems/problem_utils.jl @@ -591,6 +591,10 @@ function maybe_build_initialization_problem( p = unwrap(p) stype = symtype(p) op[p] = get_temporary_value(p) + if iscall(p) && operation(p) === getindex + arrp = arguments(p)[1] + op[arrp] = collect(arrp) + end end if is_time_dependent(sys) From 2f0f894d14bb8e242d55b9c58a7d657e501d0430 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 24 Dec 2024 20:27:28 +0530 Subject: [PATCH 0637/1337] fix: avoid trying to scalarize `missing` default of array parameters --- src/systems/systems.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/src/systems/systems.jl b/src/systems/systems.jl index 04c50bc766..9c8c272c5c 100644 --- a/src/systems/systems.jl +++ b/src/systems/systems.jl @@ -57,6 +57,7 @@ function structural_simplify( ks = collect(keys(defs)) # take copy to avoid mutating defs while iterating. for k in ks if Symbolics.isarraysymbolic(k) && Symbolics.shape(k) !== Symbolics.Unknown() + defs[k] === missing && continue for i in eachindex(k) defs[k[i]] = defs[k][i] end From ec4c32ef304cb1397abf3a6a8807f4b8fb42f452 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 24 Dec 2024 20:27:56 +0530 Subject: [PATCH 0638/1337] fix: fix observed timeseries detection --- src/systems/index_cache.jl | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/systems/index_cache.jl b/src/systems/index_cache.jl index 623623da42..e4ca0b6b48 100644 --- a/src/systems/index_cache.jl +++ b/src/systems/index_cache.jl @@ -299,6 +299,9 @@ function IndexCache(sys::AbstractSystem) for v in vs 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 + push!(timeseries, idx.clock_idx) elseif haskey(observed_syms_to_timeseries, v) union!(timeseries, observed_syms_to_timeseries[v]) elseif haskey(dependent_pars_to_timeseries, v) From 924169b2166beeba7fbc180d8988aa2005277706 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 24 Dec 2024 20:31:26 +0530 Subject: [PATCH 0639/1337] fix: fix conversion of solved array parameters to variables in initialization --- src/systems/nonlinear/initializesystem.jl | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/systems/nonlinear/initializesystem.jl b/src/systems/nonlinear/initializesystem.jl index a7f98d79b1..87be4338b8 100644 --- a/src/systems/nonlinear/initializesystem.jl +++ b/src/systems/nonlinear/initializesystem.jl @@ -193,6 +193,15 @@ function generate_initializesystem(sys::AbstractSystem; append!(eqs_ics, trueobs) end + # even if `p => tovar(p)` is in `paramsubs`, `isparameter(p[1]) === true` after substitution + # so add scalarized versions as well + for k in collect(keys(paramsubs)) + symbolic_type(k) == ArraySymbolic() || continue + for i in eachindex(k) + paramsubs[k[i]] = paramsubs[k][i] + end + end + eqs_ics = Symbolics.substitute.(eqs_ics, (paramsubs,)) if is_time_dependent(sys) vars = [vars; collect(values(paramsubs))] From 00cc0755a90986f4cff4d8e3d41ff4aec4bae979 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 24 Dec 2024 20:32:07 +0530 Subject: [PATCH 0640/1337] fix: make array hack also search callbacks --- src/structural_transformation/symbolics_tearing.jl | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/structural_transformation/symbolics_tearing.jl b/src/structural_transformation/symbolics_tearing.jl index 4382ad6284..173f430c04 100644 --- a/src/structural_transformation/symbolics_tearing.jl +++ b/src/structural_transformation/symbolics_tearing.jl @@ -593,7 +593,7 @@ function tearing_reassemble(state::TearingState, var_eq_matching, @set! sys.unknowns = unknowns obs, subeqs, deps = cse_and_array_hacks( - obs, subeqs, unknowns, neweqs; cse = cse_hack, array = array_hack) + sys, obs, subeqs, unknowns, neweqs; cse = cse_hack, array = array_hack) @set! sys.eqs = neweqs @set! sys.observed = obs @@ -627,7 +627,7 @@ if all `p[i]` are present and the unscalarized form is used in any equation (obs not) we first count the number of times the scalarized form of each observed variable occurs in observed equations (and unknowns if it's split). """ -function cse_and_array_hacks(obs, subeqs, unknowns, neweqs; cse = true, array = true) +function cse_and_array_hacks(sys, obs, subeqs, unknowns, neweqs; cse = true, array = true) # HACK 1 # mapping of rhs to temporary CSE variable # `f(...) => tmpvar` in above example @@ -725,6 +725,11 @@ function cse_and_array_hacks(obs, subeqs, unknowns, neweqs; cse = true, array = for eq in neweqs vars!(all_vars, eq.rhs) end + + # also count unscalarized variables used in callbacks + for ev in Iterators.flatten((continuous_events(sys), discrete_events(sys))) + vars!(all_vars, ev) + end obs_arr_eqs = Equation[] for (arrvar, cnt) in arr_obs_occurrences cnt == length(arrvar) || continue From 4c394c5a577adee3e000dfe4a3d49ec0264ee553 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 24 Dec 2024 20:32:35 +0530 Subject: [PATCH 0641/1337] feat: support building v2 Co-Simulation FMU components --- ext/MTKFMIExt.jl | 238 +++++++++++++++++++++++++++++++++++++---------- 1 file changed, 191 insertions(+), 47 deletions(-) diff --git a/ext/MTKFMIExt.jl b/ext/MTKFMIExt.jl index 8b6f294601..f3ebf6d970 100644 --- a/ext/MTKFMIExt.jl +++ b/ext/MTKFMIExt.jl @@ -1,6 +1,7 @@ module MTKFMIExt using ModelingToolkit +using SymbolicIndexingInterface using ModelingToolkit: t_nounits as t, D_nounits as D import ModelingToolkit as MTK import FMI @@ -17,20 +18,24 @@ macro statuscheck(expr) return quote status = $expr fnname = $fnname - if (status isa Tuple && status[1] == FMI.fmi2True) || - (!(status isa Tuple) && status != FMI.fmi2StatusOK && - status != FMI.fmi2StatusWarning) + if status !== nothing && ((status isa Tuple && status[1] == FMI.fmi2True) || + (!(status isa Tuple) && status != FMI.fmi2StatusOK && + status != FMI.fmi2StatusWarning)) if status != FMI.fmi2StatusFatal FMI.fmi2Terminate(wrapper.instance) end FMI.fmi2FreeInstance!(wrapper.instance) wrapper.instance = nothing - error("FMU Error: status $status") + error("FMU Error in $fnname: status $status") end end |> esc end -function MTK.FMIComponent(::Val{2}, ::Val{:ME}; fmu = nothing, tolerance = 1e-6, name) +function MTK.FMIComponent(::Val{2}; fmu = nothing, tolerance = 1e-6, + communication_step_size = nothing, type, name) + if type == :CS && communication_step_size === nothing + throw(ArgumentError("`communication_step_size` must be specified for Co-Simulation FMUs.")) + end value_references = Dict() defs = Dict() states = [] @@ -40,10 +45,12 @@ function MTK.FMIComponent(::Val{2}, ::Val{:ME}; fmu = nothing, tolerance = 1e-6, value_references, diffvars, states, observed) if isempty(diffvars) __mtk_internal_u = [] - else - @variables __mtk_internal_u(t)[1:length(diffvars)] + elseif type == :ME + @variables __mtk_internal_u(t)[1:length(diffvars)] [guess = diffvars] + push!(observed, __mtk_internal_u ~ copy(diffvars)) + elseif type == :CS + @parameters __mtk_internal_u(t)[1:length(diffvars)]=missing [guess = diffvars] push!(observed, __mtk_internal_u ~ copy(diffvars)) - push!(states, __mtk_internal_u) end inputs = [] @@ -52,7 +59,7 @@ function MTK.FMIComponent(::Val{2}, ::Val{:ME}; fmu = nothing, tolerance = 1e-6, if isempty(inputs) __mtk_internal_x = [] else - @variables __mtk_internal_x(t)[1:length(inputs)] + @variables __mtk_internal_x(t)[1:length(inputs)] [guess = inputs] push!(observed, __mtk_internal_x ~ copy(inputs)) push!(states, __mtk_internal_x) end @@ -60,8 +67,14 @@ function MTK.FMIComponent(::Val{2}, ::Val{:ME}; fmu = nothing, tolerance = 1e-6, outputs = [] fmi_variables_to_mtk_variables!(fmu, FMI.getOutputValueReferencesAndNames(fmu), value_references, outputs, states, observed) - # @variables __mtk_internal_o(t)[1:length(outputs)] - # push!(observed, __mtk_internal_o ~ outputs) + if type == :CS + if isempty(outputs) + __mtk_internal_o = [] + else + @parameters __mtk_internal_o(t)[1:length(outputs)]=missing [guess = zeros(length(outputs))] + push!(observed, __mtk_internal_o ~ outputs) + end + end params = [] parameter_dependencies = Equation[] @@ -82,24 +95,69 @@ function MTK.FMIComponent(::Val{2}, ::Val{:ME}; fmu = nothing, tolerance = 1e-6, output_value_references = UInt32[value_references[var] for var in outputs] buffer_length = length(diffvars) + length(outputs) - _functor = FMI2MEFunctor(zeros(buffer_length), output_value_references) - @parameters (functor::(typeof(_functor)))(..)[1:buffer_length] = _functor - call_expr = functor(wrapper, __mtk_internal_u, __mtk_internal_x, __mtk_internal_p, t) - diffeqs = Equation[] - for (i, var) in enumerate([D.(diffvars); outputs]) - push!(diffeqs, var ~ call_expr[i]) - end + initialization_eqs = Equation[] + + if type == :ME + _functor = FMI2MEFunctor(zeros(buffer_length), output_value_references) + @parameters (functor::(typeof(_functor)))(..)[1:buffer_length] = _functor + call_expr = functor( + wrapper, __mtk_internal_u, __mtk_internal_x, __mtk_internal_p, t) + + diffeqs = Equation[] + for (i, var) in enumerate([D.(diffvars); outputs]) + push!(diffeqs, var ~ call_expr[i]) + end + + finalize_affect = MTK.FunctionalAffect(fmi2Finalize!, [], [wrapper], []) + step_affect = MTK.FunctionalAffect(fmi2MEStep!, [], [wrapper], []) + instance_management_callback = MTK.SymbolicDiscreteCallback( + (t != t - 1), step_affect; finalize = finalize_affect) + + push!(params, wrapper, functor) + push!(states, __mtk_internal_u) + elseif type == :CS + state_value_references = UInt32[value_references[var] for var in diffvars] + state_and_output_value_references = vcat( + state_value_references, output_value_references) + _functor = FMI2CSFunctor(state_and_output_value_references, + state_value_references, output_value_references) + @parameters (functor::(typeof(_functor)))(..)[1:(length(__mtk_internal_u) + length(__mtk_internal_o))] = _functor + for (i, x) in enumerate(collect(__mtk_internal_o)) + push!(initialization_eqs, + x ~ functor( + wrapper, __mtk_internal_u, __mtk_internal_x, __mtk_internal_p, t)[i]) + end - finalize_affect = MTK.FunctionalAffect(fmi2MEFinalize!, [], [wrapper], []) - step_affect = MTK.FunctionalAffect(fmi2MEStep!, [], [wrapper], []) - instance_management_callback = MTK.SymbolicDiscreteCallback( - (t != t - 1), step_affect; finalize = finalize_affect) + diffeqs = Equation[] + + cb_observed = (; inputs = __mtk_internal_x, params = copy(params), + t, wrapper, dt = communication_step_size) + cb_modified = (;) + if symbolic_type(__mtk_internal_o) != NotSymbolic() + cb_modified = (cb_modified..., outputs = __mtk_internal_o) + end + if symbolic_type(__mtk_internal_u) != NotSymbolic() + cb_modified = (cb_modified..., states = __mtk_internal_u) + end + initialize_affect = MTK.ImperativeAffect(fmi2CSInitialize!; observed = cb_observed, + modified = cb_modified, ctx = _functor) + finalize_affect = MTK.FunctionalAffect(fmi2Finalize!, [], [wrapper], []) + step_affect = MTK.ImperativeAffect( + fmi2CSStep!; observed = cb_observed, modified = cb_modified, ctx = _functor) + instance_management_callback = MTK.SymbolicDiscreteCallback( + communication_step_size, step_affect; initialize = initialize_affect, finalize = finalize_affect + ) + + symbolic_type(__mtk_internal_o) == NotSymbolic() || push!(params, __mtk_internal_o) + symbolic_type(__mtk_internal_u) == NotSymbolic() || push!(params, __mtk_internal_u) + + push!(params, wrapper, functor) + end - push!(params, wrapper, functor) eqs = [observed; diffeqs] return ODESystem(eqs, t, states, params; parameter_dependencies, defaults = defs, - discrete_events = [instance_management_callback], name) + discrete_events = [instance_management_callback], name, initialization_eqs) end function fmi_variables_to_mtk_variables!(fmu, varmap, value_references, truevars, allvars, @@ -142,35 +200,42 @@ function FMI2InstanceWrapper(fmu, params, inputs, tolerance) FMI2InstanceWrapper(fmu, params, inputs, tolerance, nothing) end -function get_instance!(wrapper::FMI2InstanceWrapper, states, inputs, params, t) +function get_instance_common!(wrapper::FMI2InstanceWrapper, states, inputs, params, t) + wrapper.instance = FMI.fmi2Instantiate!(wrapper.fmu)::FMI.FMU2Component + if !isempty(params) + @statuscheck FMI.fmi2SetReal(wrapper.instance, wrapper.param_value_references, + Csize_t(length(wrapper.param_value_references)), params) + end + @statuscheck FMI.fmi2SetupExperiment( + wrapper.instance, FMI.fmi2True, wrapper.tolerance, t, FMI.fmi2False, t) + @statuscheck FMI.fmi2EnterInitializationMode(wrapper.instance) + if !isempty(inputs) + @statuscheck FMI.fmi2SetReal(wrapper.instance, wrapper.input_value_references, + Csize_t(length(wrapper.param_value_references)), inputs) + end + + return wrapper.instance +end + +function get_instance_ME!(wrapper::FMI2InstanceWrapper, states, inputs, params, t) if wrapper.instance === nothing - wrapper.instance = FMI.fmi2Instantiate!(wrapper.fmu)::FMI.FMU2Component - if !isempty(params) - @statuscheck FMI.fmi2SetReal(wrapper.instance, wrapper.param_value_references, - Csize_t(length(wrapper.param_value_references)), params) - end - @statuscheck FMI.fmi2SetupExperiment( - wrapper.instance, FMI.fmi2True, wrapper.tolerance, t, FMI.fmi2False, t) - @statuscheck FMI.fmi2EnterInitializationMode(wrapper.instance) - if !isempty(inputs) - @statuscheck FMI.fmi2SetReal(wrapper.instance, wrapper.input_value_references, - Csize_t(length(wrapper.param_value_references)), inputs) - end + get_instance_common!(wrapper, states, inputs, params, t) @statuscheck FMI.fmi2ExitInitializationMode(wrapper.instance) eventInfo = FMI.fmi2NewDiscreteStates(wrapper.instance) @assert eventInfo.newDiscreteStatesNeeded == FMI.fmi2False # TODO: Support FMU events @statuscheck FMI.fmi2EnterContinuousTimeMode(wrapper.instance) end - instance = wrapper.instance - @statuscheck FMI.fmi2SetTime(instance, t) - @statuscheck FMI.fmi2SetContinuousStates(instance, states) - if !isempty(inputs) - @statuscheck FMI.fmi2SetReal(instance, wrapper.input_value_references, - Csize_t(length(wrapper.param_value_references)), inputs) - end - return instance + return wrapper.instance +end + +function get_instance_CS!(wrapper::FMI2InstanceWrapper, states, inputs, params, t) + if wrapper.instance === nothing + get_instance_common!(wrapper, states, inputs, params, t) + @statuscheck FMI.fmi2ExitInitializationMode(wrapper.instance) + end + return wrapper.instance end function complete_step!(wrapper::FMI2InstanceWrapper) @@ -198,8 +263,19 @@ end ndims = 1 end +function update_instance_ME!(wrapper::FMI2InstanceWrapper, states, inputs, t) + instance = wrapper.instance + @statuscheck FMI.fmi2SetTime(instance, t) + @statuscheck FMI.fmi2SetContinuousStates(instance, states) + if !isempty(inputs) + @statuscheck FMI.fmi2SetReal(instance, wrapper.input_value_references, + Csize_t(length(wrapper.param_value_references)), inputs) + end +end + function (fn::FMI2MEFunctor)(wrapper::FMI2InstanceWrapper, states, inputs, params, t) - instance = get_instance!(wrapper, states, inputs, params, t) + instance = get_instance_ME!(wrapper, states, inputs, params, t) + update_instance_ME!(wrapper, states, inputs, t) states_buffer = zeros(length(states)) @statuscheck FMI.fmi2GetDerivatives!(instance, states_buffer) @@ -214,10 +290,78 @@ function fmi2MEStep!(integrator, u, p, ctx) complete_step!(wrapper) end -function fmi2MEFinalize!(integrator, u, p, ctx) +function fmi2Finalize!(integrator, u, p, ctx) wrapper_idx = p[1] wrapper = integrator.ps[wrapper_idx] reset_instance!(wrapper) end +struct FMI2CSFunctor + state_and_output_value_references::Vector{UInt32} + state_value_references::Vector{UInt32} + output_value_references::Vector{UInt32} +end + +function (fn::FMI2CSFunctor)(wrapper::FMI2InstanceWrapper, states, inputs, params, t) + states = states isa SubArray ? copy(states) : states + inputs = inputs isa SubArray ? copy(inputs) : inputs + params = params isa SubArray ? copy(params) : params + instance = get_instance_CS!(wrapper, states, inputs, params, t) + if isempty(fn.output_value_references) + return eltype(states)[] + else + return FMI.fmi2GetReal(instance, fn.output_value_references) + end +end + +@register_array_symbolic (fn::FMI2CSFunctor)( + wrapper::FMI2InstanceWrapper, states::Vector{<:Real}, + inputs::Vector{<:Real}, params::Vector{<:Real}, t::Real) begin + size = (length(states) + length(fn.output_value_references),) + eltype = eltype(states) + ndims = 1 +end + +function fmi2CSInitialize!(m, o, ctx::FMI2CSFunctor, integrator) + states = isdefined(m, :states) ? m.states : () + inputs = o.inputs + params = o.params + t = o.t + wrapper = o.wrapper + if wrapper.instance !== nothing + reset_instance!(wrapper) + end + instance = get_instance_common!(wrapper, states, inputs, params, t) + @statuscheck FMI.fmi2ExitInitializationMode(instance) + if isdefined(m, :states) + @statuscheck FMI.fmi2GetReal!(instance, ctx.state_value_references, m.states) + end + if isdefined(m, :outputs) + @statuscheck FMI.fmi2GetReal!(instance, ctx.output_value_references, m.outputs) + end + + return m +end + +function fmi2CSStep!(m, o, ctx::FMI2CSFunctor, integrator) + wrapper = o.wrapper + states = isdefined(m, :states) ? m.states : () + inputs = o.inputs + params = o.params + t = o.t + dt = o.dt + + instance = get_instance_CS!(wrapper, states, inputs, params, integrator.t) + @statuscheck FMI.fmi2DoStep(instance, integrator.t - dt, dt, FMI.fmi2True) + + if isdefined(m, :states) + @statuscheck FMI.fmi2GetReal!(instance, ctx.state_value_references, m.states) + end + if isdefined(m, :outputs) + @statuscheck FMI.fmi2GetReal!(instance, ctx.output_value_references, m.outputs) + end + + return m +end + end # module From cdb2e1f9fec7ad9603acbbce59c845c3bc1463e7 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Tue, 24 Dec 2024 20:32:44 +0530 Subject: [PATCH 0642/1337] test: test v2 Co-Simulation FMU components --- test/extensions/fmi.jl | 73 ++++++++++++++++++++++++++++++++---------- 1 file changed, 56 insertions(+), 17 deletions(-) diff --git a/test/extensions/fmi.jl b/test/extensions/fmi.jl index 7e5e01f76a..2fc385934f 100644 --- a/test/extensions/fmi.jl +++ b/test/extensions/fmi.jl @@ -3,34 +3,73 @@ using ModelingToolkit: t_nounits as t, D_nounits as D import ModelingToolkit as MTK @testset "Standalone pendulum model" begin + fmu = loadFMU("SpringPendulum1D", "Dymola", "2022x"; type = :ME) + truesol = FMI.simulate( + fmu, (0.0, 8.0); saveat = 0.0:0.1:8.0, recordValues = ["mass.s", "mass.v"]) + @testset "v2, ME" begin fmu = loadFMU("SpringPendulum1D", "Dymola", "2022x"; type = :ME) - @mtkbuild sys = MTK.FMIComponent(Val(2), Val(:ME); fmu) + @mtkbuild sys = MTK.FMIComponent(Val(2); fmu, type = :ME) prob = ODEProblem{true, SciMLBase.FullSpecialize}( sys, [sys.mass__s => 0.5, sys.mass__v => 0.0], (0.0, 8.0)) sol = solve(prob, Tsit5(); reltol = 1e-8, abstol = 1e-8) @test SciMLBase.successful_retcode(sol) - truesol = FMI.simulate(fmu, (0.0, 8.0); saveat = 0.0:0.1:8.0) - @test sol(0.0:0.1:8.0).u≈truesol.states.u atol=1e-4 + @test sol(0.0:0.1:8.0; + idxs = [sys.mass__s, sys.mass__v]).u≈collect.(truesol.values.saveval) atol=1e-4 # repeated solve works @test_nowarn solve(prob, Tsit5()) end + @testset "v2, CS" begin + fmu = loadFMU("SpringPendulum1D", "Dymola", "2022x"; type = :CS) + @named inner = MTK.FMIComponent( + Val(2); fmu, communication_step_size = 0.001, type = :CS) + @variables x(t) = 1.0 + @mtkbuild sys = ODESystem([D(x) ~ x], t; systems = [inner]) + + prob = ODEProblem{true, SciMLBase.FullSpecialize}( + sys, [sys.inner.mass__s => 0.5, sys.inner.mass__v => 0.0], (0.0, 8.0)) + sol = solve(prob, Tsit5(); reltol = 1e-8, abstol = 1e-8) + @test SciMLBase.successful_retcode(sol) + + @test sol(0.0:0.1:8.0; + idxs = [sys.inner.mass__s, sys.inner.mass__v]).u≈collect.(truesol.values.saveval) rtol=1e-2 + end end @testset "IO Model" begin - fmu = loadFMU("./fmus/SimpleAdder.fmu"; type = :ME) - @named adder = MTK.FMIComponent(Val(2), Val(:ME); fmu) - @variables a(t) b(t) c(t) [guess = 1.0] - @mtkbuild sys = ODESystem( - [adder.a ~ a, adder.b ~ b, D(a) ~ t, - D(b) ~ adder.out + adder.c, c^2 ~ adder.out + adder.value], - t; - systems = [adder]) - - # c will be solved for by initialization - # this tests that initialization also works with FMUs - prob = ODEProblem(sys, [sys.adder.c => 1.0, sys.a => 1.0, sys.b => 1.0], (0.0, 1.0)) - sol = solve(prob, Rodas5P(autodiff = false)) - @test SciMLBase.successful_retcode(sol) + @testset "v2, ME" begin + fmu = loadFMU("../../omc-fmus/SimpleAdder.fmu"; type = :ME) + @named adder = MTK.FMIComponent(Val(2); fmu, type = :ME) + @variables a(t) b(t) c(t) [guess = 1.0] + @mtkbuild sys = ODESystem( + [adder.a ~ a, adder.b ~ b, D(a) ~ t, + D(b) ~ adder.out + adder.c, c^2 ~ adder.out + adder.value], + t; + systems = [adder]) + + # c will be solved for by initialization + # this tests that initialization also works with FMUs + prob = ODEProblem(sys, [sys.adder.c => 1.0, sys.a => 1.0, sys.b => 1.0], (0.0, 1.0)) + sol = solve(prob, Rodas5P(autodiff = false)) + @test SciMLBase.successful_retcode(sol) + end + @testset "v2, CS" begin + fmu = loadFMU("../../omc-fmus/SimpleAdder.fmu"; type = :CS) + @named adder = MTK.FMIComponent( + Val(2); fmu, type = :CS, communication_step_size = 0.001) + @variables a(t) b(t) c(t) [guess = 1.0] + @mtkbuild sys = ODESystem( + [adder.a ~ a, adder.b ~ b, D(a) ~ t, + D(b) ~ adder.out + adder.c, c^2 ~ adder.out + adder.value], + t; + systems = [adder]) + + # c will be solved for by initialization + # this tests that initialization also works with FMUs + prob = ODEProblem(sys, [sys.adder.c => 1.0, sys.a => 1.0, sys.b => 1.0], + (0.0, 1.0); use_scc = false) + sol = solve(prob, Rodas5P(autodiff = false)) + @test SciMLBase.successful_retcode(sol) + end end From 88e99b15ec40f6d96eb61ed4fedaee7db9b50f67 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 2 Jan 2025 17:19:16 +0530 Subject: [PATCH 0643/1337] feat: support v3 ME FMUs --- ext/MTKFMIExt.jl | 154 ++++++++++++++++++++++++++++++++++++----- test/extensions/fmi.jl | 17 +++++ 2 files changed, 153 insertions(+), 18 deletions(-) diff --git a/ext/MTKFMIExt.jl b/ext/MTKFMIExt.jl index f3ebf6d970..45412af957 100644 --- a/ext/MTKFMIExt.jl +++ b/ext/MTKFMIExt.jl @@ -14,25 +14,46 @@ macro statuscheck(expr) fnname = fn.args[2] instance = expr.args[2] - + is_v2 = startswith("fmi2", string(fnname)) + + fmiTrue = is_v2 ? FMI.fmi2True : FMI.fmi3True + fmiStatusOK = is_v2 ? FMI.fmi2StatusOK : FMI.fmi3StatusOK + fmiStatusWarning = is_v2 ? FMI.fmi2StatusWarning : FMI.fmi3StatusWarning + fmiStatusFatal = is_v2 ? FMI.fmi2StatusFatal : FMI.fmi3StatusFatal + fmiTerminate = is_v2 ? FMI.fmi2Terminate : FMI.fmi3Terminate + fmiFreeInstance! = is_v2 ? FMI.fmi2FreeInstance! : FMI.fmi3FreeInstance! return quote status = $expr fnname = $fnname - if status !== nothing && ((status isa Tuple && status[1] == FMI.fmi2True) || - (!(status isa Tuple) && status != FMI.fmi2StatusOK && - status != FMI.fmi2StatusWarning)) - if status != FMI.fmi2StatusFatal - FMI.fmi2Terminate(wrapper.instance) + if status !== nothing && ((status isa Tuple && status[1] == $fmiTrue) || + (!(status isa Tuple) && status != $fmiStatusOK && + status != $fmiStatusWarning)) + if status != $fmiStatusFatal + $fmiTerminate(wrapper.instance) end - FMI.fmi2FreeInstance!(wrapper.instance) + $fmiFreeInstance!(wrapper.instance) wrapper.instance = nothing error("FMU Error in $fnname: status $status") end end |> esc end -function MTK.FMIComponent(::Val{2}; fmu = nothing, tolerance = 1e-6, - communication_step_size = nothing, type, name) +@static if !hasmethod(FMI.getValueReferencesAndNames, Tuple{FMI.fmi3ModelDescription}) + function FMI.getValueReferencesAndNames( + md::FMI.fmi3ModelDescription; vrs = md.valueReferences) + dict = Dict{FMI.fmi3ValueReference, Array{String}}() + for vr in vrs + dict[vr] = FMI.valueReferenceToString(md, vr) + end + return dict + end +end + +function MTK.FMIComponent(::Val{Ver}; fmu = nothing, tolerance = 1e-6, + communication_step_size = nothing, type, name) where {Ver} + if Ver != 2 && Ver != 3 + throw(ArgumentError("FMI Version must be `2` or `3`")) + end if type == :CS && communication_step_size === nothing throw(ArgumentError("`communication_step_size` must be specified for Co-Simulation FMUs.")) end @@ -90,8 +111,14 @@ function MTK.FMIComponent(::Val{2}; fmu = nothing, tolerance = 1e-6, input_value_references = UInt32[value_references[var] for var in inputs] param_value_references = UInt32[value_references[var] for var in params] - @parameters wrapper::FMI2InstanceWrapper = FMI2InstanceWrapper( - fmu, param_value_references, input_value_references, tolerance) + + if Ver == 2 + @parameters wrapper::FMI2InstanceWrapper = FMI2InstanceWrapper( + fmu, param_value_references, input_value_references, tolerance) + else + @parameters wrapper::FMI3InstanceWrapper = FMI3InstanceWrapper( + fmu, param_value_references, input_value_references) + end output_value_references = UInt32[value_references[var] for var in outputs] buffer_length = length(diffvars) + length(outputs) @@ -99,7 +126,8 @@ function MTK.FMIComponent(::Val{2}; fmu = nothing, tolerance = 1e-6, initialization_eqs = Equation[] if type == :ME - _functor = FMI2MEFunctor(zeros(buffer_length), output_value_references) + FunctorT = Ver == 2 ? FMI2MEFunctor : FMI3MEFunctor + _functor = FunctorT(zeros(buffer_length), output_value_references) @parameters (functor::(typeof(_functor)))(..)[1:buffer_length] = _functor call_expr = functor( wrapper, __mtk_internal_u, __mtk_internal_x, __mtk_internal_p, t) @@ -109,14 +137,14 @@ function MTK.FMIComponent(::Val{2}; fmu = nothing, tolerance = 1e-6, push!(diffeqs, var ~ call_expr[i]) end - finalize_affect = MTK.FunctionalAffect(fmi2Finalize!, [], [wrapper], []) - step_affect = MTK.FunctionalAffect(fmi2MEStep!, [], [wrapper], []) + finalize_affect = MTK.FunctionalAffect(fmiFinalize!, [], [wrapper], []) + step_affect = MTK.FunctionalAffect(fmiMEStep!, [], [wrapper], []) instance_management_callback = MTK.SymbolicDiscreteCallback( (t != t - 1), step_affect; finalize = finalize_affect) push!(params, wrapper, functor) push!(states, __mtk_internal_u) - elseif type == :CS + elseif type == :CS && Ver == 2 state_value_references = UInt32[value_references[var] for var in diffvars] state_and_output_value_references = vcat( state_value_references, output_value_references) @@ -142,7 +170,7 @@ function MTK.FMIComponent(::Val{2}; fmu = nothing, tolerance = 1e-6, end initialize_affect = MTK.ImperativeAffect(fmi2CSInitialize!; observed = cb_observed, modified = cb_modified, ctx = _functor) - finalize_affect = MTK.FunctionalAffect(fmi2Finalize!, [], [wrapper], []) + finalize_affect = MTK.FunctionalAffect(fmiFinalize!, [], [wrapper], []) step_affect = MTK.ImperativeAffect( fmi2CSStep!; observed = cb_observed, modified = cb_modified, ctx = _functor) instance_management_callback = MTK.SymbolicDiscreteCallback( @@ -250,6 +278,63 @@ function reset_instance!(wrapper::FMI2InstanceWrapper) wrapper.instance = nothing end +mutable struct FMI3InstanceWrapper + const fmu::FMI.FMU3 + const param_value_references::Vector{UInt32} + const input_value_references::Vector{UInt32} + instance::Union{FMI.FMU3Instance{FMI.FMU3}, Nothing} +end + +function FMI3InstanceWrapper(fmu, params, inputs) + FMI3InstanceWrapper(fmu, params, inputs, nothing) +end + +function get_instance_common!(wrapper::FMI3InstanceWrapper, states, inputs, params, t) + if !isempty(params) + @statuscheck FMI.fmi3SetFloat64(wrapper.instance, wrapper.param_value_references, + params) + end + @statuscheck FMI.fmi3EnterInitializationMode( + wrapper.instance, FMI.fmi3False, zero(FMI.fmi3Float64), t, FMI.fmi3False, t) + if !isempty(inputs) + @statuscheck FMI.fmi3SetFloat64( + wrapper.instance, wrapper.input_value_references, inputs) + end + + return wrapper.instance +end + +function get_instance_ME!(wrapper::FMI3InstanceWrapper, states, inputs, params, t) + if wrapper.instance === nothing + wrapper.instance = FMI.fmi3InstantiateModelExchange!(wrapper.fmu)::FMI.FMU3Instance + get_instance_common!(wrapper, states, inputs, params, t) + @statuscheck FMI.fmi3ExitInitializationMode(wrapper.instance) + eventInfo = FMI.fmi3UpdateDiscreteStates(wrapper.instance) + @assert eventInfo[1] == FMI.fmi2False + # TODO: Support FMU events + @statuscheck FMI.fmi3EnterContinuousTimeMode(wrapper.instance) + end + + return wrapper.instance +end + +function complete_step!(wrapper::FMI3InstanceWrapper) + wrapper.instance === nothing && return + enterEventMode = Ref(FMI.fmi3False) + terminateSimulation = Ref(FMI.fmi3False) + @statuscheck FMI.fmi3CompletedIntegratorStep!( + wrapper.instance, FMI.fmi3True, enterEventMode, terminateSimulation) + @assert enterEventMode[] == FMI.fmi3False + @assert terminateSimulation[] == FMI.fmi3False +end + +function reset_instance!(wrapper::FMI3InstanceWrapper) + wrapper.instance === nothing && return + FMI.fmi3Terminate(wrapper.instance) + FMI.fmi3FreeInstance!(wrapper.instance) + wrapper.instance = nothing +end + struct FMI2MEFunctor{T} return_buffer::Vector{T} output_value_references::Vector{UInt32} @@ -284,13 +369,46 @@ function (fn::FMI2MEFunctor)(wrapper::FMI2InstanceWrapper, states, inputs, param return [states_buffer; outputs_buffer] end -function fmi2MEStep!(integrator, u, p, ctx) +struct FMI3MEFunctor{T} + return_buffer::Vector{T} + output_value_references::Vector{UInt32} +end + +@register_array_symbolic (fn::FMI3MEFunctor)( + wrapper::FMI3InstanceWrapper, states::Vector{<:Real}, + inputs::Vector{<:Real}, params::Vector{<:Real}, t::Real) begin + size = (length(states) + length(fn.output_value_references),) + eltype = eltype(states) + ndims = 1 +end + +function update_instance_ME!(wrapper::FMI3InstanceWrapper, states, inputs, t) + instance = wrapper.instance + @statuscheck FMI.fmi3SetTime(instance, t) + @statuscheck FMI.fmi3SetContinuousStates(instance, states) + if !isempty(inputs) + @statuscheck FMI.fmi3SetFloat64(instance, wrapper.input_value_references, inputs) + end +end + +function (fn::FMI3MEFunctor)(wrapper::FMI3InstanceWrapper, states, inputs, params, t) + instance = get_instance_ME!(wrapper, states, inputs, params, t) + update_instance_ME!(wrapper, states, inputs, t) + + states_buffer = zeros(length(states)) + @statuscheck FMI.fmi3GetContinuousStateDerivatives!(instance, states_buffer) + outputs_buffer = zeros(length(fn.output_value_references)) + FMI.fmi3GetFloat64!(instance, fn.output_value_references, outputs_buffer) + return [states_buffer; outputs_buffer] +end + +function fmiMEStep!(integrator, u, p, ctx) wrapper_idx = p[1] wrapper = integrator.ps[wrapper_idx] complete_step!(wrapper) end -function fmi2Finalize!(integrator, u, p, ctx) +function fmiFinalize!(integrator, u, p, ctx) wrapper_idx = p[1] wrapper = integrator.ps[wrapper_idx] reset_instance!(wrapper) diff --git a/test/extensions/fmi.jl b/test/extensions/fmi.jl index 2fc385934f..374d340e80 100644 --- a/test/extensions/fmi.jl +++ b/test/extensions/fmi.jl @@ -35,6 +35,23 @@ import ModelingToolkit as MTK @test sol(0.0:0.1:8.0; idxs = [sys.inner.mass__s, sys.inner.mass__v]).u≈collect.(truesol.values.saveval) rtol=1e-2 end + + fmu = loadFMU("SpringPendulum1D", "Dymola", "2023x", "3.0"; type = :ME) + truesol = FMI.simulate( + fmu, (0.0, 8.0); saveat = 0.0:0.1:8.0, recordValues = ["mass.s", "mass.v"]) + @testset "v3, ME" begin + fmu = loadFMU("SpringPendulum1D", "Dymola", "2023x", "3.0"; type = :ME) + @mtkbuild sys = MTK.FMIComponent(Val(3); fmu, type = :ME) + prob = ODEProblem{true, SciMLBase.FullSpecialize}( + sys, [sys.mass__s => 0.5, sys.mass__v => 0.0], (0.0, 8.0)) + sol = solve(prob, Tsit5(); reltol = 1e-8, abstol = 1e-8) + @test SciMLBase.successful_retcode(sol) + + @test sol(0.0:0.1:8.0; + idxs = [sys.mass__s, sys.mass__v]).u≈collect.(truesol.values.saveval) atol=1e-4 + # repeated solve works + @test_nowarn solve(prob, Tsit5()) + end end @testset "IO Model" begin From 5e3e75d6c89059112dac3bc0d063567161c3cbd9 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Thu, 2 Jan 2025 18:27:45 +0530 Subject: [PATCH 0644/1337] feat: support v3 CS FMUs --- ext/MTKFMIExt.jl | 104 ++++++++++++++++++++++++++++++++++++++--- test/extensions/fmi.jl | 15 ++++++ 2 files changed, 112 insertions(+), 7 deletions(-) diff --git a/ext/MTKFMIExt.jl b/ext/MTKFMIExt.jl index 45412af957..15ef376769 100644 --- a/ext/MTKFMIExt.jl +++ b/ext/MTKFMIExt.jl @@ -144,12 +144,16 @@ function MTK.FMIComponent(::Val{Ver}; fmu = nothing, tolerance = 1e-6, push!(params, wrapper, functor) push!(states, __mtk_internal_u) - elseif type == :CS && Ver == 2 + elseif type == :CS state_value_references = UInt32[value_references[var] for var in diffvars] state_and_output_value_references = vcat( state_value_references, output_value_references) - _functor = FMI2CSFunctor(state_and_output_value_references, - state_value_references, output_value_references) + _functor = if Ver == 2 + FMI2CSFunctor(state_and_output_value_references, + state_value_references, output_value_references) + else + FMI3CSFunctor(state_value_references, output_value_references) + end @parameters (functor::(typeof(_functor)))(..)[1:(length(__mtk_internal_u) + length(__mtk_internal_o))] = _functor for (i, x) in enumerate(collect(__mtk_internal_o)) push!(initialization_eqs, @@ -168,11 +172,11 @@ function MTK.FMIComponent(::Val{Ver}; fmu = nothing, tolerance = 1e-6, if symbolic_type(__mtk_internal_u) != NotSymbolic() cb_modified = (cb_modified..., states = __mtk_internal_u) end - initialize_affect = MTK.ImperativeAffect(fmi2CSInitialize!; observed = cb_observed, + initialize_affect = MTK.ImperativeAffect(fmiCSInitialize!; observed = cb_observed, modified = cb_modified, ctx = _functor) finalize_affect = MTK.FunctionalAffect(fmiFinalize!, [], [wrapper], []) step_affect = MTK.ImperativeAffect( - fmi2CSStep!; observed = cb_observed, modified = cb_modified, ctx = _functor) + 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 ) @@ -318,6 +322,16 @@ function get_instance_ME!(wrapper::FMI3InstanceWrapper, states, inputs, params, return wrapper.instance end +function get_instance_CS!(wrapper::FMI3InstanceWrapper, states, inputs, params, t) + if wrapper.instance === nothing + wrapper.instance = FMI.fmi3InstantiateCoSimulation!( + wrapper.fmu; eventModeUsed = false)::FMI.FMU3Instance + get_instance_common!(wrapper, states, inputs, params, t) + @statuscheck FMI.fmi3ExitInitializationMode(wrapper.instance) + end + return wrapper.instance +end + function complete_step!(wrapper::FMI3InstanceWrapper) wrapper.instance === nothing && return enterEventMode = Ref(FMI.fmi3False) @@ -440,7 +454,7 @@ end ndims = 1 end -function fmi2CSInitialize!(m, o, ctx::FMI2CSFunctor, integrator) +function fmiCSInitialize!(m, o, ctx::FMI2CSFunctor, integrator) states = isdefined(m, :states) ? m.states : () inputs = o.inputs params = o.params @@ -449,6 +463,7 @@ function fmi2CSInitialize!(m, o, ctx::FMI2CSFunctor, integrator) if wrapper.instance !== nothing reset_instance!(wrapper) end + instance = get_instance_common!(wrapper, states, inputs, params, t) @statuscheck FMI.fmi2ExitInitializationMode(instance) if isdefined(m, :states) @@ -461,7 +476,7 @@ function fmi2CSInitialize!(m, o, ctx::FMI2CSFunctor, integrator) return m end -function fmi2CSStep!(m, o, ctx::FMI2CSFunctor, integrator) +function fmiCSStep!(m, o, ctx::FMI2CSFunctor, integrator) wrapper = o.wrapper states = isdefined(m, :states) ? m.states : () inputs = o.inputs @@ -482,4 +497,79 @@ function fmi2CSStep!(m, o, ctx::FMI2CSFunctor, integrator) return m end +struct FMI3CSFunctor + state_value_references::Vector{UInt32} + output_value_references::Vector{UInt32} +end + +function (fn::FMI3CSFunctor)(wrapper::FMI3InstanceWrapper, states, inputs, params, t) + states = states isa SubArray ? copy(states) : states + inputs = inputs isa SubArray ? copy(inputs) : inputs + params = params isa SubArray ? copy(params) : params + instance = get_instance_CS!(wrapper, states, inputs, params, t) + if isempty(fn.output_value_references) + return eltype(states)[] + else + return FMI.fmi3GetFloat64(instance, fn.output_value_references) + end +end + +@register_array_symbolic (fn::FMI3CSFunctor)( + wrapper::FMI3InstanceWrapper, states::Vector{<:Real}, + inputs::Vector{<:Real}, params::Vector{<:Real}, t::Real) begin + size = (length(states) + length(fn.output_value_references),) + eltype = eltype(states) + ndims = 1 +end + +function fmiCSInitialize!(m, o, ctx::FMI3CSFunctor, integrator) + states = isdefined(m, :states) ? m.states : () + inputs = o.inputs + params = o.params + t = o.t + wrapper = o.wrapper + if wrapper.instance !== nothing + reset_instance!(wrapper) + end + instance = get_instance_CS!(wrapper, states, inputs, params, t) + if isdefined(m, :states) + @statuscheck FMI.fmi3GetFloat64!(instance, ctx.state_value_references, m.states) + end + if isdefined(m, :outputs) + @statuscheck FMI.fmi3GetFloat64!(instance, ctx.output_value_references, m.outputs) + end + + return m +end + +function fmiCSStep!(m, o, ctx::FMI3CSFunctor, integrator) + wrapper = o.wrapper + states = isdefined(m, :states) ? m.states : () + inputs = o.inputs + params = o.params + t = o.t + dt = o.dt + + instance = get_instance_CS!(wrapper, states, inputs, params, integrator.t) + eventEncountered = Ref(FMI.fmi3False) + terminateSimulation = Ref(FMI.fmi3False) + earlyReturn = Ref(FMI.fmi3False) + lastSuccessfulTime = Ref(zero(FMI.fmi3Float64)) + @statuscheck FMI.fmi3DoStep!( + instance, integrator.t - dt, dt, FMI.fmi3True, eventEncountered, + terminateSimulation, earlyReturn, lastSuccessfulTime) + @assert eventEncountered[] == FMI.fmi3False + @assert terminateSimulation[] == FMI.fmi3False + @assert earlyReturn[] == FMI.fmi3False + + if isdefined(m, :states) + @statuscheck FMI.fmi3GetFloat64!(instance, ctx.state_value_references, m.states) + end + if isdefined(m, :outputs) + @statuscheck FMI.fmi3GetFloat64!(instance, ctx.output_value_references, m.outputs) + end + + return m +end + end # module diff --git a/test/extensions/fmi.jl b/test/extensions/fmi.jl index 374d340e80..743b65cae6 100644 --- a/test/extensions/fmi.jl +++ b/test/extensions/fmi.jl @@ -52,6 +52,21 @@ import ModelingToolkit as MTK # repeated solve works @test_nowarn solve(prob, Tsit5()) end + @testset "v3, CS" begin + fmu = loadFMU("SpringPendulum1D", "Dymola", "2023x", "3.0"; type = :CS) + @named inner = MTK.FMIComponent( + Val(3); fmu, communication_step_size = 0.001, type = :CS) + @variables x(t) = 1.0 + @mtkbuild sys = ODESystem([D(x) ~ x], t; systems = [inner]) + + prob = ODEProblem{true, SciMLBase.FullSpecialize}( + sys, [sys.inner.mass__s => 0.5, sys.inner.mass__v => 0.0], (0.0, 8.0)) + sol = solve(prob, Tsit5(); reltol = 1e-8, abstol = 1e-8) + @test SciMLBase.successful_retcode(sol) + + @test sol(0.0:0.1:8.0; + idxs = [sys.inner.mass__s, sys.inner.mass__v]).u≈collect.(truesol.values.saveval) rtol=1e-2 + end end @testset "IO Model" begin From 8b066e41de9618560756bf728e348fd5d4f85710 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 3 Jan 2025 14:05:22 +0530 Subject: [PATCH 0645/1337] fix: use `NoInit` for FMI callback reinitialization --- ext/MTKFMIExt.jl | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/ext/MTKFMIExt.jl b/ext/MTKFMIExt.jl index 15ef376769..e0d59c4032 100644 --- a/ext/MTKFMIExt.jl +++ b/ext/MTKFMIExt.jl @@ -4,6 +4,7 @@ using ModelingToolkit using SymbolicIndexingInterface using ModelingToolkit: t_nounits as t, D_nounits as D import ModelingToolkit as MTK +import SciMLBase import FMI macro statuscheck(expr) @@ -140,7 +141,7 @@ function MTK.FMIComponent(::Val{Ver}; fmu = nothing, tolerance = 1e-6, finalize_affect = MTK.FunctionalAffect(fmiFinalize!, [], [wrapper], []) step_affect = MTK.FunctionalAffect(fmiMEStep!, [], [wrapper], []) instance_management_callback = MTK.SymbolicDiscreteCallback( - (t != t - 1), step_affect; finalize = finalize_affect) + (t != t - 1), step_affect; finalize = finalize_affect, reinitializealg = SciMLBase.NoInit()) push!(params, wrapper, functor) push!(states, __mtk_internal_u) @@ -178,7 +179,8 @@ function MTK.FMIComponent(::Val{Ver}; fmu = nothing, tolerance = 1e-6, step_affect = MTK.ImperativeAffect( 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 + communication_step_size, step_affect; initialize = initialize_affect, + finalize = finalize_affect, reinitializealg = SciMLBase.NoInit() ) symbolic_type(__mtk_internal_o) == NotSymbolic() || push!(params, __mtk_internal_o) From 2676553d058e4f8075bb830ddafd934dd8f08488 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 3 Jan 2025 14:05:43 +0530 Subject: [PATCH 0646/1337] fix: handle array of symbolics in `InitializationProblem` type promotion --- src/systems/diffeqs/abstractodesystem.jl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/systems/diffeqs/abstractodesystem.jl b/src/systems/diffeqs/abstractodesystem.jl index ac28d41dd4..921825dac8 100644 --- a/src/systems/diffeqs/abstractodesystem.jl +++ b/src/systems/diffeqs/abstractodesystem.jl @@ -1393,11 +1393,13 @@ function InitializationProblem{iip, specialize}(sys::AbstractSystem, for sym in unknowns(isys) haskey(fullmap, sym) || continue symbolic_type(fullmap[sym]) == NotSymbolic() || continue + is_array_of_symbolics(fullmap[sym]) && continue u0T = promote_type(u0T, typeof(fullmap[sym])) end for eq in observed(isys) haskey(fullmap, eq.lhs) || continue symbolic_type(fullmap[eq.lhs]) == NotSymbolic() || continue + is_array_of_symbolics(fullmap[eq.lhs]) && continue u0T = promote_type(u0T, typeof(fullmap[eq.lhs])) end if u0T != Union{} From 6dacd6e64d73f91a0c83b4ef56e10e5b87140535 Mon Sep 17 00:00:00 2001 From: Aayush Sabharwal Date: Fri, 3 Jan 2025 14:06:00 +0530 Subject: [PATCH 0647/1337] test: test v3 FMUs as subcomponents --- test/extensions/fmi.jl | 35 ++++++++++++++++++++++++++-- test/extensions/fmus/StateSpace.fmu | Bin 0 -> 34391 bytes 2 files changed, 33 insertions(+), 2 deletions(-) create mode 100644 test/extensions/fmus/StateSpace.fmu diff --git a/test/extensions/fmi.jl b/test/extensions/fmi.jl index 743b65cae6..868d60a9bb 100644 --- a/test/extensions/fmi.jl +++ b/test/extensions/fmi.jl @@ -2,6 +2,8 @@ using ModelingToolkit, FMI, FMIZoo, OrdinaryDiffEq using ModelingToolkit: t_nounits as t, D_nounits as D import ModelingToolkit as MTK +const FMU_DIR = joinpath(@__DIR__, "fmus") + @testset "Standalone pendulum model" begin fmu = loadFMU("SpringPendulum1D", "Dymola", "2022x"; type = :ME) truesol = FMI.simulate( @@ -71,7 +73,7 @@ end @testset "IO Model" begin @testset "v2, ME" begin - fmu = loadFMU("../../omc-fmus/SimpleAdder.fmu"; type = :ME) + fmu = loadFMU(joinpath(FMU_DIR, "SimpleAdder.fmu"); type = :ME) @named adder = MTK.FMIComponent(Val(2); fmu, type = :ME) @variables a(t) b(t) c(t) [guess = 1.0] @mtkbuild sys = ODESystem( @@ -87,7 +89,7 @@ end @test SciMLBase.successful_retcode(sol) end @testset "v2, CS" begin - fmu = loadFMU("../../omc-fmus/SimpleAdder.fmu"; type = :CS) + fmu = loadFMU(joinpath(FMU_DIR, "SimpleAdder.fmu"); type = :CS) @named adder = MTK.FMIComponent( Val(2); fmu, type = :CS, communication_step_size = 0.001) @variables a(t) b(t) c(t) [guess = 1.0] @@ -104,4 +106,33 @@ end sol = solve(prob, Rodas5P(autodiff = false)) @test SciMLBase.successful_retcode(sol) end + + @testset "v3, ME" begin + fmu = loadFMU(joinpath(FMU_DIR, "StateSpace.fmu"); type = :ME) + @named sspace = MTK.FMIComponent(Val(3); fmu, type = :ME) + @variables u(t)=1.0 x(t)=1.0 y(t) [guess = 1.0] + @mtkbuild sys = ODESystem( + [sspace.u ~ u, D(u) ~ t, D(x) ~ sspace.x + sspace.y, y^2 ~ sspace.y + x], t; + systems = [sspace] + ) + + prob = ODEProblem(sys, [sys.sspace.x => 1.0], (0.0, 1.0); use_scc = false) + sol = solve(prob, Rodas5P(autodiff = false)) + @test SciMLBase.successful_retcode(sol) + 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-3, type = :CS) + @variables u(t)=1.0 x(t)=1.0 y(t) [guess = 1.0] + @mtkbuild sys = ODESystem( + [sspace.u ~ u, D(u) ~ t, D(x) ~ sspace.x + sspace.y, y^2 ~ sspace.y + x], t; + systems = [sspace] + ) + + prob = ODEProblem(sys, [sys.sspace.x => 1.0], (0.0, 1.0); use_scc = false) + sol = solve(prob, Rodas5P(autodiff = false)) + @test SciMLBase.successful_retcode(sol) + end end diff --git a/test/extensions/fmus/StateSpace.fmu b/test/extensions/fmus/StateSpace.fmu new file mode 100644 index 0000000000000000000000000000000000000000..c181081ca4222af0c306205c1d721c06905dd3a1 GIT binary patch literal 34391 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