Skip to content

Fix NonlinearSolveAlg crashing/stalling with polyalgorithm inner solvers#3959

Open
singhharsh1708 wants to merge 1 commit into
SciML:masterfrom
singhharsh1708:fix-nonlinearsolvealg-polyalg-3859
Open

Fix NonlinearSolveAlg crashing/stalling with polyalgorithm inner solvers#3959
singhharsh1708 wants to merge 1 commit into
SciML:masterfrom
singhharsh1708:fix-nonlinearsolvealg-polyalg-3859

Conversation

@singhharsh1708

Copy link
Copy Markdown
Contributor

Fixes #3859.

Bug 1: crash

NonlinearSolvePolyAlgorithmCache (used when nlsolve wraps a polyalgorithm like RobustMultiNewton or FastShortcutNonlinearPolyalg) has no top-level u/fu/trace field — those live on whichever branch cache is currently active (cache.caches[cache.current]). newton.jl read cache.cache.u / nlcache.u / nlcache.fu / nlcache.trace directly in five places, throwing FieldError the moment a polyalgorithm was used:

solve(prob, TRBDF2(nlsolve = NonlinearSolveAlg(RobustMultiNewton(; autodiff=AutoForwardDiff()))))
# ERROR: FieldError: type NonlinearSolveBase.NonlinearSolvePolyAlgorithmCache has no field `u`

Fix: a small helper resolves to the active branch cache before reading any of these fields.

@inline function _active_nlcache(nlcache)
    hasfield(typeof(nlcache), :u) && return nlcache
    return nlcache.caches[nlcache.current]
end

hasfield on a concrete type is resolved at compile time, so this is a no-op branch for the non-polyalg path.

Bug 2: stale iterate reused forever after the first successful step

Even after fixing the field access, a polyalgorithm cache stops making progress after its first successful nonlinear solve. NonlinearSolvePolyAlgorithmCache's own InternalAPI.reinit! resets current/nsteps/stats but not force_stop/retcode. Once any branch converges once, force_stop stays true, so CommonSolve.step!'s not_terminated(cache) || return guard makes every later step! call on that cache a silent no-op — the outer NLSolver loop reads the unchanged iterate as an instantly-converged residual (ndz == 0) and moves on with a stale u.

Confirmed by instrumenting newton.jl locally: on a plain u' = -u decay ODE with RobustMultiNewton, the solve looked fine for the first TRBDF2 stage, then froze — every later stage returned the exact same u regardless of the actual predictor, and adaptive step sizes ballooned as the (falsely) near-zero residual fed back into the error controller.

Fix: reset the two fields explicitly after every reinit! call on a polyalgorithm cache, until this is fixed upstream in NonlinearSolveBase.jl:

@inline function _reset_polyalg_termination!(nlcache)
    if !hasfield(typeof(nlcache), :u)
        nlcache.force_stop = false
        nlcache.retcode = SciMLBase.ReturnCode.Default
    end
    return nothing
end

Verified

  • RobustMultiNewton and NewtonRaphson (single-alg control) on u' = -u, both fixed dt and adaptive: results match to ~14 digits.
  • Mildly stiff Robertson (tspan = (0, 1)) with RobustMultiNewton: matches an FBDF reference to 1.4e-5 relative error.
  • Existing OrdinaryDiffEqNonlinearSolve Core test group (all files under test/, 15 testsets, 500+ assertions) passes unchanged with this diff.

What this does not fix

_nsa_inner_converged in #3893 (open) reads nlsolver.cache.cache.retcode to gate a different false-convergence shortcut (a TrustRegion step that gets rejected leaves the iterate unmoved, which looks like convergence from a pure-displacement check). For a polyalgorithm cache that retcode is the exact field this PR resets — without this fix, #3893's gate is permanently defeated for polyalgorithms the same way the outer loop was. I checked the combination against the harder, fully-stiff Robertson case (tspan = (0, 1e5), the repro from #3859 itself): the two fixes together are still not sufficient there. That interaction needs its own investigation and is out of scope here — this PR is scoped to the crash and the reinit staleness that #3859 actually reports.

AI Disclosure

Claude assisted with this work.

NonlinearSolvePolyAlgorithmCache (RobustMultiNewton,
FastShortcutNonlinearPolyalg, etc.) has no top-level u/fu/trace field,
so every raw cache.cache.u-style read in newton.jl threw FieldError.
Its own reinit! also leaves force_stop/retcode set after a branch
converges once, which makes CommonSolve.step!'s not_terminated guard
silently no-op every later step and reuse a stale iterate.

Route all reads through a cache that resolves to the active branch,
and reset the two termination fields after each reinit!.
@singhharsh1708
singhharsh1708 force-pushed the fix-nonlinearsolvealg-polyalg-3859 branch from d1d3bff to 7dd8e8a Compare July 21, 2026 14:30
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.

NonlinearSolveAlg: NonlinearSolve polyalg inner solvers crash (FieldError: PolyAlgorithmCache has no field u)

1 participant