# Using interval arithmetic Highlighted here is the use of interval arithmetic for calculus problems. Unlike floating point math, where floating point values are an *approximation* to real numbers, interval arithmetic uses *interval* which are **guaranteed** to contain the given value. We use the `IntervalArithmetic` package and friends to work below, but note there is nothing magic about the concept. ## Basic XXX ## Using `IntervalRootFinding` to identify zeros of a function The `IntervalRootFinding` package provides a more *rigorous* alternative to `find_zeros`. This packages leverages the interval arithmetic features of `IntervalArithmetic`. The `IntervalRootFinding` package provides a function `roots`, with usage similar to `find_zeros`. Intervals are specified with the notation `a..b`. In the following, we *qualify* `roots` to not conflict with the `roots` function from `SymPy`, which has already been loaded: ```julia import IntervalArithmetic import IntervalRootFinding ``` ```julia u(x) = sin(x) - 0.1*x^2 + 1 𝑱 = IntervalArithmetic.Interval(-10, 10) # cumbersome -10..10; needed here: .. means something in CalculusWithJulia rts = IntervalRootFinding.roots(u, 𝑱) ``` The "zeros" are returned with an enclosing interval and a flag, which for the above indicates a unique zero in the interval. The intervals with a unique answer can be filtered and refined with a construct like the following: ```julia [find_zero(u, (IntervalArithmetic.interval(I).lo, IntervalArithmetic.interval(I).hi)) for I in rts if I.status == :unique] ``` The midpoint of the returned interval can be found by composing the `mid` function with the `interval` function of the package: ```julia [(IntervalArithmetic.mid ∘ IntervalArithmetic.interval)(I) for I in rts if I.status == :unique] ``` For some problems, `find_zeros` is more direct, as with this one: ```julia find_zeros(u, (-10, 10)) ``` Which can be useful if there is some prior understanding of the zeros expected to be found. However, `IntervalRootFinding` is more efficient computationally and *offers a guarantee* on the values found. For functions where roots are not "unique" a different output may appear: ```julia; hold=true; f(x) = x*(x-1)^2 rts = IntervalRootFinding.roots(f, 𝑱) ``` The interval labeled `:unknown` contains a `0`, but it can't be proved by `roots`. Interval arithmetic finds **rigorous** **bounds** on the range of `f` values over a closed interval `a..b` (the range is `f(a..b)`). "Rigorous" means the bounds are truthful and account for possible floating point issues. "Bounds" means the answer lies within, but the bound need not be the answer. This allows one -- for some functions -- to answer affirmatively questions like: * Is the function *always* positive on `a..b`? Negative? This can be done by checking if `0` is in the bound given by `f(a..b)`. If it isn't then one of the two characterizations is true. * Is the function *strictly increasing* on `a..b`? Strictly decreasing? These questions can be answered using the (upcoming) [derivative](../derivatives/derivatives.html). If the derivative is positive on `a..b` then `f` is strictly increasing, if negative on `a..b` then `f` is strictly decreasing. Finding the derivative can be done within the `IntervalArithmetic` framework using [automatic differentiation](../derivatives/numeric_derivatives.html), a blackbox operation denoted `f'` below. Combined, for some functions and some intervals these two questions can be answered affirmatively: * the interval does not contain a zero (`0 !in f(a..b)`) * over the interval, the function crosses the `x` axis *once* (`f(a..a)` and `f(b..b)` are one positive and one negative *and* `f` is strictly monotone, or `0 !in f'(a..b)`) This allows the following (simplified) bisection-like algorithm to be used: * consider an interval `a..b` * if the function is *always* positive or negative, it can be discarded as no zero can be in the interval * if the function crosses the `x` axis *once* over this interval **then** there is a "unique" zero in the interval and the interval can be marked so and set aside * if neither of the above *and* `a..b` is not too small already, then *sub-divide* the interval and repeat the above with *both* smaller intervals * if `a..b` is too small, stop and mark it as "unknown" When terminated there will be intervals with unique zeros flagged and smaller intervals with an unknown status. Compared to the *bisection* algorithm -- which only knows for some intervals if that interval has one or more crossings -- this algorithm gives a more rigorous means to get all the zeros in `a..b`. For a last example of the value of this package, this function, which appeared in our discussion on limits, is *positive* for **every** floating point number, but has two zeros snuck in at values within the floating point neighbors of $15/11$ ```julia t(x) = x^2 + 1 +log(abs( 11*x-15 ))/99 ``` The `find_zeros` function will fail on identifying any potential zeros of this function. Even the basic call of `roots` will fail, due to it relying on some smoothness of `f`. However, explicitly asking for `Bisection` shows the *potential* for one or more zeros near $15/11$: ```julia IntervalRootFinding.roots(t, 𝑱, IntervalRootFinding.Bisection) ``` (The basic algorithm above can be sped up using a variant of [Newton's](../derivatives/newton_method.html) method, this variant assumes some "smoothness" in the function `f`, whereas this `f` is not continuous at the point ``x=15/11``.)