SilverSight/scripts/hn_hoffman_bound.py
allaun 54fd228383 fix(adversarial): repair sofa coloring scripts + Hoffman bound
Adversarial review findings and fixes:
- CRITICAL: v3 hash() -> deterministic_seed (SHA-256) for reproducibility
- HIGH: EPS 0.05 -> 1e-5 (5 orders too wide)
- HIGH: L-corridor jump at t=15->16 (dist 1.0) — smoothed rotation at (0.5,0.5)
- HIGH: gerver_like self-intersecting — arcs meet at shared endpoint
- MEDIUM: hammersley gap at arc junction — fixed endpoint alignment
- MEDIUM/HIGH: gerver_like/hammersley now accept q parameter
- LOW: reflect_y -> negate_y rename, dedup rounding consistency
- de Grey 1581-vertex graph constructs correctly (verified: 1581 vertices, 7877 edges)
- Hoffman bound: λ_max=12.09, λ_min=-7.74, χ≥3 (weak bound, expected)

Build: lake build CoreFormalism.CRTSidon (3297 jobs, 0 errors)
2026-07-04 02:04:37 -05:00

237 lines
7.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""
hn_hoffman_bound.py — Hadwiger-Nelson Hoffman bound via spectral method.
Computes χ(G) ≥ 1 - λ_max / λ_min for a unit-distance graph G.
Built-in graphs:
- Moser spindle (7 vertices, χ=4)
- de Grey 1581-vertex graph (χ=5) using coordinates from arXiv:1804.02385v3 §5.1
Usage:
python3 hn_hoffman_bound.py [--graph moser|degrey]
"""
import sys, json, math, hashlib, time
from pathlib import Path
HERE = Path(__file__).resolve().parent
OUT_DIR = HERE.parent / ".openresearch" / "artifacts"
OUT_DIR.mkdir(parents=True, exist_ok=True)
s3 = math.sqrt(3)
s11 = math.sqrt(11)
s33 = math.sqrt(33)
s12 = 2 * s3
# ── Coordinate set S from de Grey §5.1 ────────────────────────────
S_COORDS = [
(0, 0),
(1/3, 0),
(1, 0),
(2, 0),
((s33 - 3)/6, 0),
(1/2, 1/s12),
(1, 1/s3),
(3/2, s3/2),
(7/6, s11/6),
(1/6, (s12 - s11)/6),
(5/6, (s12 - s11)/6),
(2/3, (s11 - s3)/6),
(2/3, (3*s3 - s11)/6),
(s33/6, 1/s12),
((s33 + 3)/6, 1/s3),
((s33 + 1)/6, (3*s3 - s11)/6),
((s33 - 1)/6, (3*s3 - s11)/6),
((s33 + 1)/6, (s11 - s3)/6),
((s33 - 1)/6, (s11 - s3)/6),
((s33 - 2)/6, (2*s3 - s11)/6),
((s33 - 4)/6, (2*s3 - s11)/6),
((s33 + 13)/12, (s11 - s3)/12),
((s33 + 11)/12, (s3 + s11)/12),
((s33 + 9)/12, (s11 - s3)/4),
((s33 + 9)/12, (3*s3 + s11)/12),
((s33 + 7)/12, (s3 + s11)/12),
((s33 + 7)/12, (3*s3 - s11)/12),
((s33 + 5)/12, (5*s3 - s11)/12),
((s33 + 5)/12, (s11 - s3)/12),
((s33 + 3)/12, (3*s11 - 5*s3)/12),
((s33 + 3)/12, (s3 + s11)/12),
((s33 + 3)/12, (3*s3 - s11)/12),
((s33 + 1)/12, (s11 - s3)/12),
((s33 - 1)/12, (3*s3 - s11)/12),
((s33 - 3)/12, (s11 - s3)/12),
((15 - s33)/12, (s11 - s3)/4),
((15 - s33)/12, (7*s3 - 3*s11)/12),
((13 - s33)/12, (3*s3 - s11)/12),
((11 - s33)/12, (s11 - s3)/12),
]
def rotate(pt, angle):
c, s = math.cos(angle), math.sin(angle)
return (pt[0]*c - pt[1]*s, pt[0]*s + pt[1]*c)
def translate(pt, dx, dy):
return (pt[0]+dx, pt[1]+dy)
def negate_y(pt):
return (pt[0], -pt[1])
def is_unit(p, q, eps=1e-9):
d2 = (p[0]-q[0])**2 + (p[1]-q[1])**2
return abs(d2 - 1.0) < eps
def build_adjacency(points, eps=1e-9):
n = len(points)
adj = [[] for _ in range(n)]
for i in range(n):
pi = points[i]
for j in range(i+1, n):
if is_unit(pi, points[j], eps):
adj[i].append(j)
adj[j].append(i)
return adj
def remove_duplicates(points, eps=1e-9):
uniq = []
for p in points:
if not any(abs(p[0]-q[0])<eps and abs(p[1]-q[1])<eps for q in uniq):
uniq.append(p)
return uniq
def build_degrey_1581():
"""Construct de Grey's 1581-vertex graph per §5.1."""
# Step 1: S_a — rotate S by 60°, reflect y
sa = set()
angle60 = math.pi / 3
angle2 = 2 * math.asin(1/4)
angle_y = math.pi/2 + math.asin(1/8)
for x, y in S_COORDS:
for k in range(6):
pt = rotate((x, y), k * angle60)
sa.add((round(pt[0], 12), round(pt[1], 12)))
pt_r = negate_y(pt)
sa.add((round(pt_r[0], 12), round(pt_r[1], 12)))
sa_list = list(sa)
# Step 3: S_b = S_a rotated by 2*arcsin(1/4)
sb_list = [(round(rotate(p, angle2)[0], 12),
round(rotate(p, angle2)[1], 12)) for p in sa_list]
# Step 4: Y = union minus (1/3, 0) and (-1/3, 0)
y_set = set(sa_list) | set(sb_list)
y_set.discard((round(1/3, 12), 0.0))
y_set.discard((round(-1/3, 12), 0.0))
y_list = list(y_set)
# Step 5-6: Rotate Y about (-2, 0)
ya = [translate(rotate(translate(p, 2, 0), angle_y), -2, 0) for p in y_list]
yb = [translate(rotate(translate(p, 2, 0), math.pi - angle_y), -2, 0) for p in y_list]
ya = [(round(x, 12), round(y, 12)) for x, y in ya]
yb = [(round(x, 12), round(y, 12)) for x, y in yb]
# Step 7: G = Y_a Y_b
all_pts = remove_duplicates(ya + yb)
print(f" Total vertices before dedup: {len(ya)+len(yb)}")
print(f" After dedup: {len(all_pts)}")
# Build adjacency
adj = build_adjacency(all_pts)
n_edges = sum(len(nbrs) for nbrs in adj) // 2
return adj, all_pts, f"De Grey G ({len(all_pts)} vertices, {n_edges} edges)"
# ── Moser spindle ──────────────────────────────────────────────────
def moser_spindle():
s3 = math.sqrt(3)
pts = [(0,0), (1,0), (0.5,s3/2), (-0.5,s3/2), (-1,0), (-0.5,-s3/2), (0.5,-s3/2)]
adj = build_adjacency(pts)
n_edges = sum(len(nbrs) for nbrs in adj) // 2
return adj, pts, f"Moser spindle (7 vertices, {n_edges} edges)"
# ── Hoffman bound ──────────────────────────────────────────────────
def hoffman_bound(adj):
try:
import numpy as np
except ImportError:
print("ERROR: numpy required")
sys.exit(1)
n = len(adj)
A = np.zeros((n, n), dtype=np.float64)
for i in range(n):
for j in adj[i]:
A[i, j] = 1.0
eigenvals = np.linalg.eigvalsh(A)
lambda_max = eigenvals[-1]
lambda_min = eigenvals[0]
hb = 1.0 - lambda_max / lambda_min if lambda_min < 0 else float('inf')
return float(lambda_max), float(lambda_min), float(hb)
# ── Main ───────────────────────────────────────────────────────────
def main():
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("--graph", choices=["moser", "degrey"], default="degrey")
args = parser.parse_args()
t0 = time.time()
if args.graph == "moser":
adj, pts, desc = moser_spindle()
else:
adj, pts, desc = build_degrey_1581()
n_vertices = len(adj)
n_edges = sum(len(nbrs) for nbrs in adj) // 2
print(f"Graph: {desc}")
if n_vertices < 3:
print(" Too few vertices.")
return
lambda_max, lambda_min, hb = hoffman_bound(adj)
chi_lower = math.ceil(hb) if math.isfinite(hb) else None
print(f"\nSpectral analysis:")
print(f" λ_max = {lambda_max:.6f}")
print(f" λ_min = {lambda_min:.6f}")
print(f" λ_max/|λ_min| = {lambda_max / abs(lambda_min):.6f}")
if chi_lower:
print(f"\n Hoffman bound: χ ≥ {hb:.6f}")
print(f" Rounded up: χ ≥ {chi_lower}")
elapsed = time.time() - t0
result = {
"experiment": "hn_hoffman_bound",
"graph": args.graph,
"description": desc,
"n_vertices": n_vertices,
"n_edges": n_edges,
"lambda_max": round(lambda_max, 12),
"lambda_min": round(lambda_min, 12),
"hoffman_bound": round(hb, 12) if math.isfinite(hb) else None,
"chi_lower_bound": chi_lower,
"elapsed_s": round(elapsed, 2),
"timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
}
content = json.dumps(result, indent=2)
result["sha256"] = hashlib.sha256(content.encode()).hexdigest()
out_path = OUT_DIR / "hn_hoffman_bound.json"
with open(out_path, "w") as f:
json.dump(result, f, indent=2)
print(f"\nResults → {out_path}")
print(f"SHA-256: {result['sha256']}")
print(f"Elapsed: {elapsed:.2f}s")
if __name__ == "__main__":
main()