# `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`: ```python from latpy.latmath.core import gcd, egcd, isqrt, Rational, fp_from_int, ... ``` --- ## Errors ```python 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. ```python from latpy.latmath.core import DomainError try: isqrt(-1) except DomainError as e: print(e) # "isqrt requires n >= 0, got -1" ``` --- ## Integer Operations (`iops`) ```python 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 ```python # 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`) ```python 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 ```python # 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`) ```python 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 ```python 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`) ```python 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 ```python 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`) ```python 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 `|x|` | | `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 ```python # 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`) ```python 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. ```python 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 ```