diff --git a/python/bawim_mutation_engine.py b/python/bawim_mutation_engine.py index 9644ba29..e0f3f4e1 100644 --- a/python/bawim_mutation_engine.py +++ b/python/bawim_mutation_engine.py @@ -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: