Delayed signals between ModelingToolkit components
The fix
mtkcompile alias-eliminates sth₊x into an observed equation, and MTK only lowers a
delay to a history lookup when the delayed variable is an unknown of the compiled system.
So sth.x ~ lv.x can't work. Don't create the alias.
Three changes. mtkcompile, SDDEProblem and solve(prob, ImplicitEM()) stay as posted.
(1) The component that owns the signal declares it callable. x(..) rather than
x(t), applied as x(t) in its own equations. This is the part that keeps lv₊x an
unknown.
@component function StochasticLV(;name)
- @variables x(t) = 0.9 y(t) = 0.9
+ @variables x(..) y(t) = 0.9
...
eqs = [
- D(x) ~ α * x - β * x * y + η
- D(y) ~ -γ * y + δ * x * y
+ D(x(t)) ~ α * x(t) - β * x(t) * y + η
+ D(y) ~ -γ * y + δ * x(t) * y
]
- System(eqs, t; name)
+ System(eqs, t; name, initial_conditions = [x(t) => 0.9])
end
x(..) can't carry an inline = 0.9, so the initial condition moves to
initial_conditions.
(2) The consumer takes the signal as an argument, and the connection equation goes away.
-@component function SomethingElse(;name, τ)
- @variables z(t) = 0 x(..)
+@component function SomethingElse(;name, τ, u)
+ @variables z(t) = 0
@parameters θ = 0.1 τ = τ
@brownians ξ
eqs = [
- D(z) ~ -θ * x(t - τ) + ξ
+ D(z) ~ -θ * u(t - τ) + ξ
]
System(eqs, t; name)
end
+callable(v) = operation(ModelingToolkit.unwrap(v))
+
@named lv = StochasticLV()
-@named sth = SomethingElse(τ = 3)
+@named sth = SomethingElse(τ = 3, u = callable(lv.x))
-connection_eqs = [
- sth.x ~ lv.x
-]
-@named connected_model = System(connection_eqs, t, [], []; systems = [lv, sth])
+@named connected_model = System(Equation[], t, [], []; systems = [lv, sth])
lv.x auto-applies at t and hands back lv₊x(t), so callable is there to recover the
bare operator, which is the thing you can call at t - τ.
(3) Add using DelayDiffEq. Nothing to do with the modelling bug. using DifferentialEquations doesn't load DelayDiffEq, so as posted there is no SDDE integrator
available and even the fixed model dies at the solve line with
SciMLBase.NoDefaultAlgorithmError(). Don't substitute StochasticDelayDiffEq; it can no
longer co-resolve with the current stack (last section).
using ModelingToolkit
using DifferentialEquations
using ModelingToolkit: t_nounits as t, D_nounits as D
using StochasticDiffEq
+using DelayDiffEq
The original
The MRE from issue #4985, filed by bebrunn on 2026-08-17 and still open:
using ModelingToolkit
using DifferentialEquations
using ModelingToolkit: t_nounits as t, D_nounits as D
using StochasticDiffEq
@component function StochasticLV(;name)
@variables x(t) = 0.9 y(t) = 0.9
@parameters begin
α = 2/3
β = 4/3
γ = 1
δ = 1
end
@brownians η
eqs = [
D(x) ~ α * x - β * x * y + η
D(y) ~ -γ * y + δ * x * y
]
System(eqs, t; name)
end
@component function SomethingElse(;name, τ)
@variables z(t) = 0 x(..)
@parameters θ = 0.1 τ = τ
@brownians ξ
eqs = [
D(z) ~ -θ * x(t - τ) + ξ
]
System(eqs, t; name)
end
@named lv = StochasticLV()
@named sth = SomethingElse(τ = 3)
connection_eqs = [
sth.x ~ lv.x
]
@named connected_model = System(connection_eqs, t, [], []; systems = [lv, sth])
connected = mtkcompile(connected_model)
prob = SDDEProblem(connected, [], (0.0, 1.0))
sol = solve(prob, ImplicitEM())
It fails at the SDDEProblem line:
ArgumentError: sth₊x(-sth₊τ + t) is present in the system but
sth₊x(-sth₊τ + t) is not an unknown.
The intent reads fine. SomethingElse declares a callable input x(..), and the
connection equation binds it to the prey population. It just doesn't survive mtkcompile.
The issue also notes, correctly, that "calling mtkcompile() on the connected_model removes the sth₊x(t) unknown". That is the whole bug, and the cells below confirm it.
Everything here was re-run against the versions in the issue's manifest: ModelingToolkit 11.39.0, ModelingToolkitBase 1.65.0, ModelingToolkitTearing 1.20.5, StochasticDiffEq 7.1.4, StochasticDiffEqCore 2.0.6, DelayDiffEq 6.1.2, DifferentialEquations 8.0.3. The reporter is on MTK 11.38.0 / MTKBase 1.60.0 / SDECore 2.0.5, and behaviour is identical.
using ModelingToolkit
using DifferentialEquations
using ModelingToolkit: t_nounits as t, D_nounits as D
using StochasticDiffEq
using DelayDiffEq # (3) required: `using DifferentialEquations` does not load it,
# and SDDE integration lives here, not StochasticDelayDiffEq
import Pkg
VERSIONS = let names = ("ModelingToolkit", "ModelingToolkitBase", "ModelingToolkitTearing",
"StochasticDiffEq", "StochasticDiffEqCore", "DelayDiffEq",
"StochasticDelayDiffEq")
deps = filter(p -> p.second.name in names, Pkg.dependencies())
sort([p.second.name => string(p.second.version) for p in deps])
end6-element Vector{Pair{String, String}}:
"DelayDiffEq" => "6.1.2"
"ModelingToolkit" => "11.39.0"
"ModelingToolkitBase" => "1.65.0"
"ModelingToolkitTearing" => "1.20.5"
"StochasticDiffEq" => "7.1.4"
"StochasticDiffEqCore" => "2.0.6"The fixed version, running
Same physics, same parameters, same names. Only the three changes above. Changed lines are
marked # (1), # (2), # (3).
# (2) `lv.x` auto-applies at `t` and gives back `lv₊x(t)`. This recovers the bare operator
# so it can be called at `t - τ`.
callable(v) = operation(ModelingToolkit.unwrap(v))
@component function StochasticLV(; name)
@variables x(..) y(t) = 0.9 # (1) x(..) not x(t) = 0.9
@parameters begin
α = 2 / 3
β = 4 / 3
γ = 1
δ = 1
end
@brownians η
eqs = [
D(x(t)) ~ α * x(t) - β * x(t) * y + η # (1) x -> x(t)
D(y) ~ -γ * y + δ * x(t) * y # (1) x -> x(t)
]
System(eqs, t; name, initial_conditions = [x(t) => 0.9]) # (1) default moves here
end
@component function SomethingElse(; name, τ, u) # (2) takes `u`
@variables z(t) = 0 # (2) local x(..) deleted
@parameters θ = 0.1 τ = τ
@brownians ξ
eqs = [
D(z) ~ -θ * u(t - τ) + ξ # (2) delay `u`, not `x`
]
System(eqs, t; name)
end
@named lv = StochasticLV()
@named sth = SomethingElse(τ = 3, u = callable(lv.x)) # (2) signal injected
# (2) `connection_eqs = [sth.x ~ lv.x]` is gone. That alias was the whole problem.
@named connected_model = System(Equation[], t, [], []; systems = [lv, sth])
connected = mtkcompile(connected_model)
(equations = equations(connected),
unknowns = unknowns(connected),
observed = observed(connected))(equations = Symbolics.Equation[Differential(t, 1)(sth₊z(t)) ~ -lv₊x(-sth₊τ + t)*sth₊θ, Differential(t, 1)(lv₊y(t)) ~ -lv₊y(t)*lv₊γ + lv₊x(t)*lv₊y(t)*lv₊δ, Differential(t, 1)(lv₊x(t)) ~ lv₊x(t)*lv₊α - lv₊x(t)*lv₊y(t)*lv₊β], unknowns = SymbolicUtils.BasicSymbolicImpl.var"typeof(BasicSymbolicImpl)"{SymbolicUtils.SymReal}[sth₊z(t), lv₊y(t), lv₊x(t)], observed = Symbolics.Equation[])# Exactly as posted. No constant_lags, no MethodOfSteps wrapper.
prob = SDDEProblem(connected, [], (0.0, 1.0))
sol = solve(prob, ImplicitEM())
(retcode = sol.retcode, steps = length(sol.t), final = sol.u[end])(retcode = SciMLBase.ReturnCode.Success, steps = 175, final = [1.755910429359539, 0.7206220094566499, 1.0273877364780768])
Checks
- the delay reads
lv₊x(-sth₊τ + t), which is a real unknown observedis empty, so nothing got alias-eliminatednoiseis diagonal, soηandξstay independent Wiener processes. The zero entry isy, which has no noise term.
The cell below feeds a history function reporting lv₊x == 7.0 in the past. If the delay
really lowered to a history lookup, dz comes out at -θ·7 = -0.7. Default history is
constant extrapolation of u0, so lv₊x is 0.9 for t < 0.
du = zeros(3)
prob.f(du, prob.u0, (p, tt) -> [0.0, 0.9, 7.0], prob.p, 0.0)
(unknown_order = unknowns(connected),
u0 = prob.u0,
du = du,
dz_expected = -0.1 * 7.0,
dz_matches = du[1] ≈ -0.1 * 7.0,
noise = ModelingToolkit.get_noise_eqs(connected))(unknown_order = SymbolicUtils.BasicSymbolicImpl.var"typeof(BasicSymbolicImpl)"{SymbolicUtils.SymReal}[sth₊z(t), lv₊y(t), lv₊x(t)], u0 = [0.0, 0.9, 0.9], du = [-0.7000000000000001, -0.08999999999999997, -0.4800000000000001], dz_expected = -0.7000000000000001, dz_matches = true, noise = SymbolicUtils.BasicSymbolicImpl.var"typeof(BasicSymbolicImpl)"{SymbolicUtils.SymReal}[1, 0, 1])Past the delay
On (0, 1) the delay never reaches into the solved interval, so z does nothing. Running
past τ = 3 shows the coupling. The posted model puts additive noise of coefficient 1 on
x, which is large next to x ≈ 0.9, so the trajectory leaves the deterministic LV cycle
quickly. That's the model as written, not a solver artifact.
long_prob = SDDEProblem(connected, [], (0.0, 12.0))
long_sol = solve(long_prob, ImplicitEM(); seed = 20250817, saveat = 0.02)
echart(
series(:line, long_sol.t, long_sol[connected.lv.x]; name = "lv₊x (prey)"),
series(:line, long_sol.t, long_sol[connected.lv.y]; name = "lv₊y (predator)"),
series(:line, long_sol.t, long_sol[connected.sth.z]; name = "sth₊z (delay-driven)",
yAxisIndex = 1);
yAxis = [(name = "LV",), (name = "z",)],
xAxis = (name = "t",),
legend = true,
title = "Delayed coupling across components (τ = 3)",
height = 420,
zoom = true)Where this pattern stops working
Only siblings under a common parent. Two things to check before relying on it.
Nesting gives you a wrong model, quietly
The injected reference survives one level of namespacing and no more. Put the consumer a
level deeper and namespacing rewrites the operator to mid₊lv₊x, which is neither an
unknown nor observed. mtkcompile still succeeds. No scope annotation changes this.
@component function Sink_nested(; name, τ, u)
@variables z(t) = 0
@parameters θ = 0.1 τ = τ
System([D(z) ~ -θ * u(t - τ)], t; name)
end
# Build `lvn` plus a `midn` wrapper holding `sthn`, so the consumer sits two levels deep.
function build_nested(scope)
@named lvn = StochasticLV()
@named sthn = Sink_nested(τ = 3, u = callable(scope(lvn.x)))
@named midn = System(Equation[], t, [], []; systems = [sthn])
@named nested = System(Equation[], t, [], []; systems = [lvn, midn])
mtkcompile(nested)
end
nesting_results = map((:none => identity,
:ParentScope => ModelingToolkit.ParentScope,
:GlobalScope => ModelingToolkit.GlobalScope)) do (label, scope)
label => try
string(equations(build_nested(scope))[1])
catch e
first(split(sprint(showerror, e), "\n"))
end
end(:none => "Differential(t, 1)(midn₊sthn₊z(t)) ~ -midn₊lvn₊x(-midn₊sthn₊τ + t)*midn₊sthn₊θ", :ParentScope => "Differential(t, 1)(midn₊sthn₊z(t)) ~ -midn₊lvn₊x(-midn₊sthn₊τ + t)*midn₊sthn₊θ", :GlobalScope => "Differential(t, 1)(midn₊sthn₊z(t)) ~ -midn₊lvn₊x(-midn₊sthn₊τ + t)*midn₊sthn₊θ")
# The delayed name is in the equations but nowhere in the system, and the failure is the
# same silent-codegen bug as the original, reached by a different route.
nested_sys = build_nested(identity)
phantom_rhs_error = let np = SDDEProblem(nested_sys, [], (0.0, 1.0); build_initializeprob = false)
du = zeros(3)
try
np.f(du, np.u0, (p, tt) -> np.u0, np.p, 0.0)
"no error (unexpected)"
catch e
first(split(sprint(showerror, e), "\n"))
end
end
(delayed_name_in_equation = "midn₊lvn₊x",
unknowns = unknowns(nested_sys),
observed = observed(nested_sys),
mtkcompile_complained = false,
rhs_error = phantom_rhs_error)(delayed_name_in_equation = "midn₊lvn₊x", unknowns = SymbolicUtils.BasicSymbolicImpl.var"typeof(BasicSymbolicImpl)"{SymbolicUtils.SymReal}[midn₊sthn₊z(t), lvn₊y(t), lvn₊x(t)], observed = Symbolics.Equation[], mtkcompile_complained = false, rhs_error = "UndefVarError: `midn₊lvn₊x` not defined in `ModelingToolkitBase`")irreducible doesn't save the connection
Keeping sth.x ~ lv.x and marking the input [irreducible = true] so it survives alias
elimination looks promising. MTK then wants a derivative for it.
# Keep the alias, mark it irreducible so it stays an unknown. MTK asks for D(sth₊x) instead.
irreducible_result = try
@component function LV_plain2(; name)
@variables x(t) = 0.9 y(t) = 0.9
@parameters α = 2 / 3 β = 4 / 3 γ = 1 δ = 1
@brownians η
System([D(x) ~ α * x - β * x * y + η
D(y) ~ -γ * y + δ * x * y], t; name)
end
@component function Sink_irred(; name, τ)
@variables z(t) = 0 x(..) [irreducible = true]
@parameters θ = 0.1 τ = τ
@brownians ξ
System([D(z) ~ -θ * x(t - τ) + ξ], t; name)
end
@named lvi = LV_plain2()
@named sthi = Sink_irred(τ = 3)
@named irred = System([sthi.x ~ lvi.x], t, [], []; systems = [lvi, sthi])
string(equations(mtkcompile(irred)))
catch e
first(split(sprint(showerror, e), "\n"))
end"Differential(t, 1)(sthi₊x(t)) isn't handled."
Why the original fails
Diagnosis from here down. The definitions below are the posted ones with an _orig suffix
so they can share a namespace with the working versions above.
@component function StochasticLV_orig(; name)
@variables x(t) = 0.9 y(t) = 0.9
@parameters begin
α = 2 / 3
β = 4 / 3
γ = 1
δ = 1
end
@brownians η
eqs = [
D(x) ~ α * x - β * x * y + η
D(y) ~ -γ * y + δ * x * y
]
System(eqs, t; name)
end
@component function SomethingElse_orig(; name, τ)
@variables z(t) = 0 x(..)
@parameters θ = 0.1 τ = τ
@brownians ξ
eqs = [
D(z) ~ -θ * x(t - τ) + ξ
]
System(eqs, t; name)
end
@named lv_orig = StochasticLV_orig()
@named sth_orig = SomethingElse_orig(τ = 3)
# `sth.x` auto-applies at `t`, so this connection equation is well formed
connection_eqs = [sth_orig.x ~ lv_orig.x]
@named broken_model = System(connection_eqs, t, [], []; systems = [lv_orig, sth_orig])
equations(broken_model)4-element Vector{Symbolics.Equation}:
sth_orig₊x(t) ~ lv_orig₊x(t)
Differential(t, 1)(lv_orig₊x(t)) ~ lv_orig₊η + lv_orig₊x(t)*lv_orig₊α - lv_orig₊x(t)*lv_orig₊y(t)*lv_orig₊β
Differential(t, 1)(lv_orig₊y(t)) ~ -lv_orig₊y(t)*lv_orig₊γ + lv_orig₊x(t)*lv_orig₊y(t)*lv_orig₊δ
Differential(t, 1)(sth_orig₊z(t)) ~ sth_orig₊ξ - sth_orig₊x(-sth_orig₊τ + t)*sth_orig₊θ# mtkcompile succeeds. That's the first surprise.
broken = mtkcompile(broken_model)
(equations = equations(broken),
unknowns = unknowns(broken),
observed = observed(broken))(equations = Symbolics.Equation[Differential(t, 1)(sth_orig₊z(t)) ~ -sth_orig₊x(-sth_orig₊τ + t)*sth_orig₊θ, Differential(t, 1)(lv_orig₊y(t)) ~ -lv_orig₊y(t)*lv_orig₊γ + lv_orig₊x(t)*lv_orig₊y(t)*lv_orig₊δ, Differential(t, 1)(lv_orig₊x(t)) ~ lv_orig₊x(t)*lv_orig₊α - lv_orig₊x(t)*lv_orig₊y(t)*lv_orig₊β], unknowns = SymbolicUtils.BasicSymbolicImpl.var"typeof(BasicSymbolicImpl)"{SymbolicUtils.SymReal}[sth_orig₊z(t), lv_orig₊y(t), lv_orig₊x(t)], observed = Symbolics.Equation[sth_orig₊x(t) ~ lv_orig₊x(t)])Root cause
Three things in the cell above:
| equation | D(sth₊z) ~ -sth₊x(-sth₊τ + t) * sth₊θ, still naming sth₊x |
| unknowns | sth₊x isn't there |
| observed | sth₊x(t) ~ lv₊x(t), alias-eliminated |
Alias elimination rewrote sth₊x(t) and left sth₊x(t - τ) alone.
That's fatal because of how a delay gets lowered. delay_to_function
(ModelingToolkitBase/src/systems/codegen_utils.jl) turns v(t - τ) into a history
lookup h(p, t - τ)[i], but only for v ∈ unknowns(sys):
function delay_to_function(expr, iv, sts, ps, h; param_arg = MTKPARAMETERS_ARG)
if isdelay(expr, iv)
v = operation(expr)
time = arguments(expr)[1]
idx = sts[v] # sts = Dict(operation(s) => i for s in unknowns(sys))
return term(getindex, h(param_arg, time), idx, type = Real)
...
sth₊x is an observed alias rather than an unknown, so the delay term is orphaned.
Everything below is that one fact showing up in two places.
Symptom 1: the error you see
SDDEProblem builds an initialization system for consistent initial conditions. That
system is time-independent (get_iv(isys) === nothing) and still carries the delay term.
isys = ModelingToolkit.generate_initializesystem(broken)
init_iv = ModelingToolkit.get_iv(isys) # nothing, so time-independent
delay_terms = filter(eq -> occursin("sth_orig₊x(-", string(eq)), equations(isys))
(init_iv = init_iv, delay_still_present = delay_terms)(init_iv = nothing, delay_still_present = Symbolics.Equation[sth_orig₊zˍt(t) ~ -sth_orig₊x(-sth_orig₊τ + t)*sth_orig₊θ])
# SDDEProblem -> InitializationProblem -> mtkcompile -> TearingState
try
SDDEProblem(broken, [], (0.0, 1.0))
println("no error (unexpected)")
catch e
show_trimmed(e, catch_backtrace())
endArgumentError: sth_orig₊x(-sth_orig₊τ + t) is present in the system but sth_orig₊x(-sth_orig₊τ + t) is not an unknown.
Stacktrace (29 of 49 frames):
[$i] #TearingState#2
@ /Users/kburke/.julia/packages/ModelingToolkitTearing/ZusxN/src/tearingstate.jl:298
[$i] TearingState
@ /Users/kburke/.julia/packages/ModelingToolkitTearing/ZusxN/src/tearingstate.jl:149
[$i] #__mtkcompile#153
@ /Users/kburke/.julia/packages/ModelingToolkit/YCGiA/src/systems/systems.jl:34
[$i] __mtkcompile
@ /Users/kburke/.julia/packages/ModelingToolkit/YCGiA/src/systems/systems.jl:23
[$i] #_mtkcompile#1246
@ /Users/kburke/.julia/packages/ModelingToolkitBase/wq7pZ/src/systems/systems.jl:155
[$i] _mtkcompile
@ /Users/kburke/.julia/packages/ModelingToolkitBase/wq7pZ/src/systems/systems.jl:128
[$i] #mtkcompile#1245
@ /Users/kburke/.julia/packages/ModelingToolkitBase/wq7pZ/src/systems/systems.jl:100
[$i] mtkcompile
@ /Users/kburke/.julia/packages/ModelingToolkitBase/wq7pZ/src/systems/systems.jl:84
[$i] #mtkcompile_initialization_system#943
@ /Users/kburke/.julia/packages/ModelingToolkitBase/wq7pZ/src/problems/initializationproblem.jl:197
[$i] mtkcompile_initialization_system
@ /Users/kburke/.julia/packages/ModelingToolkitBase/wq7pZ/src/problems/initializationproblem.jl:193
[$i] #_#942
@ /Users/kburke/.julia/packages/ModelingToolkitBase/wq7pZ/src/problems/initializationproblem.jl:110
[$i] InitializationProblem
@ /Users/kburke/.julia/packages/ModelingToolkitBase/wq7pZ/src/problems/initializationproblem.jl:46
[$i] #_#941
@ /Users/kburke/.julia/packages/ModelingToolkitBase/wq7pZ/src/problems/initializationproblem.jl:43
[$i] InitializationProblem
@ /Users/kburke/.julia/packages/ModelingToolkitBase/wq7pZ/src/problems/initializationproblem.jl:40
[$i] #maybe_build_initialization_problem#792
@ /Users/kburke/.julia/packages/ModelingToolkitBase/wq7pZ/src/systems/problem_utils.jl:1873
[$i] maybe_build_initialization_problem
@ /Users/kburke/.julia/packages/ModelingToolkitBase/wq7pZ/src/systems/problem_utils.jl:1850
[$i] #__process_SciMLProblem#801
@ /Users/kburke/.julia/packages/ModelingToolkitBase/wq7pZ/src/systems/problem_utils.jl:2167
[$i] __process_SciMLProblem
@ /Users/kburke/.julia/packages/ModelingToolkitBase/wq7pZ/src/systems/problem_utils.jl:2123
[$i] #process_SciMLProblem#800
@ /Users/kburke/.julia/packages/ModelingToolkitBase/wq7pZ/src/systems/problem_utils.jl:2120
[$i] process_SciMLProblem
@ /Users/kburke/.julia/packages/ModelingToolkitBase/wq7pZ/src/systems/problem_utils.jl:2108
[$i] #process_SciMLProblem#799
@ /Users/kburke/.julia/packages/ModelingToolkitBase/wq7pZ/src/systems/problem_utils.jl:2090
[$i] process_SciMLProblem
@ /Users/kburke/.julia/packages/ModelingToolkitBase/wq7pZ/src/systems/problem_utils.jl:2058
[$i] #_#865
@ /Users/kburke/.julia/packages/ModelingToolkitBase/wq7pZ/src/problems/sddeproblem.jl:76
[$i] SDDEProblem
@ /Users/kburke/.julia/packages/ModelingToolkitBase/wq7pZ/src/problems/sddeproblem.jl:65
[$i] #_#864
@ ./none:-1
[$i] SDDEProblem
@ ./none:-1
[$i] #SDDEProblem#863
@ ./none:-1
[$i] SDDEProblem
@ ./none:-1
[$i] top-level scope
@ cell:symptom1_traceback:3
… 20 harness frames elided
Frame [1] is this guard in ModelingToolkitTearing/src/tearingstate.jl:
iv isa SymbolicT && isequal(v, iv) && continue
iv isa SymbolicT && MTKBase.isdelay(v, iv) && continue # exemption for delay terms,
# dead when iv === nothing
if !in(v, dvs)
...
if !isvalid
throw(ArgumentError("\$v is present in the system but \$v′ is not an unknown."))
end
The delay exemption is gated on iv isa SymbolicT, so it's dead when iv === nothing. The
delayed term then gets treated as an ordinary unknown variable, which it isn't. That's why
the error names the delay term when the actual problem is the missing alias substitution.
Symptom 2: no error at all
Skip initialization and the problem constructs fine. The generated right-hand side then
contains a literal call to sth₊x, which isn't a Julia function.
bad_prob = SDDEProblem(broken, [], (0.0, 1.0); build_initializeprob = false)
(constructed_fine = summary(bad_prob),)(constructed_fine = "SDDEProblem with uType Vector{Float64} and tType Float64. In-place: true\nNon-trivial mass matrix: false",)The lowering is visible in the emitted code. Every unknown gets bound to a closure over
the history function, getindex(i) ∘ Fix1(h, p), i.e. h(p, ·)[i]:
lv₊x = getindex(3) ∘ Fix1(h, p) # h(p, τ)[3]
sth₊x gets no binding, because it isn't an unknown. The name is emitted bare, then
called:
local var"##cse#3" = sth₊x # unbound, no history closure
local var"##cse#8" = var"##cse#3"(var"##cse#7") # called at (t - τ) regardless
It resolves in ModelingToolkitBase, where no such name exists, so you get an
UndefVarError on the first RHS evaluation instead of at build time.
rhs_expr = let e = ModelingToolkit.generate_rhs(broken; expression = Val{true})
e isa Tuple ? e[1] : e
end
rhs_lines = split(string(rhs_expr), '\n')
for (i, ln) in enumerate(rhs_lines)
if occursin("ComposedFunction", ln) || occursin("sth_orig₊x", ln) ||
occursin(r"^\s+(sth_orig₊z|lv_orig₊y|lv_orig₊x) = ", ln)
marker = occursin("sth_orig₊x", ln) ? " <== ORPHANED" : ""
println(lpad(i, 4), " │ ", strip(ln), marker)
end
end14 │ sth_orig₊z = begin 19 │ local var"##cse#4" = (ComposedFunction)(var"##cse#0", var"##cse#3") 21 │ lv_orig₊y = begin 26 │ local var"##cse#4" = (ComposedFunction)(var"##cse#0", var"##cse#3") 28 │ lv_orig₊x = begin 33 │ local var"##cse#4" = (ComposedFunction)(var"##cse#0", var"##cse#3") 39 │ local var"##cse#3" = sth_orig₊x <== ORPHANED
let du = zeros(3)
try
bad_prob.f(du, bad_prob.u0, (p, tt) -> bad_prob.u0, bad_prob.p, 0.0)
println("no error (unexpected)")
catch e
show_trimmed(e, catch_backtrace())
end
endUndefVarError: `sth_orig₊x` not defined in `ModelingToolkitBase`
Suggestion: check for spelling errors or missing imports.
Stacktrace (10 of 30 frames):
[$i] macro expansion
@ /Users/kburke/.julia/packages/RuntimeGeneratedFunctions/odmYb/src/RuntimeGeneratedFunctions.jl:247
[$i] macro expansion
@ ./none:0
[$i] generated_callfunc
@ ./none:0
[$i] RuntimeGeneratedFunction
@ /Users/kburke/.julia/packages/RuntimeGeneratedFunctions/odmYb/src/RuntimeGeneratedFunctions.jl:234
[$i] GeneratedFunctionWrapper
@ /Users/kburke/.julia/packages/ModelingToolkitBase/wq7pZ/src/systems/codegen_utils.jl:1050
[$i] macro expansion
@ ./none:-1
[$i] #invoke_with_despecialized_parameters#38
@ ./none:0
[$i] invoke_with_despecialized_parameters
@ ./none:-1
[$i] SDDEFunction
@ /Users/kburke/.julia/packages/SciMLBase/W1j29/src/scimlfunctions.jl:3019
[$i] top-level scope
@ cell:codegen_traceback:3
… 20 harness frames elided
The solver stack, change (3)
Once the model is fixed, the posted script still fails, for an unrelated reason. It loads
ModelingToolkit, DifferentialEquations and StochasticDiffEq, and none of those loads
DelayDiffEq, so no SDDE integrator is available:
SciMLBase.NoDefaultAlgorithmError()
DelayDiffEq may well be sitting in your manifest already as a transitive dependency, as
it is in the issue's manifest at 6.1.2. That isn't enough. It has to be loaded, because
the machinery arrives through a package extension.
using DelayDiffEq fixes it, because SDDE support lives there now. _sde_alg_cache and
_create_sdde_noise are defined in DelayDiffEqStochasticDiffEqCoreExt, triggered by
StochasticDiffEqCore + DiffEqNoiseProcess + Random. DelayDiffEq.__solve takes
AbstractSDDEProblem directly and check_prob_alg_pairing accepts an SDE algorithm, so
plain solve(prob, ImplicitEM()) works with no MethodOfSteps wrapper and no
constant_lags.
Skip StochasticDelayDiffEq
It's the obvious package to reach for. On the current stack it can't be installed at all:
its compat bounds conflict with DelayDiffEq 6.x, so Pkg.add reports unsatisfiable
requirements. StochasticDelayDiffEq tops out at 1.13.0 and was superseded by the move
into DelayDiffEq without being retired.
For the record, on an older stack where it does resolve it is still broken: against
StochasticDiffEq 6.102 it fails to precompile with
UndefVarError: handle_callback_modifiers!, and against 6.98 or 6.99 it loads and then dies
in build_history_function with MethodError: no method matching _sde_alg_cache(...). That
reproduces with a hand-written SDDEProblem and no MTK involved.
Which parts are bugs
Splitting these out because only some are fixable in user code.
MTK: delay terms aren't rewritten, and the bad code gets emitted anyway. mtkcompile
will produce a system whose equations contain v(t - τ) where v is neither an unknown
nor observed, then generate a function that calls that name as if it were a Julia function.
Two routes get there:
- alias elimination (
sth.x ~ lv.x) rewritessth₊x(t)but notsth₊x(t - τ) - namespacing an injected reference rewrites it to
mid₊lv₊x, which exists nowhere
Substituting through delay arguments is the right fix, and it would make the posted code
work as written. Short of that, mtkcompile should refuse. Emitting a function that throws
UndefVarError on first evaluation is the worst option, because a model can look compiled
and be silently wrong.
MTK: the error points somewhere else. The ArgumentError people actually hit names the
delay term inside a time-independent initialization system. The delay exemption in
TearingState is gated on iv isa SymbolicT and dead when iv === nothing. Nothing in
the message suggests the delayed variable was alias-eliminated.
MTK, missing feature. ParentScope and GlobalScope are ignored for the callable
operator in a delay term, so there's no way to say "delay this variable from an enclosing
scope". For now that means hoisting the delay equation up to a common parent.
Not MTK. using DifferentialEquations not being enough to solve an SDDEProblem, and
NoDefaultAlgorithmError saying nothing about which package is missing. And
StochasticDelayDiffEq still being registered while it can't co-resolve with the stack it
belongs to.