Fix "cannot evaluate norm recursively" in LowStorageRK / SSPRK under RAT v4#3513
Conversation
…RAT v4
The VectorOfArray/StructArray compatibility testsets in
`lib/OrdinaryDiffEqLowStorageRK/test/ode_low_storage_rk_tests.jl` and
`lib/OrdinaryDiffEqSSPRK/test/ode_ssprk_tests.jl` assert
`sol_SA ≈ sol_SV` where both sides are `ODESolution`s whose `u` is a
`Vector{VectorOfArray{Float64, 2, StructVector{SVector{1,Float64},…}}}`
vs `Vector{VectorOfArray{Float64, 2, Vector{SVector{1,Float64}}}}`.
`ODESolution <: AbstractVectorOfArray <: AbstractArray`, so with no
`isapprox` override the generic `LinearAlgebra.isapprox(::AbstractArray,
::AbstractArray)` fallback calls `norm(sol)`, which — under
RecursiveArrayTools v4 / Julia 1.12 — hits a stricter
`norm_recursive_check` and throws
ArgumentError: cannot evaluate norm recursively if the type of the
initial element is identical to that of the container
because `AbstractArray` iteration of the solution yields the solution
itself (`iterate(sol)[1] === sol`) when the leaf storage is a
`StructArray{<:SVector}`-backed `VectorOfArray`. No OrdinaryDiffEq
solver code is on the failing stack; `solve(...)` completes normally for
both storages. The failure is purely in the `≈` path at the test site.
Fix it by adding an `isapprox(::AbstractTimeseriesSolution,
::AbstractTimeseriesSolution)` method in `OrdinaryDiffEqCore` that
compares `t` and iterates `u` snapshot-by-snapshot, materialising
`VectorOfArray` / `StructArray` snapshots to plain `Array`s before the
inner `isapprox`. This avoids the pathological recursive `norm` path,
sidesteps the cross-container-type issue when the two solutions use
different backing arrays, and preserves the semantics callers expect
(element-wise approximate equality of the trajectory). The scalar /
`Vector{Float64}` paths still dispatch to the existing `isapprox` on the
leaf snapshot, so existing `@test sol_old.u[end] ≈ sol_new.u[end]`
assertions are untouched.
- Adds `Base.isapprox(x, y)` for `SciMLBase.AbstractTimeseriesSolution`
in `lib/OrdinaryDiffEqCore/src/misc_utils.jl`.
- Imports `LinearAlgebra.norm` (and the `LinearAlgebra` module) in
`lib/OrdinaryDiffEqCore/src/OrdinaryDiffEqCore.jl`.
- Bumps `OrdinaryDiffEqCore` to 4.0.1.
Locally verified both offending testsets now pass on Julia 1.12.6
(same tool version as CI) with the monorepo `Pkg.develop`ed against
this branch:
LowStorageRK VectorOfArray/StructArray compatibility: 16/16 passes
SSPRK VectorOfArray/StructArray compatibility: 2/2 passes
Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Diagnosis looks right, but the fix lands Suggested path:
Alternative: if the real bug is in RAT's |
|
Reproduced and tracked down: the root cause is in SciMLBase, not RecursiveArrayTools. The broken iterator is function Base.iterate(sol::AbstractTimeseriesSolution, state = 0)
state >= length(sol) && return nothing
state += 1
return (solution_new_tslocation(sol, state), state)
endFor a solution typed This iterate has been like this for years (for plotting/ I'll close this PR once the SciMLBase patch is in — or I can repurpose it to just be the version bump of |
|
SciMLBase upstream fix: SciML/SciMLBase.jl#1322 — replaces the custom Once #1322 merges and registers, this PR should close: the |
|
Superseded by SciML/SciMLBase.jl#1322 (registering as SciMLBase v3.4.0). The |
Summary
test (OrdinaryDiffEqLowStorageRK, 1, ubuntu-latest, …)(job72604636568)test (OrdinaryDiffEqSSPRK, 1, ubuntu-latest, …)(job72604636663)VectorOfArray/StructArray compatibilityblock with:Root cause
The failing assertion in both suites is
@test sol_SA ≈ sol_SV, wheresol_SAandsol_SVareODESolutions whose internalustorage is aVector{VectorOfArray{Float64, 2, StructVector{SVector{1,Float64},…}}}vsVector{VectorOfArray{Float64, 2, Vector{SVector{1,Float64}}}}.ODESolution <: AbstractVectorOfArray <: AbstractArray, so the genericLinearAlgebra.isapprox(::AbstractArray, ::AbstractArray)fallback callsnorm(sol). On Julia 1.12 + RecursiveArrayTools v4,normnow routes through a stricternorm_recursive_check, which throws for this container shape becauseAbstractArrayiteration yields the solution itself (iterate(sol)[1] === sol) when the leaf storage is aStructArray{<:SVector}-backedVectorOfArray. No OrdinaryDiffEq solver code appears on the failing stack —solvecompletes fine for both storages; the failure is purely in≈at the test site.Fix
Base.isapprox(x::AbstractTimeseriesSolution, y::AbstractTimeseriesSolution)inlib/OrdinaryDiffEqCore/src/misc_utils.jl. It:x.t ≈ y.t,x.u/y.usnapshot-by-snapshot and delegates toisapproxon leaves,VectorOfArray/StructArraysnapshots (viaparent(...)+collect) into plainArrays before the innerisapprox, soLinearAlgebra.normnever sees a container whose element type equals itself.LinearAlgebra.norm(and theLinearAlgebramodule) inOrdinaryDiffEqCore.OrdinaryDiffEqCoreto4.0.1.The scalar and
Vector{Float64}comparison paths still dispatch toisapproxon the raw snapshot, so the existing@test sol_old.u[end] ≈ sol_new.u[end]assertions elsewhere in the LowStorageRK / SSPRK tests are untouched.Tests were not removed, disabled, marked
@test_broken, or commented out.Test plan
Locally on Julia
1.12.6(matching CI), monorepoPkg.developed against this branch:OrdinaryDiffEqLowStorageRKVectorOfArray/StructArray compatibilitytestset: 16/16 pass (was 15 pass / 1 error on master).OrdinaryDiffEqSSPRKVectorOfArray/StructArray compatibilitytestset: 2/2 pass (was 1 pass / 1 error on master).72052c030) confirms theArgumentError: cannot evaluate norm recursively …with stack trace identical to the CI failure.🤖 Generated with Claude Code