roots interface

Methods

Three root-finding contractors currently available through the roots interface are the following:

  • Newton (default);
  • Krawczyk;
  • Bisection

Both the Newton and Krawczyk methods can determine if a root is unique in an interval, at the cost of requiring that the function is differentiable. The bisection method has no such requirement, but can never guarantee the existence or uniqueness of a root.

The method used is given using the contractor keyword argument:

julia> roots(log, -2..2 ; contractor = Newton)
1-element Vector{Root{Interval{Float64}}}:
 Root([0.999999, 1.00001]_com, :unique)

julia> roots(log, -2..2 ; contractor = Krawczyk)
1-element Vector{Root{Interval{Float64}}}:
 Root([0.999999, 1.00001]_com, :unique)

julia> roots(log, -2..2 ; contractor = Bisection)
1-element Vector{Root{Interval{Float64}}}:
 Root([0.999999, 1.00001]_com, :unknown)

Note that as shown in the example, the log function does not complain about being given an interval going outside of its domain. While this may be surprising, this is the expected behavior and no root will ever be found outside the domain of a function.

Explicit derivatives

Newton and Krawczyk methods require the function to be differentiable, but the derivative is usually computed automatically using forward-mode automatic differentiation, provided by the ForwardDiff.jl package. It is however possible to provide the derivative explicitly using the derivative keyword argument:

julia> roots(log, -2..2 ; contractor = Newton, derivative = x -> 1/x)
1-element Vector{Root{Interval{Float64}}}:
 Root([0.999999, 1.00001]_com_NG, :unique)

julia> roots(log, -2..2 ; contractor = Krawczyk, derivative = x -> 1/x)
1-element Vector{Root{Interval{Float64}}}:
 Root([0.999999, 1.00001]_com_NG, :unique)

When providing the derivative explicitly, the computation is expected to be slightly faster, but the precision of the result is unlikely to be affected.

julia> using BenchmarkTools

julia> @btime roots(log, -2..2 ; derivative = x -> 1/x)
  4.814 μs (53 allocations: 3.16 KiB)
1-element Vector{Root{Interval{Float64}}}:
 Root([0.999999, 1.00001]_com_NG, :unique)

julia> @btime roots(log, -2..2)
  5.767 μs (53 allocations: 3.16 KiB)
1-element Vector{Root{Interval{Float64}}}:
 Root([0.999999, 1.00001]_com, :unique)

This may be useful in some special cases where ForwardDiff.jl is unable to compute the derivative of a function. Examples are complex functions and functions whose interval extension must be manually defined (e.g. special functions like zeta).

In dimension greater than one, the derivative be given as a function returning the Jacobi matrix:

julia> function f( (x, y) )
           return [sin(x), cos(y)]
       end
f (generic function with 1 method)

julia> function df( (x, y) )
           return [cos(x) 0 ; 0 -sin(y)]
       end

julia> roots(f, [-3..3, -3..3] ; derivative = df)
2-element Vector{Root{Vector{Interval{Float64}}}}:
 Root(Interval{Float64}[[-1.24409e-21, 1.0588e-22]_com_NG, [-1.5708, -1.57079]_com_NG], :unique)
 Root(Interval{Float64}[[-1.24409e-21, 1.0588e-22]_com_NG, [1.57079, 1.5708]_com_NG], :unique)

Tolerance

An absolute tolerance for the search may be specified as the abstol keyword argument. Currently a method must first be provided in order to be able to choose the tolerance.

julia> g(x) = sin(exp(x))
g (generic function with 1 method)
        
julia> roots(g, 0..2)
2-element Vector{Root{Interval{Float64}}}:
 Root([1.14472, 1.14474]_com, :unique)
 Root([1.83787, 1.83788]_com, :unique)

julia> roots(g, 0..2 ; abstol = 1e-1)
2-element Vector{Root{Interval{Float64}}}:
 Root([1.14173, 1.15244]_com, :unique)
 Root([1.78757, 1.84273]_com, :unique)

A lower tolerance may greatly reduce the computation time, at the cost of an increased number of returned roots having :unknown status:

julia> h(x) = cos(x) * sin(1 / x)
h (generic function with 1 method)

julia> @btime roots(h, 0.05..1)
  79.500 μs (301 allocations: 13.97 KiB)
6-element Vector{Root{Interval{Float64}}}:
 Root([0.0530516, 0.0530517]_com_NG, :unique)
 Root([0.0636619, 0.063662]_com_NG, :unique)
 Root([0.0795774, 0.0795775]_com_NG, :unique)
 Root([0.106103, 0.106104]_com_NG, :unique)
 Root([0.159154, 0.159156]_com_NG, :unique)
 Root([0.318309, 0.31831]_com_NG, :unique)

julia> @btime roots(h, 0.05..1 ; abstol = 1e-2)
  48.500 μs (265 allocations: 11.61 KiB)
6-element Vector{Root{Interval{Float64}}}:
 Root([0.0514445, 0.0531087]_com_NG, :unique)
 Root([0.0570253, 0.0641615]_com, :unknown)
 Root([0.0785458, 0.0797797]_com_NG, :unknown)
 Root([0.104754, 0.107542]_com_NG, :unknown)
 Root([0.157236, 0.165989]_com_NG, :unknown)
 Root([0.31716, 0.319318]_com_NG, :unique)

julia> @btime roots(h, 0.05..1 ; abstol = 1e-1)
  17.300 μs (114 allocations: 5.16 KiB)
3-element Vector{Root{Interval{Float64}}}:
 Root([0.04999999, 0.107542]_com, :unknown)
 Root([0.107541, 0.165989]_com, :unknown)
 Root([0.283803, 0.356099]_com_NG, :unknown)

The last example shows a case where the tolerance was too large to be able to isolate the roots in distinct regions.

Warning

For a root x of some function, if the absolute tolerance is smaller than eps(x) i.e. if tol + x == x, roots may never be able to converge to the required tolerance and the function may get stuck in an infinite loop.