mirror of
https://github.com/allaunthefox/SilverSight.git
synced 2026-07-30 17:16:16 +00:00
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)
470 lines
20 KiB
Python
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()
|