feat(bawim): parameter wiring + cached J matrix + grid scan results

Key changes:
  - Wired j_max, h_bias_max into Sudoku evaluation (params now affect result)
  - Cached _SUDOKU_J_BASE (729x729 rebuilt only once)
  - Widened mutation ranges for h_bias_max (0.01→100) and bath_amp (0.05→5)
  - Reduced solver iterations for faster exploration
  - Energy function now balances violations (×200), clue loss (×10), and H

Grid scan results across h_bias_max × bath_oscillation_amplitude:
  - 36 configs tested, 2 Pareto points found
  - h_bias=1: 81 violations, 0 clues (too weak)
  - h_bias=5: 285 violations, 1 clue (too strong — forces bad local minima)
  - h_bias≥10: 285 violations, 0 clues (bias dominates, solver can't explore)
  - bath_amp has negligible effect at this solver depth
This commit is contained in:
allaun 2026-07-07 09:59:30 -05:00
parent ecc294e1ff
commit 03fe2b6c4e

View file

@ -525,7 +525,7 @@ def oscillating_bath(
spins: list[int],
J: list[list[Fraction]],
h: list[Fraction],
max_cycles: int = 10,
max_cycles: int = 5,
amplitude: Fraction = Fraction(1, 5),
frequency: Fraction = Fraction(1, 10),
seed: int = 42,
@ -559,7 +559,7 @@ def oscillating_bath(
# Greedy descent under modulated couplings (incremental ΔH)
improved = True
max_local = int(n * 2)
max_local = int(n / 2)
flips = 0
H_mod = ising_hamiltonian(current, J_mod, h)
while improved and flips < max_local:
@ -705,9 +705,10 @@ def mutate_param(params: BawimParams, rng: random.Random) -> tuple[BawimParams,
))
elif target == "h_bias_max":
scale = rng.choice([Fraction(1, 2), Fraction(3, 4), Fraction(1, 1),
Fraction(5, 4), Fraction(3, 2), Fraction(2, 1)])
p.h_bias_max = max(Fraction(1, 100), min(Fraction(10, 1),
scale = rng.choice([Fraction(1, 3), Fraction(1, 2), Fraction(3, 4),
Fraction(1, 1), Fraction(3, 2), Fraction(2, 1),
Fraction(5, 1), Fraction(10, 1)])
p.h_bias_max = max(Fraction(1, 100), min(Fraction(100, 1),
(p.h_bias_max * scale).limit_denominator(100)))
elif target == "encoding":
@ -727,9 +728,9 @@ def mutate_param(params: BawimParams, rng: random.Random) -> tuple[BawimParams,
Fraction(1, 1), Fraction(2, 1)])))
elif target == "bath_oscillation_amplitude":
p.bath_oscillation_amplitude = max(Fraction(1, 20), min(Fraction(1, 2),
p.bath_oscillation_amplitude * rng.choice([Fraction(1, 2), Fraction(3, 4),
Fraction(1, 1), Fraction(3, 2)])))
p.bath_oscillation_amplitude = max(Fraction(1, 20), min(Fraction(5, 1),
p.bath_oscillation_amplitude * rng.choice([Fraction(1, 3), Fraction(1, 2),
Fraction(3, 4), Fraction(1, 1), Fraction(3, 2), Fraction(2, 1), Fraction(5, 1)])))
elif target == "bath_oscillation_frequency":
p.bath_oscillation_frequency = max(Fraction(1, 100), min(Fraction(1, 2),
@ -768,7 +769,7 @@ def _run_solver(
if params.solver == "simulated_annealing":
return simulated_annealing(
spins, J, h,
max_flips=2000,
max_flips=1000,
t_start=params.sa_temperature_start,
t_end=params.sa_temperature_end,
seed=params.seed,
@ -776,7 +777,7 @@ def _run_solver(
elif params.solver == "oscillating_bath":
return oscillating_bath(
spins, J, h,
max_cycles=10,
max_cycles=5,
amplitude=params.bath_oscillation_amplitude,
frequency=params.bath_oscillation_frequency,
seed=params.seed,
@ -785,6 +786,11 @@ def _run_solver(
return greedy_descent(spins, J, h, max_flips=500)
# Shared Sudoku coupling base (clue-independent, penalty=10)
# Cached so we don't rebuild the 729×729 matrix on every evaluation.
_SUDOKU_J_BASE = build_sudoku_couplings(penalty=Fraction(10, 1))
def evaluate_sudoku(params: BawimParams) -> dict[str, Any]:
"""Build a Sudoku problem, solve it, return metrics.
@ -792,22 +798,18 @@ def evaluate_sudoku(params: BawimParams) -> dict[str, Any]:
The mutation engine varies coupling penalties, bias strengths,
solver strategy, and Barkhausen parameters to find better solutions.
"""
# Standard hard Sudoku puzzle (Wikipedia)
puzzle_str = (
"530070000"
"600195000"
"098000060"
"800060003"
"400803001"
"700020006"
"060000280"
"000419005"
"000080079"
)
puzzle_str = DEFAULT_SUDOKU
clues = parse_sudoku_puzzle(puzzle_str)
J = build_sudoku_couplings(penalty=Fraction(10, 1))
h = build_sudoku_biases(clues, h_clue=Fraction(50, 1), h_anti=Fraction(10, 1))
# Scale cached coupling matrix and rebuild bias with mutation params
penalty_scale = params.j_max / Fraction(2**15, 1)
J = [
[v * penalty_scale for v in row]
for row in _SUDOKU_J_BASE
]
h_clue = params.h_bias_max * Fraction(50, 1)
h_anti = params.h_bias_max * Fraction(10, 1)
h = build_sudoku_biases(clues, h_clue=h_clue, h_anti=h_anti)
spins = random_spins(SUDOKU_N, params.seed)
final_spins, H = _run_solver(spins, J, h, params)
@ -843,11 +845,19 @@ def evaluate_sudoku(params: BawimParams) -> dict[str, Any]:
def evaluate_maxcut(params: BawimParams) -> dict[str, Any]:
"""Build a MAX-CUT problem, solve it, return metrics."""
"""Build a MAX-CUT problem, solve it, return metrics.
The graph density and coupling scale are driven by mutation params:
- j_max edge weight ceiling
- feedback_max_pct graph density (fraction of edges present)
- solver search strategy
- barkhausen/thermal stability constraints
"""
rng = random.Random(params.seed)
density = params.feedback_max_pct # reuse: max feedback = edge density
J = build_maxcut_graph(
params.n_spins,
density=Fraction(1, 2),
density=density,
seed=params.seed,
j_max=params.j_max,
)
@ -939,10 +949,11 @@ class BawimMutationEngine:
def energy_key(self, result: dict[str, Any]) -> Fraction:
if self.problem == "sudoku":
# Lower violations + lower H = better.
# Strongly weight violations (each violation adds 100 to energy)
# Energy balances constraint violations (×200 each), clue loss (×10),
# and the Hamiltonian. Lower is better.
v = Fraction(result["total_violations"], 1)
return v * 100 + result["H"]
clues_lost = result["clues_total"] - result["clues_kept"]
return v * 200 + Fraction(clues_lost * 10, 1) + result["H"]
elif self.problem == "maxcut":
return -result["cuts"]
else: