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

LATMathError

Exception

Base for all latpy math errors

DomainError

LATMathError

Value outside mathematical domain (e.g. isqrt(-1))

ShapeError

LATMathError

Incompatible array shapes

DTypeError

LATMathError

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

gcd(a, b) -> int

Greatest common divisor

egcd(a, b) -> tuple[int, int, int]

Extended GCD: (g, x, y) where g = a*x + b*y

lcm(a, b) -> int

Least common multiple

modinv(a, m) -> int

Modular inverse of a modulo m

mul_div(a, b, c) -> int

(a * b) // c with full intermediate precision

clamp(x, lo, hi) -> int

Clamp x to [lo, hi]

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

isqrt(n) -> int

Floor of sqrt(n) for non-negative n

is_square(n) -> bool

True if n is a perfect square

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

fp_from_int(x, scale) -> int

x * scale

fp_to_int(x_fp, scale) -> int

x_fp // scale

fp_add(a_fp, b_fp) -> int

a_fp + b_fp

fp_sub(a_fp, b_fp) -> int

a_fp - b_fp

fp_mul(a_fp, b_fp, scale) -> int

(a_fp * b_fp) // scale

fp_div(a_fp, b_fp, scale) -> int

(a_fp * scale) // b_fp

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

Rational(num, den=1)

Construct num/den (normalized, den > 0)

Rational.from_int(n)

Rational(n, 1)

.num

Numerator

.den

Denominator

.to_float() -> float

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

Rational

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

float

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

popcount(x) -> int

Number of 1-bits in non-negative x

bit_length(x) -> int

Number of bits needed to represent `

rotl(x, n, bits=64) -> int

Rotate x left by n positions within bits-bit mask

rotr(x, n, bits=64) -> int

Rotate x right by n positions within bits-bit mask

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

require_shape(arr, expected_ndim=None, expected_shape=None) -> NDArray

Raise ShapeError if arr doesn’t match; return arr on success

require_dtype(arr, *dtypes) -> NDArray

Raise DTypeError if dtype not in allowed set; return arr on success

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