Exact math on rationals and decimals

The functions in httk.core.vectors.exactmath do exact and controlled-precision arithmetic. They are fully usable without FracVector: FracVector’s element-wise methods (sqrt, cos, exp, …) delegate here, but nothing stops you from calling these functions directly in your own code. Every value is computed with 100% exact integer/rational arithmetic — there is no floating point anywhere in the computation, so results are platform-independent and deterministic by construction.

from fractions import Fraction
from httk.core.vectors import exactmath

Parsing values exactly

any_to_fraction converts numbers and strings into exact rationals. Decimal strings are taken at their written value (unlike floats, which carry binary rounding):

exactmath.any_to_fraction("8.04")        # Fraction(201, 25)
exactmath.any_to_fraction("1/3")         # Fraction(1, 3)

A trailing parenthesized uncertainty (the common experimental notation) makes the parser pick the simplest rational inside the stated interval:

exactmath.any_to_fraction("0.33342(10)")  # Fraction(1, 3)
exactmath.string_to_val_and_delta("0.33342(10)")
# (Fraction(16671, 50000), Fraction(1, 10000))

Here 0.33342 ± 0.00010 brackets 1/3, so 1/3 is returned. Without an explicit uncertainty, min_accuracy (default 1/10000) plays the same role; pass min_accuracy=None to take a value exactly as written — including the exact binary rational of a float:

exactmath.any_to_fraction(0.1, min_accuracy=None)
# Fraction(3602879701896397, 36028797018963968)

Best rationals and continued fractions

best_rational_in_interval returns the rational with the smallest denominator in a closed interval — the workhorse behind the uncertainty parsing:

exactmath.best_rational_in_interval("3.14", "3.15")   # Fraction(22, 7)

The continued-fraction helpers round-trip exactly:

list(exactmath.get_continued_fraction(355, 113))      # [3, 7, 16]
exactmath.fraction_from_continued_fraction([3, 7, 16])  # Fraction(355, 113)

Controlled-precision transcendentals (Fraction domain)

Given Fraction/int/str input (and no digits=), the transcendentals return a Fraction within prec of the true value (default prec is very fine; pass a Fraction to control it). With limit=True (the default) the result’s denominator is kept near 1/prec rather than growing unboundedly:

exactmath.sqrt(Fraction(2), prec=Fraction(1, 10**12))
# Fraction(1402795082585, 991925915511)   — (value)**2 is within 1e-12 of 2

Exact results are returned when they exist:

exactmath.sqrt(Fraction(9, 4))               # Fraction(3, 2) — exact
exactmath.integer_sqrt(10**20)               # 10000000000 — exact integer sqrt

Exact square roots as surds (exact=True)

For an irrational square root, exact=True overrides the output-domain rule entirely and returns the value symbolically — as a SurdScalar, an element of the squarefree-radical field, with no approximation:

import fractions
from httk.core import SurdVector

root2 = exactmath.sqrt(fractions.Fraction(2), exact=True)
assert root2 * root2 == SurdVector.create(2)                       # squares back to exactly 2
assert exactmath.sqrt(fractions.Fraction(9, 4), exact=True) == SurdVector.create(fractions.Fraction(3, 2))

See Vectors (“Exact radicals: SurdVector”) for the field itself — exact Cartesian crystallographic geometry, exact comparison, and the nested-radical limit.

The trigonometric functions accept degrees=True to interpret their argument in degrees (cos, sin, …) or to return degrees (asin, acos, atan, atan2). atan2 follows the quadrant conventions of math.atan2():

exactmath.atan2(Fraction(1), Fraction(0), degrees=True)   # Fraction(90, 1)
exactmath.atan2(Fraction(0), Fraction(-1), degrees=True)  # Fraction(180, 1)

pi returns a high-precision rational for π; note that for any requested prec coarser than about 1e-13 it returns its precomputed high-precision constant (more precise than asked — use .limit_denominator() on the result if you want a small rational such as 355/113):

pi = exactmath.pi()
pi.limit_denominator(1000)               # Fraction(355, 113)

Decimal mode

The same functions render a correctly-rounded decimal.Decimal when asked. The type of the result follows a single documented rule:

The result is a Decimal iff any numeric input is a Decimal or digits= is passed; Fraction/int/str inputs otherwise get the exact Fraction behavior above.

A Decimal input therefore yields a Decimal, and digits= lets a Fraction/int caller (or the argument-less pi) request one:

import decimal
from fractions import Fraction

# Decimal input -> Decimal result:
assert isinstance(exactmath.sqrt(decimal.Decimal(2)), decimal.Decimal)
# digits= forces Decimal even from a Fraction input:
assert isinstance(exactmath.sqrt(Fraction(2), digits=10), decimal.Decimal)
# any Decimal argument promotes the whole result (mixed-argument promotion):
assert isinstance(exactmath.atan2(decimal.Decimal(1), Fraction(1)), decimal.Decimal)

digits= is the number of significant digits (default: the active decimal.getcontext() precision, matching stdlib Decimal’s own model), and rounding= selects "half_even" (the default — correctly rounded) or "down" (correct truncation toward zero). The rendering uses Ziv’s adaptive strategy over the exact rational algorithms, so results are correctly rounded:

import decimal

# sqrt(2) correctly rounded to 30 significant digits, half-even:
assert exactmath.sqrt(decimal.Decimal(2), digits=30) == decimal.Decimal(
    "1.41421356237309504880168872421"
)

Exactly-representable results short-circuit through exact arithmetic — including exact boundary cases — so, for example, the special-angle cosines/sines and perfect-square roots come back exactly:

import decimal

assert exactmath.cos(decimal.Decimal("60"), degrees=True) == decimal.Decimal("0.5")
assert exactmath.sin(decimal.Decimal("30"), degrees=True) == decimal.Decimal("0.5")
assert exactmath.sqrt(decimal.Decimal("2.25")) == decimal.Decimal("1.5")   # perfect square

Correct rounding vs correct truncation

Truncation is deterministic and gets the same adaptive treatment, so it is correct truncation of the true value — it has the same boundary hazard as rounding, and the iteration disambiguates both modes identically. Consider a value a hair above the 2-significant-digit boundary at 1.25 (constructed exactly as (1.25 + 1e-6)**2 so its square root is exactly 1.250001):

import decimal
from fractions import Fraction

boundary = (Fraction(125, 100) + Fraction(1, 10**6)) ** 2   # exact; sqrt is exactly 1.250001
assert exactmath.sqrt(boundary, digits=2, rounding="half_even") == decimal.Decimal("1.3")
assert exactmath.sqrt(boundary, digits=2, rounding="down") == decimal.Decimal("1.2")

Half-even rounds the value (just above 1.25) up to 1.3; truncation toward zero gives 1.2.

Determinism and the context default

Results are deterministic: the same call twice is identical, and with digits= given explicitly the result is independent of a changed decimal context. Only when digits= is omitted does the active context precision apply:

import decimal
from fractions import Fraction

# explicit digits= ignores the context precision entirely:
saved = decimal.getcontext().prec
decimal.getcontext().prec = 5
a = exactmath.sqrt(decimal.Decimal(2), digits=30)
decimal.getcontext().prec = 50
b = exactmath.sqrt(decimal.Decimal(2), digits=30)
decimal.getcontext().prec = saved
assert a == b

# without digits=, the context precision drives the significant-digit count:
decimal.getcontext().prec = 10
assert exactmath.sqrt(decimal.Decimal(2)) == decimal.Decimal("1.414213562")
decimal.getcontext().prec = saved

And pi(digits=n) gives π as a correctly-rounded Decimal:

import decimal

assert exactmath.pi(digits=50) == decimal.Decimal(
    "3.1415926535897932384626433832795028841971693993751"
)

Guaranteed termination

The adaptive rounding loop is not merely capped — it provably terminates, with no error path. The table-maker’s dilemma only bites when a function value sits exactly on a rounding boundary, and boundaries are rational numbers. Classical number theory rules that out for every function here once the exact special cases are handled: by the Lindemann–Weierstrass theorem, exp, log, and radian-mode trigonometry take transcendental values at nonzero rational arguments; by Niven’s theorem, degree-mode trigonometry takes rational values only at the tabulated special angles (all handled exactly); square roots of non-perfect-square rationals are irrational (perfect squares are detected exactly); and whether log(x, base) is rational is a finite exact decisionlog_base(x) = p/q requires x**q == base**p, and q is bounded by the largest prime exponent of base, so all candidates are checked with exact integer arithmetic:

import decimal
from httk.core.vectors import exactmath

# log_4(8) = 3/2 exactly: at one significant digit this is a perfect rounding tie,
# resolved by exact arithmetic — half-even rounds to 2, truncation gives 1.
assert exactmath.log(decimal.Decimal(8), decimal.Decimal(4), digits=1) == decimal.Decimal("2")
assert exactmath.log(decimal.Decimal(8), decimal.Decimal(4), digits=1, rounding="down") == decimal.Decimal("1")

# Exact special values survive truncation mode untouched:
assert exactmath.asin(decimal.Decimal("0.5"), degrees=True, digits=4, rounding="down") == decimal.Decimal("30")

Every remaining value is therefore provably irrational, hence never exactly on a boundary, and the interval refinement always disambiguates in finitely many steps — deterministically, on every platform.

Bounded time without losing determinism

Correct rounding has input-dependent cost: a value constructed to lie extremely close to a rounding boundary (e.g. sqrt((1.25 + 1e-40)**2) at two digits) forces many refinements before the interval clears the boundary. When a hard time bound matters more than correctness in that vanishing sliver, pass max_refinements=k: at most k refinements are performed, and if still ambiguous the approximation itself is rounded — the StrictMath philosophy of letting a frozen, exact algorithm define the function. The result is then correctly rounded unless the true value lies within 10**-(digits+3+4k) of a boundary, in which case it is the deterministic rounding of the deterministic approximant — off by at most one unit in the last place, and identical on every platform, every time:

import decimal
import fractions
from httk.core.vectors import exactmath

boundary = fractions.Fraction(125, 100) + fractions.Fraction(1, 10**40)
x = boundary * boundary
assert exactmath.sqrt(x, digits=2) == decimal.Decimal("1.3")     # unbounded: correct
fast = exactmath.sqrt(x, digits=2, max_refinements=0)            # bounded: one pass
assert fast == exactmath.sqrt(x, digits=2, max_refinements=0)    # ... and repeatable

The default (max_refinements=None) keeps the guaranteed-correct behavior above.

Using it with FracVector

FracVector’s element-wise transcendental methods call these functions on each element, so everything above applies vector-wide:

from httk.core import FracVector

v = FracVector.create([["9/4", "1/4"]])
v.sqrt().to_fractions()                  # [[Fraction(3, 2), Fraction(1, 2)]] — exact

See Vectors for the vector library itself and the Vector view family.