Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 53 additions & 6 deletions src/mountainash_rules/engines/accumulator/primes.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,41 @@
"""Prime table and utilities for accumulator combination identity."""
"""Prime table and utilities for accumulator combination identity.

Combinations are identified by the product of their rules' primes. Two
*independent* limits bound this scheme:

* **Combination width — intrinsic, ~15.** No single combination can multiply
more than 15 primes without exceeding int64: the primorial of the first 15
primes is ``614_889_782_588_491_410 < 2**63``, but including the 16th prime
overflows. This bound is inherent to int64 and enforced by
``checked_multiply`` / ``LatticeWidthExceededError``. It does **not** depend on
the table size.
* **Rules per partition — policy, the table size.** Every rule needs a distinct
prime, so a partition is capped at ``MAX_RULES_PER_PARTITION`` rules. Exceeding
it raises from ``get_prime``. Remediation: split the partition with a
CONTEXT_KEY dimension.

The table is simply the first ``MAX_RULES_PER_PARTITION`` primes, computed once
at import by a bounded Sieve of Eratosthenes (microseconds). There is no value in
a precomputed literal or a data file: the sieve is already instant, and a table
"up to 2**64" is impossible (~4.2e17 primes ≈ exabytes) and the wrong dimension —
we index by rule count, never by prime magnitude.
"""

from __future__ import annotations

import math

_INT64_MAX = (2**63) - 1

# Maximum rules in a single partition = the prime-table size. Each rule is
# assigned a distinct prime by position; raise this here if a partition
# legitimately needs more rules (cost is an ~instant wider sieve). The separate,
# intrinsic width-15 bound (see module docstring) is unaffected by this value.
MAX_RULES_PER_PARTITION = 10_000


def _sieve(limit: int) -> list[int]:
"""Sieve of Eratosthenes up to `limit`."""
"""All primes up to ``limit`` (Sieve of Eratosthenes)."""
is_prime = [True] * (limit + 1)
is_prime[0] = is_prime[1] = False
for i in range(2, int(limit**0.5) + 1):
Expand All @@ -16,17 +45,35 @@ def _sieve(limit: int) -> list[int]:
return [i for i, v in enumerate(is_prime) if v]


PRIME_TABLE: list[int] = _sieve(3572)
def _first_n_primes(n: int) -> list[int]:
"""The first ``n`` primes.

Sizes the sieve from the prime-counting upper bound (Rosser:
``p_n < n(ln n + ln ln n)`` for ``n >= 6``) and widens defensively if the
bound was ever too tight.
"""
if n < 1:
return []
limit = 15 if n < 6 else int(n * (math.log(n) + math.log(math.log(n)))) + 3
primes = _sieve(limit)
while len(primes) < n: # defensive; the Rosser bound is not exceeded in practice
limit *= 2
primes = _sieve(limit)
return primes[:n]


PRIME_TABLE: list[int] = _first_n_primes(MAX_RULES_PER_PARTITION)


def get_prime(index: int) -> int:
"""Return the prime at the given 0-based index."""
"""Return the prime at the given 0-based index (a rule's position)."""
if index < 0:
raise IndexError(f"Prime index must be non-negative, got {index}")
if index >= len(PRIME_TABLE):
raise IndexError(
f"Prime index {index} exceeds table size {len(PRIME_TABLE)}. "
f"Partition has too many rules."
f"Prime index {index} exceeds the prime table "
f"(MAX_RULES_PER_PARTITION={MAX_RULES_PER_PARTITION}). "
f"Partition has too many rules; split it with a CONTEXT_KEY dimension."
)
return PRIME_TABLE[index]

Expand Down
28 changes: 26 additions & 2 deletions tests/accumulator/test_primes.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
import pytest

from mountainash_rules.engines.accumulator.primes import (
MAX_RULES_PER_PARTITION,
PRIME_TABLE,
LatticeWidthExceededError,
_first_n_primes,
get_prime,
checked_multiply,
)
Expand All @@ -14,8 +16,9 @@ class TestPrimeTable:
def test_first_primes_correct(self):
assert PRIME_TABLE[:10] == [2, 3, 5, 7, 11, 13, 17, 19, 23, 29]

def test_table_has_at_least_500_entries(self):
assert len(PRIME_TABLE) >= 500
def test_table_size_equals_cap(self):
# Invariant: the table is exactly the first MAX_RULES_PER_PARTITION primes.
assert len(PRIME_TABLE) == MAX_RULES_PER_PARTITION

def test_500th_prime_is_3571(self):
assert PRIME_TABLE[499] == 3571
Expand All @@ -27,6 +30,23 @@ def test_all_entries_are_prime(self):
assert p % d != 0, f"{p} is not prime"


class TestFirstNPrimes:
def test_empty_for_non_positive(self):
assert _first_n_primes(0) == []
assert _first_n_primes(-3) == []

def test_small_counts(self):
assert _first_n_primes(1) == [2]
assert _first_n_primes(5) == [2, 3, 5, 7, 11]

def test_returns_exact_count(self):
assert len(_first_n_primes(1000)) == 1000

def test_boundary_nth_prime_correct(self):
# 10000th prime is 104729 — exercises the Rosser sieve-sizing bound.
assert _first_n_primes(10000)[-1] == 104729


class TestGetPrime:
def test_index_zero_returns_two(self):
assert get_prime(0) == 2
Expand All @@ -41,6 +61,10 @@ def test_negative_index_raises(self):
with pytest.raises(IndexError):
get_prime(-1)

def test_index_beyond_cap_raises_with_constant_name(self):
with pytest.raises(IndexError, match="MAX_RULES_PER_PARTITION"):
get_prime(MAX_RULES_PER_PARTITION)


class TestCheckedMultiply:
def test_small_product_succeeds(self):
Expand Down
Loading