From b5f6ba0d35df3f139a2928847a0fb9f9092d2594 Mon Sep 17 00:00:00 2001 From: ChrisRackauckas-Claude Date: Sat, 4 Jul 2026 18:18:28 -0400 Subject: [PATCH] NonlinearSolveAlg: let inner solver refresh J when no W is reused MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #3703 changed compute_step! from recompute_jacobian = cache.W === nothing || (iter == 1 && cache.new_W) to recompute_jacobian = iter == 1 && (cache.W === nothing || cache.new_W) which freezes the inner NonlinearSolve Jacobian after the first outer iteration on the paths without a reusable W (MTK nlstep, DAE, OOP). The inner solve silently degrades to modified Newton while the outer η·ndz < κ termination keeps its full-Newton calibration, so each step carries an unconverged remainder independent of dt. Observable: the FBDF + NSA convergence order on the MTK Robertson nlstep tests collapsed from ~1.26/1.30 to 0.615 (l∞ error stops halving at dt = 2^-18), failing both order assertions on master. Restore the pre-#3703 condition for the W === nothing paths. The W-reuse (modified-Newton) path is unchanged: with a reused W the inner Jacobian is the ODE-side W by construction, so per-iteration refreshes remain pointless there. A/B on master: reverting only this condition turns test/modelingtoolkit/nlstep_tests.jl from 17 pass / 2 fail to 19 pass / 0 fail, with orders 1.2576 (< 0.3 bound) and 1.3035 (< 0.35 bound), and restores clean error halving under dt refinement. Co-Authored-By: Chris Rackauckas --- lib/OrdinaryDiffEqNonlinearSolve/src/newton.jl | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/lib/OrdinaryDiffEqNonlinearSolve/src/newton.jl b/lib/OrdinaryDiffEqNonlinearSolve/src/newton.jl index 3775ff134d1..8f8bcd185d3 100644 --- a/lib/OrdinaryDiffEqNonlinearSolve/src/newton.jl +++ b/lib/OrdinaryDiffEqNonlinearSolve/src/newton.jl @@ -145,7 +145,13 @@ end (; tstep, invγdt) = cache nlcache = nlsolver.cache.cache - recompute_jacobian = nlsolver.iter == 1 && (cache.W === nothing || cache.new_W) + # When no reusable W exists (nlstep/DAE/OOP), the inner NonlinearSolve must be + # allowed to refresh its Jacobian every outer iteration: freezing it after iter 1 + # degrades the inner solve to modified Newton while the outer η·ndz < κ test still + # assumes its calibration, leaving a dt-independent unconverged remainder per step + # (FBDF nlstep convergence order collapsed to ~0.6). With a reused W the Jacobian + # is the ODE-side W by construction, so refreshing beyond iter 1 is pointless. + recompute_jacobian = cache.W === nothing || (nlsolver.iter == 1 && cache.new_W) step!(nlcache; recompute_jacobian) nlsolver.ztmp = nlcache.u @@ -170,7 +176,13 @@ end nlstep_data = integrator.f.nlstep_data nlcache = nlsolver.cache.cache - recompute_jacobian = nlsolver.iter == 1 && (cache.W === nothing || cache.new_W) + # When no reusable W exists (nlstep/DAE/OOP), the inner NonlinearSolve must be + # allowed to refresh its Jacobian every outer iteration: freezing it after iter 1 + # degrades the inner solve to modified Newton while the outer η·ndz < κ test still + # assumes its calibration, leaving a dt-independent unconverged remainder per step + # (FBDF nlstep convergence order collapsed to ~0.6). With a reused W the Jacobian + # is the ODE-side W by construction, so refreshing beyond iter 1 is pointless. + recompute_jacobian = cache.W === nothing || (nlsolver.iter == 1 && cache.new_W) step!(nlcache; recompute_jacobian) if nlstep_data !== nothing