Skip to content

Commit c6c875f

Browse files
Merge pull request #277 from avik-pal/ap/nlls_defaults
Add a default for NLLS
2 parents 5e8149a + be6d69e commit c6c875f

8 files changed

+75
-29
lines changed

docs/src/api/nonlinearsolve.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ GeneralKlement
1616
## Polyalgorithms
1717

1818
```@docs
19+
NonlinearSolvePolyAlgorithm
1920
FastShortcutNonlinearPolyalg
21+
FastShortcutNLLSPolyalg
2022
RobustMultiNewton
2123
```
2224

docs/src/solvers/NonlinearLeastSquaresSolvers.md

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@ Solves the nonlinear least squares problem defined by `prob` using the algorithm
77

88
## Recommended Methods
99

10-
`LevenbergMarquardt` is a good choice for most problems.
10+
The default method `FastShortcutNLLSPolyalg` is a good choice for most
11+
problems. It is a polyalgorithm that attempts to use a fast algorithm
12+
(`GaussNewton`) and if that fails it falls back to a more robust
13+
algorithm (`LevenbergMarquardt`).
1114

1215
## Full List of Methods
1316

@@ -21,10 +24,3 @@ Solves the nonlinear least squares problem defined by `prob` using the algorithm
2124
problems.
2225
- `SimpleNewtonRaphson()`: Simple Gauss Newton Implementation with `QRFactorization` to
2326
solve a linear least squares problem at each step!
24-
25-
## Example usage
26-
27-
```julia
28-
using NonlinearSolve
29-
sol = solve(prob, LevenbergMarquardt())
30-
```

ext/NonlinearSolveFastLevenbergMarquardtExt.jl

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,23 +32,24 @@ end
3232
(f::InplaceFunction{false})(fx, x, p) = (fx .= f.f(x, p))
3333

3434
function SciMLBase.__init(prob::NonlinearLeastSquaresProblem,
35-
alg::FastLevenbergMarquardtJL, args...; abstol = 1e-8, reltol = 1e-8,
36-
verbose = false, maxiters = 1000, kwargs...)
35+
alg::FastLevenbergMarquardtJL, args...; alias_u0 = false, abstol = 1e-8,
36+
reltol = 1e-8, verbose = false, maxiters = 1000, kwargs...)
3737
iip = SciMLBase.isinplace(prob)
38+
u0 = alias_u0 ? prob.u0 : deepcopy(prob.u0)
3839

3940
@assert prob.f.jac!==nothing "FastLevenbergMarquardt requires a Jacobian!"
4041

4142
f! = InplaceFunction{iip}(prob.f)
4243
J! = InplaceFunction{iip}(prob.f.jac)
4344

4445
resid_prototype = prob.f.resid_prototype === nothing ?
45-
(!iip ? prob.f(prob.u0, prob.p) : zeros(prob.u0)) :
46+
(!iip ? prob.f(u0, prob.p) : zeros(u0)) :
4647
prob.f.resid_prototype
4748

48-
J = similar(prob.u0, length(resid_prototype), length(prob.u0))
49+
J = similar(u0, length(resid_prototype), length(u0))
4950

50-
solver = _fast_lm_solver(alg, prob.u0)
51-
LM = FastLM.LMWorkspace(prob.u0, resid_prototype, J)
51+
solver = _fast_lm_solver(alg, u0)
52+
LM = FastLM.LMWorkspace(u0, resid_prototype, J)
5253

5354
return FastLevenbergMarquardtJLCache(f!, J!, prob, alg, LM, solver,
5455
(; xtol = abstol, ftol = reltol, maxit = maxiters, alg.factor, alg.factoraccept,

ext/NonlinearSolveLeastSquaresOptimExt.jl

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,17 +33,19 @@ end
3333
(f::FunctionWrapper{false})(du, u) = (du .= f.f(u, f.p))
3434

3535
function SciMLBase.__init(prob::NonlinearLeastSquaresProblem, alg::LeastSquaresOptimJL,
36-
args...; abstol = 1e-8, reltol = 1e-8, verbose = false, maxiters = 1000, kwargs...)
36+
args...; alias_u0 = false, abstol = 1e-8, reltol = 1e-8, verbose = false,
37+
maxiters = 1000, kwargs...)
3738
iip = SciMLBase.isinplace(prob)
39+
u = alias_u0 ? prob.u0 : deepcopy(prob.u0)
3840

3941
f! = FunctionWrapper{iip}(prob.f, prob.p)
4042
g! = prob.f.jac === nothing ? nothing : FunctionWrapper{iip}(prob.f.jac, prob.p)
4143

4244
resid_prototype = prob.f.resid_prototype === nothing ?
43-
(!iip ? prob.f(prob.u0, prob.p) : zeros(prob.u0)) :
45+
(!iip ? prob.f(u, prob.p) : zeros(u)) :
4446
prob.f.resid_prototype
4547

46-
lsoprob = LSO.LeastSquaresProblem(; x = prob.u0, f!, y = resid_prototype, g!,
48+
lsoprob = LSO.LeastSquaresProblem(; x = u, f!, y = resid_prototype, g!,
4749
J = prob.f.jac_prototype, alg.autodiff, output_length = length(resid_prototype))
4850
allocated_prob = LSO.LeastSquaresProblemAllocated(lsoprob, _lso_solver(alg))
4951

src/NonlinearSolve.jl

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,8 @@ export RadiusUpdateSchemes
147147
export NewtonRaphson, TrustRegion, LevenbergMarquardt, DFSane, GaussNewton, PseudoTransient,
148148
GeneralBroyden, GeneralKlement, LimitedMemoryBroyden
149149
export LeastSquaresOptimJL, FastLevenbergMarquardtJL
150-
export NonlinearSolvePolyAlgorithm, RobustMultiNewton, FastShortcutNonlinearPolyalg
150+
export NonlinearSolvePolyAlgorithm,
151+
RobustMultiNewton, FastShortcutNonlinearPolyalg, FastShortcutNLLSPolyalg
151152

152153
export LineSearch, LiFukushimaLineSearch
153154

src/default.jl

Lines changed: 49 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -250,14 +250,57 @@ function FastShortcutNonlinearPolyalg(; concrete_jac = nothing, linsolve = nothi
250250
return NonlinearSolvePolyAlgorithm(algs, Val(:NLS))
251251
end
252252

253+
"""
254+
FastShortcutNLLSPolyalg(; concrete_jac = nothing, linsolve = nothing,
255+
precs = DEFAULT_PRECS, adkwargs...)
256+
257+
A polyalgorithm focused on balancing speed and robustness. It first tries less robust methods
258+
for more performance and then tries more robust techniques if the faster ones fail.
259+
260+
### Keyword Arguments
261+
262+
- `autodiff`: determines the backend used for the Jacobian. Note that this argument is
263+
ignored if an analytical Jacobian is passed, as that will be used instead. Defaults to
264+
`AutoForwardDiff()`. Valid choices are types from ADTypes.jl.
265+
- `concrete_jac`: whether to build a concrete Jacobian. If a Krylov-subspace method is used,
266+
then the Jacobian will not be constructed and instead direct Jacobian-vector products
267+
`J*v` are computed using forward-mode automatic differentiation or finite differencing
268+
tricks (without ever constructing the Jacobian). However, if the Jacobian is still needed,
269+
for example for a preconditioner, `concrete_jac = true` can be passed in order to force
270+
the construction of the Jacobian.
271+
- `linsolve`: the [LinearSolve.jl](https://github.com/SciML/LinearSolve.jl) used for the
272+
linear solves within the Newton method. Defaults to `nothing`, which means it uses the
273+
LinearSolve.jl default algorithm choice. For more information on available algorithm
274+
choices, see the [LinearSolve.jl documentation](https://docs.sciml.ai/LinearSolve/stable/).
275+
- `precs`: the choice of preconditioners for the linear solver. Defaults to using no
276+
preconditioners. For more information on specifying preconditioners for LinearSolve
277+
algorithms, consult the
278+
[LinearSolve.jl documentation](https://docs.sciml.ai/LinearSolve/stable/).
279+
"""
280+
function FastShortcutNLLSPolyalg(; concrete_jac = nothing, linsolve = nothing,
281+
precs = DEFAULT_PRECS, adkwargs...)
282+
algs = (GaussNewton(; concrete_jac, linsolve, precs, adkwargs...),
283+
GaussNewton(; concrete_jac, linsolve, precs, linesearch = BackTracking(),
284+
adkwargs...),
285+
LevenbergMarquardt(; concrete_jac, linsolve, precs, adkwargs...))
286+
return NonlinearSolvePolyAlgorithm(algs, Val(:NLLS))
287+
end
288+
253289
## Defaults
254290

255-
function SciMLBase.__init(prob::NonlinearProblem{uType, iip}, alg::Nothing, args...;
256-
kwargs...) where {uType, iip}
257-
SciMLBase.__init(prob, FastShortcutNonlinearPolyalg(), args...; kwargs...)
291+
function SciMLBase.__init(prob::NonlinearProblem, ::Nothing, args...; kwargs...)
292+
return SciMLBase.__init(prob, FastShortcutNonlinearPolyalg(), args...; kwargs...)
293+
end
294+
295+
function SciMLBase.__solve(prob::NonlinearProblem, ::Nothing, args...; kwargs...)
296+
return SciMLBase.__solve(prob, FastShortcutNonlinearPolyalg(), args...; kwargs...)
297+
end
298+
299+
function SciMLBase.__init(prob::NonlinearLeastSquaresProblem, ::Nothing, args...; kwargs...)
300+
return SciMLBase.__init(prob, FastShortcutNLLSPolyalg(), args...; kwargs...)
258301
end
259302

260-
function SciMLBase.__solve(prob::NonlinearProblem{uType, iip}, alg::Nothing, args...;
261-
kwargs...) where {uType, iip}
262-
SciMLBase.__solve(prob, FastShortcutNonlinearPolyalg(), args...; kwargs...)
303+
function SciMLBase.__solve(prob::NonlinearLeastSquaresProblem, ::Nothing, args...;
304+
kwargs...)
305+
return SciMLBase.__solve(prob, FastShortcutNLLSPolyalg(), args...; kwargs...)
263306
end

test/23_test_problems.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ end
8585
# Broyden and Klement Tests are quite flaky and failure seems to be platform dependent
8686
# needs additional investigation before we can enable them
8787
@testset "GeneralBroyden 23 Test Problems" begin
88-
alg_ops = (GeneralBroyden(),)
88+
alg_ops = (GeneralBroyden(; max_resets = 10),)
8989

9090
broken_tests = Dict(alg => Int[] for alg in alg_ops)
9191
broken_tests[alg_ops[1]] = [1, 2, 4, 5, 6, 11, 12, 13, 14]

test/nonlinear_least_squares.jl

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,12 @@ y_target = true_function(x, θ_true)
1212

1313
function loss_function(θ, p)
1414
= true_function(p, θ)
15-
return abs2.(.- y_target)
15+
return.- y_target
1616
end
1717

1818
function loss_function(resid, θ, p)
1919
true_function(resid, p, θ)
20-
resid .= abs2.(resid .- y_target)
20+
resid .= resid .- y_target
2121
return resid
2222
end
2323

@@ -36,6 +36,7 @@ append!(solvers,
3636
LevenbergMarquardt(; linsolve = LUFactorization()),
3737
LeastSquaresOptimJL(:lm),
3838
LeastSquaresOptimJL(:dogleg),
39+
nothing,
3940
])
4041

4142
for prob in nlls_problems, solver in solvers
@@ -45,7 +46,8 @@ for prob in nlls_problems, solver in solvers
4546
end
4647

4748
function jac!(J, θ, p)
48-
ForwardDiff.jacobian!(J, resid -> loss_function(resid, θ, p), θ)
49+
resid = zeros(length(p))
50+
ForwardDiff.jacobian!(J, (resid, θ) -> loss_function(resid, θ, p), resid, θ)
4951
return J
5052
end
5153

@@ -56,6 +58,5 @@ solvers = [FastLevenbergMarquardtJL(:cholesky), FastLevenbergMarquardtJL(:qr)]
5658

5759
for solver in solvers
5860
@time sol = solve(prob, solver; maxiters = 10000, abstol = 1e-8)
59-
@test SciMLBase.successful_retcode(sol)
6061
@test norm(sol.resid) < 1e-6
6162
end

0 commit comments

Comments
 (0)