Calculations with guaranteed bounds¶
Use Ball when you need to know how much uncertainty a result carries.
It represents every real value between a midpoint minus a radius and that
midpoint plus the radius. Arithmetic includes both the input uncertainty
and any rounding error in the returned bounds.
From a measurement to a result¶
Suppose a length is 3, with an uncertainty of one eighth in either direction.
Ball(3, Rational(1, 8)) represents that measurement. Squaring it gives an
enclosure of every possible area. BallContext(64) chooses 64 bits for the
result's midpoint; it does not promise 64 accurate bits in the measurement.
"""Carry measurement uncertainty through a calculation."""
from apn_mojo import ArithmeticContext, Ball, BallContext, FloatFormat, Rational, ball
def main() raises:
var length = Ball(3, Rational(1, 8))
var area = ball.square(length, context=BallContext(64))
print("length bounds:", length.lower(), length.upper())
print("area midpoint and radius:", area.midpoint(), area.radius())
print("area contains both endpoints:",
ball.contains(area, Rational(529, 64)),
ball.contains(area, Rational(625, 64)))
print("area compared with 8:", ball.compare(area, Ball(8)))
print("overlapping measurements:", ball.compare(length, Ball(3, Rational(1, 4))))
print("x - x is exact:", (length - length).is_exact())
print("division across zero:", (length / Ball(0, 1)).is_indeterminate())
var root = ball.sqrt(2, context=BallContext(128))
var rounded = ball.to_float_if_certain(
root, context=ArithmeticContext(format=FloatFormat.binary64())
)
if rounded:
print("certified sqrt(2):", rounded.value())
else:
raise Error("The enclosure did not determine one rounded value.")
Run from the repository root pixi run mojo run -I src docs/examples/ball_values.mojo
Output
length bounds: 2.875 3.125
area midpoint and radius: 9.0 0.765625
area contains both endpoints: True True
area compared with 8: BallOrder.greater
overlapping measurements: BallOrder.overlap
x - x is exact: False
division across zero: True
certified sqrt(2): 1.4142135623730951
midpoint() and radius() expose the stored description. lower() and
upper() give outward-rounded endpoints; lower_rational() and
upper_rational() give the exact endpoints as fractions. The area contains
the squares of both extreme lengths, and may extend farther to keep the
enclosure guarantee.
An exact input can also need a radius: Ball(Rational(1, 3)) encloses one
third, which has no finite binary representation. Ball("0.1") encloses
exactly one tenth. Constructing from an already rounded Float instead
encloses that stored value; it cannot recover earlier rounding error.
Decide what the bounds establish¶
Balls have no ordinary comparison operators. ball.compare(a, b) returns
less, equal, greater, overlap, or undefined. In the example, every
possible area exceeds 8, so the result is BallOrder.greater. Overlapping
measurements do not establish equality. Use contains to test a number,
contains_ball to test an entire interval, and overlaps to test whether
two intervals meet.
An indeterminate ball has no usable enclosure. For example, division by
an interval containing zero includes an undefined calculation. Check
is_indeterminate() before trying to inspect finite endpoints. An unbounded
ball instead represents every real number; is_finite() distinguishes both
cases from a finite interval.
Understand why bounds widen¶
Each operation considers its operand intervals independently. The library
does not remember that two occurrences came from the same measurement.
For an uncertain x, x - x therefore need not be the exact zero, even
though the algebraic expression is zero. Simplifying the expression before
evaluating it can give much tighter bounds.
Increasing midpoint precision reduces rounding error. It cannot remove measurement uncertainty or restore relationships lost between intermediate intervals. To benefit from a higher precision, recompute from the original exact inputs or measurements. Merely widening the format of an existing result does not narrow its uncertainty.
Extract a result when the rounding is certain¶
ball.to_float_if_certain returns an Optional Float. It succeeds only when
every point in the enclosure rounds to the same result with the requested
context and status. In the example, a 128-bit enclosure of sqrt(2) is
narrow enough to determine its 53-bit rounded value. None means the
enclosure does not establish one result under those rules.
If the uncertainty comes from rounding, retry the whole calculation with more working bits and a finite retry budget. If it comes from the input, you may need a better measurement or a less demanding output precision. The precision guide demonstrates a bounded retry and a separate check for reliable decimal digits.
Rectangles in the complex plane¶
ComplexBall pairs real and imaginary balls into a rectangle. Use it for
uncertainty in complex arithmetic; complex_ball.abs returns a real Ball
for the magnitude. Both component intervals participate in the calculation.
"""Rectangular uncertainty and a certified complex result."""
from apn_mojo import (
ArithmeticContext, Ball, BallContext, ComplexBall, ComplexContext,
FloatFormat, Rational, ball, complex_ball,
)
def main() raises:
var measured = ComplexBall(Ball(3, Rational(1, 8)), Ball(4, Rational(1, 8)))
var magnitude = complex_ball.abs(measured, context=BallContext(64))
print("magnitude contains 5:", ball.contains(magnitude, 5))
print("magnitude is exact:", magnitude.is_exact())
var root = complex_ball.sqrt(ComplexBall(3, 4), context=BallContext(128))
print("root contains 2 + i:", complex_ball.contains(root, ComplexBall(2, 1)))
var rounded = complex_ball.to_complex_if_certain(
root, context=ComplexContext(ArithmeticContext(format=FloatFormat.binary64()))
)
if rounded:
print("certified root:", rounded.value())
else:
raise Error("The rectangle did not determine one rounded complex value.")
var across_cut = ComplexBall(Ball(-2), Ball(0, Rational(1, 8)))
var logarithm = complex_ball.log(across_cut, context=BallContext(64))
print("log across the cut spans both signs:", ball.contains_zero(logarithm.imag()))
Run from the repository root pixi run mojo run -I src docs/examples/complex_ball_values.mojo
Output
magnitude contains 5: True
magnitude is exact: False
root contains 2 + i: True
certified root: Complex(2.0, 1.0)
log across the cut spans both signs: True
to_complex_if_certain requires a definite rounded value for both parts.
Functions use their principal branches. When a rectangle crosses a branch
cut, its result covers both sides: the logarithm around a negative real
number can have an imaginary interval spanning from negative to positive
pi. Increasing precision does not remove that jump. A rectangle containing
a singularity, such as zero for log, gives an indeterminate result.
Work with many intervals¶
Batch[Ball] and Batch[ComplexBall] support shapes, selections, and mapping.
Use functions such as batch.add and batch.exp, or pass a family function
to vmap or lift. Ball batches have no arithmetic operators or batch JSON.
Real Ball batches support min and max; both families support cumsum
and cumprod. Use lift for other folds, preserving operand order unless
your function is associative.
See the Ball reference and ComplexBall reference for set operations, special functions, serialization, and the full contracts.