chore(prime-sidon): documented negative result — primes indistinguishable from random in Sidon sum-degeneracy

35 test cases across 7 scales (small through quintillion) and 5 sizes.
Result: 1/35 significant at p<0.05 (0/35 after Bonferroni).
Null hypothesis not rejected.

Key methodology fixes from adversarial review:
  - Replaced float-based eigenvalue products with integer-only sum-counting
  - Added analytical bounds showing 'between' claim is tautological
  - Added permutation test against random n-subsets at same scale
  - Documented why earlier float-based 'convergence' was a precision artifact

Receipt: docs/research/PRIME_SIDON_NEGATIVE_RESULT.md
DAG: .openresearch/artifacts/prime_sidon_dag.json (51 nodes, 35 edges)
Script: scripts/prime_sidon_explore.py

Build: N/A (Python script, no Lean build)
This commit is contained in:
allaun 2026-07-03 18:16:42 -05:00
parent f1a050277b
commit a0d95049c6
5 changed files with 4144 additions and 0 deletions

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,99 @@
# Prime Sidon Exploration DAG
**Nodes:** 51
**Edges:** 35
## Nodes
| ID | Type | Status | Elapsed | Inputs |
|----|------|--------|---------|--------|
| bounds_s5 | analytical_bounds | success | 0.0s | {"size": 5, "lower_bound": 0.36, "upper_bound": 0.6} |
| bounds_s6 | analytical_bounds | success | 0.0s | {"size": 6, "lower_bound": 0.3056, "upper_bound": 0.5833} |
| bounds_s7 | analytical_bounds | success | 0.0s | {"size": 7, "lower_bound": 0.2653, "upper_bound": 0.5714} |
| bounds_s8 | analytical_bounds | success | 0.0s | {"size": 8, "lower_bound": 0.2344, "upper_bound": 0.5625} |
| bounds_s10 | analytical_bounds | success | 0.0s | {"size": 10, "lower_bound": 0.19, "upper_bound": 0.55} |
| sidon_pow2_s5 | reference | success | 0.0s | {"type": "sidon_pow2", "size": 5, "labels": [1, 2, 4, 8, 16] |
| nonsidon_seq_s5 | reference | success | 0.0s | {"type": "nonsidon_consecutive", "size": 5, "labels": [1, 2, |
| sidon_pow2_s6 | reference | success | 0.0s | {"type": "sidon_pow2", "size": 6, "labels": [1, 2, 4, 8, 16, |
| nonsidon_seq_s6 | reference | success | 0.0s | {"type": "nonsidon_consecutive", "size": 6, "labels": [1, 2, |
| sidon_pow2_s7 | reference | success | 0.0s | {"type": "sidon_pow2", "size": 7, "labels": [1, 2, 4, 8, 16, |
| nonsidon_seq_s7 | reference | success | 0.0s | {"type": "nonsidon_consecutive", "size": 7, "labels": [1, 2, |
| sidon_pow2_s8 | reference | success | 0.0s | {"type": "sidon_pow2", "size": 8, "labels": [1, 2, 4, 8, 16, |
| nonsidon_seq_s8 | reference | success | 0.0s | {"type": "nonsidon_consecutive", "size": 8, "labels": [1, 2, |
| sidon_pow2_s10 | reference | success | 0.0s | {"type": "sidon_pow2", "size": 10, "labels": [1, 2, 4, 8, 16 |
| nonsidon_seq_s10 | reference | success | 0.0s | {"type": "nonsidon_consecutive", "size": 10, "labels": [1, 2 |
| primes_small_s5 | prime_set | success | 0.0297s | {"size": 5, "scale": "small", "labels": [2, 3, 5, 7, 11], "r |
| primes_small_s6 | prime_set | success | 0.0344s | {"size": 6, "scale": "small", "labels": [2, 3, 5, 7, 11, 13] |
| primes_small_s7 | prime_set | success | 0.0411s | {"size": 7, "scale": "small", "labels": [2, 3, 5, 7, 11, 13, |
| primes_small_s8 | prime_set | success | 0.0475s | {"size": 8, "scale": "small", "labels": [2, 3, 5, 7, 11, 13, |
| primes_small_s10 | prime_set | success | 0.0636s | {"size": 10, "scale": "small", "labels": [2, 3, 5, 7, 11, 13 |
| primes_kilo_s5 | prime_set | success | 0.0322s | {"size": 5, "scale": "kilo", "labels": [1009, 1013, 1019, 10 |
| primes_kilo_s6 | prime_set | success | 0.0415s | {"size": 6, "scale": "kilo", "labels": [1009, 1013, 1019, 10 |
| primes_kilo_s7 | prime_set | success | 0.0479s | {"size": 7, "scale": "kilo", "labels": [1009, 1013, 1019, 10 |
| primes_kilo_s8 | prime_set | success | 0.0576s | {"size": 8, "scale": "kilo", "labels": [1009, 1013, 1019, 10 |
| primes_kilo_s10 | prime_set | success | 0.0795s | {"size": 10, "scale": "kilo", "labels": [1009, 1013, 1019, 1 |
| primes_million_s5 | prime_set | success | 0.0347s | {"size": 5, "scale": "million", "labels": [1000003, 1000033, |
| primes_million_s6 | prime_set | success | 0.0411s | {"size": 6, "scale": "million", "labels": [1000003, 1000033, |
| primes_million_s7 | prime_set | success | 0.0512s | {"size": 7, "scale": "million", "labels": [1000003, 1000033, |
| primes_million_s8 | prime_set | success | 0.0603s | {"size": 8, "scale": "million", "labels": [1000003, 1000033, |
| primes_million_s10 | prime_set | success | 0.0822s | {"size": 10, "scale": "million", "labels": [1000003, 1000033 |
| primes_billion_s5 | prime_set | success | 0.0347s | {"size": 5, "scale": "billion", "labels": [1000000007, 10000 |
| primes_billion_s6 | prime_set | success | 0.0431s | {"size": 6, "scale": "billion", "labels": [1000000007, 10000 |
| primes_billion_s7 | prime_set | success | 0.0512s | {"size": 7, "scale": "billion", "labels": [1000000007, 10000 |
| primes_billion_s8 | prime_set | success | 0.0608s | {"size": 8, "scale": "billion", "labels": [1000000007, 10000 |
| primes_billion_s10 | prime_set | success | 0.0843s | {"size": 10, "scale": "billion", "labels": [1000000007, 1000 |
| primes_trillion_s5 | prime_set | success | 0.0343s | {"size": 5, "scale": "trillion", "labels": [1000000000039, 1 |
| primes_trillion_s6 | prime_set | success | 0.0423s | {"size": 6, "scale": "trillion", "labels": [1000000000039, 1 |
| primes_trillion_s7 | prime_set | success | 0.0515s | {"size": 7, "scale": "trillion", "labels": [1000000000039, 1 |
| primes_trillion_s8 | prime_set | success | 0.0614s | {"size": 8, "scale": "trillion", "labels": [1000000000039, 1 |
| primes_trillion_s10 | prime_set | success | 0.0864s | {"size": 10, "scale": "trillion", "labels": [1000000000039, |
| primes_quadrillion_s5 | prime_set | success | 0.034s | {"size": 5, "scale": "quadrillion", "labels": [1000000000000 |
| primes_quadrillion_s6 | prime_set | success | 0.0424s | {"size": 6, "scale": "quadrillion", "labels": [1000000000000 |
| primes_quadrillion_s7 | prime_set | success | 0.0512s | {"size": 7, "scale": "quadrillion", "labels": [1000000000000 |
| primes_quadrillion_s8 | prime_set | success | 0.0624s | {"size": 8, "scale": "quadrillion", "labels": [1000000000000 |
| primes_quadrillion_s10 | prime_set | success | 0.0868s | {"size": 10, "scale": "quadrillion", "labels": [100000000000 |
| primes_quintillion_s5 | prime_set | success | 0.0351s | {"size": 5, "scale": "quintillion", "labels": [1000000000000 |
| primes_quintillion_s6 | prime_set | success | 0.0441s | {"size": 6, "scale": "quintillion", "labels": [1000000000000 |
| primes_quintillion_s7 | prime_set | success | 0.0527s | {"size": 7, "scale": "quintillion", "labels": [1000000000000 |
| primes_quintillion_s8 | prime_set | success | 0.0646s | {"size": 8, "scale": "quintillion", "labels": [1000000000000 |
| primes_quintillion_s10 | prime_set | success | 0.0886s | {"size": 10, "scale": "quintillion", "labels": [100000000000 |
| summary | summary | success | 0.0s | {"schema": "prime_sidon_explore_v2", "claim_boundary": "prim |
## Edges
| From | To | Type |
|------|----|------|
| primes_small_s5 | summary | feeds_summary |
| primes_small_s6 | summary | feeds_summary |
| primes_small_s7 | summary | feeds_summary |
| primes_small_s8 | summary | feeds_summary |
| primes_small_s10 | summary | feeds_summary |
| primes_kilo_s5 | summary | feeds_summary |
| primes_kilo_s6 | summary | feeds_summary |
| primes_kilo_s7 | summary | feeds_summary |
| primes_kilo_s8 | summary | feeds_summary |
| primes_kilo_s10 | summary | feeds_summary |
| primes_million_s5 | summary | feeds_summary |
| primes_million_s6 | summary | feeds_summary |
| primes_million_s7 | summary | feeds_summary |
| primes_million_s8 | summary | feeds_summary |
| primes_million_s10 | summary | feeds_summary |
| primes_billion_s5 | summary | feeds_summary |
| primes_billion_s6 | summary | feeds_summary |
| primes_billion_s7 | summary | feeds_summary |
| primes_billion_s8 | summary | feeds_summary |
| primes_billion_s10 | summary | feeds_summary |
| primes_trillion_s5 | summary | feeds_summary |
| primes_trillion_s6 | summary | feeds_summary |
| primes_trillion_s7 | summary | feeds_summary |
| primes_trillion_s8 | summary | feeds_summary |
| primes_trillion_s10 | summary | feeds_summary |
| primes_quadrillion_s5 | summary | feeds_summary |
| primes_quadrillion_s6 | summary | feeds_summary |
| primes_quadrillion_s7 | summary | feeds_summary |
| primes_quadrillion_s8 | summary | feeds_summary |
| primes_quadrillion_s10 | summary | feeds_summary |
| primes_quintillion_s5 | summary | feeds_summary |
| primes_quintillion_s6 | summary | feeds_summary |
| primes_quintillion_s7 | summary | feeds_summary |
| primes_quintillion_s8 | summary | feeds_summary |
| primes_quintillion_s10 | summary | feeds_summary |

View file

@ -0,0 +1,117 @@
{
"schema": "prime_sidon_explore_v2",
"claim_boundary": "prime-sidon-vs-random-null:permutation-test",
"config": {
"sizes": [
5,
6,
7,
8,
10
],
"scales": [
"small",
"kilo",
"million",
"billion",
"trillion",
"quadrillion",
"quintillion"
],
"null_trials": 1000
},
"analysis": [
{
"size": 5,
"n_significant": 0,
"n_total": 7,
"all_significant": false,
"negative_effects": 2,
"positive_effects": 5,
"score_trend_increasing": false,
"scores_by_scale": {
"small": 0.52,
"kilo": 0.56,
"million": 0.6,
"billion": 0.56,
"trillion": 0.56,
"quadrillion": 0.6,
"quintillion": 0.6
}
},
{
"size": 6,
"n_significant": 0,
"n_total": 7,
"all_significant": false,
"negative_effects": 5,
"positive_effects": 2,
"score_trend_increasing": false,
"scores_by_scale": {
"small": 0.472222,
"kilo": 0.5,
"million": 0.583333,
"billion": 0.555556,
"trillion": 0.555556,
"quadrillion": 0.555556,
"quintillion": 0.555556
}
},
{
"size": 7,
"n_significant": 0,
"n_total": 7,
"all_significant": false,
"negative_effects": 5,
"positive_effects": 2,
"score_trend_increasing": true,
"scores_by_scale": {
"small": 0.428571,
"kilo": 0.489796,
"million": 0.530612,
"billion": 0.55102,
"trillion": 0.55102,
"quadrillion": 0.55102,
"quintillion": 0.55102
}
},
{
"size": 8,
"n_significant": 0,
"n_total": 7,
"all_significant": false,
"negative_effects": 4,
"positive_effects": 3,
"score_trend_increasing": false,
"scores_by_scale": {
"small": 0.390625,
"kilo": 0.453125,
"million": 0.515625,
"billion": 0.53125,
"trillion": 0.546875,
"quadrillion": 0.515625,
"quintillion": 0.546875
}
},
{
"size": 10,
"n_significant": 1,
"n_total": 7,
"all_significant": false,
"negative_effects": 7,
"positive_effects": 0,
"score_trend_increasing": false,
"scores_by_scale": {
"small": 0.33,
"kilo": 0.39,
"million": 0.48,
"billion": 0.51,
"trillion": 0.51,
"quadrillion": 0.52,
"quintillion": 0.49
}
}
],
"total_test_cases": 35,
"total_significant": 1
}

View file

@ -0,0 +1,66 @@
# Prime Sidon Spectral Signature — Negative Result
**Status:** VERIFIED NEGATIVE — null hypothesis not rejected
**Date:** 2026-07-03
**Script:** `scripts/prime_sidon_explore.py`
**Artifacts:** `.openresearch/artifacts/prime_sidon_dag.json` (51 nodes, 35 edges)
---
## Hypothesis
Prime-based label sets exhibit a spectral signature in Sidon sum-degeneracy that distinguishes them from random numbers of the same magnitude.
## Method
For each label set A = {a₁, …, aₙ}, compute:
1. **Sum matrix** M[i][j] = a_i + a_j (pure integer arithmetic)
2. **Sidon score** = distinct_sums / n² (1.0 = perfect Sidon, all pairwise sums distinct)
Compare prime clusters against a null distribution of 1000 random n-subsets of integers in the same [min, max] range. Two-sided permutation test.
**Scales tested:** small (2..19), kilo (~10³), million (~10⁶), billion (~10⁹), trillion (~10¹²), quadrillion (~10¹⁵), quintillion (~10¹⁸)
**Sizes tested:** n ∈ {5, 6, 7, 8, 10}
**Total test cases:** 35
## Result
| Measure | Value |
|---------|-------|
| Significant at p < 0.05 | **1/35** |
| Significant after Bonferroni (α = 0.0014) | **0/35** |
| Expected false positives at α = 0.05 | 1.75 |
| Observed false positives | 1 |
The null hypothesis is **not rejected**. Prime Sidon scores are indistinguishable from random n-subsets at the same scale.
## Why Earlier Analysis Was Misleading
The initial `prime_slos_explore.py` (using SLOS + eigenvalue products) found that primes sit "between" Sidon (powers of 2) and non-Sidon (consecutive integers). Adversarial review identified two fatal flaws:
1. **Mathematical tautology**: For *any* set of n distinct integers, the Sidon score is provably bounded by `(2n-1)/n² ≤ score ≤ (n+1)/(2n)`. The "BETWEEN" result adds zero empirical information — it's equivalent to "primes are positive integers."
2. **Float precision artifact**: The eigenvalue product and tensor entropy metrics used float64 arithmetic. At billion+ scales, consecutive primes have tiny relative gaps (~10⁻⁷), making the sum matrix numerically rank-1. The apparent "convergence to a fixed point" was float64 saturation, not a physical phenomenon.
## What Was Learned
1. **Adversarial review caught the tautology** — the corrected script explicitly reports analytical bounds before any empirical test
2. **Permutation test is essential** — comparing against a null distribution of random numbers at the same range, not against extreme theoretical bounds
3. **Integer-only arithmetic avoids float artifacts** — the corrected script uses only integer sum-counting, no float, no eigenvalue decompositions, no SLOS
4. **The prime structure does not manifest in pairwise sum degeneracy** — at least not for consecutive prime clusters up to quintillion scale
## DAG Structure
- 10 reference nodes (analytical bounds + baseline verification)
- 35 prime set nodes (each with permutation test against 1000 random subsets)
- 5 analysis nodes (cross-scale per size)
- 1 summary node
- 35 edges connecting each prime set to the summary
## claim_boundary
```
prime-sidon-sum-degeneracy:negative-result:permutation-test
```
This receipt is bounded to the claim that consecutive prime clusters at 7 scales and 5 sizes (35 test cases) do not deviate from random n-subsets at p < 0.05 in Sidon sum-degeneracy score. It does not claim that primes have *no* additive structure only that this specific metric cannot distinguish them from random numbers at the same scale.

View file

@ -0,0 +1,470 @@
#!/usr/bin/env python3
"""prime_sidon_explore.py — Integer-only Sidon sum-degeneracy exploration.
Tests whether prime-based label sets satisfy the Sidon property (all pairwise
sums distinct) across scales from small primes (2,3,5,7,11) up to quintillion
(10^18).
All arithmetic is pure integer. No float, no numpy eigenvalue decompositions,
no SLOS, no tensor networks. The metric is the actual Sidon property:
A set A is Sidon iff for all i,j,k,l, a_i + a_j = a_k + a_l {i,j} = {k,l}
For each label set we compute:
- Total pairs: n^2 (including order, so (i,j) counts separately from (j,i))
- Distinct sums: number of unique values in the sum matrix
- Sidon score: distinct_sums / n^2 (1.0 = perfect Sidon)
- Max degeneracy: highest multiplicity of any single sum
- Collision entropy: Shannon entropy of the degeneracy distribution
- Collision count: number of sum values that appear 2 times
Each computation step is a DAG node, checkpointed to disk for resume.
The DAG structure allows adversarial review of each step.
"""
import sys
import math
import json
import time
import hashlib
import itertools
from pathlib import Path
from collections import Counter
REPO_ROOT = Path(__file__).resolve().parent.parent
ARTIFACTS_DIR = REPO_ROOT / ".openresearch" / "artifacts"
DAG_PATH = ARTIFACTS_DIR / "prime_sidon_dag.json"
OUTPUT_PATH = ARTIFACTS_DIR / "prime_sidon_explore.json"
# ── Recoverable DAG ──────────────────────────────────────────────────────────
class ComputationDAG:
def __init__(self):
self.nodes = []
self.edges = []
ARTIFACTS_DIR.mkdir(parents=True, exist_ok=True)
def add_node(self, node_id, node_type, inputs, result, status, elapsed):
node = {
"id": node_id, "type": node_type,
"inputs": inputs, "result": result,
"status": status, "elapsed_s": round(elapsed, 4),
"timestamp": time.time(),
}
self.nodes.append(node)
return node
def add_edge(self, from_id, to_id, edge_type="depends_on"):
self.edges.append({"from": from_id, "to": to_id, "type": edge_type})
def has_node(self, node_id):
return any(n["id"] == node_id for n in self.nodes)
def get_node(self, node_id):
for n in self.nodes:
if n["id"] == node_id:
return n["result"]
return None
def save(self):
dag_state = {"nodes": self.nodes, "edges": self.edges}
DAG_PATH.write_text(json.dumps(dag_state, default=str, indent=2))
def save_report(self):
report_path = ARTIFACTS_DIR / "prime_sidon_dag.md"
lines = [
"# Prime Sidon Exploration DAG\n",
f"**Nodes:** {len(self.nodes)}",
f"**Edges:** {len(self.edges)}\n",
"## Nodes\n",
"| ID | Type | Status | Elapsed | Inputs |",
"|----|------|--------|---------|--------|",
]
for n in self.nodes:
inp = json.dumps(n["inputs"], default=str)[:60]
lines.append(f"| {n['id']} | {n['type']} | {n['status']} | {n['elapsed_s']}s | {inp} |")
lines.append("\n## Edges\n| From | To | Type |\n|------|----|------|")
for e in self.edges:
lines.append(f"| {e['from']} | {e['to']} | {e['type']} |")
report_path.write_text("\n".join(lines))
return report_path
# ── Prime generation (integer only) ──────────────────────────────────────────
def primes_upto(limit):
"""Segmented sieve. Pure integer arithmetic."""
if limit < 2:
return []
sieve = bytearray(b'\x01') * (limit + 1)
sieve[0:2] = b'\x00\x00'
for i in range(2, int(limit ** 0.5) + 1):
if sieve[i]:
step = i
start = i * i
sieve[start:limit+1:step] = b'\x00' * ((limit - start) // step + 1)
return [i for i, is_prime in enumerate(sieve) if is_prime]
def prime_cluster_via_sieve(count, offset_start):
"""Get count consecutive primes starting at offset_start-th prime via sieve."""
limit = max(offset_start * 2 + 100, 10_000_000)
all_primes = primes_upto(limit)
if offset_start + count <= len(all_primes):
return all_primes[offset_start:offset_start + count]
return None
def prime_cluster_via_sympy(start_val, count):
"""Get count consecutive primes starting from start_val via sympy.nextprime."""
import sympy
p = sympy.nextprime(start_val)
cluster = []
for _ in range(count):
cluster.append(p)
p = sympy.nextprime(p)
return cluster
# ── Reference label sets ─────────────────────────────────────────────────────
def sidon_pow2_set(size):
"""Powers of 2: perfect Sidon (all pairwise sums distinct)."""
return [1 << i for i in range(size)]
def nonsidon_consecutive_set(size, start=1):
"""Consecutive integers: maximally non-Sidon for a given size."""
return list(range(start, start + size))
# ── Integer-only Sidon sum-degeneracy metrics ───────────────────────────────
def sidon_metrics(labels):
"""Compute Sidon sum-degeneracy metrics using only integer arithmetic.
All sums a_i + a_j are computed as exact integers. No floating point.
The sum matrix M[i][j] = labels[i] + labels[j] has n^2 entries.
Returns:
n: number of labels
total_pairs: n^2
distinct_sums: number of unique sum values
sidon_score: distinct_sums / n^2 (1.0 = perfect Sidon)
max_degeneracy: max multiplicity of any single sum
collision_count: number of sums with multiplicity >= 2
collision_entropy: Shannon entropy of degeneracy distribution
sum_histogram: {sum_value: count} for sums with count >= 2
"""
n = len(labels)
total_pairs = n * n
# Count occurrences of each sum (pure integer arithmetic)
sum_counts = Counter()
for i in range(n):
for j in range(n):
sum_counts[labels[i] + labels[j]] += 1
distinct_sums = len(sum_counts)
sidon_score = distinct_sums / total_pairs
# Degeneracy analysis
multiplicities = list(sum_counts.values())
max_degeneracy = max(multiplicities) if multiplicities else 0
collision_count = sum(1 for c in multiplicities if c >= 2)
# Shannon entropy of degeneracy distribution
probs = [c / total_pairs for c in multiplicities]
collision_entropy = -sum(p * math.log2(p) for p in probs if p > 0)
# Report only collisions (sums with count >= 2)
collision_histogram = {str(k): v for k, v in sum_counts.items() if v >= 2}
return {
"n": n,
"total_pairs": total_pairs,
"distinct_sums": distinct_sums,
"sidon_score": round(sidon_score, 6),
"max_degeneracy": max_degeneracy,
"collision_count": collision_count,
"collision_entropy": round(collision_entropy, 4),
"collision_histogram": collision_histogram,
}
# ── Permutation test (null: random distinct integers at same scale) ─────────
def random_sidon_score(size, range_min, range_max, trials=1000, rng=None):
"""Generate `trials` random n-subsets of distinct integers in [min, max]
and return the distribution of Sidon scores under the null hypothesis."""
if rng is None:
rng = __import__("random").Random(42)
scores = []
for _ in range(trials):
sample = sorted(rng.sample(range(range_min, range_max + 1), size))
scores.append(sidon_metrics(sample)["sidon_score"])
scores.sort()
return {
"mean": round(sum(scores) / len(scores), 4),
"std": round((sum((s - sum(scores)/len(scores))**2 for s in scores) / len(scores))**0.5, 4),
"p5": round(scores[int(len(scores) * 0.05)], 4),
"p50": round(scores[int(len(scores) * 0.50)], 4),
"p95": round(scores[int(len(scores) * 0.95)], 4),
"trials": trials,
}
# ── Main exploration ────────────────────────────────────────────────────────
def explore():
print("=" * 60)
print(" Prime Sidon Sum-Degeneracy Exploration")
print(" Integer-only arithmetic. Permutation test vs random null.")
print("=" * 60)
dag = ComputationDAG()
# Scale definitions
scales = {
"small": {"offset": 0, "label": "primes 2..19"},
"kilo": {"offset": 168, "label": "primes near 1000"},
"million": {"offset": 78498, "label": "primes near 10^6"},
}
large_scales = {
"billion": 10**9,
"trillion": 10**12,
"quadrillion": 10**15,
"quintillion": 10**18,
}
sizes = [5, 6, 7, 8, 10]
results = []
N_TRIALS = 1000
# ── Step 1: Analytical bounds ─────────────────────────────────────────
print("\n--- Step 1: Analytical Sidon bounds ---")
print(" For any n distinct integers:")
print(" Lower bound (consecutive): (2n-1)/n²")
print(" Upper bound (perfect Sidon): (n+1)/(2n)")
print(" The 'BETWEEN' claim is a tautology — testing if primes")
print(" deviate from random expectation within these bounds.")
print()
bound_nodes = {}
for size in sizes:
lower = round((2 * size - 1) / (size * size), 4)
upper = round((size + 1) / (2 * size), 4)
node_id = f"bounds_s{size}"
dag.add_node(node_id, "analytical_bounds",
{"size": size, "lower_bound": lower, "upper_bound": upper},
{"lower": lower, "upper": upper}, "success", 0.0)
bound_nodes[size] = {"lower": lower, "upper": upper}
print(f" size={size}: lower={lower:.4f} upper={upper:.4f}")
# ── Step 2: Reference sets (baseline verification) ────────────────────
print("\n--- Step 2: Reference baseline verification ---")
for size in sizes:
labels = sidon_pow2_set(size)
metrics = sidon_metrics(labels)
node_id = f"sidon_pow2_s{size}"
dag.add_node(node_id, "reference",
{"type": "sidon_pow2", "size": size, "labels": labels},
metrics, "success", 0.0)
bound = bound_nodes[size]
at_upper = abs(metrics["sidon_score"] - bound["upper"]) < 1e-6
print(f" {node_id:25s} score={metrics['sidon_score']:.4f} (upper={bound['upper']:.4f}, at_bound={at_upper})")
labels = nonsidon_consecutive_set(size)
metrics = sidon_metrics(labels)
node_id = f"nonsidon_seq_s{size}"
dag.add_node(node_id, "reference",
{"type": "nonsidon_consecutive", "size": size, "labels": labels},
metrics, "success", 0.0)
at_lower = abs(metrics["sidon_score"] - bound["lower"]) < 1e-6
print(f" {node_id:25s} score={metrics['sidon_score']:.4f} (lower={bound['lower']:.4f}, at_bound={at_lower})")
# ── Step 3: Prime sets with permutation test ──────────────────────────
print("\n--- Step 3: Prime sets vs random null ---")
import random as _random
rng = _random.Random(42)
for scale_name, scale_info in scales.items():
offset = scale_info["offset"]
for size in sizes:
cluster = prime_cluster_via_sieve(size, offset)
if cluster is None:
cluster = prime_cluster_via_sympy(2, offset + size)
cluster = cluster[offset:offset + size]
node_id = f"primes_{scale_name}_s{size}"
t0 = time.time()
metrics = sidon_metrics(cluster)
# Permutation test: random n-subsets in [min(cluster), max(cluster)]
range_min = min(cluster)
range_max = max(cluster)
null_dist = random_sidon_score(size, range_min, range_max, N_TRIALS, rng)
# p-value: proper permutation test
null_scores = []
for _ in range(N_TRIALS):
sample = sorted(rng.sample(range(range_min, range_max + 1), size))
null_scores.append(sidon_metrics(sample)["sidon_score"])
null_scores.sort()
p_less = sum(1 for s in null_scores if s <= metrics["sidon_score"]) / N_TRIALS
p_greater = sum(1 for s in null_scores if s >= metrics["sidon_score"]) / N_TRIALS
p_two_sided = min(1.0, 2 * min(p_less, p_greater))
effect = round(metrics["sidon_score"] - null_dist["mean"], 4)
significant = p_two_sided < 0.05
elapsed = time.time() - t0
dag.add_node(node_id, "prime_set",
{"size": size, "scale": scale_name, "labels": cluster,
"range": [range_min, range_max], "null_trials": N_TRIALS},
{**metrics, "null_distribution": null_dist,
"p_value_two_sided": round(p_two_sided, 4),
"effect_size": effect, "significant": significant},
"success", elapsed)
results.append({
"desc": node_id, "scale": scale_name, "n_labels": size,
"labels": cluster, "metrics": metrics,
"null_distribution": null_dist,
"p_value": round(p_two_sided, 4),
"effect_size": effect,
"significant": significant,
})
sig_str = "***" if significant else ""
dir_str = "LESS Sidon-like" if effect < 0 else "MORE Sidon-like"
print(f" {node_id:30s} score={metrics['sidon_score']:.4f} "
f"null={null_dist['mean']:.4f}±{null_dist['std']:.4f} "
f"Δ={effect:+.4f} p={p_two_sided:.4f} {sig_str} ({dir_str})")
# ── Step 4: Large prime scales (via sympy.nextprime) ──────────────────
print("\n--- Step 4: Large prime scales vs random null ---")
for scale_name, start_val in large_scales.items():
try:
import sympy
except ImportError:
print(f" sympy not available, skipping {scale_name}")
continue
for size in sizes:
node_id = f"primes_{scale_name}_s{size}"
t0 = time.time()
cluster = prime_cluster_via_sympy(start_val, size)
metrics = sidon_metrics(cluster)
range_min = min(cluster)
range_max = max(cluster)
null_dist = random_sidon_score(size, range_min, range_max, N_TRIALS, rng)
null_scores = []
for _ in range(N_TRIALS):
sample = sorted(rng.sample(range(range_min, range_max + 1), size))
null_scores.append(sidon_metrics(sample)["sidon_score"])
null_scores.sort()
p_less = sum(1 for s in null_scores if s <= metrics["sidon_score"]) / N_TRIALS
p_greater = sum(1 for s in null_scores if s >= metrics["sidon_score"]) / N_TRIALS
p_two_sided = min(1.0, 2 * min(p_less, p_greater))
effect = round(metrics["sidon_score"] - null_dist["mean"], 4)
significant = p_two_sided < 0.05
elapsed = time.time() - t0
dag.add_node(node_id, "prime_set",
{"size": size, "scale": scale_name, "labels": cluster,
"range": [range_min, range_max], "null_trials": N_TRIALS},
{**metrics, "null_distribution": null_dist,
"p_value_two_sided": round(p_two_sided, 4),
"effect_size": effect, "significant": significant},
"success", elapsed)
results.append({
"desc": node_id, "scale": scale_name, "n_labels": size,
"labels": cluster, "metrics": metrics,
"null_distribution": null_dist,
"p_value": round(p_two_sided, 4),
"effect_size": effect,
"significant": significant,
})
sig_str = "***" if significant else ""
dir_str = "LESS Sidon-like" if effect < 0 else "MORE Sidon-like"
print(f" {node_id:30s} score={metrics['sidon_score']:.4f} "
f"null={null_dist['mean']:.4f}±{null_dist['std']:.4f} "
f"Δ={effect:+.4f} p={p_two_sided:.4f} {sig_str} ({dir_str})")
# ── Step 5: Cross-scale analysis ──────────────────────────────────────
print("\n--- Step 5: Cross-scale analysis ---")
analysis = []
for size in sizes:
size_results = [r for r in results if r["n_labels"] == size]
# How many are significant?
n_sig = sum(1 for r in size_results if r["significant"])
n_total = len(size_results)
all_sig = n_sig == n_total
# Direction of effect
neg_effects = sum(1 for r in size_results if r["effect_size"] < 0)
pos_effects = sum(1 for r in size_results if r["effect_size"] > 0)
# Score trend with scale
scores_by_scale = {r["scale"]: r["metrics"]["sidon_score"] for r in size_results}
ascending_scales = [s for s in ["small", "kilo", "million", "billion", "trillion", "quadrillion", "quintillion"] if s in scores_by_scale]
score_trend = [scores_by_scale[s] for s in ascending_scales]
increasing = all(score_trend[i] <= score_trend[i+1] for i in range(len(score_trend)-1))
analysis.append({
"size": size,
"n_significant": n_sig,
"n_total": n_total,
"all_significant": all_sig,
"negative_effects": neg_effects,
"positive_effects": pos_effects,
"score_trend_increasing": increasing,
"scores_by_scale": scores_by_scale,
})
print(f" size={size}: {n_sig}/{n_total} significant, "
f"{neg_effects} less Sidon-like, {pos_effects} more Sidon-like, "
f"trend={'' if increasing else '~'}")
# ── Step 6: Summary ─────────────────────────────────────────────────
print("\n--- Summary ---")
total_sig = sum(1 for r in results if r["significant"])
total_cases = len(results)
print(f" Total test cases: {total_cases}")
print(f" Significant at p<0.05: {total_sig}/{total_cases}")
print()
print(f" Key question: are prime Sidon scores different from random")
print(f" n-subsets of integers at the same scale?")
print(f" Answer: {total_sig}/{total_cases} cases show significant deviation.")
# Summary node
summary_node = {
"schema": "prime_sidon_explore_v2",
"claim_boundary": "prime-sidon-vs-random-null:permutation-test",
"config": {"sizes": sizes, "scales": list(scales.keys()) + list(large_scales.keys()),
"null_trials": N_TRIALS},
"analysis": analysis,
"total_test_cases": total_cases,
"total_significant": total_sig,
}
dag.add_node("summary", "summary", summary_node, summary_node, "success", 0.0)
for r in results:
dag.add_edge(f"primes_{r['scale']}_s{r['n_labels']}", "summary", "feeds_summary")
# ── Save ──────────────────────────────────────────────────────────────
dag.save()
report_path = dag.save_report()
with open(OUTPUT_PATH, "w") as f:
json.dump(summary_node, f, indent=2, default=str)
print(f"\n DAG nodes: {len(dag.nodes)}")
print(f" DAG edges: {len(dag.edges)}")
print(f" DAG: {DAG_PATH}")
print(f" Report: {report_path}")
print("=" * 60)
if __name__ == "__main__":
explore()