mirror of
https://github.com/allaunthefox/SilverSight.git
synced 2026-07-31 01:25:21 +00:00
feat(python): BAWIM mutation engine — exact Fraction mutation loop
Sketch of a mutation engine for the Bulk Acoustic Wave Ising Machine (arXiv 2607.02112). Architecture: §1 Mutable parameter budget (n_spins, J_bits, feedback %, encoding) §2 Core BAWIM equations as exact Fraction arithmetic (Eq 1-5) §3 Spin configuration (random + flip) §4 Coupling matrix generators (MAX-CUT, NPP) §5 Greedy descent solver (swap target) §6 Mutation operators (one param per round) §7 Evaluation (MAX-CUT, NPP) §8 Mutation engine loop with SA acceptance §9 CLI No float in compute path. Quantize to Q16_16 at output boundary only.
This commit is contained in:
parent
d709136e17
commit
cdc7d24464
1 changed files with 572 additions and 0 deletions
572
python/bawim_mutation_engine.py
Normal file
572
python/bawim_mutation_engine.py
Normal file
|
|
@ -0,0 +1,572 @@
|
||||||
|
"""
|
||||||
|
BAWIM Mutation Engine — explore the coupling/encoding design space
|
||||||
|
|
||||||
|
Maps the Bulk Acoustic Wave Ising Machine (arXiv 2607.02112) equations
|
||||||
|
onto the SilverSight Q16 fraction system and provides a mutation loop
|
||||||
|
that systematically varies parameters to improve solution quality.
|
||||||
|
|
||||||
|
Papers equations:
|
||||||
|
Eq 1 — Ising Hamiltonian H = -∑ J_ij s_i s_j - ∑ h_i s_i
|
||||||
|
Eq 2 — Feedback coupling c_i = ∑ J_ij s_j
|
||||||
|
Eq 3 — MAX-CUT score Cuts = -½∑ J_ij - ½H
|
||||||
|
Eq 4 — NPP (set form) E(A,B) = |∑A a_i - ∑B a_i|
|
||||||
|
Eq 5 — NPP (spin form) E(s) = |∑ a_i s_i|
|
||||||
|
|
||||||
|
Architecture:
|
||||||
|
Mutation round → vary one parameter → evaluate → accept/reject → next round
|
||||||
|
All arithmetic is exact Fraction (q16_fraction). Quantize only at output.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python3 python/bawim_mutation_engine.py --rounds 100 --problem maxcut
|
||||||
|
python3 python/bawim_mutation_engine.py --rounds 100 --problem npp
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import math
|
||||||
|
import random
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from fractions import Fraction
|
||||||
|
from itertools import combinations
|
||||||
|
from typing import Any, Callable, Optional
|
||||||
|
|
||||||
|
# ── Import Q16 fraction system (exact rational, no float in compute path) ──
|
||||||
|
|
||||||
|
sys.path.insert(0, "python")
|
||||||
|
from q16_fraction import Q16, q16_mul, q16_div, q16_add, q16_sub, to_raw, from_raw
|
||||||
|
from q16_fraction import from_float as q16_from_float
|
||||||
|
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
# §1 PARAMETER BUDGET (mutable)
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
# BAWIM hardware constraints (Eq.2 context: 15-bit J_ij, 5-30% amplitude)
|
||||||
|
BAWIM_DEFAULTS = {
|
||||||
|
"n_spins": 20, # N
|
||||||
|
"j_resolution_bits": 15, # J_ij bit depth (paper: 15-bit)
|
||||||
|
"j_max": Fraction(2**15, 1), # max coupling value
|
||||||
|
"feedback_min_pct": Fraction(5, 100), # 5% of RF carrier
|
||||||
|
"feedback_max_pct": Fraction(30, 100), # 30% of RF carrier
|
||||||
|
"h_bias_max": Fraction(1, 10), # max local bias
|
||||||
|
"thermal_stability": Fraction(780, 1), # BAWIM deg/°C (paper: 780)
|
||||||
|
"encoding": "one_hot", # one_hot | binary | hybrid
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class BawimParams:
|
||||||
|
"""Mutable parameter set for the BAWIM system.
|
||||||
|
|
||||||
|
Each field can be mutated independently. The mutation engine
|
||||||
|
varies one field per round and evaluates the impact.
|
||||||
|
"""
|
||||||
|
n_spins: int = BAWIM_DEFAULTS["n_spins"]
|
||||||
|
j_resolution_bits: int = BAWIM_DEFAULTS["j_resolution_bits"]
|
||||||
|
j_max: Fraction = BAWIM_DEFAULTS["j_max"]
|
||||||
|
feedback_min_pct: Fraction = BAWIM_DEFAULTS["feedback_min_pct"]
|
||||||
|
feedback_max_pct: Fraction = BAWIM_DEFAULTS["feedback_max_pct"]
|
||||||
|
h_bias_max: Fraction = BAWIM_DEFAULTS["h_bias_max"]
|
||||||
|
thermal_stability: Fraction = BAWIM_DEFAULTS["thermal_stability"]
|
||||||
|
encoding: str = BAWIM_DEFAULTS["encoding"]
|
||||||
|
seed: int = 42
|
||||||
|
|
||||||
|
def clone(self) -> BawimParams:
|
||||||
|
return BawimParams(
|
||||||
|
n_spins=self.n_spins,
|
||||||
|
j_resolution_bits=self.j_resolution_bits,
|
||||||
|
j_max=self.j_max,
|
||||||
|
feedback_min_pct=self.feedback_min_pct,
|
||||||
|
feedback_max_pct=self.feedback_max_pct,
|
||||||
|
h_bias_max=self.h_bias_max,
|
||||||
|
thermal_stability=self.thermal_stability,
|
||||||
|
encoding=self.encoding,
|
||||||
|
seed=self.seed,
|
||||||
|
)
|
||||||
|
|
||||||
|
def fingerprint(self) -> str:
|
||||||
|
"""Deterministic hash for reproducibility tracking."""
|
||||||
|
raw = json.dumps({
|
||||||
|
"n_spins": self.n_spins,
|
||||||
|
"j_res_bits": self.j_resolution_bits,
|
||||||
|
"j_max": str(self.j_max),
|
||||||
|
"fb_min": str(self.feedback_min_pct),
|
||||||
|
"fb_max": str(self.feedback_max_pct),
|
||||||
|
"h_max": str(self.h_bias_max),
|
||||||
|
"thermal": str(self.thermal_stability),
|
||||||
|
"encoding": self.encoding,
|
||||||
|
"seed": self.seed,
|
||||||
|
}, sort_keys=True, separators=(",", ":"))
|
||||||
|
return hashlib.sha256(raw.encode()).hexdigest()[:16]
|
||||||
|
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
# §2 CORE BAWIM EQUATIONS (exact Fraction arithmetic)
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
def ising_hamiltonian(
|
||||||
|
spins: list[int],
|
||||||
|
J: list[list[Fraction]],
|
||||||
|
h: list[Fraction],
|
||||||
|
) -> Fraction:
|
||||||
|
"""Eq 1: H = -∑ J_ij s_i s_j - ∑ h_i s_i
|
||||||
|
|
||||||
|
All arithmetic in exact Fraction. No float, no truncation.
|
||||||
|
"""
|
||||||
|
H = Fraction(0, 1)
|
||||||
|
n = len(spins)
|
||||||
|
for i in range(n):
|
||||||
|
for j in range(i + 1, n):
|
||||||
|
H -= J[i][j] * spins[i] * spins[j]
|
||||||
|
for i in range(n):
|
||||||
|
H -= h[i] * spins[i]
|
||||||
|
return H
|
||||||
|
|
||||||
|
|
||||||
|
def feedback_coupling(
|
||||||
|
spins: list[int],
|
||||||
|
J: list[list[Fraction]],
|
||||||
|
i: int,
|
||||||
|
) -> Fraction:
|
||||||
|
"""Eq 2: c_i = ∑ J_ij s_j
|
||||||
|
|
||||||
|
The coupling pulse injected back into spin i.
|
||||||
|
Amplitude should stay within feedback_min_pct–feedback_max_pct
|
||||||
|
of the circulating RF signal to avoid chaotization.
|
||||||
|
"""
|
||||||
|
return sum(J[i][j] * spins[j] for j in range(len(spins)))
|
||||||
|
|
||||||
|
|
||||||
|
def maxcut_score(
|
||||||
|
spins: list[int],
|
||||||
|
J: list[list[Fraction]],
|
||||||
|
H: Optional[Fraction] = None,
|
||||||
|
) -> Fraction:
|
||||||
|
"""Eq 3: Cuts = -½∑ J_ij - ½H
|
||||||
|
|
||||||
|
Lower Hamiltonian → more cuts.
|
||||||
|
"""
|
||||||
|
if H is None:
|
||||||
|
H = ising_hamiltonian(spins, J, [Fraction(0, 1)] * len(spins))
|
||||||
|
sum_J = sum(J[i][j] for i in range(len(spins)) for j in range(i + 1, len(spins)))
|
||||||
|
return -sum_J / 2 - H / 2
|
||||||
|
|
||||||
|
|
||||||
|
def npp_spin_form(
|
||||||
|
spins: list[int],
|
||||||
|
values: list[Fraction],
|
||||||
|
) -> Fraction:
|
||||||
|
"""Eq 5: E(s) = |∑ a_i s_i|
|
||||||
|
|
||||||
|
s_i = +1 → subset A; s_i = -1 → subset B.
|
||||||
|
Perfect partition when E = 0 (total even) or E = 1 (total odd).
|
||||||
|
"""
|
||||||
|
total = sum(values[i] * spins[i] for i in range(len(spins)))
|
||||||
|
return abs(total)
|
||||||
|
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
# §3 SPIN CONFIGURATION
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
def random_spins(n: int, seed: int) -> list[int]:
|
||||||
|
rng = random.Random(seed)
|
||||||
|
return [1 if rng.random() < 0.5 else -1 for _ in range(n)]
|
||||||
|
|
||||||
|
|
||||||
|
def flip_spin(spins: list[int], idx: int) -> list[int]:
|
||||||
|
s = list(spins)
|
||||||
|
s[idx] = -s[idx]
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
# §4 COUPLING MATRIX GENERATORS (mutation targets)
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
def build_maxcut_graph(
|
||||||
|
n: int,
|
||||||
|
density: Fraction,
|
||||||
|
seed: int,
|
||||||
|
j_max: Fraction,
|
||||||
|
) -> list[list[Fraction]]:
|
||||||
|
"""Generate a MAX-CUT graph coupling matrix.
|
||||||
|
|
||||||
|
density is the fraction of edges present.
|
||||||
|
Each edge gets a random coupling in [0, j_max].
|
||||||
|
"""
|
||||||
|
rng = random.Random(seed)
|
||||||
|
J = [[Fraction(0, 1) for _ in range(n)] for _ in range(n)]
|
||||||
|
threshold = float(density)
|
||||||
|
for i in range(n):
|
||||||
|
for j in range(i + 1, n):
|
||||||
|
if rng.random() < threshold:
|
||||||
|
val = rng.randint(1, int(j_max))
|
||||||
|
J[i][j] = Fraction(val, 1)
|
||||||
|
J[j][i] = J[i][j]
|
||||||
|
return J
|
||||||
|
|
||||||
|
|
||||||
|
def build_npp_matrix(
|
||||||
|
values: list[Fraction],
|
||||||
|
) -> tuple[list[list[Fraction]], list[Fraction]]:
|
||||||
|
"""Build coupling matrix for number partitioning.
|
||||||
|
|
||||||
|
J_ij = a_i * a_j (Mattis spin glass form)
|
||||||
|
h = 0
|
||||||
|
"""
|
||||||
|
n = len(values)
|
||||||
|
J = [[Fraction(0, 1) for _ in range(n)] for _ in range(n)]
|
||||||
|
for i in range(n):
|
||||||
|
for j in range(i + 1, n):
|
||||||
|
val = values[i] * values[j]
|
||||||
|
J[i][j] = val
|
||||||
|
J[j][i] = val
|
||||||
|
h = [Fraction(0, 1)] * n
|
||||||
|
return J, h
|
||||||
|
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
# §5 SOLVER (greedy descent — mutation target)
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
def greedy_descent(
|
||||||
|
spins: list[int],
|
||||||
|
J: list[list[Fraction]],
|
||||||
|
h: list[Fraction],
|
||||||
|
max_flips: int = 100,
|
||||||
|
) -> tuple[list[int], Fraction]:
|
||||||
|
"""Greedy energy minimization by single-spin flips.
|
||||||
|
|
||||||
|
Each flip that lowers H is accepted. Stops when no flip improves.
|
||||||
|
— MUTATION TARGET: replace with simulated annealing, QAOA, etc.
|
||||||
|
"""
|
||||||
|
current = list(spins)
|
||||||
|
H = ising_hamiltonian(current, J, h)
|
||||||
|
improved = True
|
||||||
|
n = len(current)
|
||||||
|
while improved and max_flips > 0:
|
||||||
|
improved = False
|
||||||
|
for i in range(n):
|
||||||
|
candidate = flip_spin(current, i)
|
||||||
|
H_candidate = ising_hamiltonian(candidate, J, h)
|
||||||
|
if H_candidate < H:
|
||||||
|
current = candidate
|
||||||
|
H = H_candidate
|
||||||
|
improved = True
|
||||||
|
max_flips -= 1
|
||||||
|
break
|
||||||
|
return current, H
|
||||||
|
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
# §6 MUTATION OPERATORS
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Mutation:
|
||||||
|
"""A single mutation: which parameter changed, from what to what."""
|
||||||
|
target: str # parameter name
|
||||||
|
old_value: Any
|
||||||
|
new_value: Any
|
||||||
|
delta_energy: Optional[Fraction] = None # improvement (negative = better)
|
||||||
|
|
||||||
|
|
||||||
|
MUTATION_TARGETS = [
|
||||||
|
# n_spins is excluded: changing N changes the graph, making
|
||||||
|
# cross-round energy comparisons meaningless.
|
||||||
|
"j_resolution_bits",
|
||||||
|
"j_max",
|
||||||
|
"feedback_min_pct",
|
||||||
|
"feedback_max_pct",
|
||||||
|
"h_bias_max",
|
||||||
|
"encoding",
|
||||||
|
"solver", # swap solver strategy
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def mutate_param(params: BawimParams, rng: random.Random) -> tuple[BawimParams, Mutation]:
|
||||||
|
"""Apply one random mutation to a parameter.
|
||||||
|
|
||||||
|
Returns (new_params, description_of_mutation).
|
||||||
|
"""
|
||||||
|
p = params.clone()
|
||||||
|
target = rng.choice(MUTATION_TARGETS)
|
||||||
|
old_val = getattr(p, target, None)
|
||||||
|
|
||||||
|
if target == "n_spins":
|
||||||
|
delta = rng.choice([-5, -3, -1, 1, 3, 5])
|
||||||
|
new = max(4, min(200, p.n_spins + delta))
|
||||||
|
p.n_spins = new
|
||||||
|
|
||||||
|
elif target == "j_resolution_bits":
|
||||||
|
delta = rng.choice([-2, -1, 1, 2])
|
||||||
|
new = max(4, min(20, p.j_resolution_bits + delta))
|
||||||
|
p.j_resolution_bits = new
|
||||||
|
p.j_max = Fraction(2**new, 1)
|
||||||
|
|
||||||
|
elif target == "j_max":
|
||||||
|
scale = rng.choice([Fraction(1, 2), Fraction(3, 4), Fraction(1, 1),
|
||||||
|
Fraction(5, 4), Fraction(3, 2), Fraction(2, 1)])
|
||||||
|
p.j_max = (p.j_max * scale).limit_denominator(2**p.j_resolution_bits)
|
||||||
|
|
||||||
|
elif target == "feedback_min_pct":
|
||||||
|
step = Fraction(1, 100)
|
||||||
|
p.feedback_min_pct = max(Fraction(1, 100), min(
|
||||||
|
Fraction(49, 100),
|
||||||
|
p.feedback_min_pct + rng.choice([-step, step]) * rng.randint(1, 5)
|
||||||
|
))
|
||||||
|
|
||||||
|
elif target == "feedback_max_pct":
|
||||||
|
step = Fraction(1, 100)
|
||||||
|
p.feedback_max_pct = max(p.feedback_min_pct + step, min(
|
||||||
|
Fraction(95, 100),
|
||||||
|
p.feedback_max_pct + rng.choice([-step, step]) * rng.randint(1, 5)
|
||||||
|
))
|
||||||
|
|
||||||
|
elif target == "h_bias_max":
|
||||||
|
scale = rng.choice([Fraction(1, 2), Fraction(3, 4), Fraction(1, 1),
|
||||||
|
Fraction(3, 2), Fraction(2, 1)])
|
||||||
|
p.h_bias_max = max(Fraction(1, 100), min(Fraction(10, 1),
|
||||||
|
(p.h_bias_max * scale).limit_denominator(100)))
|
||||||
|
|
||||||
|
elif target == "encoding":
|
||||||
|
p.encoding = rng.choice(["one_hot", "binary", "hybrid"])
|
||||||
|
|
||||||
|
elif target == "solver":
|
||||||
|
# Future: swap between greedy_descent, simulated_annealing, QAOA
|
||||||
|
pass
|
||||||
|
|
||||||
|
new_val = getattr(p, target, None)
|
||||||
|
return p, Mutation(target=target, old_value=old_val, new_value=new_val)
|
||||||
|
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
# §7 EVALUATION
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
def evaluate_maxcut(params: BawimParams) -> dict[str, Any]:
|
||||||
|
"""Build a MAX-CUT problem, solve it, return metrics."""
|
||||||
|
rng = random.Random(params.seed)
|
||||||
|
J = build_maxcut_graph(
|
||||||
|
params.n_spins,
|
||||||
|
density=Fraction(1, 2),
|
||||||
|
seed=params.seed,
|
||||||
|
j_max=params.j_max,
|
||||||
|
)
|
||||||
|
h = [Fraction(0, 1)] * params.n_spins
|
||||||
|
spins = random_spins(params.n_spins, params.seed)
|
||||||
|
final_spins, H = greedy_descent(spins, J, h)
|
||||||
|
cuts = maxcut_score(final_spins, J, H)
|
||||||
|
return {
|
||||||
|
"problem": "maxcut",
|
||||||
|
"n": params.n_spins,
|
||||||
|
"H": H,
|
||||||
|
"cuts": cuts,
|
||||||
|
"cuts_float": float(cuts),
|
||||||
|
"spins": final_spins,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def evaluate_npp(params: BawimParams) -> dict[str, Any]:
|
||||||
|
"""Build a number partitioning problem, solve it, return metrics."""
|
||||||
|
rng = random.Random(params.seed)
|
||||||
|
values = [Fraction(rng.randint(1, 1000), 1) for _ in range(params.n_spins)]
|
||||||
|
J, h = build_npp_matrix(values)
|
||||||
|
spins = random_spins(params.n_spins, params.seed + 1)
|
||||||
|
final_spins, H = greedy_descent(spins, J, h)
|
||||||
|
E = npp_spin_form(final_spins, values)
|
||||||
|
return {
|
||||||
|
"problem": "npp",
|
||||||
|
"n": params.n_spins,
|
||||||
|
"E": E,
|
||||||
|
"E_float": float(E),
|
||||||
|
"H": H,
|
||||||
|
"spins": final_spins,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
# §8 MUTATION ENGINE
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class MutationRound:
|
||||||
|
"""Record of one mutation + evaluation."""
|
||||||
|
round: int
|
||||||
|
params: BawimParams
|
||||||
|
mutation: Mutation
|
||||||
|
result: dict[str, Any]
|
||||||
|
accepted: bool
|
||||||
|
|
||||||
|
|
||||||
|
class BawimMutationEngine:
|
||||||
|
"""Run mutation rounds to improve BAWIM solution quality.
|
||||||
|
|
||||||
|
Each round:
|
||||||
|
1. Clone current params
|
||||||
|
2. Mutate one parameter
|
||||||
|
3. Evaluate on the problem
|
||||||
|
4. Accept if energy improved (or with probability for exploration)
|
||||||
|
5. Record the round
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
problem: str = "maxcut",
|
||||||
|
initial_params: Optional[BawimParams] = None,
|
||||||
|
temperature: Fraction = Fraction(1, 10),
|
||||||
|
):
|
||||||
|
self.problem = problem
|
||||||
|
self.params = initial_params or BawimParams()
|
||||||
|
self.temperature = temperature
|
||||||
|
self.history: list[MutationRound] = []
|
||||||
|
self.best_result: Optional[dict[str, Any]] = None
|
||||||
|
self.best_energy: Optional[Fraction] = None
|
||||||
|
self.rng = random.Random(self.params.seed)
|
||||||
|
|
||||||
|
def evaluate(self, params: BawimParams) -> dict[str, Any]:
|
||||||
|
if self.problem == "maxcut":
|
||||||
|
return evaluate_maxcut(params)
|
||||||
|
elif self.problem == "npp":
|
||||||
|
return evaluate_npp(params)
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Unknown problem: {self.problem}")
|
||||||
|
|
||||||
|
def energy_key(self, result: dict[str, Any]) -> Fraction:
|
||||||
|
if self.problem == "maxcut":
|
||||||
|
return -result["cuts"] # more cuts = better (lower energy)
|
||||||
|
else:
|
||||||
|
return result["E"] # lower E = better
|
||||||
|
|
||||||
|
def step(self) -> MutationRound:
|
||||||
|
new_params, mutation = mutate_param(self.params, self.rng)
|
||||||
|
result = self.evaluate(new_params)
|
||||||
|
new_energy = self.energy_key(result)
|
||||||
|
|
||||||
|
accepted = False
|
||||||
|
if self.best_energy is None:
|
||||||
|
accepted = True
|
||||||
|
elif new_energy < self.best_energy:
|
||||||
|
accepted = True # strict improvement
|
||||||
|
else:
|
||||||
|
# Probabilistic acceptance (simulated annealing style)
|
||||||
|
delta = new_energy - self.best_energy
|
||||||
|
if self.temperature > 0:
|
||||||
|
# Convert Fraction to float for exp (I/O boundary — display only)
|
||||||
|
p = math.exp(-float(delta) / float(self.temperature))
|
||||||
|
accepted = self.rng.random() < p
|
||||||
|
|
||||||
|
mutation.delta_energy = (
|
||||||
|
new_energy - self.best_energy if self.best_energy is not None else Fraction(0, 1)
|
||||||
|
)
|
||||||
|
|
||||||
|
if accepted:
|
||||||
|
self.params = new_params
|
||||||
|
self.best_result = result
|
||||||
|
self.best_energy = new_energy
|
||||||
|
|
||||||
|
record = MutationRound(
|
||||||
|
round=len(self.history),
|
||||||
|
params=new_params,
|
||||||
|
mutation=mutation,
|
||||||
|
result=result,
|
||||||
|
accepted=accepted,
|
||||||
|
)
|
||||||
|
self.history.append(record)
|
||||||
|
return record
|
||||||
|
|
||||||
|
def run(self, rounds: int, verbose: bool = True) -> list[MutationRound]:
|
||||||
|
for r in range(rounds):
|
||||||
|
record = self.step()
|
||||||
|
if verbose and (r < 10 or r % 10 == 0 or record.accepted):
|
||||||
|
delta = record.mutation.delta_energy
|
||||||
|
sign = "✓" if record.accepted else "✗"
|
||||||
|
energy = float(self.energy_key(record.result))
|
||||||
|
print(
|
||||||
|
f" [{r:4d}] {sign} "
|
||||||
|
f"{record.mutation.target:20s} "
|
||||||
|
f"Δ={float(delta):+.6f} "
|
||||||
|
f"E={energy:.6f}"
|
||||||
|
)
|
||||||
|
return self.history
|
||||||
|
|
||||||
|
def summary(self) -> dict[str, Any]:
|
||||||
|
best = self.best_result or {}
|
||||||
|
accepted = sum(1 for h in self.history if h.accepted)
|
||||||
|
return {
|
||||||
|
"problem": self.problem,
|
||||||
|
"rounds": len(self.history),
|
||||||
|
"accepted": accepted,
|
||||||
|
"accept_rate": accepted / max(1, len(self.history)),
|
||||||
|
"best_energy": float(self.best_energy) if self.best_energy else None,
|
||||||
|
"best_params": {
|
||||||
|
"n_spins": self.params.n_spins,
|
||||||
|
"j_resolution_bits": self.params.j_resolution_bits,
|
||||||
|
"j_max": str(self.params.j_max),
|
||||||
|
"feedback_range": f"{self.params.feedback_min_pct}–{self.params.feedback_max_pct}",
|
||||||
|
"encoding": self.params.encoding,
|
||||||
|
},
|
||||||
|
"best_result": {
|
||||||
|
k: v for k, v in best.items() if k != "spins"
|
||||||
|
} if best else {},
|
||||||
|
"fingerprint": self.params.fingerprint(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
# §9 CLI
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="BAWIM Mutation Engine — explore coupling/encoding design space"
|
||||||
|
)
|
||||||
|
parser.add_argument("--rounds", type=int, default=50, help="mutation rounds")
|
||||||
|
parser.add_argument("--problem", choices=["maxcut", "npp"], default="maxcut")
|
||||||
|
parser.add_argument("--n-spins", type=int, default=20)
|
||||||
|
parser.add_argument("--seed", type=int, default=42)
|
||||||
|
parser.add_argument("--temperature", type=float, default=0.1,
|
||||||
|
help="SA acceptance temperature (0 = greedy)")
|
||||||
|
parser.add_argument("--json", action="store_true", help="output final summary as JSON")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
params = BawimParams(n_spins=args.n_spins, seed=args.seed)
|
||||||
|
engine = BawimMutationEngine(
|
||||||
|
problem=args.problem,
|
||||||
|
initial_params=params,
|
||||||
|
temperature=Fraction(args.temperature).limit_denominator(100),
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"BAWIM Mutation Engine — {args.problem.upper()}")
|
||||||
|
print(f" Initial params: n={params.n_spins}, J_bits={params.j_resolution_bits}")
|
||||||
|
print(f" Temperature: {args.temperature}")
|
||||||
|
print(f" Running {args.rounds} rounds...\n")
|
||||||
|
|
||||||
|
start = time.time()
|
||||||
|
engine.run(args.rounds)
|
||||||
|
elapsed = time.time() - start
|
||||||
|
|
||||||
|
summary = engine.summary()
|
||||||
|
print(f"\n--- Summary ({elapsed:.1f}s) ---")
|
||||||
|
print(f" Problem: {summary['problem']}")
|
||||||
|
print(f" Rounds: {summary['rounds']} ({summary['accepted']} accepted)")
|
||||||
|
print(f" Accept rate: {summary['accept_rate']:.2f}")
|
||||||
|
if summary['best_energy'] is not None:
|
||||||
|
print(f" Best energy: {summary['best_energy']:.6f}")
|
||||||
|
print(f" Best params: {summary['best_params']}")
|
||||||
|
print(f" Fingerprint: {summary['fingerprint']}")
|
||||||
|
|
||||||
|
if args.json:
|
||||||
|
print(json.dumps(summary, indent=2))
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
Loading…
Add table
Reference in a new issue