Skip to content

fix: fix incorrect namespacing of DelayParentScope variables in collect_scoped_vars! #3528

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Apr 8, 2025
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
76 changes: 76 additions & 0 deletions src/systems/abstractsystem.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -1114,9 +1115,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
Expand All @@ -1130,9 +1147,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
Expand All @@ -1148,11 +1181,34 @@ 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)
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)
Expand All @@ -1166,9 +1222,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
Expand Down
15 changes: 6 additions & 9 deletions src/utils.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions test/jumpsystem.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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])
Expand Down
8 changes: 4 additions & 4 deletions test/variable_scope.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand Down
Loading