Skip to content

Commit 9ae749a

Browse files
singhharsh1708Your Name
authored andcommitted
NSA: gate iter-1 convergence on inner solver retcode (#3817)
1 parent 0464b30 commit 9ae749a

3 files changed

Lines changed: 47 additions & 3 deletions

File tree

lib/OrdinaryDiffEqNonlinearSolve/src/OrdinaryDiffEqNonlinearSolve.jl

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ import DiffEqBase: OrdinaryDiffEqTag, calculate_residuals, calculate_residuals!,
1414
BrownFullBasicInit, ShampineCollocationInit
1515
import ConstructionBase
1616
import PreallocationTools: DiffCache, get_tmp
17-
using SimpleNonlinearSolve: SimpleTrustRegion, SimpleGaussNewton
17+
using SimpleNonlinearSolve: SimpleTrustRegion, SimpleGaussNewton,
18+
AbstractSimpleNonlinearSolveAlgorithm
1819
using NonlinearSolve: FastShortcutNonlinearPolyalg, FastShortcutNLLSPolyalg, NewtonRaphson,
1920
HomotopySweep, step!
2021
# The operator Jacobian path is implemented in NonlinearSolveBase and needs its own floor.

lib/OrdinaryDiffEqNonlinearSolve/src/nlsolve.jl

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,21 @@
11
@inline eps_around_one::T) where {T} = 100sqrt(eps(one(θ)))
22

3+
# The `iter == 1 && ndz < 1e-5` shortcut below assumes a plain (modified-)Newton
4+
# contraction: a tiny first-iterate displacement means the initial guess was
5+
# already the solution. For `NonlinearSolveAlg` backed by a globalized inner
6+
# solver (e.g. TrustRegion), a tiny displacement can instead mean the inner
7+
# solver took a cautious/rejected trial step and has not decided anything yet —
8+
# accepting that as convergence applies zero correction and silently produces
9+
# the wrong stage value (#3817). Only the inner cache's own retcode
10+
# distinguishes "genuinely done already" from "hasn't started converging".
11+
# Iterations beyond the first use `η`/`θ` computed from actual step history and
12+
# are not vulnerable to this false positive, so they are left untouched.
13+
_nsa_inner_converged(nlsolver) = true
14+
function _nsa_inner_converged(nlsolver::NLSolver{<:NonlinearSolveAlg})
15+
nlsolver.alg.alg isa AbstractSimpleNonlinearSolveAlgorithm && return true
16+
return SciMLBase.successful_retcode(nlsolver.cache.cache.retcode)
17+
end
18+
319
"""
420
nlsolve!(nlsolver::AbstractNLSolver, integrator)
521
@@ -136,7 +152,7 @@ function nlsolve!(
136152
)
137153
)
138154
)
139-
if (iter == 1 && ndz < 1.0e-5) ||
155+
if (iter == 1 && ndz < 1.0e-5 && _nsa_inner_converged(nlsolver)) ||
140156
(check_η_convergence && η >= zero(η) && η * ndz < κ)
141157
@SciMLMessage(
142158
lazy"Newton iteration converged in $(iter) iterations: η = $(η), ndz = $(ndz)",

lib/OrdinaryDiffEqNonlinearSolve/test/nsa_jacobian_reuse_tests.jl

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
using OrdinaryDiffEqBDF, OrdinaryDiffEqSDIRK
22
using OrdinaryDiffEqNonlinearSolve
33
using OrdinaryDiffEqNonlinearSolve: NonlinearSolveAlg
4-
using NonlinearSolve: NewtonRaphson
4+
using NonlinearSolve: NewtonRaphson, TrustRegion
55
using ADTypes, LinearAlgebra, SciMLBase
66
using Test
77

@@ -48,3 +48,30 @@ refsol = solve(prob, FBDF(); reltol = 1.0e-12, abstol = 1.0e-14)
4848
@test JAC_CALLS[] >= 1
4949
end
5050
end
51+
52+
@testset "globalized inner solver does not converge on a rejected step" begin
53+
# A TrustRegion step that is rejected/truncated leaves the iterate nearly
54+
# unmoved, which the outer displacement test alone reads as convergence — the
55+
# stage is then accepted with no correction applied (#3817). Driven at a dt
56+
# that is far too coarse for the Robertson transient, so the inner solves
57+
# genuinely cannot converge and the failure must be reported rather than
58+
# silently absorbed.
59+
nsa_tr = NonlinearSolveAlg(TrustRegion(; autodiff = AutoForwardDiff()))
60+
nsa_nr = NonlinearSolveAlg(NewtonRaphson(; autodiff = AutoForwardDiff()))
61+
hard = ODEProblem(f, [1.0, 0.0, 0.0], (0.0, 1.0e3), [0.04, 3.0e7, 1.0e4])
62+
63+
sol_tr = solve(hard, FBDF(nlsolve = nsa_tr); dt = 1.0, adaptive = false)
64+
sol_nr = solve(hard, FBDF(nlsolve = nsa_nr); dt = 1.0, adaptive = false)
65+
66+
# The state must not be reported as a successful solve while frozen at u0.
67+
@test !(SciMLBase.successful_retcode(sol_tr) && sol_tr.u[end] == hard.u0)
68+
# TrustRegion must reach the same verdict as the non-globalized inner solver.
69+
@test SciMLBase.successful_retcode(sol_tr) == SciMLBase.successful_retcode(sol_nr)
70+
end
71+
72+
@testset "TrustRegion matches NewtonRaphson when the solves do converge" begin
73+
nsa_tr = NonlinearSolveAlg(TrustRegion(; autodiff = AutoForwardDiff()))
74+
sol = solve(prob, FBDF(nlsolve = nsa_tr); reltol = 1.0e-8, abstol = 1.0e-10)
75+
@test SciMLBase.successful_retcode(sol)
76+
@test norm(sol.u[end] .- refsol.u[end]) / norm(refsol.u[end]) < 1.0e-4
77+
end

0 commit comments

Comments
 (0)