Skip to content

Commit

Permalink
Merge pull request #334 from avik-pal/ap/complex
Browse files Browse the repository at this point in the history
Handle complex numbers correctly by default
  • Loading branch information
ChrisRackauckas authored Dec 21, 2023
2 parents aa282b7 + 133b1d5 commit 014c569
Show file tree
Hide file tree
Showing 4 changed files with 145 additions and 59 deletions.
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)

0 comments on commit 014c569

Please sign in to comment.