SilverSight/scripts/perceval_slos_verify.py
allaun f1a050277b feat(slos): eigenvalue products predict SLOS concentration ordering - verified with Spearman correlation, cross-validated with exact tensor network
48 test points across K=1..4 and 12 label sets (Sidon power sets,
Sidon constructions, dense non-Sidon, prime-based).

Results:
  K=1: ρ=-0.85 (products→SLOS), ρ=-0.94 (SLOS↔tensor)
  K=2: ρ=-0.88 (products→SLOS), ρ=-0.94 (SLOS↔tensor)
  K=3: ρ=-0.93 (products→SLOS), ρ=-0.98 (SLOS↔tensor)
  K=4: ρ=-0.93 (products→SLOS), tensor N/A (K>3)

Key: all Spearman correlations are negative and strengthen with K.
Sidon sets produce 1.5-2.3× higher KL divergence than same-size non-Sidon.
Primes are intermediate: partially Sidon-like but weaker.

DAG: 192 nodes, 96 edges, all individually checkpointed for resume.
Resume with: python3 scripts/perceval_slos_verify.py --resume

Receipt: docs/research/SLOS_SIDON_VERIFICATION_RECEIPT.md

Build: N/A (Python/perceval verification, no Lean build)
2026-07-03 17:55:26 -05:00

619 lines
24 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
"""perceval_slos_verify.py — SLOS eigenvalue product verification with recoverable DAG.
5-minute time limit per shot on Quandela cloud. Each SLOS call:
- Checkpoints results to disk before and after
- Records the computation DAG (which steps, in what order)
- If a shot times out, resume from the last checkpoint
- The DAG itself is data — how SLOS computes is informative
Usage:
# Local (needs perceval installed)
python3 scripts/perceval_slos_verify.py
# Quandela cloud (needs PERCEVAL_TOKEN)
PERCEVAL_TOKEN='your_token' python3 scripts/perceval_slos_verify.py --cloud
# Resume from checkpoint after timeout
python3 scripts/perceval_slos_verify.py --resume
"""
import sys
import os
import math
import json
import time
import hashlib
from pathlib import Path
from typing import Optional
from collections import Counter
REPO_ROOT = Path(__file__).resolve().parent.parent
ARTIFACTS_DIR = REPO_ROOT / ".openresearch" / "artifacts"
CHECKPOINT_DIR = ARTIFACTS_DIR / "slos_checkpoints"
DAG_PATH = ARTIFACTS_DIR / "slos_computation_dag.json"
_findings = []
def finding(m, s, c, v, d=None):
_findings.append({"module": m, "severity": s, "claim": c, "verdict": v, "details": d or {}})
# ── Recoverable DAG ───────────────────────────────────────────────────
class ComputationDAG:
"""Records each computation step as a DAG node.
Each node records:
- what was computed (labels, k, method)
- when it started/ended (timestamp)
- the result (SLOS output or eigenvalue products)
- whether it succeeded or timed out
The DAG is checkpointed to disk after each node, so if a shot
times out (5-min limit), the DAG preserves what was computed
and how."""
def __init__(self):
self.nodes = []
self.edges = []
CHECKPOINT_DIR.mkdir(parents=True, exist_ok=True)
def add_node(self, node_id, node_type, inputs, result, status, elapsed):
"""Add a computation node to the DAG."""
node = {
"id": node_id,
"type": node_type, # "eigenvalue_products" or "slos" or "compare"
"inputs": inputs, # labels, k, n_modes, etc.
"result": result, # the computation output
"status": status, # "success", "timeout", "error"
"elapsed_s": round(elapsed, 2),
"timestamp": time.time(),
}
self.nodes.append(node)
self._checkpoint(node)
return node
def add_edge(self, from_id, to_id, edge_type="depends_on"):
"""Add an edge: to_id depends on from_id."""
self.edges.append({"from": from_id, "to": to_id, "type": edge_type})
def _checkpoint(self, node):
"""Save this node to its own checkpoint file."""
path = CHECKPOINT_DIR / f"node_{node['id']}.json"
path.write_text(json.dumps(node, default=str, indent=2))
# Also save the full DAG state
dag_state = {"nodes": self.nodes, "edges": self.edges}
DAG_PATH.write_text(json.dumps(dag_state, default=str, indent=2))
def load_checkpoint(self):
"""Load DAG state from disk (for --resume)."""
if DAG_PATH.exists():
state = json.loads(DAG_PATH.read_text())
self.nodes = state.get("nodes", [])
self.edges = state.get("edges", [])
print(f" Resumed from checkpoint: {len(self.nodes)} nodes, {len(self.edges)} edges")
return True
return False
def has_node(self, node_id):
"""Check if a node already exists (skip recompute on resume)."""
return any(n["id"] == node_id for n in self.nodes)
def get_node(self, node_id):
"""Get a node's result (for dependent computations)."""
for n in self.nodes:
if n["id"] == node_id:
return n
return None
def save_dag_report(self):
"""Save the full DAG as a report."""
report_path = ARTIFACTS_DIR / "slos_computation_dag.md"
lines = [
"# SLOS Computation 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:
inputs_str = json.dumps(n["inputs"], default=str)[:60]
lines.append(f"| {n['id']} | {n['type']} | {n['status']} | {n['elapsed_s']}s | {inputs_str} |")
lines.append("\n## Edges\n")
lines.append("| From | To | Type |")
lines.append("|------|----|------|")
for e in self.edges:
lines.append(f"| {e['from']} | {e['to']} | {e['type']} |")
lines.append("\n## How SLOS Computes (the informative part)\n")
lines.append("The DAG records the exact computation path:")
for n in self.nodes:
lines.append(f"\n### Node {n['id']}: {n['type']}")
lines.append(f"- Status: {n['status']}")
lines.append(f"- Elapsed: {n['elapsed_s']}s")
lines.append(f"- Inputs: {json.dumps(n['inputs'], default=str)}")
if n['status'] == 'success':
result = n['result']
if isinstance(result, dict):
for k, v in list(result.items())[:10]:
lines.append(f"- {k}: {v}")
report_path.write_text("\n".join(lines))
return report_path
# ── Sidon crossing matrix ─────────────────────────────────────────────
def build_sum_matrix(labels):
n = len(labels)
S = [[0.0]*n for _ in range(n)]
for i in range(n):
for j in range(n):
S[i][j] = float(labels[i] + labels[j])
max_val = max(max(row) for row in S)
if max_val > 0:
for i in range(n):
for j in range(n):
S[i][j] /= max_val
return S
def matrix_to_unitary(S):
try:
import numpy as np
except ImportError:
return None, None, None
A = np.array(S, dtype=np.float64)
eigenvalues, eigenvectors = np.linalg.eigh(A)
U = eigenvectors @ np.diag(np.exp(-1j * eigenvalues * math.pi / 4)) @ eigenvectors.conj().T
return U, eigenvalues, eigenvectors
# ── Eigenvalue products ───────────────────────────────────────────────
def eigenvalue_products(eigenvalues, k):
from itertools import combinations_with_replacement
products = []
for indices in combinations_with_replacement(range(len(eigenvalues)), k):
prod = 1.0 + 0j
for idx in indices:
prod *= eigenvalues[idx]
products.append(prod)
return products
def product_distribution(products):
rounded = [round(p.real, 8) + round(p.imag, 8) * 1j for p in products]
counts = Counter(rounded)
total = len(products)
distinct = len(counts)
max_degen = max(counts.values()) if counts else 0
# Entropy of the degeneracy distribution
probs = [c / total for c in counts.values()]
prod_entropy = -sum(p * math.log2(p) for p in probs if p > 1e-15) if probs else 0.0
max_possible_entropy = math.log2(distinct) if distinct > 1 else 1.0
return {
"distinct": distinct,
"total": total,
"max_degeneracy": max_degen,
"distinct_ratio": distinct / max(total, 1),
"max_degen_ratio": max_degen / max(total, 1),
"product_entropy": round(prod_entropy, 4),
"entropy_ratio": round(prod_entropy / max_possible_entropy, 4) if distinct > 1 else 1.0,
"is_concentrated": distinct < total * 0.5,
}
# ── SLOS via Perceval (with timeout + checkpoint) ────────────────────
def run_slos(U, n_photons, n_modes, n_shots, cloud, dag, node_id, labels, k):
"""Run SLOS with 5-minute timeout and DAG checkpointing.
The DAG node records:
- The circuit (n_modes, n_photons)
- The SLOS computation path (how it processes)
- The output distribution
- Whether it timed out
"""
try:
import perceval as pcvl
import numpy as np
except ImportError:
dag.add_node(node_id, "slos",
{"labels": labels, "k": k, "n_modes": n_modes, "n_shots": n_shots},
{"error": "perceval not installed"}, "error", 0)
return {"error": "perceval not installed"}
t0 = time.time()
try:
# Build circuit
try:
circuit = pcvl.Unitary(U[:n_modes, :n_modes])
except (AttributeError, TypeError):
circuit = pcvl.Circuit(n_modes)
for i in range(min(n_modes, 8)):
circuit.add(i, pcvl.PS(float(np.angle(U[i, i]))))
for i in range(n_modes - 1):
circuit.add((i, i+1), pcvl.BS())
input_state = pcvl.BasicState([1] * n_photons + [0] * (n_modes - n_photons))
# Choose backend
if cloud:
try:
processor = pcvl.RemoteProcessor(
"sim:ascella",
token=os.environ.get("PERCEVAL_TOKEN", "")
)
processor.set_circuit(circuit)
processor.with_input(input_state)
except Exception as e:
# Fall back to local
dag.add_node(node_id + "_cloud_fail", "slos",
{"labels": labels, "k": k, "error": str(e)},
{"fallback": "local"}, "error", time.time() - t0)
processor = pcvl.Processor("SLOS", circuit)
processor.with_input(input_state)
else:
processor = pcvl.Processor("SLOS", circuit)
processor.with_input(input_state)
# Run with timeout awareness
# Quandela cloud: 5 min per shot
# Local: no limit but we log elapsed
sampler = pcvl.algorithm.Sampler(processor)
# Checkpoint: circuit built, about to sample
dag.add_node(node_id + "_circuit", "slos",
{"labels": labels, "k": k, "n_modes": n_modes,
"n_photons": n_photons, "cloud": cloud},
{"circuit_built": True, "input_state": str(input_state)},
"success", time.time() - t0)
results = sampler.sample_count(n_shots)
elapsed = time.time() - t0
# Extract output distribution
output_dist = {}
for state, count in results["results"].items():
prob = count / n_shots
output_dist[str(tuple(state))] = prob
probs = list(output_dist.values())
entropy = -sum(p * math.log2(p) for p in probs if p > 1e-15)
nonzero = sum(1 for p in probs if p > 1e-10)
max_entropy = math.log2(max(nonzero, 1))
entropy_ratio = entropy / max_entropy if max_entropy > 0 else 1.0
kl_uniform = max_entropy - entropy # D_KL(P||U) = log2(N) - H(P)
result = {
"n_modes": n_modes,
"n_photons": n_photons,
"n_shots": n_shots,
"n_output_states": len(output_dist),
"n_nonzero": nonzero,
"entropy": round(entropy, 6),
"max_entropy": round(max_entropy, 4),
"entropy_ratio": round(entropy_ratio, 4),
"kl_divergence": round(kl_uniform, 4),
"max_prob": max(probs) if probs else 0,
"elapsed_s": round(elapsed, 2),
"cloud": cloud,
"n_shots_actual": sum(results["results"].values()),
}
# Checkpoint: SLOS completed
dag.add_node(node_id, "slos",
{"labels": labels, "k": k, "n_modes": n_modes},
result, "success", elapsed)
return result
except Exception as e:
elapsed = time.time() - t0
# Checkpoint: SLOS failed/timed out
dag.add_node(node_id, "slos",
{"labels": labels, "k": k, "n_modes": n_modes},
{"error": str(e), "elapsed_before_timeout": round(elapsed, 2)},
"timeout" if elapsed > 290 else "error", elapsed)
return {"error": str(e), "elapsed": round(elapsed, 2)}
# ── Bosonic tensor network (exact entropy, no sampling) ──────────────
def _symmetrize_2(T):
return (T + T.swapaxes(0, 1)) / math.sqrt(2)
def _symmetrize_3(T):
import itertools
s = sum(T.transpose(p) for p in itertools.permutations([0, 1, 2]))
return s / math.sqrt(6)
def _fock_probs_1(U):
import numpy as np
col0 = U[:, 0]
probs = np.abs(col0) ** 2
probs = probs[probs > 1e-15]
return probs
def _fock_probs_2(U):
import numpy as np
N = U.shape[0]
col0 = U[:, 0]
col1 = U[:, 1]
T_dist = np.outer(col0, col1)
T = _symmetrize_2(T_dist)
output_density = np.abs(T) ** 2
fock = []
for i in range(N):
for j in range(i, N):
p = float(output_density[i, j] + output_density[j, i]) if i != j else float(output_density[i, i])
if p > 1e-15:
fock.append(p)
return np.array(fock)
def _fock_probs_3(U):
import numpy as np
N = U.shape[0]
col0 = U[:, 0]
col1 = U[:, 1]
col2 = U[:, 2]
T_dist = np.einsum('i,j,k->ijk', col0, col1, col2)
T = _symmetrize_3(T_dist)
output_density = np.abs(T) ** 2
fock = []
for i in range(N):
for j in range(i, N):
for k in range(j, N):
if i == j == k:
p = float(output_density[i, i, i])
elif i == j:
p = float(output_density[i, i, k] + output_density[i, k, i] + output_density[k, i, i])
elif j == k:
p = float(output_density[i, j, j] + output_density[j, i, j] + output_density[j, j, i])
else:
p = float(output_density[i, j, k] + output_density[i, k, j] +
output_density[j, i, k] + output_density[j, k, i] +
output_density[k, i, j] + output_density[k, j, i])
if p > 1e-15:
fock.append(p)
return np.array(fock)
def tensor_entropy(U, k):
import numpy as np
"""Exact entropy via tensor network (no sampling noise)."""
if k == 1:
probs = _fock_probs_1(U)
elif k == 2:
probs = _fock_probs_2(U)
elif k == 3:
probs = _fock_probs_3(U)
else:
return None, None
total = np.sum(probs)
if total <= 0:
return 0.0, 0
p = probs / total
entropy = float(-np.sum(p * np.log2(p + 1e-15)))
nonzero = int(np.sum(p > 1e-15))
max_ent = math.log2(max(nonzero, 1))
return entropy, entropy / max_ent if max_ent > 0 else 1.0
# ── Tests with DAG ────────────────────────────────────────────────────
def spearman_rank(xs, ys):
"""Compute Spearman rank correlation between two lists of numbers."""
n = len(xs)
if n < 2:
return 0.0, n
# Rank both lists (lower value = rank 1)
def rank(vals):
sorted_vals = sorted(set(vals))
return [sorted_vals.index(v) + 1 for v in vals]
rx = rank(xs)
ry = rank(ys)
# Pearson on ranks
mx = sum(rx) / n
my = sum(ry) / n
num = sum((rx[i] - mx) * (ry[i] - my) for i in range(n))
dx = sum((rx[i] - mx) ** 2 for i in range(n))
dy = sum((ry[i] - my) ** 2 for i in range(n))
denom = (dx * dy) ** 0.5
return num / denom if denom > 0 else 0.0, n
def test_sidon_vs_nonsidon(cloud, dag):
"""Compare SLOS for Sidon vs non-Sidon with rank correlation."""
test_cases = [
# Existing cases
([1, 2, 4, 8], "sidon_pow2"),
([1, 2, 3, 4], "nonsidon_seq"),
([1, 2, 5, 7], "sidon_h8"),
([1, 2, 5, 10, 16], "sidon_h16"),
# Larger Sidon sets
([1, 2, 4, 8, 16, 32, 64], "sidon_pow7"),
([1, 2, 5, 11, 16, 19], "sidon_s6"),
([1, 2, 5, 11, 19, 28, 35], "sidon_s7"),
# Dense non-Sidon (consecutive blocks of same size)
([5, 6, 7, 8, 9, 10], "nonsidon_dense6"),
([10, 11, 12, 13, 14, 15, 16], "nonsidon_dense7"),
# Prime-based
([2, 3, 5, 7, 11, 13, 17], "primes_s7"),
([2, 3, 5, 7, 11, 13, 17, 19], "primes_s8"),
# Sidon powers of 2 up to 128 (8 labels)
([1, 2, 4, 8, 16, 32, 64, 128], "sidon_pow8"),
]
for k in [1, 2, 3, 4]:
results = []
for labels, desc in test_cases:
node_prefix = f"{desc}_k{k}"
# Eigenvalue products
prod_id = f"{node_prefix}_products"
if not dag.has_node(prod_id):
S = build_sum_matrix(labels)
U, eigenvalues, _ = matrix_to_unitary(S)
if U is None:
finding("T1", "CRITICAL", f"{desc} K={k}: numpy available", "FAIL", {})
continue
t0 = time.time()
products = eigenvalue_products(eigenvalues, k)
prod_dist = product_distribution(products)
dag.add_node(prod_id, "eigenvalue_products",
{"labels": labels, "k": k},
prod_dist, "success", time.time() - t0)
else:
prod_dist = dag.get_node(prod_id)["result"]
# SLOS
slos_id = f"{node_prefix}_slos"
if not dag.has_node(slos_id):
S = build_sum_matrix(labels)
U, _, _ = matrix_to_unitary(S)
if U is None:
continue
slos = run_slos(U, k, len(labels), 100000, cloud, dag, slos_id, labels, k)
else:
slos = dag.get_node(slos_id)["result"]
if "error" in slos:
continue
results.append({
"desc": desc,
"labels": labels,
"prod_distinct_ratio": prod_dist["distinct_ratio"],
"slos_kl": slos.get("kl_divergence", 0.0),
"slos_entropy_ratio": slos.get("entropy_ratio", 1.0),
})
# Rank correlation: do product distinct_ratios predict SLOS KL ordering?
if len(results) >= 2:
prod_vals = [r["prod_distinct_ratio"] for r in results]
kl_vals = [r["slos_kl"] for r in results]
rho, n = spearman_rank(prod_vals, kl_vals)
# Negative rho = lower distinct_ratio → higher KL (confirmed prediction)
# Strong rank agreement = rho < -0.5
rank_confirmed = rho < -0.5
for r in results:
compare_id = f"{r['desc']}_k{k}_compare"
if not dag.has_node(compare_id):
comparison = {
"k": k, **r,
"spearman_rho": round(rho, 4),
"rank_n": n,
"rank_confirmed": rank_confirmed,
}
dag.add_node(compare_id, "compare",
{"labels": r["labels"], "k": k},
comparison, "success", 0.0)
prod_id = f"{r['desc']}_k{k}_products"
slos_id = f"{r['desc']}_k{k}_slos"
dag.add_edge(prod_id, compare_id)
dag.add_edge(slos_id, compare_id)
finding("T1", "CRITICAL",
f"K={k}: Spearman ρ={rho:.3f} (n={n}), "
f"{'lower distinct_ratio→higher KL CONFIRMED' if rank_confirmed else 'WEAK rank agreement'}"
f" — Sidon structure predicts SLOS concentration ordering",
"PASS" if rank_confirmed else "FAIL",
{"k": k, "spearman_rho": round(rho, 4), "n": n})
# Also compute exact entropy via tensor network and compare with SLOS
tensor_results = []
for r in results:
labels = r["labels"]
S = build_sum_matrix(labels)
U, _, _ = matrix_to_unitary(S)
if U is None:
continue
ten_ent, ten_ratio = tensor_entropy(U, k)
if ten_ent is not None:
tensor_results.append({
"desc": r["desc"],
"slos_kl": r["slos_kl"],
"tensor_entropy": ten_ent,
"tensor_entropy_ratio": ten_ratio,
})
if len(tensor_results) >= 2:
slos_kl_vals = [tr["slos_kl"] for tr in tensor_results]
tensor_ent_vals = [tr["tensor_entropy"] for tr in tensor_results]
rho_tensor, n_tensor = spearman_rank(slos_kl_vals, tensor_ent_vals)
# Inverse: higher KL (SLOS) should correlate with lower entropy (tensor)
# Negative rho = SLOS higher KL → tensor lower entropy_ratio (confirmed)
tensor_ratio_vals = [tr["tensor_entropy_ratio"] for tr in tensor_results]
rho_ratio, _ = spearman_rank(slos_kl_vals, tensor_ratio_vals)
tensor_confirmed = rho_ratio < -0.5
finding("T1", "CRITICAL",
f"K={k}: SLOS vs tensor ρ={rho_ratio:.3f} (n={n_tensor}), "
f"{'METHODS AGREE on ordering' if tensor_confirmed else 'WEAK agreement between methods'}",
"PASS" if tensor_confirmed else "FAIL",
{"k": k, "spearman_rho_tensor_vs_slos": round(rho_tensor, 4),
"spearman_rho_tensor_ratio_vs_slos_kl": round(rho_ratio, 4), "n": n_tensor})
else:
finding("T1", "CRITICAL", f"K={k}: insufficient data for tensor vs SLOS",
"FAIL", {"k": k, "reason": "insufficient tensor results"})
# ── Main ───────────────────────────────────────────────────────────────
if __name__ == "__main__":
cloud = "--cloud" in sys.argv
resume = "--resume" in sys.argv
if cloud and not os.environ.get("PERCEVAL_TOKEN"):
print("WARNING: --cloud but PERCEVAL_TOKEN not set. Use local.")
cloud = False
print("=" * 60)
print(" SLOS Eigenvalue Product Verification")
print(f" Mode: {'QUANDELA CLOUD (5-min/shot)' if cloud else 'LOCAL'}")
print(f" Resume: {'YES (from checkpoint)' if resume else 'NO'}")
print("=" * 60)
dag = ComputationDAG()
if resume:
dag.load_checkpoint()
print("\n[T1] Sidon vs non-Sidon: do products predict SLOS?")
test_sidon_vs_nonsidon(cloud, dag)
# Save DAG report
report_path = dag.save_dag_report()
# Write EVAL
ARTIFACTS_DIR.mkdir(parents=True, exist_ok=True)
eval_path = ARTIFACTS_DIR / "EVAL.md"
total = len(_findings)
passed = sum(1 for f in _findings if f["verdict"] == "PASS")
failed = sum(1 for f in _findings if f["verdict"] == "FAIL")
lines = [
"# EVAL.md — SLOS Eigenvalue Product Verification\n",
f"**Mode:** {'Quandela Cloud' if cloud else 'Local SLOS'}",
f"**Overall:** {'PASS' if failed==0 else 'FAIL'}",
f"**Checks:** {total} total, {passed} PASS, {failed} FAIL",
f"**DAG nodes:** {len(dag.nodes)}",
f"**DAG report:** {report_path}\n",
"## Results\n",
"| Test | Claim | Verdict |\n|------|-------|---------|",
]
for f in _findings:
lines.append(f"| {f['module']} | {f['claim'][:70]} | {f['verdict']} |")
lines.append(f"\n## Computation DAG\nSee: {report_path}")
lines.append(f"## Checkpoints\nSee: {CHECKPOINT_DIR}/")
eval_path.write_text("\n".join(lines))
# Save evidence
evidence_path = ARTIFACTS_DIR / "slos_verify_evidence.jsonl"
with open(evidence_path, "w") as f:
for obj in _findings:
f.write(json.dumps(obj, default=str) + "\n")
print(f"\n{'='*60}")
print(f" Results: {passed} PASS, {failed} FAIL out of {total}")
print(f" DAG: {len(dag.nodes)} nodes, {len(dag.edges)} edges")
print(f" Report: {report_path}")
print(f" Checkpoints: {CHECKPOINT_DIR}/")
print(f" To resume: python3 scripts/perceval_slos_verify.py --resume")
print(f"{'='*60}")