From a125bc5e40151e2a749ad5741885ee1e64c83417 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 10 Jul 2026 17:29:39 +0000 Subject: [PATCH] Add pytest suite, CI workflow, and hygiene fixes - tests/test_core.py: brute-force oracle check for carmichael_lambda, semiprime gcd identity, invariant-factor structure cross-checked against actual element orders, Korselt criterion vs known Carmichael numbers below 10^4, and the THEOREM.md worked propagation examples - .github/workflows/ci.yml: run pytest on push and pull request - gcd_distribution_theory.py: fix stale module docstring (the mean-gcd heuristic is sum 1/phi(d) ~ A log D, not a convergent sum 1/phi(d)^2) - .gitignore: ignore runs/ output dir and pytest cache, fix missing trailing newline Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01898JobGjDfsgm93YW6jH6s --- .github/workflows/ci.yml | 18 ++++ .gitignore | 4 +- gcd_distribution_theory.py | 3 +- tests/test_core.py | 173 +++++++++++++++++++++++++++++++++++++ 4 files changed, 196 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 tests/test_core.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..64a6db1 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,18 @@ +name: CI + +on: + push: + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + - name: Install test dependencies + run: pip install pytest + - name: Run tests + run: pytest tests/ -v diff --git a/.gitignore b/.gitignore index 994ca16..1f94156 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ /.venv -/__pycache__ \ No newline at end of file +__pycache__/ +/runs +.pytest_cache/ diff --git a/gcd_distribution_theory.py b/gcd_distribution_theory.py index e120a7f..3b4f2b5 100644 --- a/gcd_distribution_theory.py +++ b/gcd_distribution_theory.py @@ -15,7 +15,8 @@ Pr( l | gcd(p - 1, q - 1) ) over all distinct odd prime pairs and compares to the Dirichlet prediction 1/(l-1)^2. 3. Computes empirical E[gcd(p - 1, q - 1)] and the truncated heuristic - constant sum_{d <= D} 1/phi(d)^2 (the predicted limit). + sum_{d <= D} 1/phi(d), which grows like A log D with + A = 315 zeta(3) / (2 pi^4). 4. Renders a two-panel figure gcd_distribution.png: - left: empirical vs predicted Pr(l | gcd) on a log y-axis - right: the 1365 = 3*5*7*13 tie-back, showing how its propagation diff --git a/tests/test_core.py b/tests/test_core.py new file mode 100644 index 0000000..73bc098 --- /dev/null +++ b/tests/test_core.py @@ -0,0 +1,173 @@ +"""Tests for the core library in lambda_ratio_explorer.py. + +The brute-force Carmichael implementation serves as the oracle for the +fast one, and the THEOREM.md worked examples serve as fixtures for the +collapse propagation theorem. +""" + +import sys +from collections import Counter +from math import gcd +from pathlib import Path + +import pytest + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +from lambda_ratio_explorer import ( + carmichael_lambda, + collapse_index, + collapse_propagation_trace, + collapse_step, + divisors, + element_orders, + euler_totient, + factorize, + fracture_count, + invariant_factors, + is_carmichael, + is_prime, + kind, +) + + +PRIMES_TO_50 = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47] + +# Carmichael numbers below 10^4 (OEIS A002997). +CARMICHAEL_BELOW_10K = [561, 1105, 1729, 2465, 2821, 6601, 8911] + + +class TestFactorize: + @pytest.mark.parametrize("n", range(1, 500)) + def test_reconstructs_n(self, n): + product = 1 + for p, e in factorize(n).items(): + assert is_prime(p) + product *= p ** e + assert product == n + + +class TestCarmichaelLambda: + @pytest.mark.parametrize("n", range(1, 151)) + def test_fast_matches_bruteforce(self, n): + assert carmichael_lambda(n) == carmichael_lambda(n, method="brute") + + def test_powers_of_two(self): + # lambda(2) = 1, lambda(4) = 2, lambda(2^k) = 2^(k-2) for k >= 3 + assert carmichael_lambda(2) == 1 + assert carmichael_lambda(4) == 2 + assert carmichael_lambda(8) == 2 + assert carmichael_lambda(16) == 4 + assert carmichael_lambda(1024) == 256 + + @pytest.mark.parametrize("n", range(2, 300)) + def test_divides_phi(self, n): + assert euler_totient(n) % carmichael_lambda(n) == 0 + + +class TestCollapseIndex: + def test_semiprime_gcd_identity(self): + odd_primes = [p for p in PRIMES_TO_50 if p > 2] + for i, p in enumerate(odd_primes): + for q in odd_primes[i + 1:]: + assert collapse_index(p * q) == gcd(p - 1, q - 1) + + @pytest.mark.parametrize("p", PRIMES_TO_50) + def test_primes_are_cyclic(self, p): + assert collapse_index(p) == 1 + + def test_known_extremes(self): + assert collapse_index(1365) == 48 # 3 * 5 * 7 * 13 + assert collapse_index(1729) == 36 # 7 * 13 * 19 + + +class TestInvariantFactors: + @pytest.mark.parametrize("n", range(2, 300)) + def test_structure(self, n): + factors = invariant_factors(n) + assert factors == sorted(factors) + # divisibility chain d_1 | d_2 | ... | d_k + for a, b in zip(factors, factors[1:]): + assert b % a == 0 + product = 1 + for d in factors: + product *= d + assert product == euler_totient(n) + assert factors[-1] == carmichael_lambda(n) + assert fracture_count(n) == len(factors) + + @pytest.mark.parametrize("n", [7, 8, 15, 16, 24, 35, 63, 91, 105]) + def test_matches_element_orders(self, n): + """The order histogram of the abstract product of cyclic groups + must match the actual element orders in (Z/nZ)*. + + In a cyclic group of order d there are phi(e) elements of order e + for each e | d; in a product, orders combine by lcm. + """ + expected = Counter({1: 1}) + for d in invariant_factors(n): + combined = Counter() + for order, count in expected.items(): + for e in divisors(d): + combined[lcm_pair(order, e)] += count * euler_totient(e) + expected = combined + actual = Counter(element_orders(n).values()) + assert actual == expected + + +def lcm_pair(a, b): + return a * b // gcd(a, b) + + +class TestCollapsePropagation: + def test_theorem_worked_example_1365(self): + trace = collapse_propagation_trace([3, 5, 7, 13]) + assert [s["C_kp"] for s in trace] == [1, 2, 4, 48] + assert trace[-1]["kp"] == 1365 + + def test_theorem_worked_example_1729(self): + trace = collapse_propagation_trace([7, 13, 19]) + assert [s["C_kp"] for s in trace] == [1, 6, 36] + assert trace[-1]["kp"] == 1729 + + def test_order_independence(self): + for perm in ([13, 7, 5, 3], [5, 13, 3, 7]): + assert collapse_propagation_trace(perm)[-1]["C_kp"] == 48 + + def test_rejects_duplicate_prime(self): + with pytest.raises(ValueError): + collapse_propagation_trace([3, 3]) + + def test_rejects_composite(self): + with pytest.raises(ValueError): + collapse_step(1, 6) + + def test_rejects_shared_factor(self): + with pytest.raises(ValueError): + collapse_step(15, 5) + + +class TestIsCarmichael: + def test_known_carmichael_numbers(self): + found = [n for n in range(2, 10000) if is_carmichael(n)] + assert found == CARMICHAEL_BELOW_10K + + @pytest.mark.parametrize("n", range(2, 3000)) + def test_matches_lambda_criterion(self, n): + """Carmichael numbers are exactly the composites with lambda(n) | n - 1 + (and more than one prime factor, i.e. squarefree composites).""" + expected = ( + not is_prime(n) + and len(factorize(n)) > 1 + and all(e == 1 for e in factorize(n).values()) + and (n - 1) % carmichael_lambda(n) == 0 + ) + assert is_carmichael(n) == expected + + +class TestKind: + def test_classification(self): + assert kind(7) == "prime" + assert kind(8) == "prime_power" + assert kind(9) == "prime_power" + assert kind(12) == "composite"