Skip to content

Commit d434b8d

Browse files
Merge pull request #414 from SciML/ap/linearsolve_retcode
Robust Singular Jacobian Handling
2 parents b389d0e + 914f556 commit d434b8d

16 files changed

+259
-73
lines changed

Diff for: Project.toml

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name = "NonlinearSolve"
22
uuid = "8913a72c-1f9b-4ce2-8d82-65094dcecaec"
33
authors = ["SciML"]
4-
version = "3.10.1"
4+
version = "3.11.0"
55

66
[deps]
77
ADTypes = "47edcb42-4c32-4615-8424-f2b9edc5f35b"
@@ -60,7 +60,7 @@ NonlinearSolveZygoteExt = "Zygote"
6060
ADTypes = "0.2.6"
6161
Aqua = "0.8"
6262
ArrayInterface = "7.9"
63-
BandedMatrices = "1.4"
63+
BandedMatrices = "1.5"
6464
BenchmarkTools = "1.4"
6565
CUDA = "5.2"
6666
ConcreteStructs = "0.2.3"
@@ -76,7 +76,7 @@ LazyArrays = "1.8.2"
7676
LeastSquaresOptim = "0.8.5"
7777
LineSearches = "7.2"
7878
LinearAlgebra = "1.10"
79-
LinearSolve = "2.21"
79+
LinearSolve = "2.29"
8080
MINPACK = "1.2"
8181
MaybeInplace = "0.1.1"
8282
NLSolvers = "0.5"

Diff for: docs/src/devdocs/internal_interfaces.md

+6
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@ NonlinearSolve.AbstractDescentAlgorithm
1515
NonlinearSolve.AbstractDescentCache
1616
```
1717

18+
## Descent Results
19+
20+
```@docs
21+
NonlinearSolve.DescentResult
22+
```
23+
1824
## Approximate Jacobian
1925

2026
```@docs

Diff for: src/NonlinearSolve.jl

+5-4
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ import PrecompileTools: @recompile_invalidations, @compile_workload, @setup_work
1212
LinearAlgebra, LinearSolve, MaybeInplace, Preferences, Printf, SciMLBase,
1313
SimpleNonlinearSolve, SparseArrays, SparseDiffTools
1414

15-
import ArrayInterface: undefmatrix, can_setindex, restructure, fast_scalar_indexing,
16-
ismutable
15+
import ArrayInterface: ArrayInterface, undefmatrix, can_setindex, restructure,
16+
fast_scalar_indexing, ismutable
1717
import DiffEqBase: AbstractNonlinearTerminationMode,
1818
AbstractSafeNonlinearTerminationMode,
1919
AbstractSafeBestNonlinearTerminationMode,
@@ -50,6 +50,7 @@ include("adtypes.jl")
5050
include("timer_outputs.jl")
5151
include("internal/helpers.jl")
5252

53+
include("descent/common.jl")
5354
include("descent/newton.jl")
5455
include("descent/steepest.jl")
5556
include("descent/dogleg.jl")
@@ -135,10 +136,10 @@ include("default.jl")
135136

136137
@compile_workload begin
137138
for prob in probs_nls, alg in nls_algs
138-
solve(prob, alg; abstol = 1e-2)
139+
solve(prob, alg; abstol = 1e-2, verbose = false)
139140
end
140141
for prob in probs_nlls, alg in nlls_algs
141-
solve(prob, alg; abstol = 1e-2)
142+
solve(prob, alg; abstol = 1e-2, verbose = false)
142143
end
143144
end
144145
end

Diff for: src/abstract_types.jl

+2-7
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ Abstract Type for all Descent Caches.
6666
### `__internal_solve!` specification
6767
6868
```julia
69-
δu, success, intermediates = __internal_solve!(
69+
descent_result = __internal_solve!(
7070
cache::AbstractDescentCache, J, fu, u, idx::Val; skip_solve::Bool = false, kwargs...)
7171
```
7272
@@ -81,12 +81,7 @@ Abstract Type for all Descent Caches.
8181
8282
#### Returned values
8383
84-
- `δu`: the descent direction.
85-
- `success`: Certain Descent Algorithms can reject a descent direction for example
86-
`GeodesicAcceleration`.
87-
- `intermediates`: A named tuple containing intermediates computed during the solve.
88-
For example, `GeodesicAcceleration` returns `NamedTuple{(:v, :a)}` containing the
89-
"velocity" and "acceleration" terms.
84+
- `descent_result`: Result in a [`DescentResult`](@ref).
9085
9186
### Interface Functions
9287

Diff for: src/core/approximate_jacobian.jl

+32-9
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ end
114114
retcode::ReturnCode.T
115115
force_stop::Bool
116116
force_reinit::Bool
117+
kwargs
117118
end
118119

119120
store_inverse_jacobian(::ApproximateJacobianSolveCache{INV}) where {INV} = INV
@@ -211,10 +212,10 @@ function SciMLBase.__init(
211212

212213
return ApproximateJacobianSolveCache{INV, GB, iip, maxtime !== nothing}(
213214
fu, u, u_cache, p, du, J, alg, prob, initialization_cache,
214-
descent_cache, linesearch_cache, trustregion_cache,
215-
update_rule_cache, reinit_rule_cache, inv_workspace, 0, 0, 0,
216-
alg.max_resets, maxiters, maxtime, alg.max_shrink_times, 0, timer,
217-
0.0, termination_cache, trace, ReturnCode.Default, false, false)
215+
descent_cache, linesearch_cache, trustregion_cache, update_rule_cache,
216+
reinit_rule_cache, inv_workspace, 0, 0, 0, alg.max_resets,
217+
maxiters, maxtime, alg.max_shrink_times, 0, timer, 0.0,
218+
termination_cache, trace, ReturnCode.Default, false, false, kwargs)
218219
end
219220
end
220221

@@ -282,16 +283,38 @@ function __step!(cache::ApproximateJacobianSolveCache{INV, GB, iip};
282283
@static_timeit cache.timer "descent" begin
283284
if cache.trustregion_cache !== nothing &&
284285
hasfield(typeof(cache.trustregion_cache), :trust_region)
285-
δu, descent_success, descent_intermediates = __internal_solve!(
286+
descent_result = __internal_solve!(
286287
cache.descent_cache, J, cache.fu, cache.u; new_jacobian,
287-
trust_region = cache.trustregion_cache.trust_region)
288+
trust_region = cache.trustregion_cache.trust_region, cache.kwargs...)
288289
else
289-
δu, descent_success, descent_intermediates = __internal_solve!(
290-
cache.descent_cache, J, cache.fu, cache.u; new_jacobian)
290+
descent_result = __internal_solve!(
291+
cache.descent_cache, J, cache.fu, cache.u; new_jacobian, cache.kwargs...)
291292
end
292293
end
293294

294-
if descent_success
295+
if !descent_result.linsolve_success
296+
if new_jacobian && cache.steps_since_last_reset == 0
297+
# Extremely pathological case. Jacobian was just reset and linear solve
298+
# failed. Should ideally never happen in practice unless true jacobian init
299+
# is used.
300+
cache.retcode = LinearSolveFailureCode
301+
cache.force_stop = true
302+
return
303+
else
304+
# Force a reinit because the problem is currently un-solvable
305+
if !haskey(cache.kwargs, :verbose) || cache.kwargs[:verbose]
306+
@warn "Linear Solve Failed but Jacobian Information is not current. \
307+
Retrying with reinitialized Approximate Jacobian."
308+
end
309+
cache.force_reinit = true
310+
__step!(cache; recompute_jacobian = true)
311+
return
312+
end
313+
end
314+
315+
δu, descent_intermediates = descent_result.δu, descent_result.extras
316+
317+
if descent_result.success
295318
if GB === :LineSearch
296319
@static_timeit cache.timer "linesearch" begin
297320
needs_reset, α = __internal_solve!(cache.linesearch_cache, cache.u, δu)

Diff for: src/core/generalized_first_order.jl

+30-7
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ concrete_jac(::GeneralizedFirstOrderAlgorithm{CJ}) where {CJ} = CJ
111111
trace
112112
retcode::ReturnCode.T
113113
force_stop::Bool
114+
kwargs
114115
end
115116

116117
SymbolicIndexingInterface.state_values(cache::GeneralizedFirstOrderAlgorithmCache) = cache.u
@@ -201,8 +202,8 @@ function SciMLBase.__init(
201202

202203
return GeneralizedFirstOrderAlgorithmCache{iip, GB, maxtime !== nothing}(
203204
fu, u, u_cache, p, du, J, alg, prob, jac_cache, descent_cache, linesearch_cache,
204-
trustregion_cache, 0, 0, maxiters, maxtime, alg.max_shrink_times,
205-
timer, 0.0, true, termination_cache, trace, ReturnCode.Default, false)
205+
trustregion_cache, 0, 0, maxiters, maxtime, alg.max_shrink_times, timer,
206+
0.0, true, termination_cache, trace, ReturnCode.Default, false, kwargs)
206207
end
207208
end
208209

@@ -221,16 +222,38 @@ function __step!(cache::GeneralizedFirstOrderAlgorithmCache{iip, GB};
221222
@static_timeit cache.timer "descent" begin
222223
if cache.trustregion_cache !== nothing &&
223224
hasfield(typeof(cache.trustregion_cache), :trust_region)
224-
δu, descent_success, descent_intermediates = __internal_solve!(
225+
descent_result = __internal_solve!(
225226
cache.descent_cache, J, cache.fu, cache.u; new_jacobian,
226-
trust_region = cache.trustregion_cache.trust_region)
227+
trust_region = cache.trustregion_cache.trust_region, cache.kwargs...)
227228
else
228-
δu, descent_success, descent_intermediates = __internal_solve!(
229-
cache.descent_cache, J, cache.fu, cache.u; new_jacobian)
229+
descent_result = __internal_solve!(
230+
cache.descent_cache, J, cache.fu, cache.u; new_jacobian, cache.kwargs...)
230231
end
231232
end
232233

233-
if descent_success
234+
if !descent_result.linsolve_success
235+
if new_jacobian
236+
# Jacobian Information is current and linear solve failed terminate the solve
237+
cache.retcode = LinearSolveFailureCode
238+
cache.force_stop = true
239+
return
240+
else
241+
# Jacobian Information is not current and linear solve failed, recompute
242+
# Jacobian
243+
if !haskey(cache.kwargs, :verbose) || cache.kwargs[:verbose]
244+
@warn "Linear Solve Failed but Jacobian Information is not current. \
245+
Retrying with updated Jacobian."
246+
end
247+
# In the 2nd call the `new_jacobian` is guaranteed to be `true`.
248+
cache.make_new_jacobian = true
249+
__step!(cache; recompute_jacobian = true, kwargs...)
250+
return
251+
end
252+
end
253+
254+
δu, descent_intermediates = descent_result.δu, descent_result.extras
255+
256+
if descent_result.success
234257
cache.make_new_jacobian = true
235258
if GB === :LineSearch
236259
@static_timeit cache.timer "linesearch" begin

Diff for: src/core/spectral_methods.jl

+2-1
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ concrete_jac(::GeneralizedDFSane) = nothing
7474
trace
7575
retcode::ReturnCode.T
7676
force_stop::Bool
77+
kwargs
7778
end
7879

7980
function __reinit_internal!(
@@ -150,7 +151,7 @@ function SciMLBase.__init(prob::AbstractNonlinearProblem, alg::GeneralizedDFSane
150151
return GeneralizedDFSaneCache{isinplace(prob), maxtime !== nothing}(
151152
fu, fu_cache, u, u_cache, prob.p, du, alg, prob, σ_n, T(alg.σ_min),
152153
T(alg.σ_max), linesearch_cache, 0, 0, maxiters, maxtime,
153-
timer, 0.0, tc_cache, trace, ReturnCode.Default, false)
154+
timer, 0.0, tc_cache, trace, ReturnCode.Default, false, kwargs)
154155
end
155156
end
156157

Diff for: src/descent/common.jl

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
"""
2+
DescentResult(; δu = missing, u = missing, success::Bool = true,
3+
linsolve_success::Bool = true, extras = (;))
4+
5+
Construct a `DescentResult` object.
6+
7+
### Keyword Arguments
8+
9+
- `δu`: The descent direction.
10+
- `u`: The new iterate. This is provided only for multi-step methods currently.
11+
- `success`: Certain Descent Algorithms can reject a descent direction for example
12+
[`GeodesicAcceleration`](@ref).
13+
- `linsolve_success`: Whether the line search was successful.
14+
- `extras`: A named tuple containing intermediates computed during the solve.
15+
For example, [`GeodesicAcceleration`](@ref) returns `NamedTuple{(:v, :a)}` containing
16+
the "velocity" and "acceleration" terms.
17+
"""
18+
@concrete struct DescentResult
19+
δu
20+
u
21+
success::Bool
22+
linsolve_success::Bool
23+
extras
24+
end
25+
26+
function DescentResult(; δu = missing, u = missing, success::Bool = true,
27+
linsolve_success::Bool = true, extras = (;))
28+
@assert δu !== missing || u !== missing
29+
return DescentResult(δu, u, success, linsolve_success, extras)
30+
end

Diff for: src/descent/damped_newton.jl

+8-4
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ function __internal_solve!(cache::DampedNewtonDescentCache{INV, mode}, J, fu,
135135
u, idx::Val{N} = Val(1); skip_solve::Bool = false,
136136
new_jacobian::Bool = true, kwargs...) where {INV, N, mode}
137137
δu = get_du(cache, idx)
138-
skip_solve && return δu, true, (;)
138+
skip_solve && return DescentResult(; δu)
139139

140140
recompute_A = idx === Val(1)
141141

@@ -200,15 +200,19 @@ function __internal_solve!(cache::DampedNewtonDescentCache{INV, mode}, J, fu,
200200
end
201201

202202
@static_timeit cache.timer "linear solve" begin
203-
δu = cache.lincache(;
203+
linres = cache.lincache(;
204204
A, b, reuse_A_if_factorization = !new_jacobian && !recompute_A,
205205
kwargs..., linu = _vec(δu))
206-
δu = _restructure(get_du(cache, idx), δu)
206+
δu = _restructure(get_du(cache, idx), linres.u)
207+
if !linres.success
208+
set_du!(cache, δu, idx)
209+
return DescentResult(; δu, success = false, linsolve_success = false)
210+
end
207211
end
208212

209213
@bb @. δu *= -1
210214
set_du!(cache, δu, idx)
211-
return δu, true, (;)
215+
return DescentResult(; δu)
212216
end
213217

214218
# Define special concatenation for certain Array combinations

Diff for: src/descent/dogleg.jl

+7-7
Original file line numberDiff line numberDiff line change
@@ -81,14 +81,14 @@ function __internal_solve!(cache::DoglegCache{INV, NF}, J, fu, u, idx::Val{N} =
8181
want to use a Trust Region."
8282
δu = get_du(cache, idx)
8383
T = promote_type(eltype(u), eltype(fu))
84-
δu_newton, _, _ = __internal_solve!(
85-
cache.newton_cache, J, fu, u, idx; skip_solve, kwargs...)
84+
δu_newton = __internal_solve!(
85+
cache.newton_cache, J, fu, u, idx; skip_solve, kwargs...).δu
8686

8787
# Newton's Step within the trust region
8888
if cache.internalnorm(δu_newton) trust_region
8989
@bb copyto!(δu, δu_newton)
9090
set_du!(cache, δu, idx)
91-
return δu, true, (; δuJᵀJδu = T(NaN))
91+
return DescentResult(; δu, extras = (; δuJᵀJδu = T(NaN)))
9292
end
9393

9494
# Take intersection of steepest descent direction and trust region if Cauchy point lies
@@ -102,8 +102,8 @@ function __internal_solve!(cache::DoglegCache{INV, NF}, J, fu, u, idx::Val{N} =
102102
@bb cache.δu_cache_mul = JᵀJ × vec(δu_cauchy)
103103
δuJᵀJδu = __dot(δu_cauchy, cache.δu_cache_mul)
104104
else
105-
δu_cauchy, _, _ = __internal_solve!(
106-
cache.cauchy_cache, J, fu, u, idx; skip_solve, kwargs...)
105+
δu_cauchy = __internal_solve!(
106+
cache.cauchy_cache, J, fu, u, idx; skip_solve, kwargs...).δu
107107
J_ = INV ? inv(J) : J
108108
l_grad = cache.internalnorm(δu_cauchy)
109109
@bb cache.JᵀJ_cache = J × vec(δu_cauchy) # TODO: Rename
@@ -115,7 +115,7 @@ function __internal_solve!(cache::DoglegCache{INV, NF}, J, fu, u, idx::Val{N} =
115115
λ = trust_region / l_grad
116116
@bb @. δu = λ * δu_cauchy
117117
set_du!(cache, δu, idx)
118-
return δu, true, (; δuJᵀJδu = λ^2 * δuJᵀJδu)
118+
return DescentResult(; δu, extras = (; δuJᵀJδu = λ^2 * δuJᵀJδu))
119119
end
120120

121121
# FIXME: For anything other than 2-norm a quadratic root will give incorrect results
@@ -133,5 +133,5 @@ function __internal_solve!(cache::DoglegCache{INV, NF}, J, fu, u, idx::Val{N} =
133133

134134
@bb @. δu = cache.δu_cache_1 + τ * cache.δu_cache_2
135135
set_du!(cache, δu, idx)
136-
return δu, true, (; δuJᵀJδu = T(NaN))
136+
return DescentResult(; δu, extras = (; δuJᵀJδu = T(NaN)))
137137
end

Diff for: src/descent/geodesic_acceleration.jl

+6-6
Original file line numberDiff line numberDiff line change
@@ -105,9 +105,9 @@ end
105105
function __internal_solve!(cache::GeodesicAccelerationCache, J, fu, u, idx::Val{N} = Val(1);
106106
skip_solve::Bool = false, kwargs...) where {N}
107107
a, v, δu = get_acceleration(cache, idx), get_velocity(cache, idx), get_du(cache, idx)
108-
skip_solve && return δu, true, (; a, v)
109-
v, _, _ = __internal_solve!(
110-
cache.descent_cache, J, fu, u, Val(2N - 1); skip_solve, kwargs...)
108+
skip_solve && return DescentResult(; δu, extras = (; a, v))
109+
v = __internal_solve!(
110+
cache.descent_cache, J, fu, u, Val(2N - 1); skip_solve, kwargs...).δu
111111

112112
@bb @. cache.u_cache = u + cache.h * v
113113
cache.fu_cache = evaluate_f!!(cache.f, cache.fu_cache, cache.u_cache, cache.p)
@@ -116,8 +116,8 @@ function __internal_solve!(cache::GeodesicAccelerationCache, J, fu, u, idx::Val{
116116
Jv = _restructure(cache.fu_cache, cache.Jv)
117117
@bb @. cache.fu_cache = (2 / cache.h) * ((cache.fu_cache - fu) / cache.h - Jv)
118118

119-
a, _, _ = __internal_solve!(cache.descent_cache, J, cache.fu_cache, u, Val(2N);
120-
skip_solve, kwargs..., reuse_A_if_factorization = true)
119+
a = __internal_solve!(cache.descent_cache, J, cache.fu_cache, u, Val(2N);
120+
skip_solve, kwargs..., reuse_A_if_factorization = true).δu
121121

122122
norm_v = cache.internalnorm(v)
123123
norm_a = cache.internalnorm(a)
@@ -130,5 +130,5 @@ function __internal_solve!(cache::GeodesicAccelerationCache, J, fu, u, idx::Val{
130130
cache.last_step_accepted = false
131131
end
132132

133-
return δu, cache.last_step_accepted, (; a, v)
133+
return DescentResult(; δu, success = cache.last_step_accepted, extras = (; a, v))
134134
end

0 commit comments

Comments
 (0)