Every operation in this package returns an
enclosure: an interval that provably contains the exact
real result. Not an estimate of it, not an error bar around it. If the
true answer is a real number y, then y lies
inside the returned interval, and that statement is a theorem about the
code rather than an observation about a sample of inputs.
The guarantee is deliberately narrow. An enclosure that contains the answer may still be wide, and a wide enclosure is honest but not useful. Throughout the package the two properties are kept apart and reported apart: validity is never traded for tightness, and where a result cannot be tightened the package says so instead of narrowing it.
Conformance with IEEE Std 1788.1-2017 is not
claimed, for the reason the standard itself gives: its subclause 1.5
makes conformance a list of requirements an implementation shall
satisfy, with no partial grade available. What is followed, measured one
requirement at a time, is documented in ?RobustArithmetic
and summarised at the end of this vignette.
An interval is built from its endpoints and carries a decoration that records what is known about the operation that produced it.
x <- ra_interval(1, 2)
y <- ra_interval(3, 4)
ra_add(x, y)
#> <ra_ivl[1]>
#> [3.999999999999999, 6.000000000000001]_com
ra_mul(x, y)
#> <ra_ivl[1]>
#> [2.999999999999999, 8.000000000000002]_comThe endpoints of a computed interval are rounded outward, so the enclosure can only grow and never silently shrink past the true result. R offers no way to change the processor’s rounding mode, and changing it would not be a local act in any case: it is per-thread state of the processor, so it would govern every floating-point operation executed afterwards on that thread, inside this package or anywhere else. Outward rounding is therefore obtained arithmetically, from the predecessor and successor formulas of Rump, Zimmermann, Boldo and Melquiond (2009), which are valid under round-to-nearest.
Division by an interval containing zero is where a naive implementation quietly returns nonsense. Here it returns the mathematically correct answer, which is a pair of intervals, through the extended division of the interval Newton literature.
Elementary functions are provided at two levels, and the difference between them is a difference in what can be proved rather than a difference in speed alone.
The fast level evaluates over the system math
library and widens the result by a declared slack derived from published
accuracy measurements. The rigorous level evaluates
over Rmpfr with a directed-rounding bridge. When a verdict
would otherwise fall inside the slack — that is, when the fast level
cannot decide the question being asked — the package escalates through a
ladder of precisions rather than returning a confident answer it cannot
defend.
ra_elem("exp", ra_interval(0, 1))
#> <ra_ivl[1]>
#> [0.9999999999999996, 2.718281828459047]_com*
#> * measured provenance: fast-level slack, a declared convention over a measured error, not a theorem
ra_has_mpfr()The slack is not a tuning knob chosen for convenience. It is measured
against the library actually installed, which matters more than it
sounds: R does not call the system libm along a single fixed path, so a
bound published for one library does not certify what R computes.
ra_measure_library_error() is the instrument that measures
it.
Evaluating an expression by substituting an interval for each occurrence of the variable — the natural extension — is valid but often loose, because each occurrence is treated as if it varied independently of the others. The centered extension trades that looseness for a derivative bound.
ra_eval_natural(quote(x^2 - x), list(x = ra_interval(0, 1)))
#> <ra_ivl[1]>
#> [-1.0000000000000003, 1.0000000000000005]_com
ra_enclose_expr(quote(x^2 - x), ra_interval(0, 1))
#> <ra_ivl[1]>
#> [-0.7500000000000008, 0.2500000000000008]_comThe natural extension gives [-1, 1];
ra_enclose_expr() gives [-0.75, 0.25], which
is the exact range. Neither form is uniformly better and the package
does not pretend otherwise, so ra_enclose_expr() takes the
best of the methods it is given —- natural, mean value and a
monotonicity test -— rather than committing to one.
ra_solve() finds roots of a univariate expression over a
box and returns, for each one, a word about what was established. The
engine is the Hansen-Sengupta operator with extended division and
epsilon-inflated candidate verification, driven by a subdivision
(paving) loop.
ra_solve(quote(x^2 - 2), ra_interval(0, 2))
#> <ra_paving> roots of x^2 - 2 over [0, 2]_com
#> unique roots: 1 (provenance: theorem)
#> [1.4142135623730947, 1.4142135623730956]_trv unique
#> not excludable: 0
#> absence demonstrated over the rest of the window (0 excluded boxes, re-verified rigorously)
#> budget: 2 of 10000 boxesThe important property is the failure mode. The loop has a budget, and when the budget runs out it abstains by name and prints what it spent, rather than returning a smaller set of roots as though the search had been exhaustive. An abstention is information; a silently truncated answer is not.
The solver above is univariate by contract. For fixed points of maps
on R^n the package takes a different route, and the route
is the point of the design: the whole certificate is a scalar
inequality, so a fixed point in R^n is certified
without interval matrices, without factorisations and without an
interval spectral decomposition. The norm collapses the dimension into
three numbers, and the caller may bound those three numbers by any means
they can defend.
ra_ball_certificate(rho = 0.2, lipschitz = 0.5)
#> fixed point certificate over a ball in the unspecified norm
#> confinement : certified by the lipschitz route, radius 0.40000000000000013
#> contraction : certified, the fixed point is unique in the ball
#> the centre is within 0.4000000000000002 of it
#> bounds used : rho 0.20000000000000002, L 0.5, image not given
#> provenance : theoremTwo theorems are applied and the object says which one decided the verdict. Confinement — the map carries the ball into itself — gives existence by Brouwer and says nothing about what happens inside. Contraction adds uniqueness and convergence by Banach. They are certified separately because they are not the same statement, and a package that reported one word for both would be reporting less than it knows.
ra_ball_certificate(rho = 0.24, lipschitz = 1.6, radius = 0.4, image_radius = 0.4)
#> fixed point certificate over a ball in the unspecified norm
#> confinement : certified by the image route, radius 0.4
#> contraction : not certified, so what happens inside is not claimed
#> reason : L = 1.6000000000000001 is not below one, so there is no contraction.
#> bounds used : rho 0.24, L 1.6000000000000001, image 0.4000000000000001
#> provenance : theoremThat second call is confined but not contractive: the image radius route needs no Lipschitz constant, which is why it is not redundant.
Two assemblers help build the three inputs in the matrix case without
factorising anything: ra_gershgorin() encloses eigenvalues
from the matrix entries, and ra_spectral_sum() bounds a
spectral quantity of a sum from bounds on its terms.
ra_gershgorin(matrix(c(2, 0.1, 0.1, 3), 2, 2))
#> <ra_ivl[1]>
#> [1.8999999999999996, 3.100000000000001]_comThe responsibility that cannot be delegated: the
three bounds must be measured in the same norm, and the package cannot
check that. The label travels with the object. Mixing norms silently
would be a failure mode indistinguishable, at run time, from a valid
certificate. Likewise the Lipschitz constant must hold over the whole
ball and not merely at its centre; a caller who bounds the derivative at
the centre receives a certificate that means nothing, and no arithmetic
can detect it. In one dimension the package closes that loop itself, in
ra_certify_fixed_point().
This is worth stating on its own, because it is the most common way a verified result is misread. The hypotheses behind every certificate here are sufficient and not necessary. There is no test of exclusion in this package and no word of demonstrated absence. A map that this package declines to certify may still have a fixed point in the ball; a root that the solver does not report may still exist inside a box the budget never reached. What the package proves, it proves; what it declines, it declines without claiming the opposite.
Measured against IEEE Std 1788.1-2017, one requirement at a time: the
interval type and the decoration system of clause 5; 22 of the 39
arithmetic operations of Table 4.1; and the seven numeric functions of
Table 4.3. What is not provided is the cancellative
operations, the interval comparison relations, the text
input of subclause 6.8.2, the interchange
representation of subclause 7.3, and the tightest accuracy that
subclause 6.5.2 requires of the basic operations — which here are one
unit in the last place wider at each end. The text
output of subclause 6.8.3 is provided, by
ra_interval_to_text() and by the printing of every
interval: the pair of decimals on the page encloses the pair of doubles
underneath, which is what that subclause asks and what rounding each
endpoint to nearest would break.
ra_operator_table() prints the coverage as data rather
than as prose, and ra_verify_operator_table() checks the
claim against the code.
Hansen, E., & Walster, G. W. (2004). Global optimization using interval analysis (2nd ed.). Marcel Dekker.
Neumaier, A. (1990). Interval methods for systems of equations. Cambridge University Press.
Rump, S. M. (2010). Verification methods: Rigorous results using floating-point arithmetic. Acta Numerica, 19, 287-449.
Rump, S. M., Zimmermann, P., Boldo, S., & Melquiond, G. (2009). Computing predecessor and successor in rounding to nearest. BIT Numerical Mathematics, 49(2), 419-431.