Tutorial

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.mojo Download
"""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.

float_arithmetic.mojo Download
"""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.

float_functions.mojo Download
"""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.

float_elementary.mojo Download
"""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.mojo Download
"""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.

float_text.mojo Download
"""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.