latmath.core — Core Mathematics
Integer arithmetic, rational numbers, fixed-point, bit operations, precondition checks, and error types — the foundational layer of latpy.
All symbols are re-exported from latmath.core:
from latpy.latmath.core import gcd, egcd, isqrt, Rational, fp_from_int, ...
Errors
from latpy.latmath.core.errors import LATMathError, DomainError, ShapeError, DTypeError
Exception |
Base |
When Raised |
|---|---|---|
|
|
Base for all latpy math errors |
|
|
Value outside mathematical domain (e.g. |
|
|
Incompatible array shapes |
|
|
Incompatible or unsupported data types |
All four exceptions form a flat hierarchy rooted at LATMathError, so callers can except LATMathError to catch any latpy math error.
from latpy.latmath.core import DomainError
try:
isqrt(-1)
except DomainError as e:
print(e) # "isqrt requires n >= 0, got -1"
Integer Operations (iops)
from latpy.latmath.core.iops import gcd, egcd, lcm, modinv, mul_div, clamp
Signature |
Description |
|---|---|
|
Greatest common divisor |
|
Extended GCD: |
|
Least common multiple |
|
Modular inverse of |
|
|
|
Clamp |
Rationale. Pure integer arithmetic is essential for cryptographic and number-theoretic routines where floating-point rounding is unacceptable. These functions operate on arbitrary-precision Python int values and raise DomainError on invalid input.
Examples
# Standard usage
gcd(12, 8) # 4
lcm(6, 10) # 30
egcd(12, 8) # (4, 1, -1) => 12*1 + 8*(-1) = 4
modinv(3, 11) # 4 => (3*4) % 11 = 1
mul_div(7, 3, 5) # 4 => (7*3)//5 = 4 (no overflow)
clamp(15, 0, 10) # 10
clamp(-3, 0, 10) # 0
# Edge: gcd(0, 0) by convention returns 0
gcd(0, 0) # 0
# Edge: egcd with negative numbers
egcd(-12, 8) # (4, -1, -1) => (-12)*(-1) + 8*(-1) = 4
egcd(12, -8) # (4, 1, 1) => 12*1 + (-8)*1 = 4
# Edge: modinv raises DomainError when inverse does not exist
modinv(2, 4) # DomainError: gcd(2,4) != 1, no modular inverse
modinv(0, 7) # DomainError: a=0 has no inverse modulo any m
# Edge: lcm with zero
lcm(0, 5) # 0
Integer Square Root (isqrt)
from latpy.latmath.core.isqrt import isqrt, is_square
Signature |
Description |
|---|---|
|
Floor of |
|
True if |
Both functions raise DomainError for negative inputs.
Examples
# Small numbers
isqrt(0) # 0
isqrt(1) # 1
isqrt(2) # 1
isqrt(9) # 3
isqrt(10) # 3
# Perfect squares
is_square(0) # True
is_square(1) # True
is_square(144) # True
is_square(2) # False
# Large numbers (arbitrary precision)
isqrt(10**100)
# 100000000000000000000000000000000000000000000000000
is_square(10**100) # True (10**100 = (10**50)^2)
is_square(10**100 + 1) # False
# DomainError on negative
isqrt(-1) # DomainError: isqrt requires n >= 0
is_square(-4) # DomainError: is_square requires n >= 0
Fixed-Point (fx)
from latpy.latmath.core.fx import fp_from_int, fp_to_int, fp_add, fp_sub, fp_mul, fp_div
All functions operate on int values representing scaled fixed-point numbers. A fixed-point number stores x * scale as an integer, enabling deterministic arithmetic without floating-point rounding.
Rationale. Fixed-point arithmetic is useful in lattice-based cryptography and deterministic ML pipelines where floating-point non-determinism across platforms must be avoided. Unlike float, fixed-point operations produce bit-identical results on any platform given the same scale.
Signature |
Description |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
Examples
scale = 1000 # 3 decimal places
# Convert to fixed-point
a = fp_from_int(3, scale) # 3000 (represents 3.0)
b = fp_from_int(2, scale) # 2000 (represents 2.0)
# Arithmetic
fp_add(a, b) # 5000 (represents 5.0)
fp_sub(a, b) # 1000 (represents 1.0)
fp_mul(a, b, scale) # 6000 (represents 6.0)
fp_div(a, b, scale) # 1500 (represents 1.5)
# Conversion back
fp_to_int(a, scale) # 3
# Negative values
neg = fp_from_int(-7, scale) # -7000
fp_add(neg, a) # -4000 (represents -4.0)
# Overflow: when the scale is too small for the product
# fp_mul(1_000_000, 1_000_000, scale=10) overflows a Python int?
# Python ints are arbitrary precision so no overflow, but precision is lost:
fp_mul(7, 3, scale=10)
# (7*10 * 3*10) // 10 = 2100 // 10 = 210 => 21.0 instead of 21.0 ✓
# But a larger product with small scale truncates more:
fp_mul(1234, 5678, scale=1)
# (1234*1 * 5678*1) // 1 = 7006652 => 7006652 ✓ (scale=1 is identity)
# Division by zero raises DomainError
fp_div(a, fp_from_int(0, scale), scale) # DomainError
Rational Numbers (rat)
from latpy.latmath.core.rat import Rational
Rational is an immutable, exact rational number. It represents num/den in lowest terms with den > 0. The numerator and denominator are reduced by their GCD at construction time.
Rationale. Lattice methods often involve exact comparisons of algebraic quantities. Using Rational avoids the rounding errors inherent in floating-point and guarantees that equality and ordering are mathematically exact. Use Rational whenever you need precise algebra and can tolerate the performance cost of arbitrary-precision integer arithmetic.
Signature |
Description |
|---|---|
|
Construct |
|
|
|
Numerator |
|
Denominator |
|
Approximate as float64 |
Operators: +, -, *, /, ==, <, <=, >, >=, abs, neg, int
Examples
from latpy.latmath.core import Rational
# Construction and normalization
r1 = Rational(2, 4) # 1/2 (auto-reduced)
r2 = Rational(3, 1) # 3/1
r3 = Rational.from_int(5) # 5/1
r1.num # 1
r1.den # 2
# Arithmetic (exact)
r1 + Rational(1, 3) # 5/6
r1 - Rational(1, 4) # 1/4
r1 * Rational(3, 2) # 3/4
r1 / Rational(1, 4) # 2/1 (i.e. 2)
# Comparison (exact, no floating-point)
Rational(1, 3) == Rational(2, 6) # True
Rational(1, 3) < Rational(1, 2) # True
# Conversion to float
r1.to_float() # 0.5
Rational(1, 3).to_float() # 0.3333333333333333
# Division by zero raises DomainError
Rational(1, 0) # DomainError: denominator must be non-zero
# Overflow behavior
# Python ints are arbitrary-precision, so "overflow" only occurs
# when converting to float via .to_float() if numerator/denominator
# exceed float64 range (~1e308):
Rational(10**200, 1).to_float() # OverflowError or inf
# Comparing Rational with float
# Direct comparison with float is supported via ==, <, etc.
Rational(1, 2) == 0.5 # True (converts Rational to float)
Rational(1, 3) < 0.34 # True
Rational(1, 3) == 0.33333333 # False (float is not exact 1/3)
When to Use Rational vs Fixed-Point vs Float
Type |
Pros |
Cons |
Use Case |
|---|---|---|---|
|
Exact arithmetic, arbitrary precision |
Slow for many operations |
Exact algebra, lattice proofs |
Fixed-point |
Deterministic, fast integer ops |
Limited precision by scale |
Deterministic ML, crypto protocols |
|
Fast, hardware-accelerated |
Non-deterministic, rounding errors |
Final output, approximate results |
Bit Operations (bits)
from latpy.latmath.core.bits import popcount, bit_length, rotl, rotr
Signature |
Description |
|---|---|
|
Number of 1-bits in non-negative |
|
Number of bits needed to represent ` |
|
Rotate |
|
Rotate |
Examples
# popcount
popcount(0) # 0
popcount(1) # 1
popcount(7) # 3 (0b111)
popcount(255) # 8 (0b11111111)
# bit_length
bit_length(0) # 0
bit_length(1) # 1
bit_length(255) # 8 (0b11111111)
bit_length(256) # 9 (0b100000000)
# Rotation
rotl(1, 1, bits=8) # 2 (binary: 00000001 -> 00000010)
rotr(4, 1, bits=8) # 2 (binary: 00000100 -> 00000010)
# Rotation with n > bits wraps around
rotl(1, 9, bits=8) # 2 (9 % 8 = 1; same as rotl(1, 1))
rotr(1, 9, bits=8) # 128 (rotate right by 1 position across 8 bits)
# Rotation preserves only bits within the mask
rotl(0b1100_0011, 4, bits=8) # 0b0011_1100
Precondition Checks (checks)
from latpy.latmath.core.checks import require_shape, require_dtype
Signature |
Description |
|---|---|
|
Raise |
|
Raise |
These are lightweight guard functions used internally for input validation. They return the array on success so they can be used inline.
from latpy.latmath.core import require_shape, require_dtype
from latpy.latmath.array import array
x = array([1, 2, 3])
require_shape(x, expected_ndim=1) # OK, returns x
require_shape(x, expected_shape=(3,)) # OK
require_shape(x, expected_shape=(4,)) # ShapeError
require_shape(x, expected_ndim=2) # ShapeError
require_dtype(x, 'int64', 'float64') # OK
require_dtype(x, 'float64') # DTypeError