On Monday, February 24, 2014 3:34:22 AM UTC-8, Hans W Borchers wrote:
>
> The following function is one of the most difficult test functions for 
> univariate root finding I know of,
>
>     julia> function fn(x::Real)
>                return log(x) + x^2/(2*exp(1)) - 2 * x/sqrt(exp(1)) + 1
>            end
>
> whose exact root between 1.0 and 3.4 is sqrt(e) = 1.6487212707001282... .
> Applying your nice version of bisect_root to this function returns
>
>     julia> bisect_root(fn, 1.0, 3.4)
>     (1.648707085670482,1.6487070856704822)
>
> with an absolute error of about 1.5e-05 ! This innocent looking test 
> function appears to be so flat that only an arbitrary precision solver will 
> locate this root more exactly.
>
> Hans Werner
>

Functions with "flat" zeros are definitely a problem for numerical root 
finding. As you suggest, pretty much any "black box" root finder that works 
with local float evaluations will lose a bunch of precision here--the issue 
isn't specific to bisect_root. Evaluating the output values of bisect_root 
gives a clue about what's going on:

    julia> fn(1.648707085670482), fn(1.6487070856704822)
    (-2.220446049250313e-16,0.0)

The test function evaluated on the upper bound returns 0.0, so as far as 
the algorithm can tell, it has found a root. As written, bisect_root will 
always do this when you have a sequence of successive floating inputs that 
all evaluate to 0.0: it will return the smallest value in the sequence that 
evaluates to 0.0 as its upper bound, and the next smallest float as its 
lower bound. You could rewrite it to give you the upper bound of the 
numerically 0 part instead, or maybe even to find both the upper and lower 
edges. The mean of these might be close to the exact root, but it isn't 
always.

The example you give isn't really any more or less pathological than other 
functions that have coincident 0's of the value and the first and second 
derivative. `x -> exp(-x^3)-1` and `x -> (x^3 + 1) - 1` behave about the 
same.

I'm actually planning to address exactly this issue in my next post, in the 
context of finding extrema of functions instead of zeros. Most functions 
are flat near their extrema--only a few are at their zeros.

BTW  It would be nice to have Ridders' algorithm available in Julia, too, 
> about which the "Numerical Recipes" say:
>
> "*In both reliability and speed, Ridders's method is generally 
> competitive with the more highly developed and better established (but more 
> complicated) method of Van Wijngaarden, Dekker, and Brent [...].*"
>
> My Julia version of Ridders' method would be as follows, perhaps it can be 
> improved with your proposed x1 < (x1 + x2)/2 < x2 :
>

I don't know whether the stopping criterion in your implementation is fine 
or not, but it makes me a little suspicious. There aren't any pairs of 
floats much larger than 1.0 that are separated by less than eps(), so it 
looks to me like if you try to isolate a root larger than 1.0, this 
algorithm will run either for maxiter iterations, or until the upper and 
lower bounds exactly coincide. Exact coincidence might be fine, or it might 
not. What I like about bisection is that it's so simple that I can analyze 
it confidently. The *only* arithmetic in bisect_root is a right shift in 
the _middle subroutine.

I'd be curious to see ridder's method benchmarked against the bisection 
code for a few different objective functions. For simple objective 
functions evaluated over floats, it's harder to outperform bisection than a 
lot of people think because bisection has such low per-iteration overhead. 
The square root in here might hurt it some.

#-- --------------------------------------------------------------------
> function ridders(f::Function, a::Real, b::Real;
>  maxiter::Integer = 1000, tol::Real = eps())
>
> x1 = a;     x2 = b
> f1 = f(x1); f2 = f(x2)
> if f1 * f2 > 0
> error("f(a) and f(b) must have different signs.")
> elseif f1 == 0
> return x1
> elseif f2 == 0
> return f2
> end
>
> niter = 2
> while niter < maxiter && abs(x1 - x2) > tol
> xm = (x1 + x2)/2.0
> fm = f(xm)
> if fm == 0; return xm; end
>
> x3 = xm + (xm - x1) * sign(f1 - f2) * fm / sqrt(fm^2 - f1 * f2)
> f3 = f(x3)
> niter += 2
> if f3 == 0; return x3; end
>
> if fm * f3 < 0
> x1 = xm;  f1 = fm
> x2 = x3;  f2 = f3
> elseif f1 * f3 < 0
> x2 = x3;  f2 = f3
> elseif f2 * f3 < 0
> x1 = x3;  f1 = f3
> else
> error("Inform the maintainer: you should never get here.")
> end
> end
>
> return (x1 + x2) / 2.0
> end
> #-- --------------------------------------------------------------------
>
>

Reply via email to