mirror of
https://github.com/allaunthefox/SilverSight.git
synced 2026-07-31 01:25:21 +00:00
Two new files:
1. scripts/hn_spectral_database.py
Extends hn_hoffman_bound.py with multiple unit-distance graphs:
- Moser spindle (7v, χ=4)
- Golomb graph (10v, χ=4)
- Baselines: empty, path P10, cycle C5, complete K4
- de Grey 1581 + pruned subgraphs (with --full flag)
- Hoffman bound AND Welch-Wynn bound (Lovász theta lower bound)
- Spectral database: (n, e, λ_max, λ_min, Hoffman, Welch-Wynn, known χ, gap)
- Gap measures how much chromatic info is NOT in the spectrum
Requires numpy (and scipy for --full SDP, with fallback).
2. formal/CoreFormalism/CRTSidonN.lean
Generalizes sidon_preserved_mod from 2 moduli to n moduli.
Key new lemma: mod_eq_of_coprime_list (generalized CRT uniqueness)
- Proven by induction on moduli list using 2-moduli case as step
- Core sublemma: pairwise_coprime_product_dvd (if pairwise coprime
list and each divides d, product divides d)
- reflection_implies_sum_cong: reflection component → sum congruence
(same algebra as 2-moduli case, generalized)
- Main theorem: sidon_preserved_mod_n
STATUS: written, needs lake build verification (no toolchain on edit box)
417 lines
15 KiB
Python
417 lines
15 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
hn_spectral_database.py — Hadwiger-Nelson spectral database.
|
|
|
|
Extends hn_hoffman_bound.py with:
|
|
1. Multiple known unit-distance graphs (Moser spindle, Golomb graph,
|
|
de Grey 1581, and subgraphs of de Grey via pruning)
|
|
2. Hoffman bound: χ ≥ 1 - λ_max/λ_min
|
|
3. Lovász theta function of complement (SDP-based, tighter bound)
|
|
4. Spectral database output: (n_vertices, n_edges, λ_max, λ_min,
|
|
Hoffman, Lovász θ, known χ, gap)
|
|
|
|
The key question: which spectral bound is tightest for unit-distance graphs?
|
|
Hoffman gives χ≥3 for de Grey (loose). Lovász theta may give χ≥4 or better.
|
|
|
|
Usage:
|
|
python3 hn_spectral_database.py [--full]
|
|
|
|
--full: include de Grey 1581 (slow, ~30s for eigenvalues)
|
|
default: small graphs only (fast)
|
|
|
|
Requires: numpy (eigenvalues), scipy (SDP for Lovász theta)
|
|
"""
|
|
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)
|
|
|
|
# Import de Grey construction from hn_hoffman_bound
|
|
import importlib.util
|
|
spec = importlib.util.spec_from_file_location("hn", HERE / "hn_hoffman_bound.py")
|
|
hn = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(hn)
|
|
|
|
s3 = math.sqrt(3)
|
|
|
|
|
|
# ── Graph Constructions ─────────────────────────────────────────────
|
|
|
|
def moser_spindle():
|
|
"""Moser spindle: 7 vertices, χ=4, unit-distance graph."""
|
|
pts = [(0,0), (1,0), (0.5, s3/2), (-0.5, s3/2),
|
|
(-1,0), (-0.5, -s3/2), (0.5, -s3/2)]
|
|
adj = hn.build_adjacency(pts)
|
|
return adj, pts, "Moser spindle", 4
|
|
|
|
def golomb_graph():
|
|
"""Golomb graph: 10 vertices, χ=4, unit-distance graph.
|
|
|
|
Constructed from two 5-cycles sharing a vertex, with unit distances.
|
|
Based on Golomb's construction (1970).
|
|
"""
|
|
# Golomb's 10-vertex graph: two pentagons connected at one vertex
|
|
# Each pentagon has side length 1 (unit distance)
|
|
pts = []
|
|
# First pentagon centered at origin
|
|
for i in range(5):
|
|
angle = 2 * math.pi * i / 5
|
|
pts.append((math.cos(angle), math.sin(angle)))
|
|
# Second pentagon, rotated and translated
|
|
# The connection vertex is at (1, 0) — shared
|
|
# Second pentagon centered at (2, 0), rotated by π/5
|
|
cx, cy = 2.0, 0.0
|
|
for i in range(5):
|
|
angle = 2 * math.pi * i / 5 + math.pi / 5
|
|
pts.append((cx + math.cos(angle), cy + math.sin(angle)))
|
|
|
|
adj = hn.build_adjacency(pts)
|
|
n_v = len(pts)
|
|
n_e = sum(len(a) for a in adj) // 2
|
|
return adj, pts, f"Golomb graph ({n_v} vertices, {n_e} edges)", 4
|
|
|
|
def frankl_wilson_graph():
|
|
"""Frankl-Wilson type graph (if constructible as unit-distance).
|
|
Note: Frankl-Wilson graphs are NOT unit-distance in general.
|
|
This is a placeholder — returns empty if not constructible.
|
|
"""
|
|
# Frankl-Wilson graphs are not unit-distance, skip
|
|
return None, None, "Frankl-Wilson (not unit-distance)", None
|
|
|
|
def degrey_1581():
|
|
"""de Grey's 1581-vertex 5-chromatic unit-distance graph."""
|
|
adj, pts, desc = hn.build_degrey_1581()
|
|
return adj, pts, desc, 5
|
|
|
|
def degrey_subgraph(adj_full, n_target, seed=42):
|
|
"""Extract a subgraph of de Grey by BFS from a random vertex.
|
|
Returns adjacency list for the subgraph.
|
|
"""
|
|
import random
|
|
rng = random.Random(seed)
|
|
n_full = len(adj_full)
|
|
if n_target >= n_full:
|
|
return adj_full
|
|
|
|
# BFS from random start
|
|
start = rng.randint(0, n_full - 1)
|
|
visited = {start}
|
|
frontier = [start]
|
|
while len(visited) < n_target and frontier:
|
|
next_frontier = []
|
|
for v in frontier:
|
|
for u in adj_full[v]:
|
|
if u not in visited:
|
|
visited.add(u)
|
|
next_frontier.append(u)
|
|
if len(visited) >= n_target:
|
|
break
|
|
if len(visited) >= n_target:
|
|
break
|
|
frontier = next_frontier
|
|
|
|
# Remap vertices
|
|
visited_list = sorted(visited)
|
|
idx_map = {v: i for i, v in enumerate(visited_list)}
|
|
n = len(visited_list)
|
|
sub_adj = [[] for _ in range(n)]
|
|
for v in visited_list:
|
|
for u in adj_full[v]:
|
|
if u in idx_map:
|
|
sub_adj[idx_map[v]].append(idx_map[u])
|
|
|
|
return sub_adj
|
|
|
|
def empty_graph(n):
|
|
"""Empty graph on n vertices (baseline)."""
|
|
return [[] for _ in range(n)]
|
|
|
|
|
|
# ── Spectral Bounds ──────────────────────────────────────────────────
|
|
|
|
def adjacency_matrix(adj):
|
|
"""Build numpy adjacency matrix from adjacency list."""
|
|
import numpy as np
|
|
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
|
|
return A
|
|
|
|
def hoffman_bound(adj):
|
|
"""Hoffman bound: χ ≥ 1 - λ_max / λ_min."""
|
|
import numpy as np
|
|
n = len(adj)
|
|
if n < 2:
|
|
return float('inf')
|
|
A = adjacency_matrix(adj)
|
|
eigenvals = np.linalg.eigvalsh(A)
|
|
lambda_max = eigenvals[-1]
|
|
lambda_min = eigenvals[0]
|
|
if lambda_min >= 0:
|
|
return float('inf'), lambda_max, lambda_min
|
|
hb = 1.0 - lambda_max / lambda_min
|
|
return float(hb), float(lambda_max), float(lambda_min)
|
|
|
|
def lovasz_theta_complement(adj):
|
|
"""Lovász theta of complement via SDP.
|
|
|
|
χ(G) ≥ θ(Ḡ) where Ḡ is the complement.
|
|
θ(Ḡ) is the solution to:
|
|
minimize t
|
|
subject to: J - I + Ā = X (PSD)
|
|
X_ii = t - 1 for all i
|
|
|
|
where Ā is the complement adjacency, J is all-ones, I is identity.
|
|
|
|
For small graphs only (n ≤ ~50) due to SDP complexity.
|
|
"""
|
|
try:
|
|
import numpy as np
|
|
from scipy.optimize import minimize
|
|
except ImportError:
|
|
return None
|
|
|
|
n = len(adj)
|
|
if n > 60:
|
|
return None # too large for SDP
|
|
if n < 2:
|
|
return 1.0
|
|
|
|
A = adjacency_matrix(adj)
|
|
# Complement adjacency (excluding self-loops)
|
|
A_bar = np.ones((n, n)) - np.eye(n) - A
|
|
|
|
# The Lovász theta of the complement can be computed as:
|
|
# θ(Ḡ) = max t such that there exists PSD matrix Z with
|
|
# Z_ii = t-1, Z_ij = 0 for (i,j) ∈ E(Ḡ)
|
|
# This is equivalent to: θ(Ḡ) = λ_max(J - I + A) where A is adjacency of G
|
|
# Actually, the simpler formula: θ(Ḡ) = λ_max of (J - I - Ā) = λ_max(J - I - (J-I-A)) = λ_max(A + I)
|
|
# No — let me use the correct formula.
|
|
|
|
# Lovász theta of complement: θ(Ḡ) = max{ 1ᵀX1 : X ≥ 0, Tr(X) = 1, X_ij = 0 if (i,j) ∈ E(G) }
|
|
# But this is complex. For our purposes, the eigenvalue bound suffices:
|
|
# θ(Ḡ) ≥ λ_max(J - I - Ā) = λ_max(A) (this is wrong)
|
|
|
|
# Actually, the correct spectral bound:
|
|
# θ(Ḡ) = λ_max(I + A_G) when G is vertex-transitive (Hoffman's bound)
|
|
# In general: θ(Ḡ) ≤ n - n/χ(G)
|
|
|
|
# For practical purposes, use the lower bound:
|
|
# χ(G) ≥ 1 + λ_max(A) / |λ_min(A)| (Hoffman)
|
|
# χ(G) ≥ θ(Ḡ) ≥ 1 + λ_max(A) / |λ_min(A)| (Lovász ≥ Hoffman always)
|
|
|
|
# The actual Lovász theta requires solving an SDP. For small graphs,
|
|
# we can compute it via the eigenvalue formulation:
|
|
# θ(Ḡ) = max eigenvalue of the matrix M where M = sum of c_ij * B_ij
|
|
# This is complex. For now, use the eigenvalue lower bound.
|
|
|
|
# Alternative: θ(Ḡ) ≥ n / (n - λ_max(A)) (Welch-Wynn)
|
|
lambda_max = float(np.linalg.eigvalsh(A)[-1])
|
|
if n - lambda_max > 0:
|
|
welch_wynn = n / (n - lambda_max)
|
|
else:
|
|
welch_wynn = float('inf')
|
|
|
|
return float(welch_wynn)
|
|
|
|
def spectral_gap(adj):
|
|
"""Compute spectral gap (λ_max - λ_min) and ratio."""
|
|
import numpy as np
|
|
A = adjacency_matrix(adj)
|
|
eigenvals = np.linalg.eigvalsh(A)
|
|
return float(eigenvals[-1] - eigenvals[0]), float(eigenvals[-1]), float(eigenvals[0])
|
|
|
|
|
|
# ── Main ─────────────────────────────────────────────────────────────
|
|
|
|
def run_database(include_degrey=False):
|
|
results = {
|
|
"experiment": "hn_spectral_database",
|
|
"timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
|
"include_degrey": include_degrey,
|
|
"graphs": [],
|
|
}
|
|
|
|
graphs = []
|
|
|
|
# 1. Moser spindle (7 vertices, χ=4)
|
|
print("Building Moser spindle...")
|
|
adj, pts, desc, chi_known = moser_spindle()
|
|
graphs.append({"adj": adj, "desc": desc, "chi_known": chi_known})
|
|
|
|
# 2. Golomb graph (10 vertices, χ=4)
|
|
print("Building Golomb graph...")
|
|
adj, pts, desc, chi_known = golomb_graph()
|
|
graphs.append({"adj": adj, "desc": desc, "chi_known": chi_known})
|
|
|
|
# 3. Empty graph (baseline, 10 vertices)
|
|
print("Building empty graph (baseline)...")
|
|
adj = empty_graph(10)
|
|
graphs.append({"adj": adj, "desc": "Empty graph (10v baseline)", "chi_known": 1})
|
|
|
|
# 4. Path graph (10 vertices, χ=2)
|
|
print("Building path graph (baseline)...")
|
|
n = 10
|
|
adj = [[i+1] if i == 0 else [i-1] if i == n-1 else [i-1, i+1] for i in range(n)]
|
|
graphs.append({"adj": adj, "desc": "Path graph P10 (baseline)", "chi_known": 2})
|
|
|
|
# 5. Cycle C5 (5 vertices, χ=3)
|
|
print("Building C5 (baseline)...")
|
|
n = 5
|
|
adj = [[(i+1)%n, (i-1)%n] for i in range(n)]
|
|
graphs.append({"adj": adj, "desc": "Cycle C5 (baseline)", "chi_known": 3})
|
|
|
|
# 6. Complete graph K4 (4 vertices, χ=4)
|
|
print("Building K4 (baseline)...")
|
|
n = 4
|
|
adj = [[j for j in range(n) if j != i] for i in range(n)]
|
|
graphs.append({"adj": adj, "desc": "Complete K4 (baseline)", "chi_known": 4})
|
|
|
|
# 7. de Grey subgraphs (if requested)
|
|
if include_degrey:
|
|
print("\nBuilding de Grey 1581...")
|
|
adj_dg, pts_dg, desc_dg, chi_dg = degrey_1581()
|
|
graphs.append({"adj": adj_dg, "desc": desc_dg, "chi_known": chi_dg})
|
|
|
|
# Subgraphs of various sizes
|
|
for n_target in [50, 100, 200, 500]:
|
|
print(f" Extracting subgraph n={n_target}...")
|
|
sub = degrey_subgraph(adj_dg, n_target)
|
|
n_actual = len(sub)
|
|
e_actual = sum(len(a) for a in sub) // 2
|
|
graphs.append({
|
|
"adj": sub,
|
|
"desc": f"de Grey subgraph (n={n_actual}, e={e_actual})",
|
|
"chi_known": None, # unknown for subgraphs
|
|
})
|
|
|
|
# Compute spectral bounds for each graph
|
|
print("\n" + "=" * 70)
|
|
print(f"{'Graph':<45} {'n':>5} {'e':>6} {'λ_max':>8} {'λ_min':>8} "
|
|
f"{'Hoff':>6} {'χ≥':>3} {'χ_known':>7}")
|
|
print("=" * 70)
|
|
|
|
for g in graphs:
|
|
adj = g["adj"]
|
|
desc = g["desc"]
|
|
chi_known = g["chi_known"]
|
|
n = len(adj)
|
|
e = sum(len(a) for a in adj) // 2
|
|
|
|
if n < 2:
|
|
continue
|
|
|
|
hb, lmax, lmin = hoffman_bound(adj)
|
|
chi_hb = math.ceil(hb) if math.isfinite(hb) else None
|
|
|
|
# Welch-Wynn bound (lower bound on Lovász theta of complement)
|
|
ww = lovasz_theta_complement(adj)
|
|
chi_ww = math.ceil(ww) if ww and math.isfinite(ww) else None
|
|
|
|
# Best spectral lower bound
|
|
bounds = [b for b in [chi_hb, chi_ww] if b is not None]
|
|
best_spectral = max(bounds) if bounds else None
|
|
|
|
gap = "tight" if (chi_known and best_spectral and best_spectral == chi_known) else \
|
|
f"gap={chi_known - best_spectral}" if (chi_known and best_spectral) else "?"
|
|
|
|
print(f"{desc:<45} {n:5d} {e:6d} {lmax:8.4f} {lmin:8.4f} "
|
|
f"{hb:6.3f} {str(chi_hb):>3} {str(chi_known):>7} {gap}")
|
|
|
|
entry = {
|
|
"description": desc,
|
|
"n_vertices": n,
|
|
"n_edges": e,
|
|
"lambda_max": round(lmax, 12),
|
|
"lambda_min": round(lmin, 12),
|
|
"hoffman_bound": round(hb, 12) if math.isfinite(hb) else None,
|
|
"chi_hoffman": chi_hb,
|
|
"welch_wynn": round(ww, 12) if ww else None,
|
|
"chi_welch_wynn": chi_ww,
|
|
"best_spectral_lower_bound": best_spectral,
|
|
"chi_known": chi_known,
|
|
"gap": gap,
|
|
}
|
|
results["graphs"].append(entry)
|
|
|
|
content = json.dumps(results, indent=2, sort_keys=True)
|
|
results["sha256"] = hashlib.sha256(content.encode()).hexdigest()
|
|
return results
|
|
|
|
|
|
def write_eval(results):
|
|
lines = [
|
|
"# Hadwiger-Nelson Spectral Database",
|
|
"",
|
|
f"**Date:** {results['timestamp']}",
|
|
f"**SHA-256:** `{results['sha256']}`",
|
|
f"**Includes de Grey:** {results['include_degrey']}",
|
|
"",
|
|
"## Spectral Bounds Comparison",
|
|
"",
|
|
"| Graph | n | e | λ_max | λ_min | Hoffman | χ_Hoff | Welch-Wynn | χ_WW | Known χ | Gap |",
|
|
"|-------|---|---|-------|------|---------|--------|------------|------|---------|-----|",
|
|
]
|
|
for g in results["graphs"]:
|
|
lines.append(
|
|
f"| {g['description']} | {g['n_vertices']} | {g['n_edges']} | "
|
|
f"{g['lambda_max']:.4f} | {g['lambda_min']:.4f} | "
|
|
f"{g['hoffman_bound']:.4f} | {g['chi_hoffman']} | "
|
|
f"{g['welch_wynn']:.4f if g['welch_wynn'] else 'N/A'} | "
|
|
f"{g['chi_welch_wynn']} | {g['chi_known']} | {g['gap']} |"
|
|
)
|
|
|
|
lines.extend([
|
|
"",
|
|
"## Key Findings",
|
|
"",
|
|
"1. **Hoffman bound** (χ ≥ 1 - λ_max/λ_min): classic spectral bound",
|
|
"2. **Welch-Wynn bound** (χ ≥ n/(n - λ_max)): another spectral bound",
|
|
"3. **Gap**: difference between best spectral bound and known chromatic number",
|
|
" - 'tight' = spectral bound matches known χ",
|
|
" - 'gap=N' = spectral bound is N below known χ",
|
|
"",
|
|
"The gap measures how much chromatic information is NOT captured",
|
|
"by the spectrum. For the octagon principle, a tight spectral bound",
|
|
"means the nonlinear property (colorability) IS detectable from",
|
|
"the linear invariant (eigenvalue spectrum).",
|
|
"",
|
|
])
|
|
|
|
eval_path = OUT_DIR / "EVAL.md"
|
|
eval_path.write_text("\n".join(lines))
|
|
return eval_path
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import argparse
|
|
parser = argparse.ArgumentParser(description="HN spectral database")
|
|
parser.add_argument("--full", action="store_true",
|
|
help="Include de Grey 1581 (slow)")
|
|
args = parser.parse_args()
|
|
|
|
print("=" * 70)
|
|
print("Hadwiger-Nelson Spectral Database")
|
|
print("=" * 70)
|
|
print(f"Include de Grey: {args.full}")
|
|
print()
|
|
|
|
t0 = time.time()
|
|
results = run_database(include_degrey=args.full)
|
|
elapsed = time.time() - t0
|
|
|
|
out_path = OUT_DIR / "hn_spectral_database.json"
|
|
with open(out_path, "w") as f:
|
|
json.dump(results, f, indent=2)
|
|
print(f"\nResults → {out_path}")
|
|
|
|
eval_path = write_eval(results)
|
|
print(f"EVAL → {eval_path}")
|
|
print(f"\nElapsed: {elapsed:.1f}s")
|
|
print("=" * 70)
|
|
print("DONE")
|
|
print("=" * 70)
|