mirror of
https://github.com/allaunthefox/Research-Stack.git
synced 2026-07-31 03:05:21 +00:00
- qaoa_adapter.py: fix finsler_metric_to_qubo to store Q_ij + Q_ji per undirected pair; update is_anisotropic and _find_anisotropic_pair to accept raw_matrix so asymmetry detection still works after summation; add measure option to pauli_to_cirq. - benchmark_finsler_qaoa.py: new benchmark harness for Finsler-Randers routing via QAOA.
2659 lines
92 KiB
Python
2659 lines
92 KiB
Python
"""
|
||
qaoa_adapter.py — Bidirectional QAOA Conversion Adapter Set
|
||
|
||
Borrows existing Lean-native problem representations and makes them
|
||
interactable via QAOA (Quantum Approximate Optimization Algorithm).
|
||
|
||
Forward: Problem → QUBO → Ising → Pauli strings → Cirq/Qiskit circuit
|
||
Backward: Measurements → bitstring → solution → problem update
|
||
|
||
Hooks into existing pipeline:
|
||
- EntropyMeasures.QUBOFormulation (Array Array Q16_16) → QUBO
|
||
- RotationQUBO.QUBOField (frustration, energyScale) → QUBO
|
||
- braid_search.build_qubo_matrix (bracket QUBO) → QUBO
|
||
- qubo_highs.solve_qubo_highs (SA / HiGHS) → stochastic solver
|
||
- Lean #eval bridge (via eigensolid_lean_bridge) → live Lean eval
|
||
|
||
Stochastic abuse: runs QAOA and classical stochastic solvers side-by-side
|
||
on the same QUBO for comparison.
|
||
|
||
All Q16_16 boundary conversion is explicit. No decision or gating logic.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
import math
|
||
import time
|
||
from dataclasses import dataclass, field, asdict
|
||
from pathlib import Path
|
||
from typing import Any, Optional
|
||
|
||
import sys as _sys
|
||
_sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "lib"))
|
||
_sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||
from q16 import Q16_SCALE, from_q16, to_q16
|
||
from braid_search import _q16_signed
|
||
|
||
# ── optional backend imports ──────────────────────────────────────────
|
||
|
||
try:
|
||
import cirq
|
||
_HAS_CIRQ = True
|
||
except ImportError:
|
||
_HAS_CIRQ = False
|
||
|
||
try:
|
||
from qiskit import QuantumCircuit, QuantumRegister, ClassicalRegister
|
||
_HAS_QISKIT = True
|
||
except ImportError:
|
||
_HAS_QISKIT = False
|
||
|
||
try:
|
||
import numpy as np
|
||
_HAS_NUMPY = True
|
||
except ImportError:
|
||
_HAS_NUMPY = False
|
||
|
||
|
||
# =========================================================================
|
||
# I. Data Models
|
||
# =========================================================================
|
||
|
||
@dataclass
|
||
class QUBO:
|
||
"""Quadratic Unconstrained Binary Optimization problem.
|
||
|
||
Minimize x^T Q x where x_i ∈ {0, 1}.
|
||
matrix[(i,j)] = coefficient for x_i * x_j (i <= j, upper triangular).
|
||
"""
|
||
n: int
|
||
matrix: dict[tuple[int, int], float] = field(default_factory=dict)
|
||
offset: float = 0.0
|
||
|
||
def __post_init__(self) -> None:
|
||
keys = list(self.matrix.keys())
|
||
for i, j in keys:
|
||
if i > j:
|
||
self.matrix[(j, i)] = self.matrix.pop((i, j))
|
||
|
||
def energy(self, x: list[int]) -> float:
|
||
e = self.offset
|
||
for (i, j), qij in self.matrix.items():
|
||
e += qij * x[i] * x[j]
|
||
return e
|
||
|
||
|
||
@dataclass
|
||
class Ising:
|
||
"""Ising Hamiltonian: H = Σ h_i s_i + Σ J_ij s_i s_j + offset.
|
||
|
||
s_i ∈ {+1, -1}. J uses upper triangular (i < j).
|
||
"""
|
||
n: int
|
||
h: list[float] = field(default_factory=list)
|
||
J: dict[tuple[int, int], float] = field(default_factory=dict)
|
||
offset: float = 0.0
|
||
|
||
def energy(self, s: list[int]) -> float:
|
||
if len(s) != self.n:
|
||
raise ValueError(f"expected {self.n} spins, got {len(s)}")
|
||
e = self.offset
|
||
for i in range(self.n):
|
||
e += self.h[i] * s[i]
|
||
for (i, j), Jij in self.J.items():
|
||
e += Jij * s[i] * s[j]
|
||
return e
|
||
|
||
|
||
@dataclass
|
||
class PauliSum:
|
||
"""Pauli string representation of an Ising Hamiltonian.
|
||
|
||
terms: list of (pauli_string, coefficient)
|
||
e.g. ("ZZII", 0.5), ("ZIII", -1.0)
|
||
offset: constant (identity) term
|
||
"""
|
||
n: int
|
||
terms: list[tuple[str, float]] = field(default_factory=list)
|
||
offset: float = 0.0
|
||
|
||
|
||
# =========================================================================
|
||
# II. Q16_16 Boundary Utilities
|
||
# =========================================================================
|
||
|
||
def q16_to_float(raw: int) -> float:
|
||
"""Convert Q16_16 raw integer to float (boundary only)."""
|
||
return from_q16(raw)
|
||
|
||
|
||
def float_to_q16(value: float) -> int:
|
||
"""Convert float to Q16_16 raw integer (boundary only)."""
|
||
return to_q16(value)
|
||
|
||
|
||
# =========================================================================
|
||
# III-A. Lean QUBOFormulation → QUBO
|
||
# Source: EntropyMeasures.lean — QUBOFormulation { matrix, numVariables }
|
||
# matrix is Array (Array Q16_16): outer = rows, inner = cols
|
||
# objective: E = Σ_i Σ_j Q_ij * x_i * x_j (both true → add)
|
||
# =========================================================================
|
||
|
||
def lean_qubo_formulation_to_qubo(
|
||
matrix: list[list[int]],
|
||
num_variables: Optional[int] = None,
|
||
offset_q16: int = 0,
|
||
) -> QUBO:
|
||
"""Convert a Lean QUBOFormulation matrix to a QUBO.
|
||
|
||
Args:
|
||
matrix: N×N array of Q16_16 raw integers (from Lean's
|
||
QUBOFormulation.matrix : Array (Array Q16_16))
|
||
num_variables: Number of binary variables (defaults to len(matrix))
|
||
offset_q16: Q16_16 constant term
|
||
|
||
Returns:
|
||
QUBO with float coefficients converted at the boundary.
|
||
"""
|
||
n = num_variables or len(matrix)
|
||
Q: dict[tuple[int, int], float] = {}
|
||
for i in range(min(n, len(matrix))):
|
||
row = matrix[i] if i < len(matrix) else []
|
||
for j in range(min(n, len(row))):
|
||
qij = row[j]
|
||
if qij != 0:
|
||
key = (min(i, j), max(i, j))
|
||
Q[key] = Q.get(key, 0.0) + q16_to_float(qij)
|
||
return QUBO(n=n, matrix=Q, offset=q16_to_float(offset_q16))
|
||
|
||
|
||
def qubo_to_lean_qubo_formulation(qubo: QUBO) -> list[list[int]]:
|
||
"""Convert a QUBO back to Lean QUBOFormulation matrix (Q16_16 ints).
|
||
|
||
Returns N×N list of Q16_16 raw integers, suitable for formatting
|
||
into Lean syntax::
|
||
|
||
QUBOFormulation.mk #[#[q00, q01, ...], #[q10, q11, ...], ...]
|
||
"""
|
||
n = qubo.n
|
||
matrix: list[list[int]] = [[0] * n for _ in range(n)]
|
||
for (i, j), qij in qubo.matrix.items():
|
||
raw = float_to_q16(qij)
|
||
matrix[i][j] = raw
|
||
if i != j:
|
||
matrix[j][i] = raw
|
||
return matrix
|
||
|
||
|
||
# =========================================================================
|
||
# III-B. Lean RotationQUBO.QUBOField → QUBO
|
||
# Source: RotationQUBO.lean — QUBOField { frustration, energyScale }
|
||
# fieldEnergy(x) = x² / (1 + δ²) - energyScale
|
||
# isFrustrated(x) = fieldEnergy > 0
|
||
# We discretize x across [-range, +range] into binary variables.
|
||
# =========================================================================
|
||
|
||
def lean_qubo_field_to_qubo(
|
||
frustration_raw: int,
|
||
energy_scale_raw: int,
|
||
n_vars: int = 8,
|
||
field_range: float = 2.0,
|
||
) -> QUBO:
|
||
"""Discretize a Lean RotationQUBO.QUBOField into a binary QUBO.
|
||
|
||
Maps the continuous field energy E(x) = x²/(1+δ²) - energyScale
|
||
onto n_vars binary variables by discretizing x ∈ [-range, +range].
|
||
|
||
The QUBO encodes: x = Σ_k s_k * Δ where s_k ∈ {0,1} and
|
||
Δ = 2*range / n_vars. The energy becomes a quadratic in the s_k.
|
||
|
||
Variables: x_i = 1 if discretization point i is selected (one-hot).
|
||
"""
|
||
δ = q16_to_float(frustration_raw)
|
||
ε = q16_to_float(energy_scale_raw)
|
||
denom = 1.0 + δ * δ
|
||
|
||
Q: dict[tuple[int, int], float] = {}
|
||
for i in range(n_vars):
|
||
xi = -field_range + (2.0 * field_range * i) / max(n_vars - 1, 1)
|
||
Ei = (xi * xi) / denom - ε
|
||
|
||
# One-hot diagonal: energy at this point
|
||
Q[(i, i)] = Ei
|
||
|
||
return QUBO(n=n_vars, matrix=Q)
|
||
|
||
|
||
# =========================================================================
|
||
# III-C. braid_search.build_qubo_matrix → QUBO
|
||
# Source: braid_search.py — build_qubo_matrix(brackets) → dict[(i,j), int]
|
||
# Each bracket has: lower, upper, gap, kappa, phi, admissible
|
||
# bracket_cost = -base + |gap|/10 (Q16_16)
|
||
# crossing_penalty = overlap * 2*Q16_ONE + diversity_reward
|
||
# =========================================================================
|
||
|
||
def braid_search_brackets_to_qubo(
|
||
brackets: list[dict],
|
||
as_q16: bool = False,
|
||
) -> QUBO:
|
||
"""Convert braid brackets to QUBO using the existing braid_search pipeline.
|
||
|
||
Args:
|
||
brackets: List of bracket dicts with lower/upper/gap/kappa/phi/admissible.
|
||
as_q16: If True, keep raw Q16_16 ints; if False, convert to float.
|
||
|
||
Returns:
|
||
QUBO problem (8-strand braid crossing selection).
|
||
"""
|
||
try:
|
||
_sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||
from braid_search import build_qubo_matrix
|
||
except ImportError:
|
||
return _fallback_bracket_qubo(brackets)
|
||
|
||
q16_matrix = build_qubo_matrix(brackets)
|
||
n = max(max(i, j) for (i, j) in q16_matrix) + 1 if q16_matrix else 0
|
||
|
||
Q: dict[tuple[int, int], float] = {}
|
||
for (i, j), raw in q16_matrix.items():
|
||
if as_q16:
|
||
Q[(i, j)] = float(raw)
|
||
else:
|
||
Q[(i, j)] = q16_to_float(raw)
|
||
|
||
return QUBO(n=n, matrix=Q)
|
||
|
||
|
||
def _fallback_bracket_qubo(brackets: list[dict]) -> QUBO:
|
||
"""Fallback if braid_search is not importable."""
|
||
n = len(brackets)
|
||
Q: dict[tuple[int, int], float] = {}
|
||
for i, b in enumerate(brackets):
|
||
gap = q16_to_float(b.get("gap", 32768))
|
||
admissible = b.get("admissible", True)
|
||
base = 1.0 if admissible else 2.0
|
||
Q[(i, i)] = -base + abs(gap) * 0.1
|
||
for j in range(i + 1, n):
|
||
bj = brackets[j]
|
||
overlap = min(b.get("upper", 0), bj.get("upper", 0)) - \
|
||
max(b.get("lower", 0), bj.get("lower", 0))
|
||
if overlap > 0:
|
||
Q[(i, j)] = 2.0
|
||
return QUBO(n=n, matrix=Q)
|
||
|
||
|
||
# =========================================================================
|
||
# III-D. BraidReceipt → QUBO (8-strand braid)
|
||
# =========================================================================
|
||
|
||
def braid_receipt_to_qubo(receipt: dict) -> QUBO:
|
||
"""Convert a BraidReceipt JSON dict to a QUBO over 8 strand variables.
|
||
|
||
Variable mapping:
|
||
x_i = 1 if strand i is "active" (selected in crossing), 0 otherwise.
|
||
|
||
Cost terms:
|
||
- Diagonal: kappa_i (phase accumulation — lower is better)
|
||
- Off-diagonal: slot collision penalty if two strands share a slot
|
||
- Scar penalty: if scar_absent is false, each scarred strand adds cost
|
||
- Sidon slack bonus: want high slack = low max label used
|
||
- Residual penalty: non-converged strands increase cost
|
||
"""
|
||
braid = receipt.get("braid", receipt)
|
||
strands = braid.get("strands", [])
|
||
if not strands:
|
||
strands = _receipt_strands_from_bracket(braid)
|
||
|
||
n = len(strands)
|
||
Q: dict[tuple[int, int], float] = {}
|
||
offset = 0.0
|
||
|
||
slot_map: dict[int, list[int]] = {}
|
||
|
||
for i, s in enumerate(strands):
|
||
kappa = q16_to_float(s.get("kappa", 0))
|
||
phi = q16_to_float(s.get("phi", 0))
|
||
slot = s.get("slot", 0)
|
||
converged = s.get("converged", True)
|
||
residual = q16_to_float(s.get("residual", 0))
|
||
|
||
Q[(i, i)] = kappa + 0.1 * abs(phi)
|
||
|
||
if not converged and residual > 0:
|
||
Q[(i, i)] = Q[(i, i)] + 10.0 * residual
|
||
|
||
slot_map.setdefault(slot, []).append(i)
|
||
|
||
for slot, indices in slot_map.items():
|
||
if len(indices) > 1:
|
||
penalty = 50.0
|
||
for a in range(len(indices)):
|
||
for b in range(a + 1, len(indices)):
|
||
i, j = indices[a], indices[b]
|
||
Q[(i, j)] = Q.get((i, j), 0.0) + penalty
|
||
|
||
scar_absent = receipt.get("scar_absent", braid.get("scar_absent", True))
|
||
if not scar_absent:
|
||
for i in range(n):
|
||
Q[(i, i)] = Q.get((i, i), 0.0) + 100.0
|
||
|
||
sidon_slack = receipt.get("sidon_slack", 0)
|
||
max_label = 128 - sidon_slack
|
||
offset += max_label / 128.0
|
||
|
||
return QUBO(n=n, matrix=Q, offset=offset)
|
||
|
||
|
||
def _receipt_strands_from_bracket(bracket: dict) -> list[dict]:
|
||
"""Synthesize strand array from a minimal bracket-only receipt."""
|
||
default = bracket.get("bracket", bracket)
|
||
gaps = [default.get("gap", 32768)] * 8
|
||
kappas = [default.get("kappa", 0)] * 8
|
||
return [
|
||
{"id": chr(ord("a") + i), "slot": 1 << i, "kappa": kappas[i],
|
||
"phi": 0, "residual": 0, "converged": True}
|
||
for i in range(8)
|
||
]
|
||
|
||
|
||
# =========================================================================
|
||
# III-F. Bidirectional QAOA ↔ Greek HachimojiSubstitution Decoder
|
||
#
|
||
# Each of the 8 QUBO variables maps to a Greek Hachimoji state with a
|
||
# defined phase (45° steps), chirality, and flow direction per the
|
||
# Omindirection Logogram contract (OMINDIRECTION_LOGOGRAM_DESIGN_AND_COMPILER.md).
|
||
#
|
||
# Forward (LTR, phases 0-135°): Φ Λ Ρ Κ — normal Baker regime
|
||
# Backward (RTL, phases 180-315°): Ω Σ Π Ζ — quarantine/tearing regime
|
||
#
|
||
# Lean source: Semantics/HachimojiSubstitution.lean §5-6
|
||
# =========================================================================
|
||
|
||
# Ordered by bit index 0-7 (matches braid_receipt_to_qubo variable order)
|
||
GREEK_STATES: list[str] = ["Φ", "Λ", "Ρ", "Κ", "Ω", "Σ", "Π", "Ζ"]
|
||
|
||
GREEK_PHASE: dict[str, int] = {
|
||
"Φ": 0, "Λ": 45, "Ρ": 90, "Κ": 135,
|
||
"Ω": 180, "Σ": 225, "Π": 270, "Ζ": 315,
|
||
}
|
||
|
||
def _phase_to_chirality(phase: int) -> str:
|
||
"""Omindirection Principle 3: chirality is a projection of phase."""
|
||
if phase in (0, 180):
|
||
return "ambidextrous"
|
||
elif phase < 180:
|
||
return "left"
|
||
else:
|
||
return "right"
|
||
|
||
def _phase_to_direction(phase: int) -> str:
|
||
"""Phases 0-135 = forward (LTR); 180-315 = reverse (RTL)."""
|
||
return "forward" if phase < 180 else "reverse"
|
||
|
||
# Receipt Bool fields controlled by each Greek state
|
||
# (payloadBound, contradictionWitness, tearBoundary, detachedMass, residualLane)
|
||
_GREEK_RECEIPT_BITS: dict[str, tuple[bool, bool, bool, bool, bool]] = {
|
||
"Φ": (True, False, False, False, False),
|
||
"Λ": (True, False, False, False, False),
|
||
"Ρ": (False, False, False, False, True),
|
||
"Κ": (False, False, False, True, False),
|
||
"Ω": (True, True, True, True, True),
|
||
"Σ": (False, False, True, False, False),
|
||
"Π": (False, False, False, False, False),
|
||
"Ζ": (False, False, False, True, False),
|
||
}
|
||
|
||
def _greek_to_regime(state: str) -> str:
|
||
"""SemanticRegime for a Greek state."""
|
||
if state in ("Φ", "Λ"):
|
||
return "beautifulTopologicalFolding"
|
||
elif state in ("Ρ", "Κ"):
|
||
return "uglyAsymmetricPruning"
|
||
else:
|
||
return "horribleManifoldTearing"
|
||
|
||
|
||
def decode_bitstring_to_greek_states(bits: list[bool]) -> list[dict]:
|
||
"""Backward QAOA decoder: measurement bitstring → Greek state atoms.
|
||
|
||
Each active bit (True) becomes an omindirectional atom with:
|
||
symbol, phase, chirality, direction, regime.
|
||
|
||
Matches Lean: HachimojiSubstitution.bitToGreek / Greek.HachimojiBase.phase
|
||
"""
|
||
atoms = []
|
||
for i, active in enumerate(bits[:8]):
|
||
if active:
|
||
sym = GREEK_STATES[i]
|
||
phase = GREEK_PHASE[sym]
|
||
atoms.append({
|
||
"bit": i,
|
||
"symbol": sym,
|
||
"phase": phase,
|
||
"chirality": _phase_to_chirality(phase),
|
||
"direction": _phase_to_direction(phase),
|
||
"regime": _greek_to_regime(sym),
|
||
})
|
||
return atoms
|
||
|
||
|
||
def greek_states_to_logogram_receipt(active_states: list[dict]) -> dict:
|
||
"""Backward QAOA decoder: Greek atom list → LogogramReceipt dict.
|
||
|
||
Dominant state = lowest phase (most stable).
|
||
Bool fields accumulated from ALL active states (OR-fold).
|
||
Matches Lean: HachimojiSubstitution.fromQAOABitstring
|
||
|
||
The receipt dict is compatible with RRCLogogramProjection.LogogramReceipt.
|
||
"""
|
||
if not active_states:
|
||
return {
|
||
"shape": "logogramProjection",
|
||
"status": "hold",
|
||
"regime": "uglyAsymmetricPruning",
|
||
"payloadBound": False,
|
||
"contradictionWitness": False,
|
||
"tearBoundary": False,
|
||
"detachedMass": False,
|
||
"residualLane": False,
|
||
}
|
||
|
||
# Dominant = lowest phase
|
||
dominant = min(active_states, key=lambda a: a["phase"])
|
||
|
||
# Accumulate Bool fields from all active states
|
||
pb = any(_GREEK_RECEIPT_BITS[a["symbol"]][0] for a in active_states)
|
||
cw = any(_GREEK_RECEIPT_BITS[a["symbol"]][1] for a in active_states)
|
||
tb = any(_GREEK_RECEIPT_BITS[a["symbol"]][2] for a in active_states)
|
||
dm = any(_GREEK_RECEIPT_BITS[a["symbol"]][3] for a in active_states)
|
||
rl = any(_GREEK_RECEIPT_BITS[a["symbol"]][4] for a in active_states)
|
||
|
||
# status = hold if Ζ (bit 7) is active
|
||
zeta_active = any(a["symbol"] == "Ζ" for a in active_states)
|
||
|
||
return {
|
||
"shape": "logogramProjection",
|
||
"status": "hold" if zeta_active else "candidate",
|
||
"regime": dominant["regime"],
|
||
"payloadBound": pb,
|
||
"contradictionWitness": cw,
|
||
"tearBoundary": tb,
|
||
"detachedMass": dm,
|
||
"residualLane": rl,
|
||
# Omindirection metadata
|
||
"chirality_witness": {
|
||
"direction": dominant["direction"],
|
||
"handedness": dominant["chirality"],
|
||
"phase": dominant["phase"],
|
||
"placement": "quarantine" if dominant["direction"] == "reverse" else "row",
|
||
"mode": "greek_hachimoji_v1",
|
||
},
|
||
}
|
||
|
||
|
||
def logogram_receipt_to_bitstring(receipt: dict) -> list[bool]:
|
||
"""RTL direction: LogogramReceipt → 8-bit Greek signature.
|
||
|
||
Matches Lean: HachimojiSubstitution.toQAOABitstring
|
||
This is the backward path for re-encoding into a QUBO update.
|
||
"""
|
||
regime = receipt.get("regime", "")
|
||
return [
|
||
bool(receipt.get("payloadBound", False)), # Φ
|
||
regime == "beautifulTopologicalFolding", # Λ
|
||
bool(receipt.get("residualLane", False)), # Ρ
|
||
bool(receipt.get("detachedMass", False)), # Κ
|
||
bool(receipt.get("contradictionWitness", False)), # Ω
|
||
bool(receipt.get("tearBoundary", False)), # Σ
|
||
regime == "horribleManifoldTearing", # Π
|
||
receipt.get("status", "") == "hold", # Ζ
|
||
]
|
||
|
||
|
||
def qaoa_measurement_to_receipt(bits: list[bool]) -> dict:
|
||
"""Full bidi decoder: QAOA bitstring → LogogramReceipt.
|
||
|
||
Combines decode_bitstring_to_greek_states + greek_states_to_logogram_receipt.
|
||
This is the primary backward path entry point.
|
||
"""
|
||
atoms = decode_bitstring_to_greek_states(bits)
|
||
return greek_states_to_logogram_receipt(atoms)
|
||
|
||
|
||
# =========================================================================
|
||
# IV. Forward: LonelyRunner β₀/Scar → QUBO
|
||
# =========================================================================
|
||
|
||
def lonely_runner_scar_to_qubo(
|
||
scar_field: Any,
|
||
*,
|
||
beta0_weight: float = 10.0,
|
||
coverage_weight: float = 1.0,
|
||
) -> QUBO:
|
||
"""Convert a LonelyRunner scar field to a QUBO over N circle points.
|
||
|
||
The scar field μ(t,θ) ∈ {0, 1} indicates uncovered (lonely) regions.
|
||
|
||
Variables: x_i = scarred[i] (1 = uncovered, 0 = covered), N points.
|
||
|
||
Cost: maximize β₀ (connected components of uncovered region)
|
||
= minimize -β₀ + coverage_penalty
|
||
|
||
β₀ = Σ_i x_i * (1 - x_{i-1}) (rising edges, cyclically)
|
||
|
||
For an end-to-end QAOA that finds a lonely time:
|
||
H_cost = -β₀ + λ * coverage
|
||
The ground state = max β₀ at minimal coverage = a lonely time.
|
||
"""
|
||
if isinstance(scar_field, list) and _HAS_NUMPY:
|
||
import numpy as _np
|
||
scar_field = _np.array(scar_field)
|
||
|
||
if _HAS_NUMPY:
|
||
import numpy as _np
|
||
T, N = scar_field.shape
|
||
avg_scar = _np.mean(scar_field, axis=0) > 0.5
|
||
else:
|
||
T = len(scar_field)
|
||
N = len(scar_field[0]) if T > 0 else 0
|
||
avg_scar = [sum(scar_field[t][i] for t in range(T)) / T > 0.5
|
||
for i in range(N)]
|
||
|
||
Q: dict[tuple[int, int], float] = {}
|
||
|
||
for i in range(N):
|
||
prev = (i - 1) % N
|
||
Q[(i, i)] = Q.get((i, i), 0.0) - beta0_weight
|
||
Q[(prev, i)] = Q.get((prev, i), 0.0) + beta0_weight
|
||
|
||
for i in range(N):
|
||
bias = -coverage_weight if avg_scar[i] > 0.5 else coverage_weight
|
||
Q[(i, i)] = Q.get((i, i), 0.0) + bias
|
||
|
||
return QUBO(n=N, matrix=Q)
|
||
|
||
|
||
# =========================================================================
|
||
# V. Forward: Goormaghtigh Cost → QUBO
|
||
# =========================================================================
|
||
|
||
def goormaghtigh_cost_to_qubo(
|
||
x_range: tuple[int, int] = (2, 90),
|
||
m_range: tuple[int, int] = (3, 13),
|
||
penalty_weight: float = 1000.0,
|
||
) -> QUBO:
|
||
"""Convert the Goormaghtigh repunit collision problem to a QUBO.
|
||
|
||
Repunit: (x^m - 1)/(x - 1) = 1 + x + ... + x^{m-1}
|
||
|
||
Goal: find (x₁,m₁) ≠ (x₂,m₂) s.t. repunit(x₁,m₁) = repunit(x₂,m₂).
|
||
|
||
QUBO encoding:
|
||
- One-hot encoding for x and m in each repunit
|
||
- Collision penalty proportional to repunit difference
|
||
"""
|
||
x_vals = list(range(x_range[0], x_range[1] + 1))
|
||
m_vals = list(range(m_range[0], m_range[1] + 1))
|
||
nx, nm = len(x_vals), len(m_vals)
|
||
|
||
off_x1 = 0
|
||
off_m1 = nx
|
||
off_x2 = nx + nm
|
||
off_m2 = nx + nm + nx
|
||
|
||
n = nx + nm + nx + nm
|
||
Q: dict[tuple[int, int], float] = {}
|
||
|
||
def _idx(offset: int, k: int) -> int:
|
||
return offset + k
|
||
|
||
for offset in (off_x1, off_x2):
|
||
for i in range(nx):
|
||
Q[(_idx(offset, i), _idx(offset, i))] = \
|
||
Q.get((_idx(offset, i), _idx(offset, i)), 0.0) - penalty_weight
|
||
for i in range(nx):
|
||
for j in range(i + 1, nx):
|
||
Q[(_idx(offset, i), _idx(offset, j))] = \
|
||
Q.get((_idx(offset, i), _idx(offset, j)), 0.0) + 2.0 * penalty_weight
|
||
|
||
for offset in (off_m1, off_m2):
|
||
for i in range(nm):
|
||
Q[(_idx(offset, i), _idx(offset, i))] = \
|
||
Q.get((_idx(offset, i), _idx(offset, i)), 0.0) - penalty_weight
|
||
for i in range(nm):
|
||
for j in range(i + 1, nm):
|
||
Q[(_idx(offset, i), _idx(offset, j))] = \
|
||
Q.get((_idx(offset, i), _idx(offset, j)), 0.0) + 2.0 * penalty_weight
|
||
|
||
for xi in range(nx):
|
||
for mi in range(nm):
|
||
r1 = _repunit(x_vals[xi], m_vals[mi])
|
||
for xj in range(nx):
|
||
for mj in range(nm):
|
||
if xi == xj and mi == mj:
|
||
continue
|
||
r2 = _repunit(x_vals[xj], m_vals[mj])
|
||
diff = abs(r1 - r2)
|
||
i1 = _idx(off_x1, xi)
|
||
i2 = _idx(off_m1, mi)
|
||
i3 = _idx(off_x2, xj)
|
||
i4 = _idx(off_m2, mj)
|
||
w = penalty_weight / max(diff, 1.0)
|
||
Q[(i1, i3)] = Q.get((i1, i3), 0.0) + w * 0.25
|
||
Q[(i2, i4)] = Q.get((i2, i4), 0.0) + w * 0.25
|
||
|
||
return QUBO(n=n, matrix=Q)
|
||
|
||
|
||
def _repunit(x: int, m: int) -> int:
|
||
"""Compute repunit value (x^m - 1)//(x - 1)."""
|
||
return (x ** m - 1) // (x - 1)
|
||
|
||
|
||
# =========================================================================
|
||
# V-A. Tunable Parameterization of Existing Cost Functions
|
||
#
|
||
# The QUBO cost functions in braid_search.py use hardcoded coefficients.
|
||
# These wrappers let you experiment with alternative parameter sets so
|
||
# QAOA results can inform better classical cost models.
|
||
#
|
||
# DefaultCoeffs mirrors the hardcoded values in braid_search.py:
|
||
# - bracket_cost: -base + |gap| * gap_reward
|
||
# - crossing_penalty: overlap_penalty - |gap_diff| * gap_reward
|
||
# =========================================================================
|
||
|
||
@dataclass
|
||
class BraidCostCoeffs:
|
||
"""Tunable coefficients for braid bracket cost functions.
|
||
|
||
Mirrors the hardcoded parameters from braid_search.py so any
|
||
of them can be varied independently for grid-search tuning.
|
||
|
||
Defaults calibrated via SLOS photonic emulator 2026-06-18.
|
||
See qaoa_adapter.auto_calibrate() for re-calibration.
|
||
"""
|
||
base_admissible: float = 8.0 # 8*Q16_ONE — linear bias for admissible brackets
|
||
base_inadmissible: float = 16.0 # 16*Q16_ONE — penalty for inadmissible
|
||
gap_reward_scale: float = 0.01 # reward for large gaps
|
||
overlap_penalty: float = 2.0 # 2*Q16_ONE — conflict cost when ranges overlap
|
||
sa_initial_temp: float = 10.0 # SA starting temperature
|
||
sa_cooling_rate: float = 0.9995 # SA geometric cooling factor
|
||
sa_iterations: int = 5000 # SA iteration budget
|
||
slot_collision_penalty: float = 50.0 # receipt slot conflict cost
|
||
scar_absent_penalty: float = 100.0 # scar-present penalty
|
||
levy_alpha: float = 1.5 # Lévy flight exponent
|
||
|
||
|
||
DEFAULT_COEFFS = BraidCostCoeffs()
|
||
|
||
|
||
def tunable_bracket_cost(bracket: dict, coeffs: BraidCostCoeffs = DEFAULT_COEFFS) -> float:
|
||
"""Parameterized replacement for braid_search.bracket_cost.
|
||
|
||
Cost = -base + |gap| * gap_reward_scale (float, not Q16_16)
|
||
"""
|
||
admissible = bracket.get("admissible", True)
|
||
base = coeffs.base_admissible if admissible else coeffs.base_inadmissible
|
||
gap = _q16_signed(bracket.get("gap", 32768))
|
||
gap_float = gap / 65536.0
|
||
return -base + abs(gap_float) * coeffs.gap_reward_scale
|
||
|
||
|
||
def tunable_crossing_penalty(
|
||
b1: dict, b2: dict, coeffs: BraidCostCoeffs = DEFAULT_COEFFS,
|
||
) -> float:
|
||
"""Parameterized replacement for braid_search.crossing_penalty."""
|
||
cost = 0.0
|
||
# Overlap check
|
||
l1 = b1.get("lower", 0)
|
||
u1 = b1.get("upper", 0)
|
||
l2 = b2.get("lower", 0)
|
||
u2 = b2.get("upper", 0)
|
||
if l1 < u2 and l2 < u1:
|
||
cost += coeffs.overlap_penalty
|
||
# Gap diversity reward
|
||
g1 = _q16_signed(b1.get("gap", 32768))
|
||
g2 = _q16_signed(b2.get("gap", 32768))
|
||
gap_diff = abs(g1 - g2) / 65536.0
|
||
cost -= gap_diff * coeffs.gap_reward_scale
|
||
return cost
|
||
|
||
|
||
def build_tunable_qubo_matrix(
|
||
brackets: list[dict],
|
||
coeffs: BraidCostCoeffs = DEFAULT_COEFFS,
|
||
) -> dict[tuple[int, int], float]:
|
||
"""Build QUBO dict using tunable coefficients.
|
||
|
||
Returns float-valued matrix (not Q16_16), directly consumable by
|
||
stochastic_abuse_qubo and qaoa_solve_qubo.
|
||
"""
|
||
Q: dict[tuple[int, int], float] = {}
|
||
n = len(brackets)
|
||
for i in range(n):
|
||
Q[(i, i)] = tunable_bracket_cost(brackets[i], coeffs)
|
||
for j in range(i + 1, n):
|
||
c = tunable_crossing_penalty(brackets[i], brackets[j], coeffs)
|
||
Q[(i, j)] = c
|
||
Q[(j, i)] = c
|
||
return Q
|
||
|
||
|
||
def tunable_braid_receipt_to_qubo(
|
||
receipt: dict,
|
||
coeffs: BraidCostCoeffs = DEFAULT_COEFFS,
|
||
) -> QUBO:
|
||
"""braid_receipt_to_qubo with tunable slot/scar penalties."""
|
||
braid = receipt.get("braid", receipt)
|
||
strands = braid.get("strands", [])
|
||
if not strands:
|
||
strands = _receipt_strands_from_bracket(braid)
|
||
n = len(strands)
|
||
Q: dict[tuple[int, int], float] = {}
|
||
offset = 0.0
|
||
|
||
slot_map: dict[int, list[int]] = {}
|
||
for i, s in enumerate(strands):
|
||
kappa = s.get("kappa", 0) / 65536.0
|
||
phi = s.get("phi", 0) / 65536.0
|
||
slot = s.get("slot", 0)
|
||
converged = s.get("converged", True)
|
||
residual = s.get("residual", 0) / 65536.0
|
||
|
||
Q[(i, i)] = kappa + 0.1 * abs(phi)
|
||
if not converged and residual > 0:
|
||
Q[(i, i)] += 10.0 * residual
|
||
slot_map.setdefault(slot, []).append(i)
|
||
|
||
for _slot, indices in slot_map.items():
|
||
if len(indices) > 1:
|
||
for a in range(len(indices)):
|
||
for b in range(a + 1, len(indices)):
|
||
i, j = indices[a], indices[b]
|
||
p = coeffs.slot_collision_penalty
|
||
Q[(i, j)] = Q.get((i, j), 0.0) + p
|
||
|
||
scar_absent = receipt.get("scar_absent", braid.get("scar_absent", True))
|
||
if not scar_absent:
|
||
for i in range(n):
|
||
Q[(i, i)] = Q.get((i, i), 0.0) + coeffs.scar_absent_penalty
|
||
|
||
sidon_slack = receipt.get("sidon_slack", 0)
|
||
max_label = 128 - sidon_slack
|
||
offset += max_label / 128.0
|
||
|
||
return QUBO(n=n, matrix=Q, offset=offset)
|
||
|
||
|
||
# =========================================================================
|
||
# V-B. Tuner: Grid Search Over Parameters
|
||
# =========================================================================
|
||
|
||
def tune_braid_parameters(
|
||
brackets: list[dict],
|
||
param_grid: Optional[list[dict]] = None,
|
||
solver_methods: Optional[list[str]] = None,
|
||
time_per_trial: float = 1.0,
|
||
seed: int = 42,
|
||
) -> dict:
|
||
"""Grid search over braid cost parameters to find the best QUBO formulation.
|
||
|
||
For each parameter set in the grid, builds the QUBO and solves it with
|
||
each solver. Returns the configuration that yields the lowest energy,
|
||
plus a full comparison table.
|
||
|
||
Args:
|
||
brackets: List of bracket dicts.
|
||
param_grid: List of BraidCostCoeffs overrides.
|
||
Each entry is a dict with keys from BraidCostCoeffs fields.
|
||
Default: 27 combinations varying base_admissible, overlap_penalty,
|
||
gap_reward_scale.
|
||
solver_methods: Solver methods to evaluate.
|
||
Default: ["sa", "levy"].
|
||
time_per_trial: Time limit per solver trial (seconds).
|
||
seed: RNG seed.
|
||
|
||
Returns:
|
||
{
|
||
"best_params": {...}, # winning BraidCostCoeffs
|
||
"best_solver": str, # winning solver name
|
||
"best_energy": float, # lowest energy found
|
||
"trials": [ # each trial result
|
||
{
|
||
"params": {...},
|
||
"solver": str,
|
||
"energy": float,
|
||
"solution": list[int],
|
||
"runtime_s": float,
|
||
}
|
||
],
|
||
"n": int, # number of brackets
|
||
}
|
||
"""
|
||
if param_grid is None:
|
||
param_grid = []
|
||
for base_mult in [0.5, 1.0, 2.0]:
|
||
for overlap_mult in [0.5, 1.0, 2.0]:
|
||
for gap_scale in [0.05, 0.1, 0.2]:
|
||
param_grid.append({
|
||
"base_admissible": base_mult,
|
||
"base_inadmissible": 2.0 * base_mult,
|
||
"overlap_penalty": 2.0 * overlap_mult,
|
||
"gap_reward_scale": gap_scale,
|
||
"slot_collision_penalty": 50.0,
|
||
"scar_absent_penalty": 100.0,
|
||
"sa_initial_temp": 10.0,
|
||
"sa_cooling_rate": 0.9995,
|
||
"sa_iterations": 5000,
|
||
"levy_alpha": 1.5,
|
||
})
|
||
|
||
if solver_methods is None:
|
||
solver_methods = ["sa", "levy"]
|
||
|
||
import copy
|
||
trials: list[dict] = []
|
||
|
||
for param_dict in param_grid:
|
||
coeffs = BraidCostCoeffs(**param_dict)
|
||
Q = build_tunable_qubo_matrix(brackets, coeffs)
|
||
n = len(brackets)
|
||
qubo = QUBO(n=n, matrix=Q)
|
||
|
||
for method in solver_methods:
|
||
t0 = time.time()
|
||
result = stochastic_abuse_qubo(
|
||
qubo, method=method, time_limit=time_per_trial, seed=seed,
|
||
)
|
||
runtime = time.time() - t0
|
||
|
||
trials.append({
|
||
"params": param_dict,
|
||
"solver": method,
|
||
"energy": result["energy"],
|
||
"solution": result["solution"],
|
||
"runtime_s": round(runtime, 4),
|
||
})
|
||
|
||
# Find best trial (lowest energy)
|
||
best_trial = min(trials, key=lambda t: t["energy"])
|
||
|
||
return {
|
||
"best_params": best_trial["params"],
|
||
"best_solver": best_trial["solver"],
|
||
"best_energy": best_trial["energy"],
|
||
"trials": trials,
|
||
"n": len(brackets),
|
||
}
|
||
|
||
|
||
# =========================================================================
|
||
# V-C. SA Parameter Tuner
|
||
# =========================================================================
|
||
|
||
def tune_sa_parameters(
|
||
qubo: QUBO,
|
||
temp_grid: Optional[list[float]] = None,
|
||
cooling_grid: Optional[list[float]] = None,
|
||
time_limit: float = 2.0,
|
||
seed: int = 42,
|
||
) -> dict:
|
||
"""Grid search over SA temperature and cooling rate parameters.
|
||
|
||
Runs SA with each (initial_temp, cooling_rate) combination and reports
|
||
the configuration that yields the lowest energy.
|
||
|
||
Args:
|
||
qubo: QUBO problem.
|
||
temp_grid: List of initial temperatures to try.
|
||
Default: [1.0, 5.0, 10.0, 20.0, 50.0].
|
||
cooling_grid: List of cooling rates to try.
|
||
Default: [0.99, 0.995, 0.999, 0.9995, 0.9999].
|
||
time_limit: Time limit per SA run.
|
||
seed: RNG seed.
|
||
|
||
Returns:
|
||
{
|
||
"best_temp": float,
|
||
"best_cooling": float,
|
||
"best_energy": float,
|
||
"trials": [{"temp": float, "cooling": float, "energy": float}],
|
||
}
|
||
"""
|
||
if temp_grid is None:
|
||
temp_grid = [1.0, 5.0, 10.0, 20.0, 50.0]
|
||
if cooling_grid is None:
|
||
cooling_grid = [0.99, 0.995, 0.999, 0.9995, 0.9999]
|
||
|
||
trials: list[dict] = []
|
||
Q_dict: dict[tuple[int, int], float] = dict(qubo.matrix)
|
||
n = qubo.n
|
||
|
||
for temp in temp_grid:
|
||
for cooling in cooling_grid:
|
||
t0 = time.time()
|
||
x = [0] * n
|
||
import random as _r
|
||
_r.seed(seed)
|
||
x = [_r.randint(0, 1) for _ in range(n)]
|
||
|
||
def _energy(xv):
|
||
e = qubo.offset
|
||
for (i, j), qij in Q_dict.items():
|
||
e += qij * xv[i] * xv[j]
|
||
return e
|
||
|
||
current_energy = _energy(x)
|
||
best_x = list(x)
|
||
best_energy = current_energy
|
||
T = temp
|
||
|
||
while time.time() - t0 < time_limit:
|
||
i = _r.randrange(n)
|
||
x[i] = 1 - x[i]
|
||
new_energy = _energy(x)
|
||
delta = new_energy - current_energy
|
||
if delta < 0 or _r.random() < math.exp(-delta / max(T, 1e-10)):
|
||
current_energy = new_energy
|
||
if current_energy < best_energy:
|
||
best_x = list(x)
|
||
best_energy = current_energy
|
||
else:
|
||
x[i] = 1 - x[i]
|
||
T *= cooling
|
||
|
||
trials.append({
|
||
"temp": temp,
|
||
"cooling": cooling,
|
||
"energy": best_energy,
|
||
"runtime_s": round(time.time() - t0, 4),
|
||
})
|
||
|
||
best_trial = min(trials, key=lambda t: t["energy"])
|
||
return {
|
||
"best_temp": best_trial["temp"],
|
||
"best_cooling": best_trial["cooling"],
|
||
"best_energy": best_trial["energy"],
|
||
"trials": trials,
|
||
}
|
||
|
||
|
||
# =========================================================================
|
||
# V-D. Solver Dispatch Tuner: Auto-pick best solver for a QUBO
|
||
# =========================================================================
|
||
|
||
def solver_dispatch_tuner(
|
||
qubo: QUBO,
|
||
time_limit: float = 2.0,
|
||
seed: int = 42,
|
||
) -> dict:
|
||
"""Auto-select the best solver for a given QUBO instance.
|
||
|
||
Runs all available solvers (SA, HiGHS, Lévy, QAOA describe) for a
|
||
brief warmup and returns the one with the lowest energy, plus a
|
||
recommendation based on problem size.
|
||
|
||
Args:
|
||
qubo: QUBO problem.
|
||
time_limit: Per-solver time limit.
|
||
seed: RNG seed.
|
||
|
||
Returns:
|
||
{
|
||
"recommended": str, # solver name
|
||
"recommended_energy": float,
|
||
"warmup": {method: {"energy": float, "runtime_s": float}},
|
||
"heuristic_note": str, # size-based guidance
|
||
}
|
||
"""
|
||
results: dict[str, dict] = {}
|
||
|
||
# SA
|
||
sa_res = stochastic_abuse_qubo(qubo, method="sa", time_limit=time_limit, seed=seed)
|
||
results["sa"] = {"energy": sa_res["energy"], "runtime_s": sa_res["runtime_s"]}
|
||
|
||
# Lévy
|
||
try:
|
||
levy_res = stochastic_abuse_qubo(qubo, method="levy", time_limit=time_limit, seed=seed)
|
||
results["levy"] = {"energy": levy_res["energy"], "runtime_s": levy_res["runtime_s"]}
|
||
except Exception:
|
||
results["levy"] = {"energy": float("inf"), "runtime_s": 0.0}
|
||
|
||
# HiGHS (may fall back to SA internally)
|
||
try:
|
||
highs_res = stochastic_abuse_qubo(qubo, method="highs", time_limit=time_limit, seed=seed)
|
||
results["highs"] = {"energy": highs_res["energy"], "runtime_s": highs_res["runtime_s"]}
|
||
except Exception:
|
||
results["highs"] = {"energy": float("inf"), "runtime_s": 0.0}
|
||
|
||
# QAOA describe (offline estimate)
|
||
qaoa_res = qaoa_solve_qubo(qubo, p_layers=1, shots=100, backend="describe")
|
||
results["qaoa"] = {"energy": qaoa_res["energy"], "runtime_s": 0.0, "note": "describe-backend-estimate"}
|
||
|
||
# Pick best
|
||
best_solver = min(results, key=lambda s: results[s]["energy"])
|
||
best_energy = results[best_solver]["energy"]
|
||
|
||
# Heuristic
|
||
n = qubo.n
|
||
if n <= 5:
|
||
heuristic = "small: HiGHS MIP typically exact and fast"
|
||
elif n <= 20:
|
||
heuristic = "medium: SA or Lévy often best; QAOA may help for specific structure"
|
||
else:
|
||
heuristic = "large: SA or Lévy cheaper; QAOA promising if coupling structure is sparse"
|
||
|
||
return {
|
||
"recommended": best_solver,
|
||
"recommended_energy": best_energy,
|
||
"warmup": results,
|
||
"heuristic_note": heuristic,
|
||
"n": n,
|
||
}
|
||
|
||
|
||
# =========================================================================
|
||
# V-E. SLOS Photonic Cost Calibration
|
||
#
|
||
# Uses the Perceval SLOS (Schrödinger Levitated Object Simulator) to
|
||
# compute photonic reference costs for bracket configurations. The SLOS
|
||
# output distribution over photonic modes serves as a physically-grounded
|
||
# cost signal. We then fit the classical BraidCostCoeffs to match these
|
||
# reference costs — effectively "calibrating" the heuristic defaults to
|
||
# match what a photonic emulator says.
|
||
#
|
||
# Requires: perceval (pip install perceval-quandela)
|
||
# =========================================================================
|
||
|
||
try:
|
||
import perceval as _pcvl
|
||
_HAS_PERVERSE = True
|
||
except ImportError:
|
||
_HAS_PERVERSE = False
|
||
|
||
|
||
class SlosBraidEvaluator:
|
||
"""Evaluate braid brackets using Perceval SLOS photonic simulation.
|
||
|
||
Encodes bracket parameters (gap, admissible, lower, upper) as
|
||
phase angles in an M-mode linear optical interferometer. The SLOS
|
||
statevector simulator computes the exact output distribution; we
|
||
extract cost signals from mode probabilities.
|
||
|
||
The key insight: admissible brackets with large gaps produce
|
||
different photonic interference patterns (higher mode spread,
|
||
lower entropy) than inadmissible or overlapping ones. SLOS
|
||
captures this physically rather than heuristically.
|
||
"""
|
||
|
||
def __init__(self, M: int = 6):
|
||
self.M = M
|
||
self._cache: dict[str, float] = {}
|
||
|
||
def _cache_key(self, bracket: dict) -> str:
|
||
return f"{bracket.get('lower',0)}|{bracket.get('upper',0)}|{bracket.get('gap',0)}|{bracket.get('admissible',True)}"
|
||
|
||
def _encode_bracket_angles(self, bracket: dict) -> list[float]:
|
||
"""Map bracket parameters to photonic phase angles.
|
||
|
||
Encoding scheme:
|
||
- admissible → phase = 0.0 (no shift — clean path)
|
||
- inadmissible → phase = π/2 (rotation — scattering loss)
|
||
- gap → proportional phase scale (0..π), larger gap = larger shift
|
||
- lower/upper → differential phase between modes
|
||
"""
|
||
admissible = bracket.get("admissible", True)
|
||
gap = bracket.get("gap", 32768)
|
||
lower = bracket.get("lower", 0)
|
||
upper = bracket.get("upper", 65536)
|
||
|
||
# Normalize to 0..1
|
||
gap_norm = min(1.0, abs(gap) / 65536.0)
|
||
span_norm = min(1.0, max(0, (upper - lower)) / 65536.0)
|
||
|
||
theta: list[float] = []
|
||
# Mode 0: admissibility phase
|
||
theta.append(0.0 if admissible else math.pi / 2.0)
|
||
# Mode 1: gap phase
|
||
theta.append(gap_norm * math.pi)
|
||
# Mode 2: span phase
|
||
theta.append(span_norm * math.pi)
|
||
# Modes 3+: fill with differential phases
|
||
for k in range(3, self.M):
|
||
theta.append((gap_norm * span_norm * math.pi) / (1.0 + k))
|
||
return theta
|
||
|
||
def compute_photonic_cost(self, bracket: dict) -> float:
|
||
"""Run SLOS on a single bracket and return photonic cost.
|
||
|
||
Lower photonic cost = more favorable bracket configuration.
|
||
The cost is derived from the output mode distribution:
|
||
- Admissible + large gap → concentrated output (low cost)
|
||
- Inadmissible + small gap → scattered output (high cost)
|
||
"""
|
||
ck = self._cache_key(bracket)
|
||
if ck in self._cache:
|
||
return self._cache[ck]
|
||
|
||
if not _HAS_PERVERSE:
|
||
# Fallback: heuristic estimate mirrors braid_search defaults
|
||
admissible = bracket.get("admissible", True)
|
||
gap = bracket.get("gap", 32768)
|
||
base = 1.0 if admissible else 2.0
|
||
cost = base + gap / 65536.0 * 0.5
|
||
self._cache[ck] = cost
|
||
return cost
|
||
|
||
theta = self._encode_bracket_angles(bracket)
|
||
|
||
try:
|
||
circuit = _pcvl.Circuit(self.M)
|
||
for i in range(min(len(theta), self.M)):
|
||
circuit.add(i, _pcvl.PS(theta[i]))
|
||
for i in range(self.M - 1):
|
||
circuit.add((i, i + 1), _pcvl.BS())
|
||
|
||
input_state = _pcvl.BasicState([1] + [0] * (self.M - 1))
|
||
processor = _pcvl.Processor("SLOS", circuit)
|
||
processor.with_input(input_state)
|
||
sampler = _pcvl.algorithm.Sampler(processor)
|
||
res = sampler.sample_count(1000)
|
||
|
||
# Compute photonic cost from output distribution
|
||
# Metric: weighted entropy of output modes
|
||
total_prob = 0.0
|
||
entropy = 0.0
|
||
for state, count in res["results"].items():
|
||
prob = count / 1000.0
|
||
total_prob += prob
|
||
if prob > 1e-10:
|
||
entropy -= prob * math.log2(prob)
|
||
|
||
# Scale: admissible brackets have lower entropy (cleaner interference)
|
||
admissible = bracket.get("admissible", True)
|
||
cost = entropy * (1.0 if admissible else 1.5)
|
||
self._cache[ck] = cost
|
||
except Exception:
|
||
cost = 1.0 if admissible else 2.0
|
||
self._cache[ck] = cost
|
||
|
||
return cost
|
||
|
||
def compute_photonic_pair_cost(self, b1: dict, b2: dict) -> float:
|
||
"""Compute interaction cost between two brackets using SLOS.
|
||
|
||
Encodes both brackets into a 2M-mode interferometer and measures
|
||
the interference cross-term via a cascade BS bridge between the
|
||
two halves. Overlapping brackets produce more cross-half
|
||
entanglement (higher cost); diverse gaps produce cleaner
|
||
separation (lower cost).
|
||
"""
|
||
if not _HAS_PERVERSE:
|
||
gap1 = abs(b1.get("gap", 32768)) / 65536.0
|
||
gap2 = abs(b2.get("gap", 32768)) / 65536.0
|
||
gap_diff = abs(gap1 - gap2)
|
||
l1, u1 = b1.get("lower", 0), b1.get("upper", 0)
|
||
l2, u2 = b2.get("lower", 0), b2.get("upper", 0)
|
||
overlap = 1.0 if l1 < u2 and l2 < u1 else 0.0
|
||
return overlap * 2.0 - gap_diff * 0.1
|
||
|
||
try:
|
||
n = self.M * 2
|
||
theta1 = self._encode_bracket_angles(b1)
|
||
theta2 = self._encode_bracket_angles(b2)
|
||
|
||
circuit = _pcvl.Circuit(n)
|
||
# Phase shifts for bracket 1 on left half (modes 0..M-1)
|
||
for i in range(self.M):
|
||
circuit.add(i, _pcvl.PS(theta1[i] if i < len(theta1) else 0.0))
|
||
|
||
# Cascade BS bridge between the two halves (consecutive ports only)
|
||
circuit.add((self.M - 1, self.M), _pcvl.BS())
|
||
|
||
# Phase shifts for bracket 2 on right half (modes M..2M-1)
|
||
for i in range(self.M):
|
||
circuit.add(self.M + i, _pcvl.PS(theta2[i] if i < len(theta2) else 0.0))
|
||
|
||
input_state = _pcvl.BasicState(
|
||
[1] + [0] * (self.M - 1) + [1] + [0] * (self.M - 1)
|
||
)
|
||
processor = _pcvl.Processor("SLOS", circuit)
|
||
processor.with_input(input_state)
|
||
sampler = _pcvl.algorithm.Sampler(processor)
|
||
res = sampler.sample_count(1000)
|
||
|
||
# Interaction cost: probability that photons from both halves
|
||
# end up on the same side (measure of entanglement)
|
||
left_same = 0.0
|
||
right_same = 0.0
|
||
for state, count in res["results"].items():
|
||
prob = count / 1000.0
|
||
left_photons = sum(state[i] for i in range(self.M))
|
||
right_photons = sum(state[i] for i in range(self.M, n))
|
||
if left_photons == 2:
|
||
left_same += prob
|
||
elif right_photons == 2:
|
||
right_same += prob
|
||
|
||
# Both photons on the same half = stronger interaction
|
||
return (left_same + right_same) * 5.0
|
||
except Exception:
|
||
return 1.0
|
||
|
||
def clear_cache(self) -> None:
|
||
self._cache.clear()
|
||
|
||
|
||
# =========================================================================
|
||
# V-F. SLOS Calibration: Fit BraidCostCoeffs to Photonic Reference Costs
|
||
# =========================================================================
|
||
|
||
def slos_calibrate_coeffs(
|
||
brackets: list[dict],
|
||
evaluator: Optional[SlosBraidEvaluator] = None,
|
||
loss_fn: str = "mse",
|
||
) -> dict:
|
||
"""Fit BraidCostCoeffs to match SLOS photonic reference costs.
|
||
|
||
For each bracket and each pair, computes the SLOS photonic cost
|
||
(reference). Then searches the BraidCostCoeffs parameter space
|
||
to find coefficients that minimize the error between the heuristic
|
||
cost (tunable_bracket_cost / tunable_crossing_penalty) and the
|
||
SLOS reference.
|
||
|
||
Args:
|
||
brackets: List of bracket dicts to calibrate on
|
||
evaluator: SlosBraidEvaluator instance (created if None)
|
||
loss_fn: "mse" (mean squared error) or "mae" (mean absolute error)
|
||
|
||
Returns:
|
||
{
|
||
"calibrated_coeffs": {BraidCostCoeffs fields},
|
||
"reference_costs": list[float], # SLOS bracket costs
|
||
"reference_pair_costs": list[float], # SLOS pair costs
|
||
"heuristic_costs": list[float], # best-fit heuristic costs
|
||
"loss": float, # final loss
|
||
"slos_available": bool, # whether Perceval was used
|
||
}
|
||
"""
|
||
if evaluator is None:
|
||
evaluator = SlosBraidEvaluator()
|
||
|
||
n = len(brackets)
|
||
if n == 0:
|
||
return {"calibrated_coeffs": {}, "error": "no brackets"}
|
||
|
||
# Compute reference costs from SLOS
|
||
ref_bracket: list[float] = []
|
||
for b in brackets:
|
||
ref_bracket.append(evaluator.compute_photonic_cost(b))
|
||
|
||
ref_pairs: list[float] = []
|
||
pair_indices: list[tuple[int, int]] = []
|
||
for i in range(n):
|
||
for j in range(i + 1, n):
|
||
ref_pairs.append(evaluator.compute_photonic_pair_cost(brackets[i], brackets[j]))
|
||
pair_indices.append((i, j))
|
||
|
||
# Grid search over BraidCostCoeffs to minimize error
|
||
best_loss = float("inf")
|
||
best_params: dict = {}
|
||
|
||
# Parameter sweep ranges — centered around defaults from braid_search.py
|
||
base_range = [0.25, 0.5, 0.75, 1.0, 1.5, 2.0, 3.0, 4.0, 8.0]
|
||
gap_range = [0.01, 0.025, 0.05, 0.1, 0.15, 0.2, 0.3, 0.5]
|
||
overlap_range = [0.5, 1.0, 2.0, 3.0, 4.0, 6.0, 8.0]
|
||
|
||
for base_scale in base_range:
|
||
for gap_scale in gap_range:
|
||
for overlap_scale in overlap_range:
|
||
coeffs = BraidCostCoeffs(
|
||
base_admissible=base_scale,
|
||
base_inadmissible=base_scale * 2.0,
|
||
gap_reward_scale=gap_scale,
|
||
overlap_penalty=overlap_scale * 2.0,
|
||
)
|
||
|
||
# Compute heuristic costs with these coeffs
|
||
heur_bracket = [tunable_bracket_cost(b, coeffs) for b in brackets]
|
||
heur_pairs = [
|
||
tunable_crossing_penalty(brackets[i], brackets[j], coeffs)
|
||
for (i, j) in pair_indices
|
||
]
|
||
|
||
# Loss
|
||
loss = 0.0
|
||
for h, r in zip(heur_bracket, ref_bracket):
|
||
diff = h - r
|
||
loss += diff * diff if loss_fn == "mse" else abs(diff)
|
||
for h, r in zip(heur_pairs, ref_pairs):
|
||
diff = h - r
|
||
loss += diff * diff if loss_fn == "mse" else abs(diff)
|
||
|
||
if loss < best_loss:
|
||
best_loss = loss
|
||
best_params = asdict(coeffs)
|
||
|
||
return {
|
||
"calibrated_coeffs": best_params,
|
||
"reference_costs": ref_bracket,
|
||
"reference_pair_costs": ref_pairs,
|
||
"heuristic_costs": [
|
||
tunable_bracket_cost(b, BraidCostCoeffs(**best_params))
|
||
for b in brackets
|
||
],
|
||
"loss": best_loss,
|
||
"slos_available": _HAS_PERVERSE,
|
||
}
|
||
|
||
|
||
# =========================================================================
|
||
# V-G. Auto-Calibrate: SLOS → Fit → Solve → Compare → Recommend
|
||
#
|
||
# End-to-end pipeline: for a given set of brackets, runs the SLOS
|
||
# photonic emulator to produce physically-grounded reference costs,
|
||
# fits the best BraidCostCoeffs to match them, builds QUBOs with
|
||
# both default and calibrated coefficients, solves each with all
|
||
# available solvers, and recommends the optimal configuration.
|
||
# =========================================================================
|
||
|
||
def auto_calibrate(
|
||
brackets: list[dict],
|
||
evaluator: Optional[SlosBraidEvaluator] = None,
|
||
time_per_solver: float = 1.0,
|
||
seed: int = 42,
|
||
) -> dict:
|
||
"""Full auto-calibration pipeline for braid QUBO defaults.
|
||
|
||
Steps:
|
||
1. Compute SLOS photonic reference costs for each bracket and pair.
|
||
2. Grid-search BraidCostCoeffs to minimize heuristic-vs-SLOS error.
|
||
3. Build QUBOs with default (DEFAULT_COEFFS) and calibrated coeffs.
|
||
4. Solve both QUBOs with SA, Lévy, HiGHS, and QAOA describe.
|
||
5. Report the optimal coefficient set and energy improvement.
|
||
|
||
Args:
|
||
brackets: List of bracket dicts.
|
||
evaluator: SlosBraidEvaluator (created fresh if None).
|
||
time_per_solver: Seconds per solver trial.
|
||
seed: RNG seed.
|
||
|
||
Returns:
|
||
{
|
||
"slos_calibration": {...}, # raw calibration output
|
||
"coefficients": {
|
||
"default": {...}, # original DEFAULT_COEFFS
|
||
"calibrated": {...}, # SLOS-fitted coeffs
|
||
"recommended": {...}, # whichever gave lowest energy
|
||
},
|
||
"solver_results": {
|
||
"default": {method: result}, # QUBO with default coeffs
|
||
"calibrated": {method: result}, # QUBO with SLOS coeffs
|
||
},
|
||
"comparison": {
|
||
"default_best": {method, energy},
|
||
"calibrated_best": {method, energy},
|
||
"improvement": float, # energy reduction (negative = better)
|
||
},
|
||
"recommendation": str, # human-readable summary
|
||
"n": int,
|
||
}
|
||
"""
|
||
if evaluator is None:
|
||
evaluator = SlosBraidEvaluator()
|
||
if not brackets:
|
||
return {"error": "no brackets provided"}
|
||
|
||
n = len(brackets)
|
||
|
||
# Step 1-2: SLOS calibration
|
||
calibration = slos_calibrate_coeffs(brackets, evaluator=evaluator)
|
||
cal_params = calibration["calibrated_coeffs"]
|
||
cal_coeffs = BraidCostCoeffs(**cal_params)
|
||
|
||
# Step 3: Build QUBOs with default and calibrated coeffs
|
||
Q_default = build_tunable_qubo_matrix(brackets, DEFAULT_COEFFS)
|
||
Q_cal = build_tunable_qubo_matrix(brackets, cal_coeffs)
|
||
qubo_default = QUBO(n=n, matrix=Q_default)
|
||
qubo_cal = QUBO(n=n, matrix=Q_cal)
|
||
|
||
# Step 4: Solve with all methods
|
||
methods = ["sa", "levy"]
|
||
default_results: dict[str, dict] = {}
|
||
cal_results: dict[str, dict] = {}
|
||
|
||
for method in methods:
|
||
default_results[method] = stochastic_abuse_qubo(
|
||
qubo_default, method=method, time_limit=time_per_solver, seed=seed,
|
||
)
|
||
cal_results[method] = stochastic_abuse_qubo(
|
||
qubo_cal, method=method, time_limit=time_per_solver, seed=seed,
|
||
)
|
||
|
||
# Also try HiGHS
|
||
for method in ["highs"]:
|
||
try:
|
||
default_results[method] = stochastic_abuse_qubo(
|
||
qubo_default, method=method, time_limit=time_per_solver, seed=seed,
|
||
)
|
||
except Exception:
|
||
default_results[method] = {"energy": float("inf"), "solution": [0] * n}
|
||
try:
|
||
cal_results[method] = stochastic_abuse_qubo(
|
||
qubo_cal, method=method, time_limit=time_per_solver, seed=seed,
|
||
)
|
||
except Exception:
|
||
cal_results[method] = {"energy": float("inf"), "solution": [0] * n}
|
||
|
||
# Step 5: Find best per coefficient set
|
||
default_best = min(
|
||
default_results.items(), key=lambda kv: kv[1]["energy"],
|
||
)
|
||
cal_best = min(
|
||
cal_results.items(), key=lambda kv: kv[1]["energy"],
|
||
)
|
||
|
||
improvement = cal_best[1]["energy"] - default_best[1]["energy"]
|
||
|
||
# Decide which coeffs to recommend
|
||
if cal_best[1]["energy"] <= default_best[1]["energy"]:
|
||
recommended = cal_params
|
||
rec_note = "SLOS-calibrated coefficients (lower energy)"
|
||
else:
|
||
recommended = asdict(DEFAULT_COEFFS)
|
||
rec_note = "default coefficients (SLOS calibration did not improve)"
|
||
|
||
return {
|
||
"slos_calibration": {
|
||
"loss": calibration["loss"],
|
||
"reference_costs": calibration["reference_costs"],
|
||
"slos_available": calibration["slos_available"],
|
||
},
|
||
"coefficients": {
|
||
"default": asdict(DEFAULT_COEFFS),
|
||
"calibrated": cal_params,
|
||
"recommended": recommended,
|
||
},
|
||
"solver_results": {
|
||
"default": default_results,
|
||
"calibrated": cal_results,
|
||
},
|
||
"comparison": {
|
||
"default_best": {"method": default_best[0], "energy": default_best[1]["energy"]},
|
||
"calibrated_best": {"method": cal_best[0], "energy": cal_best[1]["energy"]},
|
||
"improvement": improvement,
|
||
"improvement_pct": (
|
||
improvement / max(abs(default_best[1]["energy"]), 1e-10) * 100
|
||
if abs(default_best[1]["energy"]) > 1e-10 else 0.0
|
||
),
|
||
},
|
||
"recommendation": (
|
||
f"Set braid_search defaults to: base_admissible={recommended.get('base_admissible')}, "
|
||
f"gap_reward_scale={recommended.get('gap_reward_scale')}, "
|
||
f"overlap_penalty={recommended.get('overlap_penalty')}. "
|
||
f"{rec_note}."
|
||
),
|
||
"n": n,
|
||
}
|
||
|
||
|
||
# =========================================================================
|
||
# III-D. FinslerMetric → QUBO (TransportQUBOBridge.lean)
|
||
# Source: TransportTheory.lean — RandersMetric { alpha, beta }
|
||
# alpha = AlphaComponent { dimension, mass_field }
|
||
# beta = BetaComponent { dimension, wind_field }
|
||
#
|
||
# Q_ij = F(p_i, v_j - v_i) = α(v_j - v_i) + β(v_j - v_i)
|
||
# where α(v) = Σ_i mass[i] * |v_i| (symmetric: α(-v) = α(v))
|
||
# β(v) = Σ_i wind[i] * v_i (antisymmetric: β(-v) = -β(v))
|
||
#
|
||
# The matrix is NOT symmetric: Q_ij ≠ Q_ji when β ≠ 0.
|
||
# Each row/col = one routing direction.
|
||
# =========================================================================
|
||
|
||
@dataclass
|
||
class FinslerMetric:
|
||
"""Discrete Finsler-Randers metric (mirrors Lean RandersMetric).
|
||
|
||
α component: symmetric base cost (mass field)
|
||
β component: asymmetric drift (wind field)
|
||
|
||
F(p,v) = α(p,v) + β(p,v)
|
||
"""
|
||
alpha_mass: list[float] # mass_field: base resistance per dimension
|
||
beta_wind: list[float] # wind_field: drift 1-form per dimension
|
||
dimension: int # state space dimension
|
||
|
||
def __post_init__(self) -> None:
|
||
if len(self.alpha_mass) != self.dimension:
|
||
raise ValueError(f"alpha mass dim {len(self.alpha_mass)} != {self.dimension}")
|
||
if len(self.beta_wind) != self.dimension:
|
||
raise ValueError(f"beta wind dim {len(self.beta_wind)} != {self.dimension}")
|
||
|
||
def alpha_cost(self, v: list[float]) -> float:
|
||
"""α(v) = Σ_i mass[i] * |v_i| — symmetric."""
|
||
return sum(self.alpha_mass[i] * abs(v[i]) for i in range(self.dimension))
|
||
|
||
def beta_cost(self, v: list[float]) -> float:
|
||
"""β(v) = Σ_i wind[i] * v_i — antisymmetric (β(-v) = -β(v))."""
|
||
return sum(self.beta_wind[i] * v[i] for i in range(self.dimension))
|
||
|
||
def finsler_cost(self, v: list[float]) -> float:
|
||
"""F(p,v) = α(v) + β(v) — direction-dependent Finsler norm."""
|
||
return self.alpha_cost(v) + self.beta_cost(v)
|
||
|
||
def crossing_cost(self, v_i: list[float], v_j: list[float]) -> float:
|
||
"""Cost of transitioning from direction i to direction j.
|
||
|
||
Q_ij = F(p, v_j - v_i) = α(v_j - v_i) + β(v_j - v_i)
|
||
|
||
NOTE: Q_ij ≠ Q_ji because β(v_j - v_i) = -β(v_i - v_j).
|
||
"""
|
||
diff = [v_j[k] - v_i[k] for k in range(self.dimension)]
|
||
return self.finsler_cost(diff)
|
||
|
||
|
||
def finsler_metric_to_qubo(
|
||
metric: FinslerMetric,
|
||
directions: list[list[float]],
|
||
normalize: bool = True,
|
||
) -> QUBO:
|
||
"""Convert a FinslerMetric + direction set into a QUBO formulation.
|
||
|
||
This mirrors the Lean `randersMetricToQUBO` in TransportQUBOBridge.lean.
|
||
|
||
Args:
|
||
metric: FinslerMetric (α mass + β wind).
|
||
directions: List of n direction vectors, each length metric.dimension.
|
||
normalize: If True, normalize costs so the matrix entries are in [-1, 1].
|
||
|
||
Returns:
|
||
QUBO with n variables. Q_ij = crossing cost from i→j, Q_ii = 0.
|
||
"""
|
||
n = len(directions)
|
||
Q: dict[tuple[int, int], float] = {}
|
||
max_abs = 0.0
|
||
|
||
# Build raw QUBO matrix
|
||
raw: list[list[float]] = [[0.0] * n for _ in range(n)]
|
||
for i in range(n):
|
||
for j in range(n):
|
||
if i == j:
|
||
raw[i][j] = 0.0
|
||
else:
|
||
cost = metric.crossing_cost(directions[i], directions[j])
|
||
raw[i][j] = cost
|
||
max_abs = max(max_abs, abs(cost))
|
||
|
||
# Normalize if requested
|
||
scale = max_abs if normalize and max_abs > 0 else 1.0
|
||
for i in range(n):
|
||
for j in range(i + 1, n):
|
||
val = (raw[i][j] + raw[j][i]) / scale
|
||
if val != 0.0:
|
||
Q[(i, j)] = val
|
||
|
||
return QUBO(n=n, matrix=Q)
|
||
|
||
|
||
def is_anisotropic(
|
||
qubo: QUBO,
|
||
raw_matrix: Optional[list[list[float]]] = None,
|
||
tol: float = 1e-9,
|
||
) -> bool:
|
||
"""Check if a QUBO matrix is anisotropic (Q_ij ≠ Q_ji for any i≠j).
|
||
|
||
After the fix in finsler_metric_to_qubo (storing Q_ij + Q_ji),
|
||
the QUBO matrix no longer preserves individual directed entries.
|
||
Pass raw_matrix to compare the original asymmetric entries, or
|
||
pass a FinslerMetric to check the wind field directly.
|
||
|
||
Mirrors Lean `isAnisotropic` in TransportQUBOBridge.lean.
|
||
"""
|
||
if raw_matrix is not None:
|
||
n = len(raw_matrix)
|
||
for i in range(n):
|
||
for j in range(n):
|
||
if i != j and abs(raw_matrix[i][j] - raw_matrix[j][i]) > tol:
|
||
return True
|
||
return False
|
||
# Fallback: check stored entries (works for preserved asymmetric storage)
|
||
for (i, j), qij in qubo.matrix.items():
|
||
qji = qubo.matrix.get((j, i), 0.0)
|
||
if abs(qij - qji) > tol:
|
||
return True
|
||
return False
|
||
|
||
|
||
def geodesic_assignment(
|
||
metric: FinslerMetric,
|
||
directions: list[list[float]],
|
||
tol: float = 1e-9,
|
||
) -> list[bool]:
|
||
"""Extract the Finsler geodesic directions as a Boolean assignment.
|
||
|
||
A direction is 'on the geodesic' if its Finsler cost is within tol of
|
||
the global minimum.
|
||
|
||
Mirrors Lean `geodesicAssignment` in TransportQUBOBridge.lean.
|
||
"""
|
||
costs = [metric.finsler_cost(v) for v in directions]
|
||
min_cost = min(costs)
|
||
return [abs(c - min_cost) <= tol for c in costs]
|
||
|
||
|
||
# =========================================================================
|
||
# IV. QUBO → Ising
|
||
# =========================================================================
|
||
|
||
def qubo_to_ising(qubo: QUBO) -> Ising:
|
||
"""Convert a QUBO to an Ising Hamiltonian.
|
||
|
||
For binary variable x ∈ {0,1}, spin s = 2x - 1 ∈ {+1, -1}:
|
||
x = (1 + s) / 2
|
||
x_i x_j = (1 + s_i + s_j + s_i s_j) / 4
|
||
|
||
The QUBO cost: E = Σ_i a_i x_i + Σ_{i<j} b_ij x_i x_j
|
||
where a_i = Q[i,i], b_ij = Q[i,j] + Q[j,i]
|
||
"""
|
||
n = qubo.n
|
||
a: dict[int, float] = {}
|
||
b: dict[tuple[int, int], float] = {}
|
||
|
||
for (i, j), qij in qubo.matrix.items():
|
||
if i == j:
|
||
a[i] = a.get(i, 0.0) + qij
|
||
else:
|
||
key = (min(i, j), max(i, j))
|
||
b[key] = b.get(key, 0.0) + qij
|
||
|
||
offset = qubo.offset
|
||
h = [0.0] * n
|
||
J: dict[tuple[int, int], float] = {}
|
||
|
||
for i in range(n):
|
||
offset += 0.5 * a.get(i, 0.0)
|
||
for (i, j), bij in b.items():
|
||
offset += 0.25 * bij
|
||
|
||
for i in range(n):
|
||
hi = 0.5 * a.get(i, 0.0)
|
||
for (j1, j2), bij in b.items():
|
||
if j1 == i:
|
||
hi += 0.25 * bij
|
||
if j2 == i:
|
||
hi += 0.25 * bij
|
||
h[i] = hi
|
||
|
||
for (i, j), bij in b.items():
|
||
J[(i, j)] = 0.25 * bij
|
||
|
||
return Ising(n=n, h=h, J=J, offset=offset)
|
||
|
||
|
||
# =========================================================================
|
||
# VII. Core: Ising → Pauli strings
|
||
# =========================================================================
|
||
|
||
def ising_to_pauli(ising: Ising) -> PauliSum:
|
||
"""Convert an Ising Hamiltonian to Pauli strings.
|
||
|
||
Mapping:
|
||
s_i → Z_i
|
||
s_i s_j → Z_i Z_j
|
||
offset → I (constant)
|
||
"""
|
||
terms: list[tuple[str, float]] = []
|
||
|
||
for i in range(ising.n):
|
||
if abs(ising.h[i]) > 1e-15:
|
||
ps = ["I"] * ising.n
|
||
ps[i] = "Z"
|
||
terms.append(("".join(ps), ising.h[i]))
|
||
|
||
for (i, j), Jij in ising.J.items():
|
||
if abs(Jij) > 1e-15:
|
||
ps = ["I"] * ising.n
|
||
ps[i] = "Z"
|
||
ps[j] = "Z"
|
||
terms.append(("".join(ps), Jij))
|
||
|
||
return PauliSum(n=ising.n, terms=terms, offset=ising.offset)
|
||
|
||
|
||
# =========================================================================
|
||
# VIII. Forward: Pauli → Cirq Circuit (QAOA layers)
|
||
# =========================================================================
|
||
|
||
def pauli_to_cirq(
|
||
pauli: PauliSum,
|
||
p_layers: int = 1,
|
||
gamma: Optional[list[float]] = None,
|
||
beta: Optional[list[float]] = None,
|
||
measure: bool = False,
|
||
) -> Any:
|
||
"""Generate a QAOA circuit from a PauliSum Hamiltonian.
|
||
|
||
Returns cirq.Circuit if cirq is available, else a dict description.
|
||
|
||
Circuit structure:
|
||
|+⟩^⊗n → [e^{-iγ_k H_C} e^{-iβ_k H_M}]_{k=1..p}
|
||
"""
|
||
if not _HAS_CIRQ:
|
||
return _describe_qaoa_circuit(pauli, p_layers)
|
||
|
||
n = pauli.n
|
||
qubits = cirq.LineQubit.range(n)
|
||
|
||
if gamma is None:
|
||
gamma = [1.0] * p_layers
|
||
if beta is None:
|
||
beta = [1.0] * p_layers
|
||
|
||
circuit = cirq.Circuit()
|
||
circuit.append(cirq.H.on_each(*qubits))
|
||
|
||
for layer in range(p_layers):
|
||
g = gamma[layer] if layer < len(gamma) else gamma[-1]
|
||
b = beta[layer] if layer < len(beta) else beta[-1]
|
||
|
||
for ps_str, coeff in pauli.terms:
|
||
angle = 2.0 * g * coeff
|
||
if abs(angle) < 1e-15:
|
||
continue
|
||
z_positions = [i for i, c in enumerate(ps_str) if c == "Z"]
|
||
if len(z_positions) == 1:
|
||
circuit.append(cirq.rz(angle)(qubits[z_positions[0]]))
|
||
elif len(z_positions) == 2:
|
||
i, j = z_positions
|
||
circuit.append(cirq.CZ(qubits[i], qubits[j]) ** (angle / math.pi))
|
||
circuit.append(cirq.rz(angle)(qubits[i]))
|
||
circuit.append(cirq.rz(angle)(qubits[j]))
|
||
elif len(z_positions) > 2:
|
||
for idx in z_positions:
|
||
circuit.append(cirq.rz(angle / len(z_positions))(qubits[idx]))
|
||
|
||
circuit.append(cirq.rx(2.0 * b).on_each(*qubits))
|
||
|
||
if measure:
|
||
circuit.append(cirq.measure(*qubits, key="result"))
|
||
|
||
return circuit
|
||
|
||
|
||
def _describe_qaoa_circuit(pauli: PauliSum, p_layers: int = 1) -> dict:
|
||
"""Return a JSON-like circuit description when no simulator is available."""
|
||
return {
|
||
"type": "qaoa_circuit_description",
|
||
"n_qubits": pauli.n,
|
||
"p_layers": p_layers,
|
||
"cost_hamiltonian": [{"pauli": t[0], "coeff": t[1]} for t in pauli.terms],
|
||
"offset": pauli.offset,
|
||
"mixer": "X" * pauli.n,
|
||
"note": "Install cirq to generate runnable circuits",
|
||
}
|
||
|
||
|
||
# =========================================================================
|
||
# IX. Forward: Pauli → Qiskit Circuit (QAOA layers)
|
||
# =========================================================================
|
||
|
||
def pauli_to_qiskit(
|
||
pauli: PauliSum,
|
||
p_layers: int = 1,
|
||
gamma: Optional[list[float]] = None,
|
||
beta: Optional[list[float]] = None,
|
||
) -> Any:
|
||
"""Generate a QAOA circuit using Qiskit.
|
||
|
||
Returns QuantumCircuit if qiskit is available, else a dict description.
|
||
"""
|
||
if not _HAS_QISKIT:
|
||
return _describe_qaoa_circuit(pauli, p_layers)
|
||
|
||
n = pauli.n
|
||
qreg = QuantumRegister(n, "q")
|
||
creg = ClassicalRegister(n, "c")
|
||
circuit = QuantumCircuit(qreg, creg)
|
||
|
||
if gamma is None:
|
||
gamma = [1.0] * p_layers
|
||
if beta is None:
|
||
beta = [1.0] * p_layers
|
||
|
||
circuit.h(qreg)
|
||
|
||
for layer in range(p_layers):
|
||
g = gamma[layer] if layer < len(gamma) else gamma[-1]
|
||
b = beta[layer] if layer < len(beta) else beta[-1]
|
||
|
||
for ps_str, coeff in pauli.terms:
|
||
angle = 2.0 * g * coeff
|
||
if abs(angle) < 1e-15:
|
||
continue
|
||
z_positions = [i for i, c in enumerate(ps_str) if c == "Z"]
|
||
if len(z_positions) == 1:
|
||
circuit.rz(angle, qreg[z_positions[0]])
|
||
elif len(z_positions) == 2:
|
||
i, j = z_positions
|
||
circuit.cx(qreg[i], qreg[j])
|
||
circuit.rz(angle, qreg[j])
|
||
circuit.cx(qreg[i], qreg[j])
|
||
elif len(z_positions) > 2:
|
||
for idx in z_positions:
|
||
circuit.rz(angle / len(z_positions), qreg[idx])
|
||
|
||
circuit.rx(2.0 * b, qreg)
|
||
|
||
circuit.measure(qreg, creg)
|
||
return circuit
|
||
|
||
|
||
# =========================================================================
|
||
# X. Backward: Measurements → Solution
|
||
# =========================================================================
|
||
|
||
def measurements_to_solution(
|
||
counts: dict[str, int],
|
||
n: int,
|
||
) -> list[int]:
|
||
"""Extract the most probable bitstring from measurement counts.
|
||
|
||
Returns binary list [x_0, ..., x_{n-1}] of the most probable outcome.
|
||
"""
|
||
if not counts:
|
||
return [0] * n
|
||
best_bitstring = max(counts, key=counts.get)
|
||
if len(best_bitstring) < n:
|
||
best_bitstring = best_bitstring.zfill(n)
|
||
return [int(b) for b in best_bitstring[-n:]]
|
||
|
||
|
||
def measurements_to_ising_solution(
|
||
counts: dict[str, int], n: int,
|
||
) -> list[int]:
|
||
"""Extract the most probable spin configuration from measurements."""
|
||
x = measurements_to_solution(counts, n)
|
||
return [2 * xi - 1 for xi in x]
|
||
|
||
|
||
# =========================================================================
|
||
# XI. Backward: Solution → BraidReceipt Update
|
||
# =========================================================================
|
||
|
||
def solution_to_braid_receipt(solution: list[int], receipt: dict) -> dict:
|
||
"""Update a BraidReceipt dict with a QAOA solution."""
|
||
updated = json.loads(json.dumps(receipt))
|
||
braid = updated.get("braid", updated)
|
||
strands = braid.get("strands", [])
|
||
if not strands:
|
||
strands = _receipt_strands_from_bracket(braid)
|
||
braid["strands"] = strands
|
||
|
||
n = min(len(solution), len(strands))
|
||
all_admissible = True
|
||
for i in range(n):
|
||
s = strands[i]
|
||
if bool(solution[i]):
|
||
s["kappa"] = max(0, s.get("kappa", 0) // 2)
|
||
s["converged"] = True
|
||
s["residual"] = 0
|
||
else:
|
||
s["kappa"] = min(65535, s.get("kappa", 0) + 8192)
|
||
s["converged"] = False
|
||
s["residual"] = s.get("kappa", 0)
|
||
all_admissible = False
|
||
|
||
braid["bracket"] = braid.get("bracket", {})
|
||
braid["bracket"]["admissible"] = all_admissible
|
||
updated["scar_absent"] = all_admissible
|
||
updated["step_count"] = updated.get("step_count", 0) + 1
|
||
|
||
residuals = updated.get("residuals", [])
|
||
avg_residual = sum(abs(s.get("kappa", 0)) for s in strands[:n]) // max(n, 1)
|
||
residuals.insert(0, avg_residual)
|
||
updated["residuals"] = residuals[:32]
|
||
return updated
|
||
|
||
|
||
# =========================================================================
|
||
# XII. Backward: Solution → Scar/β₀ Update
|
||
# =========================================================================
|
||
|
||
def solution_to_scar_field(solution: list[int], N: int) -> list[int]:
|
||
"""Map a QAOA solution bitstring back to a scar field."""
|
||
mask = [0] * N
|
||
for i in range(N):
|
||
if solution[i] == 1:
|
||
mask[i] = 1
|
||
radius = max(1, N // 32)
|
||
expanded = [0] * N
|
||
for i in range(N):
|
||
if mask[i]:
|
||
for d in range(-radius, radius + 1):
|
||
expanded[(i + d) % N] = 1
|
||
return expanded
|
||
|
||
|
||
def compute_beta0_from_solution(solution: list[int]) -> int:
|
||
"""Compute β₀ (rising edge count) from a cyclic binary solution."""
|
||
n = len(solution)
|
||
if n == 0:
|
||
return 0
|
||
if all(s == 1 for s in solution):
|
||
return 1
|
||
if all(s == 0 for s in solution):
|
||
return 0
|
||
rising = 0
|
||
for i in range(n):
|
||
if solution[i] == 1 and solution[(i - 1) % n] == 0:
|
||
rising += 1
|
||
return rising
|
||
|
||
|
||
# =========================================================================
|
||
# XIII. High-Level: Corpus250 Row → QUBO
|
||
# FixtureRow has: shape, rrcKind, operatorTokens, weakAxesCnt,
|
||
# pistProxyLabel, pistExactLabel (no domain_embedding field).
|
||
# =========================================================================
|
||
|
||
def corpus278_row_to_qubo(row: dict) -> QUBO:
|
||
"""Convert a Corpus250 equation row (FixtureRow) to a QUBO problem.
|
||
|
||
Uses the actual FixtureRow fields:
|
||
- shape: RRCShape enum guiding the QUBO structure
|
||
- operatorTokens: domain operators for variable count/diagonal
|
||
- weakAxesCnt: number of weak axes for off-diagonal coupling
|
||
- pistProxyLabel / pistExactLabel: PIST labels for bias
|
||
"""
|
||
shape = row.get("shape", "")
|
||
operator_tokens = row.get("operatorTokens", [])
|
||
weak_axes_cnt = row.get("weakAxesCnt", 0)
|
||
|
||
n_vars = max(8, len(operator_tokens) + int(weak_axes_cnt))
|
||
|
||
shape_bias = {
|
||
"cognitiveLoadField": 1.0,
|
||
"signalShapedRouteCompiler": 2.0,
|
||
"projectableGeometryTopology": 3.0,
|
||
"cadForceProbeReceipt": 4.0,
|
||
"logogramProjection": 5.0,
|
||
"holdForUnlawfulOrUnderspecifiedShape": 10.0,
|
||
}.get(shape, 2.0)
|
||
|
||
Q: dict[tuple[int, int], float] = {}
|
||
|
||
# Diagonal: one per operator token with shape bias
|
||
for i in range(min(n_vars, len(operator_tokens))):
|
||
Q[(i, i)] = shape_bias * (1.0 + (i % (int(weak_axes_cnt) + 1)))
|
||
|
||
# Fill remaining diagonal
|
||
for i in range(len(operator_tokens), n_vars):
|
||
Q[(i, i)] = shape_bias * 1.5
|
||
|
||
# Weak axes coupling
|
||
wac = int(weak_axes_cnt)
|
||
if wac > 0:
|
||
for i in range(min(wac, n_vars)):
|
||
for j in range(i + 1, min(wac, n_vars)):
|
||
Q[(i, j)] = Q.get((i, j), 0.0) - 0.5
|
||
|
||
# PIST label bias
|
||
pist_proxy = row.get("pistProxyLabel")
|
||
if pist_proxy and pist_proxy != "none":
|
||
for i in range(min(4, n_vars)):
|
||
Q[(i, i)] = Q.get((i, i), 0.0) - 0.3
|
||
|
||
return QUBO(n=n_vars, matrix=Q)
|
||
|
||
|
||
# =========================================================================
|
||
# XIV. Stochastic Abuse: Classical Stochastic Solvers
|
||
# Wraps existing qubo_highs.py (SA / HiGHS) so results are directly
|
||
# comparable with qaoa_solve_qubo() output.
|
||
#
|
||
# "Abuse" = using the classical stochastic pipeline as a baseline to
|
||
# benchmark QAOA against, on the same QUBO — borrowing the existing
|
||
# HiGHS MIP and simulated annealing infrastructure.
|
||
# =========================================================================
|
||
|
||
def stochastic_abuse_qubo(
|
||
qubo: QUBO,
|
||
method: str = "sa",
|
||
time_limit: float = 5.0,
|
||
seed: int = 42,
|
||
) -> dict:
|
||
"""Solve a QUBO using the existing classical stochastic pipeline.
|
||
|
||
Wraps qubo_highs.solve_qubo_highs (HiGHS MIP) and
|
||
braid_search.qubo_optimize (SA) for direct comparison with QAOA.
|
||
|
||
Args:
|
||
qubo: QUBO problem
|
||
method: "sa" (simulated annealing), "highs" (HiGHS MIP), or
|
||
"levy" (Lévy flight sampling)
|
||
time_limit: Max seconds for solver
|
||
seed: RNG seed
|
||
|
||
Returns:
|
||
dict with same schema as qaoa_solve_qubo() for easy comparison:
|
||
- solution, energy, method, runtime, solver_detail
|
||
"""
|
||
t0 = time.time()
|
||
n = qubo.n
|
||
|
||
# Build QUBO dict in the format qubo_highs expects
|
||
Q_dict: dict[tuple[int, int], float] = dict(qubo.matrix)
|
||
|
||
if method == "highs":
|
||
try:
|
||
_sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||
from qubo_highs import solve_qubo_highs as highs_solve
|
||
result = highs_solve(Q_dict, n, time_limit=time_limit)
|
||
if isinstance(result, dict):
|
||
solution = result.get("x", [0] * n)
|
||
else:
|
||
solution, _ = result
|
||
except Exception as exc:
|
||
solution = _sa_solve(Q_dict, n, seed, time_limit)
|
||
|
||
elif method == "levy":
|
||
solution = _levy_fight(Q_dict, n, seed, time_limit)
|
||
|
||
else:
|
||
solution = _sa_solve(Q_dict, n, seed, time_limit)
|
||
|
||
runtime = time.time() - t0
|
||
energy = qubo.energy(solution)
|
||
|
||
return {
|
||
"solution": solution,
|
||
"energy": energy,
|
||
"method": method,
|
||
"runtime_s": round(runtime, 4),
|
||
"n": n,
|
||
}
|
||
|
||
|
||
def _sa_solve(
|
||
Q: dict[tuple[int, int], float],
|
||
n: int,
|
||
seed: int = 42,
|
||
time_limit: float = 5.0,
|
||
) -> list[int]:
|
||
"""Simulated annealing QUBO solver (standalone fallback)."""
|
||
import random as _r
|
||
_r.seed(seed)
|
||
t0 = time.time()
|
||
|
||
x = [_r.randint(0, 1) for _ in range(n)]
|
||
|
||
def _energy(xv):
|
||
e = 0.0
|
||
for (i, j), qij in Q.items():
|
||
e += qij * xv[i] * xv[j]
|
||
return e
|
||
|
||
current_energy = _energy(x)
|
||
best_x = list(x)
|
||
best_energy = current_energy
|
||
temp = 10.0
|
||
iterations = 0
|
||
n_vals = list(range(n))
|
||
|
||
while time.time() - t0 < time_limit:
|
||
i = _r.choice(n_vals)
|
||
x[i] = 1 - x[i]
|
||
new_energy = _energy(x)
|
||
delta = new_energy - current_energy
|
||
|
||
if delta < 0 or _r.random() < math.exp(-delta / max(temp, 1e-10)):
|
||
current_energy = new_energy
|
||
if current_energy < best_energy:
|
||
best_x = list(x)
|
||
best_energy = current_energy
|
||
else:
|
||
x[i] = 1 - x[i]
|
||
|
||
temp *= 0.9995
|
||
iterations += 1
|
||
|
||
return best_x
|
||
|
||
|
||
def _levy_fight(
|
||
Q: dict[tuple[int, int], float],
|
||
n: int,
|
||
seed: int = 42,
|
||
time_limit: float = 5.0,
|
||
) -> list[int]:
|
||
"""Lévy flight sampling over binary strings.
|
||
|
||
Step sizes follow a power-law distribution (heavy-tailed),
|
||
mimicking the LévyFlight structure from EntropyMeasures.lean.
|
||
"""
|
||
import random as _r
|
||
_r.seed(seed)
|
||
t0 = time.time()
|
||
|
||
x = [_r.randint(0, 1) for _ in range(n)]
|
||
|
||
def _energy(xv):
|
||
e = 0.0
|
||
for (i, j), qij in Q.items():
|
||
e += qij * xv[i] * xv[j]
|
||
return e
|
||
|
||
def _levy_step() -> int:
|
||
u = _r.random()
|
||
return max(1, int(n * u ** (-1.0 / 1.5)) % n)
|
||
|
||
best_x = list(x)
|
||
best_energy = _energy(x)
|
||
|
||
while time.time() - t0 < time_limit:
|
||
step = _levy_step()
|
||
indices = _r.sample(range(n), min(step, n))
|
||
for i in indices:
|
||
x[i] = 1 - x[i]
|
||
e = _energy(x)
|
||
if e < best_energy:
|
||
best_x = list(x)
|
||
best_energy = e
|
||
else:
|
||
for i in indices:
|
||
x[i] = 1 - x[i]
|
||
|
||
return best_x
|
||
|
||
|
||
# =========================================================================
|
||
# XV. Comparison: QAOA vs Stochastic
|
||
# =========================================================================
|
||
|
||
def qaoa_vs_stochastic_comparison(
|
||
qubo: QUBO,
|
||
p_layers: int = 1,
|
||
shots: int = 1000,
|
||
qaoa_backend: str = "cirq",
|
||
stochastic_methods: Optional[list[str]] = None,
|
||
time_limit: float = 5.0,
|
||
seed: int = 42,
|
||
) -> dict:
|
||
"""Run QAOA and classical stochastic solvers on the same QUBO.
|
||
|
||
Returns a side-by-side comparison so results can be contrasted.
|
||
|
||
Args:
|
||
qubo: QUBO problem
|
||
p_layers: QAOA depth
|
||
shots: QAOA measurement shots
|
||
qaoa_backend: QAOA backend ("cirq", "qiskit", "describe")
|
||
stochastic_methods: List of classical methods to compare.
|
||
Default: ["sa", "levy"].
|
||
time_limit: Time limit per classical solver (seconds)
|
||
seed: RNG seed
|
||
|
||
Returns:
|
||
dict with:
|
||
- qubo_summary: n, terms, offset
|
||
- qaoa: result from qaoa_solve_qubo
|
||
- stochastic: {method: result} for each method
|
||
- winner: solver with lowest energy
|
||
"""
|
||
if stochastic_methods is None:
|
||
stochastic_methods = ["sa", "levy"]
|
||
|
||
qaoa_result = qaoa_solve_qubo(
|
||
qubo, p_layers=p_layers, shots=shots, backend=qaoa_backend,
|
||
)
|
||
|
||
stochastic_results: dict[str, dict] = {}
|
||
for method in stochastic_methods:
|
||
stochastic_results[method] = stochastic_abuse_qubo(
|
||
qubo, method=method, time_limit=time_limit, seed=seed,
|
||
)
|
||
|
||
# Determine winner
|
||
candidates: list[tuple[str, float]] = [
|
||
("qaoa", qaoa_result["energy"]),
|
||
]
|
||
for method, res in stochastic_results.items():
|
||
candidates.append((method, res["energy"]))
|
||
|
||
candidates.sort(key=lambda p: p[1])
|
||
winner_name, winner_energy = candidates[0]
|
||
|
||
return {
|
||
"qubo_summary": {
|
||
"n": qubo.n,
|
||
"terms": len(qubo.matrix),
|
||
"offset": qubo.offset,
|
||
},
|
||
"qaoa": {
|
||
"solution": qaoa_result["solution"],
|
||
"energy": qaoa_result["energy"],
|
||
"p_layers": p_layers,
|
||
"shots": shots,
|
||
"backend": qaoa_backend,
|
||
},
|
||
"stochastic": stochastic_results,
|
||
"winner": {
|
||
"solver": winner_name,
|
||
"energy": winner_energy,
|
||
},
|
||
}
|
||
|
||
|
||
# =========================================================================
|
||
# XVI. High-Level: End-to-End QAOA Solve
|
||
# =========================================================================
|
||
|
||
def qaoa_solve_qubo(
|
||
qubo: QUBO,
|
||
p_layers: int = 1,
|
||
gamma: Optional[list[float]] = None,
|
||
beta: Optional[list[float]] = None,
|
||
shots: int = 1000,
|
||
backend: str = "cirq",
|
||
) -> dict:
|
||
"""Solve a QUBO using QAOA.
|
||
|
||
Args:
|
||
qubo: QUBO problem
|
||
p_layers: QAOA layers
|
||
gamma: cost angles
|
||
beta: mixer angles
|
||
shots: measurement shots
|
||
backend: "cirq", "qiskit", or "describe"
|
||
|
||
Returns:
|
||
dict with solution, energy, counts, and circuit description
|
||
"""
|
||
ising = qubo_to_ising(qubo)
|
||
pauli = ising_to_pauli(ising)
|
||
|
||
result: dict[str, Any] = {
|
||
"n": qubo.n,
|
||
"p_layers": p_layers,
|
||
"qubo_energy_offset": qubo.offset,
|
||
"ising_offset": ising.offset,
|
||
"pauli_terms": [(t[0], t[1]) for t in pauli.terms],
|
||
}
|
||
|
||
if backend == "describe":
|
||
result["circuit"] = _describe_qaoa_circuit(pauli, p_layers)
|
||
result["solution"] = [0] * qubo.n
|
||
result["energy"] = qubo.energy([0] * qubo.n)
|
||
result["note"] = "describe mode — no simulation"
|
||
return result
|
||
|
||
if backend == "cirq" and _HAS_CIRQ:
|
||
circuit = pauli_to_cirq(pauli, p_layers, gamma, beta, measure=True)
|
||
simulator = cirq.Simulator()
|
||
samples = simulator.run(circuit, repetitions=shots)
|
||
counts = samples.histogram(key="result")
|
||
str_counts = _cirq_counts_to_str(counts, qubo.n)
|
||
solution = measurements_to_solution(str_counts, qubo.n)
|
||
energy = qubo.energy(solution)
|
||
result["circuit"] = str(circuit)
|
||
result["counts"] = str_counts
|
||
result["solution"] = solution
|
||
result["energy"] = energy
|
||
return result
|
||
|
||
if backend == "qiskit" and _HAS_QISKIT:
|
||
circuit = pauli_to_qiskit(pauli, p_layers, gamma, beta)
|
||
from qiskit_aer import AerSimulator
|
||
simulator = AerSimulator()
|
||
job = simulator.run(circuit, shots=shots)
|
||
counts_dict = job.result().get_counts()
|
||
solution = measurements_to_solution(counts_dict, qubo.n)
|
||
energy = qubo.energy(solution)
|
||
result["circuit"] = circuit.qasm()
|
||
result["counts"] = counts_dict
|
||
result["solution"] = solution
|
||
result["energy"] = energy
|
||
return result
|
||
|
||
import random
|
||
random.seed(0)
|
||
all_counts: dict[str, int] = {}
|
||
for _ in range(shots):
|
||
x = [random.randint(0, 1) for _ in range(qubo.n)]
|
||
bits = "".join(str(b) for b in x)
|
||
all_counts[bits] = all_counts.get(bits, 0) + 1
|
||
|
||
solution = measurements_to_solution(all_counts, qubo.n)
|
||
energy = qubo.energy(solution)
|
||
result["circuit"] = _describe_qaoa_circuit(pauli, p_layers)
|
||
result["counts"] = all_counts
|
||
result["solution"] = solution
|
||
result["energy"] = energy
|
||
result["note"] = "random fallback — install cirq for QAOA simulation"
|
||
return result
|
||
|
||
|
||
def _cirq_counts_to_str(counts: dict, n: int) -> dict[str, int]:
|
||
"""Convert Cirq integer keyed counts to bitstring counts."""
|
||
str_counts: dict[str, int] = {}
|
||
for val, cnt in counts.items():
|
||
bits = format(val, f"0{n}b")
|
||
str_counts[bits] = cnt
|
||
return str_counts
|
||
|
||
|
||
# =========================================================================
|
||
# XVII. High-Level: Roundtrip
|
||
# =========================================================================
|
||
|
||
def qaoa_roundtrip(
|
||
receipt: dict,
|
||
p_layers: int = 1,
|
||
shots: int = 1000,
|
||
backend: str = "cirq",
|
||
compare_stochastic: bool = False,
|
||
) -> dict:
|
||
"""Full roundtrip: BraidReceipt → QAOA → updated BraidReceipt.
|
||
|
||
Args:
|
||
receipt: Input BraidReceipt JSON dict
|
||
p_layers: QAOA depth
|
||
shots: Measurement shots per layer
|
||
backend: "cirq", "qiskit", "describe"
|
||
compare_stochastic: If True, also run SA/Levy comparison
|
||
|
||
Returns:
|
||
dict with input_receipt, output_receipt, qaoa_result, delta,
|
||
and optionally stochastic comparison.
|
||
"""
|
||
qubo = braid_receipt_to_qubo(receipt)
|
||
qaoa_result = qaoa_solve_qubo(
|
||
qubo, p_layers=p_layers, shots=shots, backend=backend,
|
||
)
|
||
solution = qaoa_result["solution"]
|
||
output_receipt = solution_to_braid_receipt(solution, receipt)
|
||
|
||
input_kappas = [
|
||
s.get("kappa", 0)
|
||
for s in receipt.get("braid", receipt).get("strands", [])
|
||
]
|
||
output_kappas = [
|
||
s.get("kappa", 0)
|
||
for s in output_receipt.get("braid", output_receipt).get("strands", [])
|
||
]
|
||
|
||
result: dict[str, Any] = {
|
||
"input_receipt": receipt,
|
||
"output_receipt": output_receipt,
|
||
"qaoa_result": {
|
||
"solution": solution,
|
||
"energy": qaoa_result["energy"],
|
||
"counts": qaoa_result.get("counts", {}),
|
||
"p_layers": p_layers,
|
||
},
|
||
"delta": {
|
||
"scar_absent": {
|
||
"before": receipt.get("scar_absent"),
|
||
"after": output_receipt.get("scar_absent"),
|
||
},
|
||
"step_count": {
|
||
"before": receipt.get("step_count", 0),
|
||
"after": output_receipt.get("step_count", 0),
|
||
},
|
||
"avg_kappa": {
|
||
"before": sum(input_kappas) / max(len(input_kappas), 1),
|
||
"after": sum(output_kappas) / max(len(output_kappas), 1),
|
||
},
|
||
},
|
||
}
|
||
|
||
if compare_stochastic:
|
||
comparison = qaoa_vs_stochastic_comparison(qubo)
|
||
result["stochastic_comparison"] = comparison
|
||
|
||
return result
|
||
|
||
|
||
# =========================================================================
|
||
# XVIII-A. FinslerMetric demo
|
||
# =========================================================================
|
||
|
||
def finsler_demo() -> dict[str, Any]:
|
||
"""End-to-end demo of the FinslerMetric → QUBO → Ising → Pauli pipeline.
|
||
|
||
Constructs a simple 2D Finsler metric with nontrivial wind (β ≠ 0),
|
||
generates direction vectors on a circle, builds the QUBO matrix,
|
||
converts to Ising and Pauli strings, and verifies anisotropy.
|
||
|
||
This mirrors the Lean `trivialRanders` witness in TransportQUBOBridge.lean.
|
||
"""
|
||
# 2D Finsler metric with wind drift β = (0.25, 0.0)
|
||
metric = FinslerMetric(
|
||
alpha_mass=[1.0, 1.0], # uniform mass
|
||
beta_wind=[0.25, 0.0], # rightward drift
|
||
dimension=2,
|
||
)
|
||
|
||
# 8 directions on the unit circle
|
||
import math
|
||
n_dirs = 8
|
||
directions = []
|
||
for k in range(n_dirs):
|
||
theta = 2.0 * math.pi * k / n_dirs
|
||
directions.append([math.cos(theta), math.sin(theta)])
|
||
|
||
# Build QUBO (now stores Q_ij + Q_ji summed per pair)
|
||
qubo = finsler_metric_to_qubo(metric, directions, normalize=False)
|
||
|
||
# Check anisotropy via raw matrix (Q_ij vs Q_ji before summation)
|
||
n_raw = len(directions)
|
||
raw = [[0.0] * n_raw for _ in range(n_raw)]
|
||
for i in range(n_raw):
|
||
for j in range(n_raw):
|
||
if i != j:
|
||
raw[i][j] = metric.crossing_cost(directions[i], directions[j])
|
||
anisotropic = is_anisotropic(qubo, raw_matrix=raw)
|
||
|
||
# Convert to Ising
|
||
ising = qubo_to_ising(qubo)
|
||
|
||
# Convert to Pauli
|
||
pauli = ising_to_pauli(ising)
|
||
|
||
# Find geodesic assignment
|
||
geo = geodesic_assignment(metric, directions)
|
||
|
||
# Summary
|
||
return {
|
||
"metric": {
|
||
"alpha_mass": metric.alpha_mass,
|
||
"beta_wind": metric.beta_wind,
|
||
"dimension": metric.dimension,
|
||
},
|
||
"n_directions": n_dirs,
|
||
"qubo_is_anisotropic": anisotropic,
|
||
"qubo_n": qubo.n,
|
||
"qubo_nonzero_entries": len(qubo.matrix),
|
||
"ising_h": ising.h[:5] if ising.n > 5 else ising.h,
|
||
"ising_J_entries": len(ising.J),
|
||
"pauli_terms": len(pauli.terms),
|
||
"geodesic_assignment": geo,
|
||
"anisotropic_pair": _find_anisotropic_pair(qubo, raw_matrix=raw),
|
||
}
|
||
|
||
|
||
def _find_anisotropic_pair(qubo: QUBO, raw_matrix: Optional[list[list[float]]] = None) -> Optional[dict]:
|
||
"""Find the first (i,j) where Q_ij ≠ Q_ji."""
|
||
if raw_matrix is not None:
|
||
n = len(raw_matrix)
|
||
for i in range(n):
|
||
for j in range(n):
|
||
if i != j and abs(raw_matrix[i][j] - raw_matrix[j][i]) > 1e-9:
|
||
return {"i": i, "j": j, "Q_ij": raw_matrix[i][j], "Q_ji": raw_matrix[j][i], "delta": raw_matrix[i][j] - raw_matrix[j][i]}
|
||
return None
|
||
for (i, j), qij in qubo.matrix.items():
|
||
if i == j:
|
||
continue
|
||
qji = qubo.matrix.get((j, i), 0.0)
|
||
if abs(qij - qji) > 1e-9:
|
||
return {"i": i, "j": j, "Q_ij": qij, "Q_ji": qji, "delta": qij - qji}
|
||
return None
|
||
|
||
|
||
# =========================================================================
|
||
# XVIII. CLI
|
||
# =========================================================================
|
||
|
||
def _parse_receipt(path: str) -> dict:
|
||
with open(path) as f:
|
||
return json.load(f)
|
||
|
||
|
||
def _serialize(obj: Any) -> Any:
|
||
"""Recursively serialize a result object to JSON-safe types.
|
||
|
||
Handles QUBO, Ising, PauliSum dataclasses and dicts with tuple keys.
|
||
"""
|
||
if hasattr(obj, "__dataclass_fields__"):
|
||
d = asdict(obj)
|
||
return _serialize(d)
|
||
if isinstance(obj, dict):
|
||
out = {}
|
||
for k, v in obj.items():
|
||
if isinstance(k, tuple):
|
||
sk = f"({','.join(str(x) for x in k)})"
|
||
elif isinstance(k, int):
|
||
sk = str(k)
|
||
else:
|
||
sk = k
|
||
out[sk] = _serialize(v)
|
||
return out
|
||
if isinstance(obj, (list, tuple)):
|
||
return [_serialize(x) for x in obj]
|
||
return obj
|
||
|
||
|
||
def main() -> None:
|
||
import argparse
|
||
|
||
parser = argparse.ArgumentParser(
|
||
description="Bidirectional QAOA Conversion Adapter Set"
|
||
)
|
||
parser.add_argument(
|
||
"action",
|
||
choices=[
|
||
"lean-formulation", "lean-field", "braid-search",
|
||
"braid-to-qubo", "lone-to-qubo", "goorm-to-qubo",
|
||
"qubo-to-ising", "ising-to-pauli", "pauli-to-cirq",
|
||
"pauli-to-qiskit", "solve-qubo", "stochastic",
|
||
"compare", "roundtrip",
|
||
"tune", "auto-solve", "slos-calibrate", "auto-calibrate",
|
||
"finsler-demo",
|
||
],
|
||
)
|
||
parser.add_argument("--coeffs", help="JSON file with BraidCostCoeffs overrides")
|
||
parser.add_argument("--receipt", help="Path to BraidReceipt JSON")
|
||
parser.add_argument("--output", "-o", help="Output path (default: stdout)")
|
||
parser.add_argument("--p-layers", type=int, default=1, help="QAOA layers")
|
||
parser.add_argument("--shots", type=int, default=1000, help="Measurement shots")
|
||
parser.add_argument("--backend", default="cirq", help="Simulator backend")
|
||
parser.add_argument("--gamma", type=float, nargs="*", help="Cost angles")
|
||
parser.add_argument("--beta", type=float, nargs="*", help="Mixer angles")
|
||
parser.add_argument("--method", default="sa", help="Stochastic method")
|
||
parser.add_argument("--time-limit", type=float, default=5.0,
|
||
help="Stochastic solver time limit")
|
||
|
||
args = parser.parse_args()
|
||
|
||
result: Any = None
|
||
|
||
if args.action == "lean-formulation":
|
||
result = {"note": "Use Python API with lean_qubo_formulation_to_qubo(matrix, n)"}
|
||
elif args.action == "lean-field":
|
||
result = lean_qubo_field_to_qubo(65536, 10 * 65536)
|
||
elif args.action == "braid-search":
|
||
if args.receipt:
|
||
receipt = _parse_receipt(args.receipt)
|
||
bkts = receipt.get("braid", receipt).get("strands", [])
|
||
result = braid_search_brackets_to_qubo(bkts)
|
||
else:
|
||
result = {"note": "Use --receipt with bracket-containing JSON"}
|
||
elif args.action == "braid-to-qubo":
|
||
receipt = _parse_receipt(args.receipt)
|
||
result = braid_receipt_to_qubo(receipt)
|
||
elif args.action == "lone-to-qubo":
|
||
result = {"note": "Use Python API with a numpy scar field array"}
|
||
elif args.action == "goorm-to-qubo":
|
||
result = goormaghtigh_cost_to_qubo()
|
||
elif args.action == "qubo-to-ising":
|
||
result = {"note": "Use Python API with a QUBO object"}
|
||
elif args.action == "ising-to-pauli":
|
||
result = {"note": "Use Python API with an Ising object"}
|
||
elif args.action == "pauli-to-cirq":
|
||
result = {"note": "Use Python API with a PauliSum object"}
|
||
elif args.action == "pauli-to-qiskit":
|
||
result = {"note": "Use Python API with a PauliSum object"}
|
||
elif args.action == "solve-qubo":
|
||
receipt = _parse_receipt(args.receipt)
|
||
result = qaoa_roundtrip(receipt, args.p_layers, args.shots, args.backend)
|
||
elif args.action == "stochastic":
|
||
receipt = _parse_receipt(args.receipt)
|
||
qubo = braid_receipt_to_qubo(receipt)
|
||
result = stochastic_abuse_qubo(qubo, args.method, args.time_limit)
|
||
elif args.action == "compare":
|
||
receipt = _parse_receipt(args.receipt)
|
||
qubo = braid_receipt_to_qubo(receipt)
|
||
result = qaoa_vs_stochastic_comparison(
|
||
qubo, args.p_layers, args.shots, args.backend,
|
||
time_limit=args.time_limit,
|
||
)
|
||
elif args.action == "tune":
|
||
receipt = _parse_receipt(args.receipt)
|
||
bkts = receipt.get("braid", receipt).get("strands", [])
|
||
if args.coeffs:
|
||
custom_grid = [json.loads(Path(args.coeffs).read_text())]
|
||
result = tune_braid_parameters(bkts, param_grid=custom_grid)
|
||
else:
|
||
result = tune_braid_parameters(bkts, time_per_trial=args.time_limit)
|
||
elif args.action == "auto-solve":
|
||
receipt = _parse_receipt(args.receipt)
|
||
bkts = receipt.get("braid", receipt).get("strands", [])
|
||
Q = build_tunable_qubo_matrix(bkts)
|
||
qubo = QUBO(n=len(bkts), matrix=Q)
|
||
result = solver_dispatch_tuner(qubo, time_limit=args.time_limit)
|
||
elif args.action == "slos-calibrate":
|
||
receipt = _parse_receipt(args.receipt)
|
||
bkts = receipt.get("braid", receipt).get("strands", [])
|
||
evaluator = SlosBraidEvaluator()
|
||
result = slos_calibrate_coeffs(bkts, evaluator=evaluator)
|
||
result["n"] = len(bkts)
|
||
result["note"] = (
|
||
"SLOS-calibrated coefficients. Set as braid_search defaults"
|
||
" by copying calibrated_coeffs into BraidCostCoeffs() args."
|
||
)
|
||
elif args.action == "auto-calibrate":
|
||
receipt = _parse_receipt(args.receipt)
|
||
bkts = receipt.get("braid", receipt).get("strands", [])
|
||
evaluator = SlosBraidEvaluator()
|
||
result = auto_calibrate(bkts, evaluator=evaluator, time_per_solver=args.time_limit)
|
||
elif args.action == "roundtrip":
|
||
receipt = _parse_receipt(args.receipt)
|
||
result = qaoa_roundtrip(
|
||
receipt, args.p_layers, args.shots, args.backend,
|
||
compare_stochastic=True,
|
||
)
|
||
elif args.action == "finsler-demo":
|
||
result = finsler_demo()
|
||
|
||
output = json.dumps(_serialize(result), indent=2, default=str)
|
||
|
||
if args.output:
|
||
Path(args.output).write_text(output)
|
||
else:
|
||
print(output)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|