Skip to content

Commit 8a15dd6

Browse files
Merge pull request #218 from avik-pal/ap/nls2
NonlinearLeastSquaresProblem Solvers: Levenberg & Gauss Newton
2 parents cc04a70 + 9a579c8 commit 8a15dd6

19 files changed

+446
-197
lines changed

.github/workflows/CI.yml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,7 @@ jobs:
1616
strategy:
1717
matrix:
1818
group:
19-
- Core
20-
- 23TestProblems
19+
- All
2120
version:
2221
- '1'
2322
steps:
@@ -41,6 +40,8 @@ jobs:
4140
GROUP: ${{ matrix.group }}
4241
JULIA_NUM_THREADS: 11
4342
- uses: julia-actions/julia-processcoverage@v1
43+
with:
44+
directories: src,ext
4445
- uses: codecov/codecov-action@v3
4546
with:
4647
file: lcov.info

.github/workflows/Downstream.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ jobs:
2020
- {user: SciML, repo: ModelingToolkit.jl, group: All}
2121
- {user: SciML, repo: OrdinaryDiffEq.jl, group: Interface}
2222
- {user: SciML, repo: OrdinaryDiffEq.jl, group: Regression}
23+
- {user: SciML, repo: BoundaryValueDiffEq.jl, group: All}
2324
steps:
2425
- uses: actions/checkout@v4
2526
- uses: julia-actions/setup-julia@v1
@@ -50,6 +51,8 @@ jobs:
5051
exit(0) # Exit immediately, as a success
5152
end
5253
- uses: julia-actions/julia-processcoverage@v1
54+
with:
55+
directories: src,ext
5356
- uses: codecov/codecov-action@v3
5457
with:
5558
file: lcov.info

Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ NonlinearProblemLibrary = "0.1"
3939
PrecompileTools = "1"
4040
RecursiveArrayTools = "2"
4141
Reexport = "0.2, 1"
42-
SciMLBase = "1.97, 2"
42+
SciMLBase = "2.4"
4343
SimpleNonlinearSolve = "0.1"
4444
SparseDiffTools = "2.6"
4545
StaticArraysCore = "1.4"

docs/pages.jl

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ pages = ["index.md",
1212
"basics/FAQ.md"],
1313
"Solver Summaries and Recommendations" => Any["solvers/NonlinearSystemSolvers.md",
1414
"solvers/BracketingSolvers.md",
15-
"solvers/SteadyStateSolvers.md"],
15+
"solvers/SteadyStateSolvers.md",
16+
"solvers/NonlinearLeastSquaresSolvers.md"],
1617
"Detailed Solver APIs" => Any["api/nonlinearsolve.md",
1718
"api/simplenonlinearsolve.md",
1819
"api/minpack.md",

docs/src/api/nonlinearsolve.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ These are the native solvers of NonlinearSolve.jl.
77
```@docs
88
NewtonRaphson
99
TrustRegion
10+
LevenbergMarquardt
11+
GaussNewton
1012
```
1113

1214
## Radius Update Schemes for Trust Region (RadiusUpdateSchemes)
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# Nonlinear Least Squares Solvers
2+
3+
`solve(prob::NonlinearLeastSquaresProblem, alg; kwargs...)`
4+
5+
Solves the nonlinear least squares problem defined by `prob` using the algorithm
6+
`alg`. If no algorithm is given, a default algorithm will be chosen.
7+
8+
## Recommended Methods
9+
10+
`LevenbergMarquardt` is a good choice for most problems.
11+
12+
## Full List of Methods
13+
14+
- `LevenbergMarquardt()`: An advanced Levenberg-Marquardt implementation with the
15+
improvements suggested in the [paper](https://arxiv.org/abs/1201.5885) "Improvements to
16+
the Levenberg-Marquardt algorithm for nonlinear least-squares minimization". Designed for
17+
large-scale and numerically-difficult nonlinear systems.
18+
- `GaussNewton()`: An advanced GaussNewton implementation with support for efficient
19+
handling of sparse matrices via colored automatic differentiation and preconditioned
20+
linear solvers. Designed for large-scale and numerically-difficult nonlinear least squares
21+
problems.
22+
- `SimpleNewtonRaphson()`: Newton Raphson implementation that uses Linear Least Squares
23+
solution at every step to compute the descent direction. **WARNING**: This method is not
24+
a robust solver for nonlinear least squares problems. The computed delta step might not
25+
be the correct descent direction!
26+
27+
## Example usage
28+
29+
```julia
30+
using NonlinearSolve
31+
sol = solve(prob, LevenbergMarquardt())
32+
```

docs/src/solvers/NonlinearSystemSolvers.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@ features, but have a bit of overhead on very small problems.
4242
methods for high performance on large and sparse systems.
4343
- `TrustRegion()`: A Newton Trust Region dogleg method with swappable nonlinear solvers and
4444
autodiff methods for high performance on large and sparse systems.
45+
- `LevenbergMarquardt()`: An advanced Levenberg-Marquardt implementation with the
46+
improvements suggested in the [paper](https://arxiv.org/abs/1201.5885) "Improvements to
47+
the Levenberg-Marquardt algorithm for nonlinear least-squares minimization". Designed for
48+
large-scale and numerically-difficult nonlinear systems.
4549

4650
### SimpleNonlinearSolve.jl
4751

docs/src/tutorials/advanced.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -278,12 +278,12 @@ For more information on the preconditioner interface, see the
278278

279279
## Speed up Jacobian computation with sparsity exploitation and matrix coloring
280280

281-
To cut down the of Jacobian building overhead, we can choose to exploit the sparsity pattern and deploy matrix coloring during Jacobian construction. With NonlinearSolve.jl, we can simply use ```autodiff=AutoSparseForwardDiff()``` to automatically exploit the sparsity pattern of Jacobian matrices:
281+
To cut down the of Jacobian building overhead, we can choose to exploit the sparsity pattern and deploy matrix coloring during Jacobian construction. With NonlinearSolve.jl, we can simply use `autodiff=AutoSparseForwardDiff()` to automatically exploit the sparsity pattern of Jacobian matrices:
282282

283283
```@example ill_conditioned_nlprob
284284
@benchmark solve(prob_brusselator_2d,
285-
NewtonRaphson(linsolve=KrylovJL_GMRES(), precs=incompletelu, concrete_jac=true,
286-
autodiff=AutoSparseForwardDiff()));
285+
NewtonRaphson(linsolve = KrylovJL_GMRES(), precs = incompletelu, concrete_jac = true,
286+
autodiff = AutoSparseForwardDiff()));
287287
nothing # hide
288288
```
289289

@@ -292,11 +292,11 @@ To setup matrix coloring for the jacobian sparsity pattern, we can simply get th
292292
```@example ill_conditioned_nlprob
293293
using ArrayInterface
294294
colorvec = ArrayInterface.matrix_colors(jac_sparsity)
295-
ff = NonlinearFunction(brusselator_2d_loop; jac_prototype=float.(jac_sparsity), colorvec)
295+
ff = NonlinearFunction(brusselator_2d_loop; jac_prototype = float.(jac_sparsity), colorvec)
296296
prob_brusselator_2d_sparse = NonlinearProblem(ff, u0, p)
297297
298298
@benchmark solve(prob_brusselator_2d_sparse,
299-
NewtonRaphson(linsolve=KrylovJL_GMRES(), precs=incompletelu, concrete_jac=true,
300-
autodiff=AutoSparseForwardDiff()));
299+
NewtonRaphson(linsolve = KrylovJL_GMRES(), precs = incompletelu, concrete_jac = true,
300+
autodiff = AutoSparseForwardDiff()));
301301
nothing # hide
302-
```
302+
```

src/NonlinearSolve.jl

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,17 +28,43 @@ const AbstractSparseADType = Union{ADTypes.AbstractSparseFiniteDifferences,
2828
abstract type AbstractNonlinearSolveAlgorithm <: AbstractNonlinearAlgorithm end
2929
abstract type AbstractNewtonAlgorithm{CJ, AD} <: AbstractNonlinearSolveAlgorithm end
3030

31-
function SciMLBase.__solve(prob::NonlinearProblem, alg::AbstractNonlinearSolveAlgorithm,
32-
args...; kwargs...)
31+
abstract type AbstractNonlinearSolveCache{iip} end
32+
33+
isinplace(::AbstractNonlinearSolveCache{iip}) where {iip} = iip
34+
35+
function SciMLBase.__solve(prob::Union{NonlinearProblem, NonlinearLeastSquaresProblem},
36+
alg::AbstractNonlinearSolveAlgorithm, args...; kwargs...)
3337
cache = init(prob, alg, args...; kwargs...)
3438
return solve!(cache)
3539
end
3640

41+
function not_terminated(cache::AbstractNonlinearSolveCache)
42+
return !cache.force_stop && cache.stats.nsteps < cache.maxiters
43+
end
44+
get_fu(cache::AbstractNonlinearSolveCache) = cache.fu1
45+
46+
function SciMLBase.solve!(cache::AbstractNonlinearSolveCache)
47+
while not_terminated(cache)
48+
perform_step!(cache)
49+
cache.stats.nsteps += 1
50+
end
51+
52+
if cache.stats.nsteps == cache.maxiters
53+
cache.retcode = ReturnCode.MaxIters
54+
else
55+
cache.retcode = ReturnCode.Success
56+
end
57+
58+
return SciMLBase.build_solution(cache.prob, cache.alg, cache.u, get_fu(cache);
59+
cache.retcode, cache.stats)
60+
end
61+
3762
include("utils.jl")
3863
include("linesearch.jl")
3964
include("raphson.jl")
4065
include("trustRegion.jl")
4166
include("levenberg.jl")
67+
include("gaussnewton.jl")
4268
include("jacobian.jl")
4369
include("ad.jl")
4470

@@ -66,7 +92,7 @@ end
6692

6793
export RadiusUpdateSchemes
6894

69-
export NewtonRaphson, TrustRegion, LevenbergMarquardt
95+
export NewtonRaphson, TrustRegion, LevenbergMarquardt, GaussNewton
7096

7197
export LineSearch
7298

src/gaussnewton.jl

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
"""
2+
GaussNewton(; concrete_jac = nothing, linsolve = nothing, precs = DEFAULT_PRECS,
3+
adkwargs...)
4+
5+
An advanced GaussNewton implementation with support for efficient handling of sparse
6+
matrices via colored automatic differentiation and preconditioned linear solvers. Designed
7+
for large-scale and numerically-difficult nonlinear least squares problems.
8+
9+
!!! note
10+
In most practical situations, users should prefer using `LevenbergMarquardt` instead! It
11+
is a more general extension of `Gauss-Newton` Method.
12+
13+
### Keyword Arguments
14+
15+
- `autodiff`: determines the backend used for the Jacobian. Note that this argument is
16+
ignored if an analytical Jacobian is passed, as that will be used instead. Defaults to
17+
`AutoForwardDiff()`. Valid choices are types from ADTypes.jl.
18+
- `concrete_jac`: whether to build a concrete Jacobian. If a Krylov-subspace method is used,
19+
then the Jacobian will not be constructed and instead direct Jacobian-vector products
20+
`J*v` are computed using forward-mode automatic differentiation or finite differencing
21+
tricks (without ever constructing the Jacobian). However, if the Jacobian is still needed,
22+
for example for a preconditioner, `concrete_jac = true` can be passed in order to force
23+
the construction of the Jacobian.
24+
- `linsolve`: the [LinearSolve.jl](https://github.com/SciML/LinearSolve.jl) used for the
25+
linear solves within the Newton method. Defaults to `nothing`, which means it uses the
26+
LinearSolve.jl default algorithm choice. For more information on available algorithm
27+
choices, see the [LinearSolve.jl documentation](https://docs.sciml.ai/LinearSolve/stable/).
28+
- `precs`: the choice of preconditioners for the linear solver. Defaults to using no
29+
preconditioners. For more information on specifying preconditioners for LinearSolve
30+
algorithms, consult the
31+
[LinearSolve.jl documentation](https://docs.sciml.ai/LinearSolve/stable/).
32+
33+
!!! warning
34+
35+
Jacobian-Free version of `GaussNewton` doesn't work yet, and it forces jacobian
36+
construction. This will be fixed in the near future.
37+
"""
38+
@concrete struct GaussNewton{CJ, AD} <: AbstractNewtonAlgorithm{CJ, AD}
39+
ad::AD
40+
linsolve
41+
precs
42+
end
43+
44+
function GaussNewton(; concrete_jac = nothing, linsolve = NormalCholeskyFactorization(),
45+
precs = DEFAULT_PRECS, adkwargs...)
46+
ad = default_adargs_to_adtype(; adkwargs...)
47+
return GaussNewton{_unwrap_val(concrete_jac)}(ad, linsolve, precs)
48+
end
49+
50+
@concrete mutable struct GaussNewtonCache{iip} <: AbstractNonlinearSolveCache{iip}
51+
f
52+
alg
53+
u
54+
fu1
55+
fu2
56+
fu_new
57+
du
58+
p
59+
uf
60+
linsolve
61+
J
62+
JᵀJ
63+
Jᵀf
64+
jac_cache
65+
force_stop
66+
maxiters::Int
67+
internalnorm
68+
retcode::ReturnCode.T
69+
abstol
70+
prob
71+
stats::NLStats
72+
end
73+
74+
function SciMLBase.__init(prob::NonlinearLeastSquaresProblem{uType, iip}, alg::GaussNewton,
75+
args...; alias_u0 = false, maxiters = 1000, abstol = 1e-6, internalnorm = DEFAULT_NORM,
76+
kwargs...) where {uType, iip}
77+
@unpack f, u0, p = prob
78+
u = alias_u0 ? u0 : deepcopy(u0)
79+
if iip
80+
fu1 = f.resid_prototype === nothing ? zero(u) : f.resid_prototype
81+
f(fu1, u, p)
82+
else
83+
fu1 = f(u, p)
84+
end
85+
uf, linsolve, J, fu2, jac_cache, du, JᵀJ, Jᵀf = jacobian_caches(alg, f, u, p, Val(iip);
86+
linsolve_with_JᵀJ = Val(true))
87+
88+
return GaussNewtonCache{iip}(f, alg, u, fu1, fu2, zero(fu1), du, p, uf, linsolve, J,
89+
JᵀJ, Jᵀf, jac_cache, false, maxiters, internalnorm, ReturnCode.Default, abstol,
90+
prob, NLStats(1, 0, 0, 0, 0))
91+
end
92+
93+
function perform_step!(cache::GaussNewtonCache{true})
94+
@unpack u, fu1, f, p, alg, J, JᵀJ, Jᵀf, linsolve, du = cache
95+
jacobian!!(J, cache)
96+
mul!(JᵀJ, J', J)
97+
mul!(Jᵀf, J', fu1)
98+
99+
# u = u - J \ fu
100+
linres = dolinsolve(alg.precs, linsolve; A = JᵀJ, b = _vec(Jᵀf), linu = _vec(du),
101+
p, reltol = cache.abstol)
102+
cache.linsolve = linres.cache
103+
@. u = u - du
104+
f(cache.fu_new, u, p)
105+
106+
(cache.internalnorm(cache.fu_new .- cache.fu1) < cache.abstol ||
107+
cache.internalnorm(cache.fu_new) < cache.abstol) &&
108+
(cache.force_stop = true)
109+
cache.fu1 .= cache.fu_new
110+
cache.stats.nf += 1
111+
cache.stats.njacs += 1
112+
cache.stats.nsolve += 1
113+
cache.stats.nfactors += 1
114+
return nothing
115+
end
116+
117+
function perform_step!(cache::GaussNewtonCache{false})
118+
@unpack u, fu1, f, p, alg, linsolve = cache
119+
120+
cache.J = jacobian!!(cache.J, cache)
121+
122+
cache.JᵀJ = cache.J' * cache.J
123+
cache.Jᵀf = cache.J' * fu1
124+
# u = u - J \ fu
125+
if linsolve === nothing
126+
cache.du = fu1 / cache.J
127+
else
128+
linres = dolinsolve(alg.precs, linsolve; A = cache.JᵀJ, b = _vec(cache.Jᵀf),
129+
linu = _vec(cache.du), p, reltol = cache.abstol)
130+
cache.linsolve = linres.cache
131+
end
132+
cache.u = @. u - cache.du # `u` might not support mutation
133+
cache.fu_new = f(cache.u, p)
134+
135+
(cache.internalnorm(cache.fu_new) < cache.abstol) && (cache.force_stop = true)
136+
cache.fu1 = cache.fu_new
137+
cache.stats.nf += 1
138+
cache.stats.njacs += 1
139+
cache.stats.nsolve += 1
140+
cache.stats.nfactors += 1
141+
return nothing
142+
end
143+
144+
function SciMLBase.reinit!(cache::GaussNewtonCache{iip}, u0 = cache.u; p = cache.p,
145+
abstol = cache.abstol, maxiters = cache.maxiters) where {iip}
146+
cache.p = p
147+
if iip
148+
recursivecopy!(cache.u, u0)
149+
cache.f(cache.fu1, cache.u, p)
150+
else
151+
# don't have alias_u0 but cache.u is never mutated for OOP problems so it doesn't matter
152+
cache.u = u0
153+
cache.fu1 = cache.f(cache.u, p)
154+
end
155+
cache.abstol = abstol
156+
cache.maxiters = maxiters
157+
cache.stats.nf = 1
158+
cache.stats.nsteps = 1
159+
cache.force_stop = false
160+
cache.retcode = ReturnCode.Default
161+
return cache
162+
end

0 commit comments

Comments
 (0)