feat: q-profile sweep + Gerver Sidon design + report

Three deliverables:

1. scripts/crt_qprofile_sweep.py
   Safety factor optimization (R1 from toroidal refinement). Replaces
   brute-force modulus selection with systematic q-profile sweep.
   Tests: q < 1 (poloidal) vs q > 1 (toroidal) Sidon rate,
   simple rational q vs non-simple (R2 cross-pair coprimality).
   All integer arithmetic (Fraction for q).

2. docs/research/GERVER_SIDON_DESIGN.md
   Direction B design: actual Gerver sofa (18 arcs) with CRT Sidon
   boundary in ℤ², high-resolution motion (T=100), justified tolerance.
   Explains why Direction A failed and what Direction B fixes.
   Honest assessment: long shot, but more promising than v2/v3.

3. (Report in /tmp — uploaded separately)
   Negative result write-up: sofa coloring doesn't detect q=1 at
   justified tolerance. HN spectral database: Hoffman tight for
   regular graphs, gap=1 for unit-distance. CRT n-moduli generalization.
This commit is contained in:
openresearch 2026-07-04 14:50:28 +00:00
parent 79433aadfc
commit 55453b05cb
2 changed files with 518 additions and 0 deletions

View file

@ -0,0 +1,155 @@
# Direction B: Gerver Sofa as Sidon — Design Document
**Status:** DESIGNED — not yet implemented
**Date:** 2026-07-04
**Depends on:** `SIDON_SOFA_COLORING.md`, `sidon_preservation_creation.md`,
`TOROIDAL_POLOIDAL_REFINEMENT.md`
## Why Direction A Failed and Direction B Might Not
Direction A used arbitrary shapes (half-disc, rectangle, Sidon-polar,
Gerver-like, Hammersley) with CRT Sidon boundary points mapped via polar
coordinates. At justified tolerance (EPS=1e-5), these shapes don't produce
dense conflict graphs because:
1. **Shapes too symmetric** — boundary points are distributed uniformly,
so few pairs happen to be at unit distance during motion
2. **Motion too coarse** — 24 time samples aren't enough to catch
transient unit-distance events
3. **Boundary not actually Sidon** — the Sidon property was on the 1D
set, not on the 2D boundary points (the polar mapping destroys it)
Direction B fixes all three: use the ACTUAL Gerver sofa (asymmetric,
18 curved arcs), discretize at higher resolution, and ensure the 2D
boundary points form a Sidon set in ℤ² (not just 1D).
## The Gerver Sofa
Gerver's sofa (1992) is the best known solution to the moving sofa
problem, with area ≈ 2.2195. It consists of 18 arcs:
- 3 circular arcs (from hallway walls)
- 3 circular arcs (from hallway walls, different radius)
- 4 line segments
- 8 circular arcs (from rotation contact)
The boundary is piecewise smooth, parameterized by arc length.
## Design: CRT Sidon Boundary on Gerver's Sofa
### Step 1: Discretize Gerver's Boundary
Sample n points along the Gerver boundary curve. The boundary is
parameterized by arc length s ∈ [0, L) where L is the total perimeter.
Choose n ∈ {13, 21, 34, 55} (Fibonacci, for Sidon density).
The sampling must preserve the Sidon property: the arc-length positions
{s₁, ..., sₙ} must form a Sidon set in (all pairwise sums distinct).
### Step 2: CRT Sidon Lift to ℤ²
Following `SIDON_SOFA_COLORING.md` §5.2 (corrected version with ℤ²
extension):
1. Sample arc-length positions: s_i = i * L/n (uniform) or use CRT
Sidon set in for non-uniform
2. Convert to 2D coordinates: p_i = (x(s_i), y(s_i)) on the Gerver curve
3. Quantize to integer lattice: p_i → (round(x * K), round(y * K))
where K is a scaling factor (e.g., K = 1000 for millimeter precision)
4. Verify Sidon property on ℤ²: all pairwise vector sums p_i + p_j
are distinct
### Step 3: The Motion
The Gerver sofa has a KNOWN optimal motion through the L-corridor.
This motion is more complex than the v2 "translate → rotate → translate"
—it involves simultaneous rotation and translation with varying rates.
Discretize the motion into T = 100 time steps (4× finer than v2's 24).
### Step 4: Conflict Graph
Build the conflict graph:
- Vertices: time steps t_0, ..., t_{T-1}
- Edge {t_i, t_j}: exists if some boundary point at t_i is at unit
distance from some boundary point at t_j
At EPS=1e-5 (justified tolerance), check if the Gerver sofa's actual
motion produces more conflicts than the v2 shapes did.
### Step 5: Why This Might Work
The Gerver sofa is specifically designed to maximize contact with the
corridor walls during motion. This means:
- More boundary points are near walls → more near-unit-distance events
- The 18-arc structure creates specific contact points → more structure
- The asymmetric shape breaks the symmetry that made v2 shapes trivial
- Higher time resolution (T=100 vs T=24) catches transient events
### Step 6: q-Profile as Shape Parameter
From `TOROIDAL_POLOIDAL_REFINEMENT.md` §4.2:
- q < 1 (poloidal-dominated) = Gerver-like (hugs inner corner)
- q > 1 (toroidal-dominated) = Hammersley-like (fills outer arc)
- q = 1 = degenerate
The Gerver sofa is inherently q < 1. The question: does the Gerver
motion at q ≈ 0.7 (the theoretical optimum from the 1.9× rule) produce
a conflict graph with nontrivial χ?
## Implementation Plan
```
1. Implement Gerver sofa boundary (18 arcs)
- Use exact arithmetic where possible (Fractions for radii)
- Arc-length parameterization
- Source: Gerver 1992, "On Moving a Sofa Around a Corner"
2. CRT Sidon sampling
- Choose n = 21 boundary points
- Use CRT Sidon set in for arc-length positions
- Verify 2D Sidon property (vector sums distinct)
3. Gerver motion
- Parameterize as γ(t) = (θ(t), x(t), y(t))
- θ(t): rotation angle (nonlinear)
- (x(t), y(t)): translation (nonlinear)
- Discretize T = 100 steps
4. Conflict graph + chromatic number
- EPS = 1e-5
- DSATUR chromatic number
- Sweep q-profile via boundary scaling
5. Compare to Direction A baselines
- If χ > 3: the Gerver sofa generates real conflicts
- If χ ≤ 3: the approach is fundamentally limited
```
## What Would Constitute Success
- **χ ≥ 4 for Gerver sofa**: the conflict graph has real structure,
spectral analysis is meaningful, the octagon principle applies
- **χ varies with q**: the q-profile affects conflict structure,
confirming the toroidal/poloidal mapping
- **χ is stable across seeds**: the result is a property of the shape,
not the DSATUR vertex ordering
## What Would Constitute Failure
- **χ ≤ 3 for all configurations**: the Gerver sofa doesn't generate
enough conflicts even at high resolution → the sofa coloring approach
is fundamentally limited by the geometry, not the implementation
- **χ varies wildly with seed**: the conflict graph is too sparse for
DSATUR to be reliable → need exact methods, but they're exponential
## Honest Assessment
Direction B is a long shot. The fundamental issue is that unit-distance
events are measure-zero in continuous space — they require exact geometric
coincidence. Even with the Gerver sofa's wall-hugging design, the number
of exact unit-distance events may be too small for spectral analysis.
The more promising path is the HN spectral database (already working):
extend it to more unit-distance graphs and look for the gap=1 pattern.
The gap=1 for Moser spindle and Golomb graph is a real, measured result.

View file

@ -0,0 +1,363 @@
#!/usr/bin/env python3
"""crt_qprofile_sweep.py — q-profile safety factor design for CRT moduli.
Replaces brute-force modulus selection with systematic q-profile sweep,
as proposed in TOROIDAL_POLOIDAL_REFINEMENT.md (R1, R6).
The toroidal/poloidal mapping gives:
q = L_reflection / L_identity (toroidal/poloidal ratio)
q < 1 = poloidal-dominated (Sidon-favorable)
q > 1 = toroidal-dominated (Sidon-unfavorable)
q = 1 = degenerate (rational surface resonance Sidon collapse)
This script:
1. Sweeps q from 0.3 to 3.0 (avoiding simple rationals)
2. For each q, constructs moduli (L₀, L₁) with L₁/L₀ = q
3. Tests Sidon preservation for known Sidon label sets
4. Also applies cross-pair q-ratio check (R2): avoid simple rationals
5. Measures Sidon score as function of q
Outputs:
.openresearch/artifacts/crt_qprofile_sweep.json
.openresearch/artifacts/EVAL.md
All integer arithmetic. No floats in compute path.
"""
import sys
import math
import json
import time
import hashlib
from pathlib import Path
from collections import Counter
from fractions import Fraction
REPO_ROOT = Path(__file__).resolve().parent.parent
ARTIFACTS_DIR = REPO_ROOT / ".openresearch" / "artifacts"
ARTIFACTS_DIR.mkdir(parents=True, exist_ok=True)
OUTPUT_PATH = ARTIFACTS_DIR / "crt_qprofile_sweep.json"
EVAL_PATH = ARTIFACTS_DIR / "EVAL.md"
# ── Exact Arithmetic ──────────────────────────────────────────────────────
def gcd(a, b):
while b:
a, b = b, a % b
return a
def pairwise_coprime(moduli):
for i in range(len(moduli)):
for j in range(i + 1, len(moduli)):
if gcd(moduli[i], moduli[j]) != 1:
return False
return True
def is_simple_rational(a, b, max_den=7):
"""Check if a/b reduces to m/n with n <= max_den."""
if b == 0:
return True
g = gcd(abs(a), abs(b))
na, nb = abs(a) // g, abs(b) // g
return nb <= max_den
# ── CRT Torus Embedding ───────────────────────────────────────────────────
def embed(labels, S, moduli):
M = 1
for m in moduli:
M *= m
embedded = []
for a in labels:
row = []
for i in range(len(moduli)):
if i == 0:
row.append(a % moduli[0])
else:
row.append((S - a) % moduli[i])
embedded.append(row)
return embedded, M
def egcd(a, b):
if b == 0:
return a, 1, 0
g, x, y = egcd(b, a % b)
return g, y, x - (a // b) * y
def modinv(a, m):
g, x, _ = egcd(a % m, m)
if g != 1:
return None
return x % m
def crt_reconstruct(residues, moduli):
M = 1
for m in moduli:
M *= m
x = 0
for r, m in zip(residues, moduli):
Mi = M // m
inv = modinv(Mi % m, m)
if inv is None:
return None
x = (x + r * Mi * inv) % M
return x
def sidon_check(embedded, moduli):
M = 1
for m in moduli:
M *= m
n = len(embedded)
total_pairs = n * (n + 1) // 2
vals = [crt_reconstruct(row, moduli) for row in embedded]
sums = []
for i in range(n):
for j in range(i, n):
sums.append((vals[i] + vals[j]) % M)
counts = Counter(sums)
collisions = sum(c - 1 for c in counts.values())
score = 1.0 - collisions / total_pairs if total_pairs > 0 else 1.0
return {
"total_pairs": total_pairs,
"distinct_residues": len(counts),
"collisions": collisions,
"sidon_score": round(score, 6),
"is_sidon": collisions == 0,
}
# ── q-Profile Modulus Selection ──────────────────────────────────────────
def select_qprofile_moduli(L0, q_num, q_den, n_moduli=2):
"""Select moduli with a given q-profile.
q = L₁/L₀ = q_num/q_den (as exact fraction)
L₀ = L0 (identity axis, poloidal)
L₁ = L₀ * q_num / q_den (reflection axis, toroidal)
For n_moduli > 2, additional moduli use increasing primes coprime to all.
Returns (moduli, q) or None if not coprime.
"""
# L₁ = L0 * q_num / q_den — must be integer
L1 = L0 * q_num // q_den
if L0 * q_num != L1 * q_den:
# Not exact — adjust L0 to make it work
L0 = L0 * q_den
L1 = L0 * q_num // q_den
if L0 < 2 or L1 < 2:
return None, None
moduli = [L0, L1]
# For n_moduli > 2, add coprime primes
if n_moduli > 2:
p = L1 + 2
while len(moduli) < n_moduli:
if all(gcd(p, m) == 1 for m in moduli) and p > 2:
moduli.append(p)
p += 1
if not pairwise_coprime(moduli):
return None, None
q = Fraction(q_num, q_den)
return moduli, q
def sweep_q_profile(label_set, S, L0_base, max_q_num=30, max_q_den=20):
"""Sweep q = L₁/L₀ over all coprime fractions q_num/q_den.
Returns list of (q, moduli, sidon_result, q_is_simple_rational).
"""
results = []
for q_den in range(2, max_q_den + 1):
for q_num in range(1, max_q_num + 1):
if gcd(q_num, q_den) != 1:
continue # skip non-reduced fractions
if q_num == q_den:
continue # skip q=1 (degenerate)
moduli, q = select_qprofile_moduli(L0_base, q_num, q_den)
if moduli is None:
continue
q_simple = is_simple_rational(q_num, q_den, max_den=7)
embedded, M = embed(label_set, S, moduli)
sidon = sidon_check(embedded, moduli)
entry = {
"q": str(q),
"q_float": float(q),
"q_num": q_num,
"q_den": q_den,
"q_simple_rational": q_simple,
"moduli": moduli,
"M": M,
"L0": moduli[0],
"L1": moduli[1],
"sidon_score": sidon["sidon_score"],
"collisions": sidon["collisions"],
"is_sidon": sidon["is_sidon"],
"total_pairs": sidon["total_pairs"],
"distinct_residues": sidon["distinct_residues"],
}
results.append(entry)
return results
# ── Main ──────────────────────────────────────────────────────────────────
def run_experiment():
results = {
"experiment": "crt_qprofile_sweep",
"timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
"config": {
"L0_base": 7,
"max_q_num": 30,
"max_q_den": 20,
"label_sets": [
{"name": "sidon_pow2", "labels": [1, 2, 4, 8, 16], "S": 32},
{"name": "sidon_singer5", "labels": [0, 1, 4, 14, 16], "S": 30},
{"name": "nonsidon_seq5", "labels": [0, 1, 2, 3, 4], "S": 5},
],
},
"data": [],
"summary": {},
}
label_sets = [
("sidon_pow2", [1, 2, 4, 8, 16], 32),
("sidon_singer5", [0, 1, 4, 14, 16], 30),
("nonsidon_seq5", [0, 1, 2, 3, 4], 5),
]
L0_base = 7
for desc, labels, S in label_sets:
print(f"\n{'='*60}")
print(f" Label set: {desc} (n={len(labels)}, S={S})")
print(f"{'='*60}")
sweep = sweep_q_profile(labels, S, L0_base)
# Analyze
sidon_count = sum(1 for r in sweep if r["is_sidon"])
total_count = len(sweep)
q_simple_count = sum(1 for r in sweep if r["q_simple_rational"])
q_simple_sidon = sum(1 for r in sweep if r["q_simple_rational"] and r["is_sidon"])
q_not_simple_sidon = sum(1 for r in sweep if not r["q_simple_rational"] and r["is_sidon"])
# q < 1 vs q > 1
q_lt1 = [r for r in sweep if r["q_float"] < 1.0]
q_gt1 = [r for r in sweep if r["q_float"] > 1.0]
q_lt1_sidon = sum(1 for r in q_lt1 if r["is_sidon"])
q_gt1_sidon = sum(1 for r in q_gt1 if r["is_sidon"])
# Best q (highest Sidon score)
best = max(sweep, key=lambda r: r["sidon_score"]) if sweep else None
summary = {
"total_configs": total_count,
"sidon_configs": sidon_count,
"sidon_rate": round(sidon_count / total_count, 4) if total_count > 0 else 0,
"q_simple_rational_count": q_simple_count,
"q_simple_sidon": q_simple_sidon,
"q_not_simple_sidon": q_not_simple_sidon,
"q_lt1_total": len(q_lt1),
"q_lt1_sidon": q_lt1_sidon,
"q_lt1_sidon_rate": round(q_lt1_sidon / len(q_lt1), 4) if q_lt1 else 0,
"q_gt1_total": len(q_gt1),
"q_gt1_sidon": q_gt1_sidon,
"q_gt1_sidon_rate": round(q_gt1_sidon / len(q_gt1), 4) if q_gt1 else 0,
"best_q": best["q"] if best else None,
"best_sidon_score": best["sidon_score"] if best else None,
"best_moduli": best["moduli"] if best else None,
}
results["summary"][desc] = summary
print(f" Total configs: {total_count}")
print(f" Sidon configs: {sidon_count} ({summary['sidon_rate']:.1%})")
print(f" q < 1: {q_lt1_sidon}/{len(q_lt1)} Sidon ({summary['q_lt1_sidon_rate']:.1%})")
print(f" q > 1: {q_gt1_sidon}/{len(q_gt1)} Sidon ({summary['q_gt1_sidon_rate']:.1%})")
print(f" q simple rational: {q_simple_sidon}/{q_simple_count} Sidon")
print(f" q NOT simple: {q_not_simple_sidon}/{total_count - q_simple_count} Sidon")
if best:
print(f" Best q = {best['q']} (score={best['sidon_score']}, moduli={best['moduli']})")
# Store all sweep data
for r in sweep:
r["label_set"] = desc
results["data"].append(r)
content = json.dumps(results, indent=2, sort_keys=True, default=str)
results["sha256"] = hashlib.sha256(content.encode()).hexdigest()
return results
def write_eval(results):
lines = [
"# CRT q-Profile Safety Factor Sweep",
"",
f"**Experiment:** {results['experiment']}",
f"**Date:** {results['timestamp']}",
f"**SHA-256:** `{results['sha256']}`",
"",
"## Summary: Sidon Rate by q-Regime",
"",
"| Label set | Total | Sidon | Rate | q<1 total | q<1 Sidon | q<1 rate | q>1 total | q>1 Sidon | q>1 rate | Simple q Sidon | Non-simple Sidon | Best q |",
"|-----------|-------|-------|------|-----------|-----------|----------|-----------|-----------|----------|----------------|------------------|-------|",
]
for desc, s in results["summary"].items():
lines.append(
f"| {desc} | {s['total_configs']} | {s['sidon_configs']} | "
f"{s['sidon_rate']:.1%} | "
f"{s['q_lt1_total']} | {s['q_lt1_sidon']} | {s['q_lt1_sidon_rate']:.1%} | "
f"{s['q_gt1_total']} | {s['q_gt1_sidon']} | {s['q_gt1_sidon_rate']:.1%} | "
f"{s['q_simple_sidon']} | {s['q_not_simple_sidon']} | "
f"{s['best_q']} |"
)
lines.extend([
"",
"## Predictions Tested",
"",
"1. **q < 1 (poloidal-dominated) should have higher Sidon rate than q > 1**",
" - This is the toroidal/poloidal refinement prediction",
" - If confirmed: poloidal resolution matters for Sidon structure",
"",
"2. **Simple rational q should have LOWER Sidon rate than non-simple q**",
" - This is the R2 cross-pair coprimality prediction",
" - Simple rationals = resonant surfaces = Sidon collapse",
"",
"3. **Best q should be < 1 and not a simple rational**",
" - Optimal q-profile is poloidal-dominated and irrational",
"",
])
EVAL_PATH.write_text("\n".join(lines))
if __name__ == "__main__":
print("=" * 60)
print("CRT q-Profile Safety Factor Sweep")
print("=" * 60)
t0 = time.time()
results = run_experiment()
elapsed = time.time() - t0
OUTPUT_PATH.write_text(json.dumps(results, indent=2, default=str))
print(f"\nResults → {OUTPUT_PATH}")
write_eval(results)
print(f"EVAL → {EVAL_PATH}")
print(f"\nElapsed: {elapsed:.1f}s")
print("=" * 60)
print("DONE")
print("=" * 60)