SilverSight/scripts/prime_sidon_explore.py
allaun a0d95049c6 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)
2026-07-03 18:16:42 -05:00

470 lines
20 KiB
Python

#!/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()