Skip to content

Handle complex numbers correctly by default #334

New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Merged
merged 1 commit into from
Dec 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions Project.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name = "NonlinearSolve"
uuid = "8913a72c-1f9b-4ce2-8d82-65094dcecaec"
authors = ["SciML"]
version = "3.1.1"
version = "3.1.2"

[deps]
ADTypes = "47edcb42-4c32-4615-8424-f2b9edc5f35b"
Expand Down Expand Up @@ -71,6 +71,7 @@ MaybeInplace = "0.1.1"
NLsolve = "4.5"
NaNMath = "1"
NonlinearProblemLibrary = "0.1.1"
OrdinaryDiffEq = "6"
Pkg = "1"
PrecompileTools = "1.2"
Printf = "<0.0.1, 1"
Expand Down Expand Up @@ -106,6 +107,7 @@ MINPACK = "4854310b-de5a-5eb6-a2a5-c1dee2bd17f9"
NLsolve = "2774e3e8-f4cf-5e23-947b-6d7e65073b56"
NaNMath = "77ba4419-2d1f-58cd-9bb1-8ffee604a2e3"
NonlinearProblemLibrary = "b7050fa9-e91f-4b37-bcee-a89a063da141"
OrdinaryDiffEq = "1dea7af3-3e70-54e6-95c3-0bf5283fa5ed"
Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f"
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
SafeTestsets = "1bc83da4-3b8d-516f-aca4-4fe02f6d838f"
Expand All @@ -117,4 +119,4 @@ Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
Zygote = "e88e6eb3-aa80-5325-afca-941959d7151f"

[targets]
test = ["Aqua", "Enzyme", "BenchmarkTools", "SafeTestsets", "Pkg", "Test", "ForwardDiff", "StaticArrays", "Symbolics", "LinearSolve", "Random", "LinearAlgebra", "Zygote", "SparseDiffTools", "NonlinearProblemLibrary", "LeastSquaresOptim", "FastLevenbergMarquardt", "NaNMath", "BandedMatrices", "DiffEqBase", "StableRNGs", "MINPACK", "NLsolve"]
test = ["Aqua", "Enzyme", "BenchmarkTools", "SafeTestsets", "Pkg", "Test", "ForwardDiff", "StaticArrays", "Symbolics", "LinearSolve", "Random", "LinearAlgebra", "Zygote", "SparseDiffTools", "NonlinearProblemLibrary", "LeastSquaresOptim", "FastLevenbergMarquardt", "NaNMath", "BandedMatrices", "DiffEqBase", "StableRNGs", "MINPACK", "NLsolve", "OrdinaryDiffEq"]
166 changes: 110 additions & 56 deletions src/default.jl
Original file line number Diff line number Diff line change
Expand Up @@ -165,8 +165,8 @@ function SciMLBase.reinit!(cache::NonlinearSolvePolyAlgorithmCache, args...; kwa
end

"""
RobustMultiNewton(; concrete_jac = nothing, linsolve = nothing, precs = DEFAULT_PRECS,
autodiff = nothing)
RobustMultiNewton(::Type{T} = Float64; concrete_jac = nothing, linsolve = nothing,
precs = DEFAULT_PRECS, autodiff = nothing)

A polyalgorithm focused on robustness. It uses a mixture of Newton methods with different
globalizing techniques (trust region updates, line searches, etc.) in order to find a
Expand All @@ -176,6 +176,11 @@ Basically, if this algorithm fails, then "most" good ways of solving your proble
you may need to think about reformulating the model (either there is an issue with the model,
or more precision / more stable linear solver choice is required).

### Arguments

- `T`: The eltype of the initial guess. It is only used to check if some of the algorithms
are compatible with the problem type. Defaults to `Float64`.

### Keyword Arguments

- `autodiff`: determines the backend used for the Jacobian. Note that this argument is
Expand All @@ -196,28 +201,38 @@ or more precision / more stable linear solver choice is required).
algorithms, consult the
[LinearSolve.jl documentation](https://docs.sciml.ai/LinearSolve/stable/).
"""
function RobustMultiNewton(; concrete_jac = nothing, linsolve = nothing,
precs = DEFAULT_PRECS, autodiff = nothing)
algs = (TrustRegion(; concrete_jac, linsolve, precs),
TrustRegion(; concrete_jac, linsolve, precs, autodiff,
radius_update_scheme = RadiusUpdateSchemes.Bastin),
NewtonRaphson(; concrete_jac, linsolve, precs, linesearch = BackTracking(),
autodiff),
TrustRegion(; concrete_jac, linsolve, precs,
radius_update_scheme = RadiusUpdateSchemes.NLsolve, autodiff),
TrustRegion(; concrete_jac, linsolve, precs,
radius_update_scheme = RadiusUpdateSchemes.Fan, autodiff))
function RobustMultiNewton(::Type{T} = Float64; concrete_jac = nothing, linsolve = nothing,
precs = DEFAULT_PRECS, autodiff = nothing) where {T}
if __is_complex(T)
# Let's atleast have something here for complex numbers
algs = (NewtonRaphson(; concrete_jac, linsolve, precs, autodiff),)
else
algs = (TrustRegion(; concrete_jac, linsolve, precs),
TrustRegion(; concrete_jac, linsolve, precs, autodiff,
radius_update_scheme = RadiusUpdateSchemes.Bastin),
NewtonRaphson(; concrete_jac, linsolve, precs, linesearch = BackTracking(),
autodiff),
TrustRegion(; concrete_jac, linsolve, precs,
radius_update_scheme = RadiusUpdateSchemes.NLsolve, autodiff),
TrustRegion(; concrete_jac, linsolve, precs,
radius_update_scheme = RadiusUpdateSchemes.Fan, autodiff))
end
return NonlinearSolvePolyAlgorithm(algs, Val(:NLS))
end

"""
FastShortcutNonlinearPolyalg(; concrete_jac = nothing, linsolve = nothing,
precs = DEFAULT_PRECS, must_use_jacobian::Val = Val(false),
prefer_simplenonlinearsolve::Val{SA} = Val(false), autodiff = nothing)
FastShortcutNonlinearPolyalg(::Type{T} = Float64; concrete_jac = nothing,
linsolve = nothing, precs = DEFAULT_PRECS, must_use_jacobian::Val = Val(false),
prefer_simplenonlinearsolve::Val{SA} = Val(false), autodiff = nothing) where {T}

A polyalgorithm focused on balancing speed and robustness. It first tries less robust methods
for more performance and then tries more robust techniques if the faster ones fail.

### Arguments

- `T`: The eltype of the initial guess. It is only used to check if some of the algorithms
are compatible with the problem type. Defaults to `Float64`.

### Keyword Arguments

- `autodiff`: determines the backend used for the Jacobian. Note that this argument is
Expand All @@ -238,53 +253,76 @@ for more performance and then tries more robust techniques if the faster ones fa
algorithms, consult the
[LinearSolve.jl documentation](https://docs.sciml.ai/LinearSolve/stable/).
"""
function FastShortcutNonlinearPolyalg(; concrete_jac = nothing, linsolve = nothing,
precs = DEFAULT_PRECS, must_use_jacobian::Val{JAC} = Val(false),
function FastShortcutNonlinearPolyalg(::Type{T} = Float64; concrete_jac = nothing,
linsolve = nothing, precs = DEFAULT_PRECS, must_use_jacobian::Val{JAC} = Val(false),
prefer_simplenonlinearsolve::Val{SA} = Val(false),
autodiff = nothing) where {JAC, SA}
autodiff = nothing) where {T, JAC, SA}
if JAC
algs = (NewtonRaphson(; concrete_jac, linsolve, precs, autodiff),
NewtonRaphson(; concrete_jac, linsolve, precs, linesearch = BackTracking(),
autodiff),
TrustRegion(; concrete_jac, linsolve, precs, autodiff),
TrustRegion(; concrete_jac, linsolve, precs,
radius_update_scheme = RadiusUpdateSchemes.Bastin, autodiff))
else
# SimpleNewtonRaphson and SimpleTrustRegion are not robust to singular Jacobians
# and thus are not included in the polyalgorithm
if SA
algs = (SimpleBroyden(),
Broyden(; init_jacobian = Val(:true_jacobian)),
SimpleKlement(),
NewtonRaphson(; concrete_jac, linsolve, precs, autodiff),
NewtonRaphson(; concrete_jac, linsolve, precs, linesearch = BackTracking(),
autodiff),
NewtonRaphson(; concrete_jac, linsolve, precs, linesearch = BackTracking(),
autodiff),
TrustRegion(; concrete_jac, linsolve, precs,
radius_update_scheme = RadiusUpdateSchemes.Bastin, autodiff))
if __is_complex(T)
algs = (NewtonRaphson(; concrete_jac, linsolve, precs, autodiff),)
else
algs = (Broyden(),
Broyden(; init_jacobian = Val(:true_jacobian)),
Klement(; linsolve, precs),
NewtonRaphson(; concrete_jac, linsolve, precs, autodiff),
algs = (NewtonRaphson(; concrete_jac, linsolve, precs, autodiff),
NewtonRaphson(; concrete_jac, linsolve, precs, linesearch = BackTracking(),
autodiff),
TrustRegion(; concrete_jac, linsolve, precs, autodiff),
TrustRegion(; concrete_jac, linsolve, precs,
radius_update_scheme = RadiusUpdateSchemes.Bastin, autodiff))
end
else
# SimpleNewtonRaphson and SimpleTrustRegion are not robust to singular Jacobians
# and thus are not included in the polyalgorithm
if SA
if __is_complex(T)
algs = (SimpleBroyden(),
Broyden(; init_jacobian = Val(:true_jacobian)),
SimpleKlement(),
NewtonRaphson(; concrete_jac, linsolve, precs, autodiff))
else
algs = (SimpleBroyden(),
Broyden(; init_jacobian = Val(:true_jacobian)),
SimpleKlement(),
NewtonRaphson(; concrete_jac, linsolve, precs, autodiff),
NewtonRaphson(; concrete_jac, linsolve, precs,
linesearch = BackTracking(), autodiff),
NewtonRaphson(; concrete_jac, linsolve, precs,
linesearch = BackTracking(), autodiff),
TrustRegion(; concrete_jac, linsolve, precs,
radius_update_scheme = RadiusUpdateSchemes.Bastin, autodiff))
end
else
if __is_complex(T)
algs = (Broyden(),
Broyden(; init_jacobian = Val(:true_jacobian)),
Klement(; linsolve, precs),
NewtonRaphson(; concrete_jac, linsolve, precs, autodiff))
else
algs = (Broyden(),
Broyden(; init_jacobian = Val(:true_jacobian)),
Klement(; linsolve, precs),
NewtonRaphson(; concrete_jac, linsolve, precs, autodiff),
NewtonRaphson(; concrete_jac, linsolve, precs,
linesearch = BackTracking(), autodiff),
TrustRegion(; concrete_jac, linsolve, precs, autodiff),
TrustRegion(; concrete_jac, linsolve, precs,
radius_update_scheme = RadiusUpdateSchemes.Bastin, autodiff))
end
end
end
return NonlinearSolvePolyAlgorithm(algs, Val(:NLS))
end

"""
FastShortcutNLLSPolyalg(; concrete_jac = nothing, linsolve = nothing,
precs = DEFAULT_PRECS, kwargs...)
FastShortcutNLLSPolyalg(::Type{T} = Float64; concrete_jac = nothing, linsolve = nothing,
precs = DEFAULT_PRECS, kwargs...)

A polyalgorithm focused on balancing speed and robustness. It first tries less robust methods
for more performance and then tries more robust techniques if the faster ones fail.

### Arguments

- `T`: The eltype of the initial guess. It is only used to check if some of the algorithms
are compatible with the problem type. Defaults to `Float64`.

### Keyword Arguments

- `autodiff`: determines the backend used for the Jacobian. Note that this argument is
Expand All @@ -305,42 +343,58 @@ for more performance and then tries more robust techniques if the faster ones fa
algorithms, consult the
[LinearSolve.jl documentation](https://docs.sciml.ai/LinearSolve/stable/).
"""
function FastShortcutNLLSPolyalg(; concrete_jac = nothing, linsolve = nothing,
precs = DEFAULT_PRECS, kwargs...)
algs = (GaussNewton(; concrete_jac, linsolve, precs, kwargs...),
GaussNewton(; concrete_jac, linsolve, precs, linesearch = BackTracking(),
kwargs...),
LevenbergMarquardt(; concrete_jac, linsolve, precs, kwargs...))
function FastShortcutNLLSPolyalg(::Type{T} = Float64; concrete_jac = nothing,
linsolve = nothing, precs = DEFAULT_PRECS, kwargs...) where {T}
if __is_complex(T)
algs = (GaussNewton(; concrete_jac, linsolve, precs, kwargs...),
LevenbergMarquardt(; concrete_jac, linsolve, precs, kwargs...))
else
algs = (GaussNewton(; concrete_jac, linsolve, precs, kwargs...),
TrustRegion(; concrete_jac, linsolve, precs, kwargs...),
GaussNewton(; concrete_jac, linsolve, precs, linesearch = BackTracking(),
kwargs...),
TrustRegion(; concrete_jac, linsolve, precs,
radius_update_scheme = RadiusUpdateSchemes.Bastin, kwargs...),
LevenbergMarquardt(; concrete_jac, linsolve, precs, kwargs...))
end
return NonlinearSolvePolyAlgorithm(algs, Val(:NLLS))
end

## Defaults

## TODO: In the long run we want to use an `Assumptions` API like LinearSolve to specify
## the conditioning of the Jacobian and such

## TODO: Currently some of the algorithms like LineSearches / TrustRegion don't support
## complex numbers. We should use the `DiffEqBase` trait for this once all of the
## NonlinearSolve algorithms support it. For now we just do a check and remove the
## unsupported ones from default

## Defaults to a fast and robust poly algorithm in most cases. If the user went through
## the trouble of specifying a custom jacobian function, we should use algorithms that
## can use that!

function SciMLBase.__init(prob::NonlinearProblem, ::Nothing, args...; kwargs...)
must_use_jacobian = Val(prob.f.jac !== nothing)
return SciMLBase.__init(prob, FastShortcutNonlinearPolyalg(; must_use_jacobian),
return SciMLBase.__init(prob,
FastShortcutNonlinearPolyalg(eltype(prob.u0); must_use_jacobian),
args...; kwargs...)
end

function SciMLBase.__solve(prob::NonlinearProblem, ::Nothing, args...; kwargs...)
must_use_jacobian = Val(prob.f.jac !== nothing)
prefer_simplenonlinearsolve = Val(prob.u0 isa SArray)
return SciMLBase.__solve(prob,
FastShortcutNonlinearPolyalg(; must_use_jacobian,
FastShortcutNonlinearPolyalg(eltype(prob.u0); must_use_jacobian,
prefer_simplenonlinearsolve), args...; kwargs...)
end

function SciMLBase.__init(prob::NonlinearLeastSquaresProblem, ::Nothing, args...; kwargs...)
return SciMLBase.__init(prob, FastShortcutNLLSPolyalg(), args...; kwargs...)
return SciMLBase.__init(prob, FastShortcutNLLSPolyalg(eltype(prob.u0)), args...;
kwargs...)
end

function SciMLBase.__solve(prob::NonlinearLeastSquaresProblem, ::Nothing, args...;
kwargs...)
return SciMLBase.__solve(prob, FastShortcutNLLSPolyalg(), args...; kwargs...)
return SciMLBase.__solve(prob, FastShortcutNLLSPolyalg(eltype(prob.u0)), args...;
kwargs...)
end
5 changes: 5 additions & 0 deletions src/utils.jl
Original file line number Diff line number Diff line change
Expand Up @@ -494,3 +494,8 @@ end
@inline __diag(x::AbstractMatrix) = diag(x)
@inline __diag(x::AbstractVector) = x
@inline __diag(x::Number) = x

@inline __is_complex(::Type{ComplexF64}) = true
@inline __is_complex(::Type{ComplexF32}) = true
@inline __is_complex(::Type{Complex}) = true
@inline __is_complex(::Type{T}) where {T} = false
27 changes: 26 additions & 1 deletion test/polyalgs.jl
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using NonlinearSolve, Test, NaNMath
using NonlinearSolve, Test, NaNMath, OrdinaryDiffEq

f(u, p) = u .* u .- 2
u0 = [1.0, 1.0]
Expand Down Expand Up @@ -58,3 +58,28 @@ p = 2.0
prob = NonlinearProblem(ff_interval, u0, p)
sol = solve(prob; abstol = 1e-9)
@test SciMLBase.successful_retcode(sol)

# Shooting Problem: Taken from BoundaryValueDiffEq.jl
# Testing for Complex Valued Root Finding. For Complex valued inputs we drop some of the
# algorithms which dont support those.
function ode_func!(du, u, p, t)
du[1] = u[2]
du[2] = -u[1]
return nothing
end

function objective_function!(resid, u0, p)
odeprob = ODEProblem{true}(ode_func!, u0, (0.0, 100.0), p)
sol = solve(odeprob, Tsit5(), abstol = 1e-9, reltol = 1e-9, verbose = false)
resid[1] = sol(0.0)[1]
resid[2] = sol(100.0)[1] - 1.0
return nothing
end

prob = NonlinearProblem{true}(objective_function!, [0.0, 1.0] .+ 1im)
sol = solve(prob; abstol = 1e-10)
@test SciMLBase.successful_retcode(sol)
# This test is not meant to return success but test that all the default solvers can handle
# complex valued problems
@test_nowarn solve(prob; abstol = 1e-19, maxiters = 10)
@test_nowarn solve(prob, RobustMultiNewton(eltype(prob.u0)); abstol = 1e-19, maxiters = 10)