Floats of any precision¶
With Float, you choose how many bits of precision
your calculation needs. Each arithmetic operation returns the exact
mathematical result rounded once to its destination format. That guarantee
applies to each operation; a longer calculation can still accumulate rounding
error.
Choose a format¶
Start with a FloatFormat to set the significand precision and exponent
bounds. An ArithmeticContext adds a rounding mode and traps for conditions
such as division by zero. The Float remembers its format, while rounding
modes and traps apply to the call that uses them.
"""Float values, formats and exact conversion."""
from apn_mojo import Float, Rational, FloatFormat, ArithmeticContext
def main() raises:
var context = ArithmeticContext(format=FloatFormat(3))
var value = Float(Rational(1, 3), context=context)
print("stored value:", value)
var saved = value
value = value.to_format(FloatFormat(173))
print("precisions:", saved.precision(), value.precision())
print("same stored value:", value == saved)
print("below exact third:", value < Rational(1, 3))
print("native half:", Float.from_native(Float64(0.5)))
var zero = Float.zero(negative=True)
print("signed zero:", zero, zero == Float())
var nan = Float.nan()
print("NaN:", nan, nan == nan)
Run from the repository root pixi run mojo run -I src docs/examples/float_values.mojo
Output
stored value: 0.3
precisions: 3 173
same stored value: True
below exact third: True
native half: 0.5
signed zero: -0.0 True
NaN: nan False
Floats print the shortest decimal that reads back as the same value in their
format. In the example, the stored value is 0.3: in a 3-bit format no
shorter decimal reads back as 5 * 2**-4, the nearest approximation to one
third, and to_string(16) shows that exact value as 0x5p-4.
Increasing precision preserves that stored value when it fits the new exponent
range; it does not make the original approximation more accurate.
Comparisons with integers and fractions use the exact stored value. Positive
and negative zero compare equal, and NaN compares unequal to every value,
including itself.
Arithmetic rounds once¶
Operators derive a result format from their operands and use nearest-even
rounding without condition traps. Named functions such as add, subtract,
multiply, and divide accept context= for an explicit destination format,
rounding mode, or traps. Compound assignments keep the destination's format.
"""Correctly rounded arithmetic with an explicit context."""
from apn_mojo import (
Float,
Integer,
Rational,
FloatFormat,
ArithmeticContext,
add,
divide,
)
def main() raises:
var value = Float(
Rational(5, 4), context=ArithmeticContext(format=FloatFormat(3))
)
var increment = Rational(1, 64)
print("ordinary:", value + increment)
print("native result precision:", (Integer(2) + Float64(0.5)).precision())
var target = ArithmeticContext(format=FloatFormat(2))
var result = add(value, increment, context=target)
print("direct rounding:", result)
var saved = value
value += Float(1)
print("updated:", value, "precision:", value.precision())
print("saved:", saved)
var quotient = divide(Float(1), Float.zero(negative=True))
print("zero divisor:", quotient)
Run from the repository root pixi run mojo run -I src docs/examples/float_arithmetic.mojo
Output
ordinary: 1.2
native result precision: 53
direct rounding: 1.5
updated: 2.0 precision: 3
saved: 1.2
zero divisor: -inf
Exact integer and rational operands keep their full value until the final
rounding. A typed native float contributes the binary approximation it already
holds. Dividing a finite nonzero value by zero returns a signed infinity by
default; trap_divide_by_zero=True makes the call raise. 0 / 0 is an invalid
operation and returns NaN unless the invalid condition is trapped.
Adding an Integer and a Rational without a context returns an exact
Rational. Passing an arithmetic context to the top-level add instead asks
for a Float rounded to that format.
Functions¶
sqrt, square, pow_int, ldexp, and fma follow the same rounding rule.
fma(a, b, c) computes a * b + c with no rounding between the product and
the sum. This can preserve a small residual that separate multiplication and
addition would lose; the final result still rounds if necessary.
"""Square roots, powers and other rounded functions."""
from apn_mojo import (
Float,
Rational,
FloatFormat,
ArithmeticContext,
square,
sqrt,
pow_int,
ldexp,
fma,
)
def main() raises:
var context = ArithmeticContext(format=FloatFormat(53))
var root = sqrt(Rational(2), context=context)
print("native root:", root.to_native[DType.float64]())
print("square:", square(Float(3)))
print("reciprocal power:", pow_int(Float(2), -3))
print("scaled:", ldexp(Float(3), -2))
var fused = fma(
Rational(9, 8), Rational(9, 8), Rational(-5, 4), context=context
)
print("fused residual:", fused)
var value = Float(Rational(-7, 4))
print("floor, ceil, trunc:", value.floor(), value.ceil(), value.trunc())
print(
"checked native integer:", value.floor().to_native_exact[DType.int64]()
)
var saved = value
value **= -2
print("power kept precision:", value.precision() == saved.precision())
Run from the repository root pixi run mojo run -I src docs/examples/float_functions.mojo
Output
native root: 1.4142135623730951
square: 9.0
reciprocal power: 0.125
scaled: 0.75
fused residual: 0.015625
floor, ceil, trunc: -2 -1 -1
checked native integer: -2
power kept precision: True
floor, ceil, and trunc return an Integer after the requested rounding.
to_integer_exact() raises if a finite value is not integral. For native
floating-point output, to_native[dtype]() rounds directly to the requested
type, while to_native_exact[dtype]() requires an exact conversion.
Use sum and dot for batch totals that round only once. To carry interval
bounds through a calculation, use Ball. The
precision guide shows how to choose working bits,
avoid cancellation, and certify the digits you print.
Constants and elementary functions¶
pi, euler_e, ln2, and the other constants use the requested context.
So do exponentials, logarithms, trigonometric and hyperbolic functions, and
their inverses. Trigonometric arguments are in radians. Exact Integer and
Rational arguments retain their value until the function's final rounding.
"""Constants, elementary functions and small increments."""
from apn_mojo import ArithmeticContext, FloatFormat, Rational, exp, log, log1p, pi, sin_cos
def main() raises:
var context = ArithmeticContext(format=FloatFormat(128))
print("pi:", pi(context=context).to_string(digits=20))
print("exp(1/10):", exp(Rational(1, 10), context=context).to_string(digits=12))
print("log(2):", log(2, context=context).to_string(digits=12))
var sine, cosine = sin_cos(Rational(1, 2), context=context)
print("sin and cos of 1/2 radian:", sine.to_string(digits=12), cosine.to_string(digits=12))
var increment = Rational("1e-20")
print("log1p of a small increment:", log1p(increment, context=context).to_string(digits=12))
Run from the repository root pixi run mojo run -I src docs/examples/float_elementary.mojo
Output
pi: 3.1415926535897932385
exp(1/10): 1.10517091808
log(2): 0.693147180560
sin and cos of 1/2 radian: 0.479425538604 0.877582561890
log1p of a small increment: 1.00000000000e-20
Use sin_cos when you need both results. For a small increment, log1p(x)
computes log(1 + x) without rounding away x in an intermediate addition;
expm1(x) similarly avoids the cancellation in exp(x) - 1. Each returned
value is correctly rounded, even when the example prints fewer digits.
Special functions¶
The library includes gamma and beta functions, error functions, normal
probabilities and quantiles, zeta, Lambert W, and hypergeometric functions.
Names follow scipy.special; the reference
states their domains, branches, normalizations, and special values.
"""Special functions for probabilities and exact fractional inputs."""
from apn_mojo import ArithmeticContext, FloatFormat, Rational, beta, gamma, gammaincc, ndtr, ndtri
def main() raises:
var context = ArithmeticContext(format=FloatFormat(128))
print("Gamma(5):", gamma(5, context=context))
print("B(2, 3):", beta(2, 3, context=context).to_string(digits=12))
print("normal CDF at zero:", ndtr(0, context=context))
print("normal 97.5% quantile:", ndtri(Rational(975, 1000), context=context).to_string(digits=12))
print("gamma upper tail, shape 3 at 2:", gammaincc(3, 2, context=context).to_string(digits=12))
Run from the repository root pixi run mojo run -I src docs/examples/special_functions.mojo
Output
Gamma(5): 24.0
B(2, 3): 0.0833333333333
normal CDF at zero: 0.5
normal 97.5% quantile: 1.95996398454
gamma upper tail, shape 3 at 2: 0.676676416183
Here ndtr is the standard normal cumulative probability, and ndtri
finds its quantile. gammaincc(a, x) gives the regularized upper incomplete
gamma function, also the upper-tail probability for a gamma distribution
with shape a and scale 1. Use it directly instead of subtracting
gammainc(a, x) from 1 when a small upper tail matters. Likewise,
log_ndtr computes a log probability without first rounding the probability.
The real-valued functions also have Ball versions for enclosing uncertain inputs. Complex elementary functions are covered in the next tutorial; special-function availability differs by family.
Text input and formatting¶
How you supply a number affects the result. Float("19.95") parses the
decimal value and rounds it once to the chosen binary format.
Float(Float64(19.95)) starts from the approximation already stored in the
native float. In either case, a non-binary fraction still needs rounding to
fit a finite binary format.
Printing and to_string() write positional decimals, like 0.00001 and
3.0, from 1e-6 up to 1e21, and scientific notation beyond, as JavaScript
does; notation="positional" or notation="scientific" forces one.
to_string(digits=n) writes n significant digits with the requested
rounding mode. notation="hexadecimal", or to_string(16), writes the exact
value, which suits tests and benchmarks. Neither form keeps the format; use
JSON when you also need to save precision and exponent bounds.
"""Decimal and hexadecimal text."""
from apn_mojo import (
Float,
FloatFormat,
ArithmeticContext,
RoundingMode,
)
def main() raises:
var price = Float("19.95")
print(price.to_string(10, digits=4))
print(Float("0x1.8p2").to_string(10, digits=3))
var value = Float(
"1.125", context=ArithmeticContext(format=FloatFormat(3))
)
print(value)
print(
Float("1.25").to_string(
10, digits=2, rounding=RoundingMode.toward_positive
)
)
var restored = Float.parse(
price.to_string(), context=ArithmeticContext(format=price.format())
)
print(restored == price)
Run from the repository root pixi run mojo run -I src docs/examples/float_text.mojo
Output
19.95
6.00
1.0
1.3
True
The Float reference also covers neighboring values, shortest decimal output, representation comparisons, and keys for caching. Continue with complex numbers for arithmetic on pairs of floats.