Repository: JuliaMath/Roots.jl (julia)

Issue:
Gentler exit when no root exists
Hello,

I was wondering if there was a way other than
```
try
    find_zero(f, (a,b))
catch
    missing
end
```

to handle the case when there is no root.

To contextualise: I am trying to find the solutions to a parametric integral function. And for a certain range of the parameter the solution does not exists (can be proven theoretically).
Exactly pinning down the critical value of the parameter from which to start looking at the solutions (knowing that they exist) is not always possible. Hence i found myself trying to find zeros in the whole range.

At first i used `find_zeros` which in case it doesn't find any root it simply returns an empty array which then can be handled via a simple `isempty()`. But when using `find_zero` for a parameter value without root, it crashes the whole computation instead of simply returning a return code.

I realise that the convergence error message can be helpful and also needed in other situations.
But maybe it can be changed to return a `missing` and print the convergence error as a warning so it doesn't distrupt other routines that implement the function and does not requires the use of `try ... catch ... end`.

Pull request description:
Close #209 by adding example to new docstring for `find_zero(M,f,state,...)`. Adjust argument order to this signature.

Patch:
diff --git a/Project.toml b/Project.toml
index efc8d276..b19c04ac 100644
--- a/Project.toml
+++ b/Project.toml
@@ -1,6 +1,6 @@
 name = "Roots"
 uuid = "f2b01f46-fcfa-551c-844a-d8ac1e96c665"
-version = "1.0.7"
+version = "1.0.8"
 
 [deps]
 Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7"
diff --git a/src/bracketing.jl b/src/bracketing.jl
index dfb73758..c56ce24d 100644
--- a/src/bracketing.jl
+++ b/src/bracketing.jl
@@ -347,7 +347,7 @@ function find_zero(fs, x0, method::M;
         end
     end
 
-    find_zero(method, F, options, state, l)
+    find_zero(method, F, state, options, l)
 
     verbose && show_trace(method, nothing, state, l)
 
@@ -1019,7 +1019,7 @@ function find_bracket(fs, x0, method::M=A42(); kwargs...) where {M <: Union{Abst
     # check if tolerances are exactly 0
     iszero_tol = iszero(options.xabstol) && iszero(options.xreltol) && iszero(options.abstol) && iszero(options.reltol)
 
-    find_zero(method, F, options, state, NullTracks())
+    find_zero(method, F, state, options, NullTracks())
 
     (xstar=state.xstar, bracket=(state.xn0, state.xn1), exact=iszero(state.fxstar))
 
diff --git a/src/find_zero.jl b/src/find_zero.jl
index 6e28c93c..ba58d3d4 100644
--- a/src/find_zero.jl
+++ b/src/find_zero.jl
@@ -82,7 +82,7 @@ end
 # out = zeros(Float64, n)
 # for (i,x0) in enumerate(linspace(0.9, 1.0, n))
 #    Roots.init_state!(state, M, f1, x0)
-#    out[i] = find_zero(M, f1, options, state)
+#    out[i] = find_zero(M, f1, state, options)
 # end
 # init_state has a method call variant
 function init_state!(state::UnivariateZeroState{T,S}, x1::T, x0::T, m::Vector{T}, y1::S, y0::S, fm::Vector{S}) where {T,S}
@@ -651,9 +651,9 @@ function find_zero(fs, x0, method::AbstractUnivariateZeroMethod,
     l = (verbose && isa(tracks, NullTracks)) ? Tracks(eltype(state.xn1)[], eltype(state.fxn1)[]) : tracks
 
     if N == nothing  || isa(method, AbstractBracketing)
-        xstar = find_zero(method, F, options, state, l)
+        xstar = find_zero(method, F, state, options, l)
     else
-        xstar = find_zero(method, N, F, options, state, l)
+        xstar = find_zero(method, N, F, state, options, l)
     end
 
     if verbose
@@ -668,17 +668,47 @@ function find_zero(fs, x0, method::AbstractUnivariateZeroMethod,
 
 end
 
+# defaults when method is not specified
+# if a number, use Order0
+# O/w use a bracketing method of an assumed iterable
 find_zero(f, x0::T; kwargs...)  where {T <: Number}= find_zero(f, x0, Order0(); kwargs...)
-find_zero(f, x0::Vector; kwargs...) = find_zero(f, x0, Bisection(); kwargs...)
-find_zero(f, x0::Tuple; kwargs...) = find_zero(f, x0, Bisection(); kwargs...)
+function find_zero(f, x0; kwargs...)
+    a, s = iterate(x0)
+    b, _ = iterate(x0, s)
+    find_zero(f, (a,b), Bisection(); kwargs...)
+end
 
 # Main method
 # return a zero or NaN.
 ## Updates state, could be `find_zero!(state, M, F, options, l)...
+"""
+    find_zero(M, F, state, [options], [l])
+
+Find zero using method `M`, function(s) `F`, and initial state `state`. Returns an approximate zero or `NaN`. Useful when some part of the processing pipeline is to be adjusted.
+
+* `M::AbstractUnivariateZeroMethod` a method, such as `Secant()`
+* `F`: A callable object (or tuple of callable objects for certain methods)
+* `state`: An initial state, as created by `init_state` (or `_init_state`).
+* `options::UnivariateZeroOptions`: specification of tolerances
+* `l::AbstractTracks`: used to record steps in algorithm, when requested.
+
+# Examples
+
+```
+f(x) = sin(x)
+xs = (3.0, 4.0)
+fxs = f.(xs)
+if prod((sign∘f).(xs)) < 0 # check bracket
+    M = Bisection()
+    state = Roots._init_state(M, f, xs, fxs) # reuse fxs from test
+    find_zero(M, f, state)
+end
+```
+"""
 function find_zero(M::AbstractUnivariateZeroMethod,
                    F,
-                   options::UnivariateZeroOptions,
                    state::AbstractUnivariateZeroState,
+                   options::UnivariateZeroOptions=init_options(M, state),
                    l::AbstractTracks=NullTracks()
                    )  #where {T<:Number, S<:Number}
 
@@ -695,16 +725,6 @@ function find_zero(M::AbstractUnivariateZeroMethod,
     return decide_convergence(M, F, state, options)
 end
 
-function find_zero(M::AbstractUnivariateZeroMethod,
-                   F,
-                   state::AbstractUnivariateZeroState,
-                   l::AbstractTracks=NullTracks()
-                   )  #where {T<:Number, S<:Number}
-
-    options = init_options(M, state)
-    find_zero(M, F, options, state, l)
-end
-
 
 ## --
 
@@ -773,7 +793,7 @@ function run_bisection(N::AbstractBracketing, f, xs, state, options)
     state.steps += steps
     state.fnevals += fnevals # could avoid 2 fn calls, with fxs
     init_options!(options, N)
-    find_zero(N, f, options, state)
+    find_zero(N, f, state, options)
     a, b = xs
     u,v = a > b ? (b, a) : (a, b)
     state.message *= "Bracketing used over ($u, $v), those steps not shown. "
@@ -791,8 +811,8 @@ end
 function find_zero(M::AbstractUnivariateZeroMethod,
                    N::AbstractBracketing,
                    F,
-                   options::UnivariateZeroOptions,
                    state::AbstractUnivariateZeroState,
+                   options::UnivariateZeroOptions,
                    l::AbstractTracks=NullTracks()
                    )
 


Test patch:
diff --git a/test/test_find_zero.jl b/test/test_find_zero.jl
index d43d55d5..ab2d9176 100644
--- a/test/test_find_zero.jl
+++ b/test/test_find_zero.jl
@@ -85,6 +85,7 @@ end
 
 
 @testset "find_zero internals" begin
+
     ##  init_state! method
     g1(x) = x^5 - x - 1
     x0_, xstar_ = (1.0, 2.0), 1.1673039782614187
@@ -93,7 +94,7 @@ end
     options = Roots.init_options(M, state)
     for M in (Roots.A42(), Roots.Bisection(), Roots.FalsePosition())
         Roots.init_state!(state, M, g1, x0_)
-        @test find_zero(M, g1, options, state) ≈ xstar_
+        @test find_zero(M, g1, state, options) ≈ xstar_
     end
 
     # alternate constructor find_zero!
@@ -109,6 +110,14 @@ end
         @test find_zero!(P) ≈ xstar_
     end
 
+    ## test with early evaluation of bracket
+    f(x) = sin(x)
+    xs = (3.0, 4.0)
+    fxs = f.(xs)
+    M = Bisection()
+    state = Roots._init_state(M, f, xs, fxs)
+    @test find_zero(M, f, state) ≈ π
+
     
     ## hybrid
     g1(x) = exp(x) - x^4
@@ -116,11 +125,11 @@ end
     M = Roots.Bisection()
     state = Roots.init_state(M, g1, x0_)
     options = Roots.init_options(M, state, xatol=1/2)
-    find_zero(M, g1, options, state)
+    find_zero(M, g1, state, options)
     M = Roots.Order1()
     Roots.init_state!(state, M, g1, state.m[1])
     Roots.init_options!(options, M)
-    @test find_zero(M, g1, options, state) ≈ xstar_
+    @test find_zero(M, g1, state, options) ≈ xstar_
 
 
     # test conversion between states/options
@@ -278,7 +287,7 @@ end
         F = Roots.DerivativeFree(lhs)
         state = Roots.init_state(M, F, x0)
         options = Roots.init_options(M, state)
-        find_zero(M, F, options, state)
+        find_zero(M, F, state, options)
 
         @test state.steps <= 45 # 15
     end