mirror of
https://github.com/allaunthefox/Research-Stack.git
synced 2026-07-31 03:05:21 +00:00
331 lines
11 KiB
Python
331 lines
11 KiB
Python
#!/usr/bin/env python3
|
|
# ==============================================================================
|
|
# COPYRIGHT NO ONE EVERYWHERE LLC (WYOMING HOLDING COMPANY)
|
|
# PROJECT: SOVEREIGN STACK
|
|
# This artifact is entirely proprietary and cryptographically proven.
|
|
# Open-Source usage requires explicit permission from Brandon Scott Schneider.
|
|
# ==============================================================================
|
|
"""
|
|
qubo_chunk_field_benchmark.py
|
|
|
|
Benchmark a solvable-but-slow dense QUBO instance against two search policies:
|
|
|
|
1. baseline simulated annealing with uniform variable proposals
|
|
2. chunk-field guided annealing that prioritizes high-energy-density chunks
|
|
|
|
This script is intentionally narrow. It does not claim to implement the full
|
|
waveprobe/phonon-graph hardware path. It tests one grounded hypothesis from the
|
|
matrix/operator thread: if we treat the QUBO surface as a field and rank chunks
|
|
by energy density, does that help a bounded stochastic solver reach the optimum
|
|
more often or earlier on a frustrating instance?
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import math
|
|
import time
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
from typing import Dict, List, Optional
|
|
|
|
import sys
|
|
import os
|
|
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
|
from math_harness_compat import xp, AnyArray
|
|
|
|
|
|
@dataclass
|
|
class RunStats:
|
|
best_energy: float
|
|
hit_optimum: bool
|
|
first_hit_step: Optional[int]
|
|
elapsed_s: float
|
|
|
|
|
|
def generate_dense_qubo(
|
|
n: int,
|
|
seed: int,
|
|
density: float = 1.0,
|
|
weight_scale: float = 1.0,
|
|
bias_scale: float = 0.35,
|
|
) -> AnyArray:
|
|
"""Generate a dense frustrated symmetric QUBO matrix."""
|
|
rng = xp.random.default_rng(seed)
|
|
raw = rng.normal(0.0, weight_scale, size=(n, n))
|
|
mask = rng.random((n, n)) < density
|
|
raw *= mask
|
|
raw = xp.triu(raw, 1)
|
|
sym = raw + raw.T
|
|
diag = rng.normal(0.0, bias_scale, size=n)
|
|
xp.fill_diagonal(sym, diag)
|
|
return sym.astype(xp.float64)
|
|
|
|
|
|
def energy(Q: AnyArray, x: AnyArray) -> float:
|
|
return float(x @ Q @ x)
|
|
|
|
|
|
def exact_solve_qubo(Q: AnyArray, batch_size: int = 65536) -> Dict[str, object]:
|
|
"""
|
|
Exact solver by batched brute force.
|
|
Practical for n <= ~22 on a normal machine.
|
|
"""
|
|
n = Q.shape[0]
|
|
total = 1 << n
|
|
best_energy = float("inf")
|
|
best_state = None
|
|
col_shifts = xp.arange(n, dtype=xp.uint64)
|
|
|
|
for start in range(0, total, batch_size):
|
|
end = min(start + batch_size, total)
|
|
vals = xp.arange(start, end, dtype=xp.uint64)
|
|
bits = ((vals[:, None] >> col_shifts) & 1).astype(xp.float64)
|
|
energies = xp.einsum("bi,ij,bj->b", bits, Q, bits, optimize=True)
|
|
idx = int(xp.argmin(energies))
|
|
e = float(energies[idx])
|
|
if e < best_energy:
|
|
best_energy = e
|
|
best_state = bits[idx].astype(xp.int8)
|
|
|
|
return {
|
|
"best_energy": best_energy,
|
|
"best_state": best_state.tolist() if best_state is not None else None,
|
|
}
|
|
|
|
|
|
def build_chunk_index(n: int, chunk_size: int) -> List[AnyArray]:
|
|
return [
|
|
xp.arange(start, min(start + chunk_size, n), dtype=xp.int64)
|
|
for start in range(0, n, chunk_size)
|
|
]
|
|
|
|
|
|
def chunk_static_density(Q: AnyArray, chunks: List[AnyArray]) -> AnyArray:
|
|
scores = []
|
|
for idxs in chunks:
|
|
block = Q[xp.ix_(idxs, xp.arange(Q.shape[0]))]
|
|
scores.append(float(xp.mean(block * block)))
|
|
return xp.asarray(scores, dtype=xp.float64)
|
|
|
|
|
|
def run_baseline_sa(
|
|
Q: AnyArray,
|
|
optimum: float,
|
|
steps: int,
|
|
seed: int,
|
|
temp_start: float,
|
|
temp_end: float,
|
|
) -> RunStats:
|
|
rng = xp.random.default_rng(seed)
|
|
n = Q.shape[0]
|
|
x = rng.integers(0, 2, size=n, dtype=xp.int8).astype(xp.float64)
|
|
Qx = Q @ x
|
|
current_energy = energy(Q, x)
|
|
best_energy = current_energy
|
|
first_hit_step = 0 if math.isclose(current_energy, optimum, abs_tol=1e-9) else None
|
|
|
|
t0 = time.perf_counter()
|
|
for step in range(steps):
|
|
frac = step / max(steps - 1, 1)
|
|
temp = temp_start * ((temp_end / temp_start) ** frac)
|
|
i = int(rng.integers(0, n))
|
|
d = 1.0 - 2.0 * x[i]
|
|
delta = 2.0 * d * Qx[i] + Q[i, i]
|
|
if delta < 0.0 or rng.random() < math.exp(-delta / max(temp, 1e-12)):
|
|
x[i] = 1.0 - x[i]
|
|
Qx += d * Q[:, i]
|
|
current_energy += delta
|
|
if current_energy < best_energy:
|
|
best_energy = current_energy
|
|
if first_hit_step is None and math.isclose(current_energy, optimum, abs_tol=1e-9):
|
|
first_hit_step = step + 1
|
|
elapsed = time.perf_counter() - t0
|
|
return RunStats(
|
|
best_energy=best_energy,
|
|
hit_optimum=math.isclose(best_energy, optimum, abs_tol=1e-9),
|
|
first_hit_step=first_hit_step,
|
|
elapsed_s=elapsed,
|
|
)
|
|
|
|
|
|
def run_chunk_guided_sa(
|
|
Q: AnyArray,
|
|
optimum: float,
|
|
steps: int,
|
|
seed: int,
|
|
temp_start: float,
|
|
temp_end: float,
|
|
chunk_size: int,
|
|
guided_prob: float,
|
|
recompute_every: int,
|
|
) -> RunStats:
|
|
rng = xp.random.default_rng(seed)
|
|
n = Q.shape[0]
|
|
chunks = build_chunk_index(n, chunk_size)
|
|
static_density = chunk_static_density(Q, chunks)
|
|
|
|
x = rng.integers(0, 2, size=n, dtype=xp.int8).astype(xp.float64)
|
|
Qx = Q @ x
|
|
current_energy = energy(Q, x)
|
|
best_energy = current_energy
|
|
first_hit_step = 0 if math.isclose(current_energy, optimum, abs_tol=1e-9) else None
|
|
|
|
chunk_prob = xp.ones(len(chunks), dtype=xp.float64) / max(len(chunks), 1)
|
|
t0 = time.perf_counter()
|
|
for step in range(steps):
|
|
if step % max(recompute_every, 1) == 0:
|
|
local_flip_gain = xp.abs((1.0 - 2.0 * x) * Qx)
|
|
dynamic = xp.asarray(
|
|
[float(xp.mean(local_flip_gain[idxs])) for idxs in chunks],
|
|
dtype=xp.float64,
|
|
)
|
|
chunk_score = static_density * (0.5 + dynamic)
|
|
if xp.all(chunk_score <= 0.0):
|
|
chunk_prob = xp.ones(len(chunks), dtype=xp.float64) / max(len(chunks), 1)
|
|
else:
|
|
chunk_prob = chunk_score / xp.sum(chunk_score)
|
|
|
|
frac = step / max(steps - 1, 1)
|
|
temp = temp_start * ((temp_end / temp_start) ** frac)
|
|
|
|
if rng.random() < guided_prob:
|
|
chunk_idx = int(rng.choice(len(chunks), p=chunk_prob))
|
|
idxs = chunks[chunk_idx]
|
|
local_flip_gain = xp.abs((1.0 - 2.0 * x[idxs]) * Qx[idxs]) + 1e-9
|
|
var_prob = local_flip_gain / xp.sum(local_flip_gain)
|
|
i = int(rng.choice(idxs, p=var_prob))
|
|
else:
|
|
i = int(rng.integers(0, n))
|
|
|
|
d = 1.0 - 2.0 * x[i]
|
|
delta = 2.0 * d * Qx[i] + Q[i, i]
|
|
if delta < 0.0 or rng.random() < math.exp(-delta / max(temp, 1e-12)):
|
|
x[i] = 1.0 - x[i]
|
|
Qx += d * Q[:, i]
|
|
current_energy += delta
|
|
if current_energy < best_energy:
|
|
best_energy = current_energy
|
|
if first_hit_step is None and math.isclose(current_energy, optimum, abs_tol=1e-9):
|
|
first_hit_step = step + 1
|
|
elapsed = time.perf_counter() - t0
|
|
return RunStats(
|
|
best_energy=best_energy,
|
|
hit_optimum=math.isclose(best_energy, optimum, abs_tol=1e-9),
|
|
first_hit_step=first_hit_step,
|
|
elapsed_s=elapsed,
|
|
)
|
|
|
|
|
|
def summarize_runs(runs: List[RunStats], optimum: float) -> Dict[str, object]:
|
|
hit_steps = [r.first_hit_step for r in runs if r.first_hit_step is not None]
|
|
return {
|
|
"trials": len(runs),
|
|
"optimum_energy": optimum,
|
|
"hit_rate": sum(r.hit_optimum for r in runs) / max(len(runs), 1),
|
|
"avg_best_energy": float(xp.mean([r.best_energy for r in runs])),
|
|
"median_best_energy": float(xp.median([r.best_energy for r in runs])),
|
|
"avg_gap_to_optimum": float(xp.mean([r.best_energy - optimum for r in runs])),
|
|
"median_first_hit_step": int(xp.median(hit_steps)) if hit_steps else None,
|
|
"avg_elapsed_s": float(xp.mean([r.elapsed_s for r in runs])),
|
|
}
|
|
|
|
|
|
def main() -> int:
|
|
ap = argparse.ArgumentParser(description="Benchmark chunk-field-guided SA on a dense QUBO")
|
|
ap.add_argument("--n", type=int, default=20, help="QUBO variable count (exact solve grows as 2^n)")
|
|
ap.add_argument("--seed", type=int, default=1337)
|
|
ap.add_argument("--trials", type=int, default=48)
|
|
ap.add_argument("--steps", type=int, default=6000)
|
|
ap.add_argument("--chunk-size", type=int, default=4)
|
|
ap.add_argument("--guided-prob", type=float, default=0.8)
|
|
ap.add_argument("--recompute-every", type=int, default=32)
|
|
ap.add_argument("--temp-start", type=float, default=3.0)
|
|
ap.add_argument("--temp-end", type=float, default=0.02)
|
|
ap.add_argument(
|
|
"--out",
|
|
type=Path,
|
|
default=Path("5-Applications/out/qubo_chunk_benchmark/spin_glass.json"),
|
|
)
|
|
args = ap.parse_args()
|
|
|
|
if args.n > 24:
|
|
raise SystemExit("Refusing exact solve above n=24 without a stronger backend.")
|
|
|
|
Q = generate_dense_qubo(args.n, seed=args.seed)
|
|
exact = exact_solve_qubo(Q)
|
|
optimum = float(exact["best_energy"])
|
|
|
|
baseline_runs: List[RunStats] = []
|
|
guided_runs: List[RunStats] = []
|
|
for trial in range(args.trials):
|
|
trial_seed = args.seed + 1000 + trial
|
|
baseline_runs.append(
|
|
run_baseline_sa(
|
|
Q=Q,
|
|
optimum=optimum,
|
|
steps=args.steps,
|
|
seed=trial_seed,
|
|
temp_start=args.temp_start,
|
|
temp_end=args.temp_end,
|
|
)
|
|
)
|
|
guided_runs.append(
|
|
run_chunk_guided_sa(
|
|
Q=Q,
|
|
optimum=optimum,
|
|
steps=args.steps,
|
|
seed=trial_seed,
|
|
temp_start=args.temp_start,
|
|
temp_end=args.temp_end,
|
|
chunk_size=args.chunk_size,
|
|
guided_prob=args.guided_prob,
|
|
recompute_every=args.recompute_every,
|
|
)
|
|
)
|
|
|
|
result = {
|
|
"schema_version": "qubo.chunk.field.benchmark.v1",
|
|
"params": {
|
|
"n": args.n,
|
|
"seed": args.seed,
|
|
"trials": args.trials,
|
|
"steps": args.steps,
|
|
"chunk_size": args.chunk_size,
|
|
"guided_prob": args.guided_prob,
|
|
"recompute_every": args.recompute_every,
|
|
"temp_start": args.temp_start,
|
|
"temp_end": args.temp_end,
|
|
},
|
|
"exact": exact,
|
|
"baseline": summarize_runs(baseline_runs, optimum),
|
|
"chunk_guided": summarize_runs(guided_runs, optimum),
|
|
}
|
|
result["delta"] = {
|
|
"hit_rate_gain": result["chunk_guided"]["hit_rate"] - result["baseline"]["hit_rate"],
|
|
"avg_gap_reduction": result["baseline"]["avg_gap_to_optimum"] - result["chunk_guided"]["avg_gap_to_optimum"],
|
|
"median_first_hit_step_gain": (
|
|
None
|
|
if result["baseline"]["median_first_hit_step"] is None
|
|
or result["chunk_guided"]["median_first_hit_step"] is None
|
|
else result["baseline"]["median_first_hit_step"] - result["chunk_guided"]["median_first_hit_step"]
|
|
),
|
|
}
|
|
|
|
args.out.parent.mkdir(parents=True, exist_ok=True)
|
|
with open(args.out, "w", encoding="utf-8") as fh:
|
|
json.dump(result, fh, indent=2)
|
|
|
|
print(json.dumps({
|
|
"exact_best_energy": optimum,
|
|
"baseline": result["baseline"],
|
|
"chunk_guided": result["chunk_guided"],
|
|
"delta": result["delta"],
|
|
"out": str(args.out.resolve()),
|
|
}, indent=2))
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|