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
62 changes: 62 additions & 0 deletions tests/test_triadic_domains.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
from triadic_domains import TriadicDomains
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Add repo-root path setup before importing triadic_domains

This test module imports triadic_domains directly without first adding the repository root to sys.path, unlike tests/test_basic.py, so invoking tests via the pytest entrypoint can fail during collection with ModuleNotFoundError: No module named 'triadic_domains'. In this environment, pytest -q tests/test_triadic_domains.py reproduces the error immediately, which blocks the new test suite from running in common CI/test invocations.

Useful? React with 👍 / 👎.



def test_unity_even_odd_placements():
domains = TriadicDomains([2, 3, 5, 7])

assert domains.place(1).domain == "U"
assert domains.place(1).axis == "U1"

placed_even = domains.place(40)
assert placed_even.domain == "E"
assert placed_even.axis == "E3"
assert placed_even.cofactor == 5

placed_prime = domains.place(31)
assert placed_prime.domain == "O"
assert placed_prime.axis == "O1"
assert placed_prime.kind == "odd-prime"


def test_odd_composites_are_lpf_axis_placed():
domains = TriadicDomains([2, 3, 5, 7])

expected = {
9: ("O2", 3, 3),
15: ("O2", 3, 5),
21: ("O2", 3, 7),
25: ("O3", 5, 5),
27: ("O2", 3, 9),
35: ("O3", 5, 7),
49: ("O4", 7, 7),
}

for value, (axis, lpf, cofactor) in expected.items():
placement = domains.place(value)
assert placement.domain == "O"
assert placement.kind == "odd-composite"
assert placement.axis == axis
assert placement.least_prime_factor == lpf
assert placement.cofactor == cofactor


def test_axis_law_first_hole_examples():
domains_after_3 = TriadicDomains([2, 3])
assert domains_after_3.axis_law_successor_gap(3, limit=16) == (5, 2)
assert domains_after_3.axis_law_successor_gap(5, limit=16) == (7, 2)
assert domains_after_3.axis_law_successor_gap(7, limit=16) == (11, 4)

domains_after_7 = TriadicDomains([2, 3, 5, 7])
assert domains_after_7.axis_law_successor_gap(29, limit=64) == (31, 2)
assert domains_after_7.place(31).axis == "O1"


def test_composite_stream_has_canonical_lpf_entries_without_duplicate_axes():
domains = TriadicDomains([2, 3, 5, 7])
placements = {p.value: p for p in domains.odd_composite_stream(50)}

assert placements[15].axis == "O2"
assert placements[15].least_prime_factor == 3
assert placements[25].axis == "O3"
assert placements[35].axis == "O3"
assert placements[49].axis == "O4"
177 changes: 177 additions & 0 deletions triadic_domains.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
"""
Triadic Completeness domain model for McCrackn's Prime Law.

This module keeps the TC / UEO layer separate from the gap-motif scheduler.
It models the paper-level bridge:

U -> E -> O
M_j = <{2} union P_j>
p_{j+1} = min(N>=1 \ M_j)

The implementation is deliberately finite-prefix and audit oriented. It does
not perform trial division or primality testing. Composite placement is derived
from already realized axes.
"""
from __future__ import annotations

from dataclasses import dataclass
from heapq import heappop, heappush
from typing import Iterable, Iterator, Literal


AxisKind = Literal["unity", "even", "odd-prime", "odd-composite"]


@dataclass(frozen=True)
class AxisPlacement:
"""Canonical U/E/O placement for a positive integer in a realized prefix."""

value: int
domain: str
axis: str
kind: AxisKind
least_prime_factor: int | None = None
cofactor: int | None = None


class TriadicDomains:
"""
Finite-prefix TC axis machine.

The odd domain is split into:
- O1: the free / prime odd axis in the user's notation.
- O{k}: least-prime-factor composite strata, where O2 is lpf=3,
O3 is lpf=5, O4 is lpf=7, and so on.

The paper notation uses O_0 for the prime/free layer and O_i^(min)
for least-prime-factor composite strata. This class exposes both via
explicit placement metadata.
"""

UNITY = 1

def __init__(self, realized_primes: Iterable[int]):
primes = tuple(int(p) for p in realized_primes)
if not primes or primes[0] != 2:
raise ValueError("realized_primes must start with 2")
if any(p <= 0 for p in primes):
raise ValueError("realized_primes must be positive")
self.realized_primes = primes
self.odd_primes = tuple(p for p in primes if p != 2)
self._odd_axis_index = {p: i + 2 for i, p in enumerate(self.odd_primes)}

@staticmethod
def odd_axis_value(position: int) -> int:
"""Return the primitive odd-axis value at zero-based position."""
if position < 0:
raise ValueError("position must be non-negative")
return 2 * position + 1

def even_face(self, n: int) -> tuple[int, int]:
"""Return (v2(n), odd_face) for a positive integer."""
if n <= 0:
raise ValueError("n must be positive")
depth = 0
value = n
while value % 2 == 0:
depth += 1
value //= 2
return depth, value

def emitted_smooth_numbers(self, limit: int) -> list[int]:
"""
Emit the finite prefix of <{2} union P_j> up to limit.

This uses merge/multiply stream operations over already realized axes.
It is a finite-prefix witness for Axis Law coverage, not a primality
test over arbitrary candidates.
"""
if limit < 1:
return []
seen = {1}
heap = [1]
out: list[int] = []
while heap:
value = heappop(heap)
if value > limit:
continue
out.append(value)
for axis in self.realized_primes:
nxt = value * axis
if nxt <= limit and nxt not in seen:
seen.add(nxt)
heappush(heap, nxt)
return out

def first_hole_after(self, current_prime: int, limit: int | None = None) -> int:
"""
Return the first positive integer after current_prime not emitted by
the realized-axis monoid within a finite bound.
"""
if current_prime < 1:
raise ValueError("current_prime must be positive")
bound = limit if limit is not None else max(current_prime * current_prime, current_prime + 32)
covered = set(self.emitted_smooth_numbers(bound))
for n in range(current_prime + 1, bound + 1):
if n not in covered:
return n
raise ValueError("finite bound did not expose a first hole")

def axis_law_successor_gap(self, current_prime: int, limit: int | None = None) -> tuple[int, int]:
"""Return (next_prime, gap) from the finite first-hole law."""
nxt = self.first_hole_after(current_prime, limit=limit)
return nxt, nxt - current_prime

def odd_composite_stream(self, limit: int) -> Iterator[AxisPlacement]:
"""Yield canonical odd composite placements up to limit by lpf strata."""
if limit < 3:
return
for p in self.odd_primes:
for odd in range(p, limit // p + 1, 2):
value = p * odd
if value > limit:
break
if self._least_realized_odd_prime_factor(value) == p and value != p:
yield AxisPlacement(
value=value,
domain="O",
axis=f"O{self._odd_axis_index[p]}",
kind="odd-composite",
least_prime_factor=p,
cofactor=odd,
)

def place(self, n: int) -> AxisPlacement:
"""Return the canonical finite-prefix U/E/O placement for n."""
if n <= 0:
raise ValueError("n must be positive")
if n == 1:
return AxisPlacement(n, "U", "U1", "unity")

depth, odd_face = self.even_face(n)
if depth > 0:
return AxisPlacement(n, "E", f"E{depth}", "even", cofactor=odd_face)

if n in self.odd_primes:
return AxisPlacement(n, "O", "O1", "odd-prime")

lpf = self._least_realized_odd_prime_factor(n)
if lpf is None:
return AxisPlacement(n, "O", "O1", "odd-prime")

return AxisPlacement(
value=n,
domain="O",
axis=f"O{self._odd_axis_index[lpf]}",
kind="odd-composite",
least_prime_factor=lpf,
cofactor=n // lpf,
)

def _least_realized_odd_prime_factor(self, n: int) -> int | None:
for p in self.odd_primes:
if p * p > n:
break
if n % p == 0:
return p
return None