Skip to content

Fix get_du/get_du! reading the wrong derivative under CompositeAlgorithm - #4092

Merged
ChrisRackauckas merged 1 commit into
SciML:masterfrom
ChrisRackauckas-Claude:fix-get-du-composite
Aug 2, 2026
Merged

Fix get_du/get_du! reading the wrong derivative under CompositeAlgorithm#4092
ChrisRackauckas merged 1 commit into
SciML:masterfrom
ChrisRackauckas-Claude:fix-get-du-composite

Conversation

@ChrisRackauckas-Claude

Copy link
Copy Markdown
Member

Note

Please ignore this PR until it has been reviewed by @ChrisRackauckas.

Fixes the underlying cause of SciML/DifferentialEquations.jl#1058, including the quieter variant reported in this comment.

The bug

get_du and get_du! choose between reading integrator.fsallast and evaluating the interpolant using the traits of integrator.alg:

# lib/OrdinaryDiffEqCore/src/integrators/integrator_interface.jl:84, :119
return if isfsal(integrator.alg) && !has_stiff_interpolation(integrator.alg)
    integrator.fsallast

CompositeAlgorithm <: OrdinaryDiffEqCompositeAlgorithm <: OrdinaryDiffEqAlgorithm, and neither trait has a CompositeAlgorithm method — isfsal(alg::CompositeAlgorithm) is commented out at alg_utils.jl:83. So both fall back to the generic defaults:

CompositeAlgorithm   isfsal=true  has_stiff_interpolation=false
Rodas5P              isfsal=false has_stiff_interpolation=true
Vern9                isfsal=false has_stiff_interpolation=false

Every Auto* algorithm therefore takes the fsallast branch regardless of which sub-algorithm is running. The buffer read there belongs to whichever sub-cache supplied it via get_fsalfirstlast, and a non-FSAL sub-algorithm never writes it:

  • AutoVern9(Rodas4())Rodas4Cache.fsallast, allocated but never written → exact zeros
  • AutoTsit5(Rodas5P()) on the stiff branch → same → exact zeros
  • default algorithm → get_fsalfirstlast(::DefaultCache, u) = (cache.u, cache.u)u itself, not du

Every other consumer in the integrator already routes through get_current_isfsal(integrator.alg, integrator.cache) (integrator_utils.jl:191, solve.jl:888, initdt.jl:71). get_du/get_du! were the exception.

Why this is worse than the original report

Issue #1058 reported a loud MethodError (fsallast === nothingfill!(dest, nothing)). That crash is gone, but only because Rosenbrock caches gained real fsalfirst/fsallast buffers — turning nothing into an allocated buffer that Rosenbrock never writes. The failure became silent.

On OrdinaryDiffEq v7.2.1 / DiffEqCallbacks v4.19.0, the MRE from #1058 no longer errors; it returns retcode = Terminated on a trajectory that has not converged:

Vern9()               retcode=Terminated t_end=117.909  true|du|=[8.6e-10, 7.5e-9]  converged=true
Rodas4()              retcode=Terminated t_end=124.293  true|du|=[4.1e-10, 3.6e-9]  converged=true
AutoVern9(Rodas4())   retcode=Terminated t_end=0.11889  true|du|=[0.0275, 0.0185]   converged=false

It terminates on the first step, because get_du! reports zero at every step:

step 1  t=0.11889  current=1  get_du!=[0.0, 0.0]  truth=[0.02751, -0.01851]
step 2  t=0.5347   current=1  get_du!=[0.0, 0.0]  truth=[0.03032, -0.017]

The scope is wider than TerminateSteadyState + AutoVern*: it is any get_du/get_du! caller under any composite algorithm, including the plain default solver with no algorithm specified.

The fix

Route both functions through the currently-active sub-algorithm's traits, reusing the existing get_current_isfsal and adding a matching get_current_has_stiff_interpolation.

Verification

Ran locally against the issue's MRE, a stiff variant that forces the Rosenbrock branch to be active at termination, and the default-algorithm path:

case before after
AutoVern9(Rodas4()), RM model t_end=0.11889, not converged t_end=117.909, converged (matches Vern9() exactly)
AutoTsit5(Rodas5P()), stiff t_end=0.0267, not converged t_end=10778.6, converged (Rodas5P() standalone: 10975.2)
default alg, get_du! [0.1017, 0.09878] (= u) [0.02716, -0.01871] (= truth)

test/InterfaceII/get_du.jl covered 12 standalone algorithms but no composite ones — that is the gap that let this through. Extended with five composites. On unpatched OrdinaryDiffEqCore the new cases fail:

Test Failed at test/InterfaceII/get_du.jl:34
  Expression: ≈(res, cache, rtol = 1.0e-5)
   Evaluated: [45.19984609080418, 96.17270725538896, 29.61827095739944] ≈ [0.0, 0.0, 0.0]

With the fix the whole file passes (exit 0) for all 17 algorithms.

Runic clean; no reformatting needed.

🤖 Generated with Claude Code

https://claude.ai/code/session_01TNSoDRgtXouPK8UgKz7avi

get_du and get_du! chose between reading integrator.fsallast and evaluating
the interpolant using isfsal(integrator.alg) and
has_stiff_interpolation(integrator.alg). Neither trait has a
CompositeAlgorithm method, so both fell back to the generic defaults (true
and false) and every Auto* algorithm took the fsallast branch regardless of
which sub-algorithm was active.

The buffer read there belongs to whichever sub-cache supplied it and is not
written by a non-FSAL sub-algorithm, so the reported derivative was a
never-written zero buffer for AutoVern9(Rodas4()) and, via
get_fsalfirstlast(::DefaultCache, u) = (cache.u, cache.u), the state vector
itself for the default algorithm. TerminateSteadyState consequently accepted
a false steady state instead of erroring.

Route both through the current sub-algorithm's traits, as the rest of the
integrator already does via get_current_isfsal, adding a matching
get_current_has_stiff_interpolation.

Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
@ChrisRackauckas
ChrisRackauckas marked this pull request as ready for review August 2, 2026 10:55
@ChrisRackauckas
ChrisRackauckas merged commit b809b2b into SciML:master Aug 2, 2026
226 of 253 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants