SciML / SciML/ModelingToolkit.jl
`MTKParameters` type mismatch in `init_dae!` when tunable buffer is a `ReinterpretArray` (DiffCache + setsym parameter-estimation pattern)
Nobody has claimed this yet.
- Dominant language
- Julia
- Stars
- 1.7k
- Forks
- 270
- Avg merge
- 20h 8m
- Merged PRs (30d)
- 92
Description
This report was drafted by Claude based on a sandboxed investigation, then reviewed by me before posting.
Summary
When a user follows a parameter-estimation pattern that puts a PreallocationTools.DiffCache buffer into the tunable slot of MTKParameters (e.g. via SciMLStructures.replace(Tunable(), p, get_tmp(cache, x))), the first type parameter of MTKParameters becomes Base.ReinterpretArray{...} instead of Vector{...}. The primal solve works, but ForwardDiff.gradient on the loss then crashes inside init_dae! with a MethodError: Cannot convert between two MTKParameters types that differ only in the array-type of the tunable field.
The mismatch is between:
- the integrator slot type —
MTKParameters{ReinterpretArray{Dual,1,Float64,SubArray{...},false}, ...}(set when the integrator was built fromprob.pwhosetunableis theReinterpretArray), and - the
preconstructed byOverrideInit—MTKParameters{Vector{Dual}, ...}(a plainVectorbecause the MTK reconstructor’sPConstructorApplicatorcallscollect(::SubArray)).
MWE
using ModelingToolkit
using ModelingToolkit: t_nounits as t, subset_tunables
using ModelingToolkitStandardLibrary.Electrical
using ModelingToolkitStandardLibrary.Blocks: Sine
using OrdinaryDiffEqRosenbrock
using OrdinaryDiffEqNonlinearSolve
using PreallocationTools: DiffCache, get_tmp
using SymbolicIndexingInterface: setsym
using SciMLStructures: Tunable, replace as smlreplace
using SciMLBase: remake
using ForwardDiff
function create_model(; C₁ = 3e-5, C₂ = 1e-6)
@named resistor1 = Resistor(R = 5.0)
@named resistor2 = Resistor(R = 2.0)
@named capacitor1 = Capacitor(C = C₁)
@named capacitor2 = Capacitor(C = C₂)
@named source = Voltage()
@named input_signal = Sine(frequency = 100.0)
@named ground = Ground()
@named ampermeter = CurrentSensor()
eqs = [connect(input_signal.output, source.V)
connect(source.p, capacitor1.n, capacitor2.n)
connect(source.n, resistor1.p, resistor2.p, ground.g)
connect(resistor1.n, capacitor1.p, ampermeter.n)
connect(resistor2.n, capacitor2.p, ampermeter.p)]
@named circuit_model = System(eqs, t,
systems = [resistor1, resistor2, capacitor1, capacitor2,
source, input_signal, ground, ampermeter])
end
sys = mtkcompile(create_model())
guesses = Dict(
sys.resistor1.p.i => 0.0, sys.resistor2.p.i => 0.0,
sys.capacitor1.p.i => 0.0, sys.capacitor2.p.i => 0.0,
sys.ampermeter.p.i => 0.0,
)
tspan, saveat = (0.0, 0.05), 0.0005
true_prob = ODEProblem(sys, Dict(), tspan; guesses)
data = solve(true_prob, Rodas5P(); saveat)[sys.ampermeter.i]
sys_sub = subset_tunables(sys, [sys.capacitor2.C])
prob = ODEProblem(sys_sub, Dict(), tspan; guesses = (
sys_sub.resistor1.p.i => 0.0, sys_sub.resistor2.p.i => 0.0,
sys_sub.capacitor1.p.i => 0.0, sys_sub.capacitor2.p.i => 0.0,
sys_sub.ampermeter.p.i => 0.0) |> Dict)
cache = DiffCache(copy(prob.p.tunable))
set_C2! = setsym(sys_sub, sys_sub.capacitor2.C)
function loss(x)
tunables = get_tmp(cache, x) # ReinterpretArray when x::Vector{Dual}
copyto!(tunables, prob.p.tunable)
new_p = smlreplace(Tunable(), prob.p, tunables)
set_C2!(new_p, x[1]) # in-place setsym update
sol = solve(remake(prob; p = new_p), Rodas5P(); saveat)
return sum(abs2, sol[sys_sub.ampermeter.i] .- data)
end
loss([1e-6]) # 0.0 — primal works
loss([2e-6]) # ~1.6e-6 — primal works
ForwardDiff.gradient(loss, [2e-6]) # 💥 see error below
Error
ERROR: MethodError: Cannot `convert` an object of type
MTKParameters{Vector{ForwardDiff.Dual{Tag{typeof(loss),Float64},Float64,1}}, ...}
to an object of type
MTKParameters{Base.ReinterpretArray{ForwardDiff.Dual{Tag{typeof(loss),Float64},Float64,1},
1, Float64,
SubArray{Float64,1,Vector{Float64},
Tuple{UnitRange{Int64}}, true},
false}, ...}
Stacktrace:
[1] setproperty!(x::OrdinaryDiffEqCore.ODEIntegrator{…, MTKParameters{ReinterpretArray,…}, …}, …)
[3] @ OrdinaryDiffEqCore OrdinaryDiffEq.jl/lib/OrdinaryDiffEqCore/src/initialize_dae.jl:148
[4] @ initialize_dae.jl:35 [inlined] # _initialize_dae! for OverrideInit
[6] @ OrdinaryDiffEqCore solve.jl:670
...
Diagnosis
SciMLStructures.replace(::Tunable, p, newvals)atlib/ModelingToolkitBase/src/systems/parameter_buffer.jl:373is literally@set! p.tunable = newvals— it stores theReinterpretArrayas-is, so the returnedMTKParameters’ first type parameter isReinterpretArray{...}.- The integrator gets typed against that. So far so good — primal
solveworks. - During AD-typed
solve,_initialize_dae!’sOverrideInitbranch (lib/OrdinaryDiffEqCore/src/initialize_dae.jl:115-159) rebuilds parameters viainitdata.initializeprobpmap, which goes throughget_mtkparameters_reconstructor(lib/ModelingToolkitBase/src/systems/problem_utils.jl:735). - The reconstructor uses
PConstructorApplicator(problem_utils.jl:693-719); for theSubArraybranch (lines 705-707) with the defaultp_constructor = identity, it callscollect(x)and returns a plainVector. That makes the reconstructedMTKParameterscarryVector{Dual}as its tunable type. - Back in
initialize_dae.jl:148,integrator.p = ptriggersconvert(MTKParameters{ReinterpretArray,…}, MTKParameters{Vector{Dual},…})— no such method exists.
Possible fixes
In rough order of how surgical they are:
- MTK — make
get_mtkparameters_reconstructor/PConstructorApplicatorpreserve the array-type of the originalp.tunableinstead of collapsingSubArrays withcollecttoVector. Most direct fix at the root cause. - OrdinaryDiffEqCore — in
_initialize_dae!’sOverrideInitbranch, coerce the returnedp.tunable(and friends) into the same array type asintegrator.p.tunablebefore the assignment atinitialize_dae.jl:148. More defensive — also protects against any other reconstructor losing the array type. - MTK — make
SciMLStructures.replace(::Tunable, p, newvals)collectnewvalsto aVectorso aReinterpretArraycan never become theTparameter ofMTKParameters. Removes the in-place AD buffer benefit users are after, so probably not desirable.
Environment
ModelingToolkitmaster (commit8b82464)OrdinaryDiffEqmaster (OrdinaryDiffEqCore/OrdinaryDiffEqRosenbrock/OrdinaryDiffEqNonlinearSolvedev'd from local checkout)SciMLBasemaster (dev'd locally)ModelingToolkitStandardLibrary 2.28.0,PreallocationTools 1.2.0,SciMLStructures 1.10.0,SymbolicIndexingInterface 0.3.47,ForwardDiff 1.3.3- Julia 1.12.6
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Reproduce the MWE first, then inspect parameter_buffer.jl:373 and the PConstructorApplicator and get_mtkparameters_reconstructor paths in problem_utils.jl:693-735. Compare them with the OverrideInit assignment in OrdinaryDiffEqCore's initialize_dae.jl:115-159. Done means the ForwardDiff.gradient call completes without the MTKParameters conversion error, with regression coverage for a ReinterpretArray tunable buffer.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- julia
- Domain
- backend
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 45/100