Skip to content

Commit d5cde76

Browse files
committed
fix(pa): track best per sweep, reject heating, fix CI ruff format
Three fixes following a textbook-correctness audit of population_annealing (see tasks/test/audit_pa_textbook.py, 11/11 passes): * Track best_obj after every MCMC sweep, not just at temperature-step boundaries. Matches qqa.simulated_annealing semantics; previously PA could miss low-energy transients within a K-sweep batch and report a worse best_obj than SA on the same trajectory (PA -45 vs SA -48 in the Δβ=0 head-to-head test on 20-node ER MaxCut). * Reject heating schedules (beta_end < beta_start) at the API boundary with ValueError; PA reweighting requires non-decreasing β, otherwise it silently up-weights the wrong replicas. * Re-format tests/test_gui_apptest.py to match ruff's CI-scope invocation. The previous push passed ``ruff format --check .`` locally but failed CI's ``ruff format --check src tests scripts app`` after a follow-up edit slipped through unformatted (CI run 24631948353).
1 parent 3a49e72 commit d5cde76

2 files changed

Lines changed: 18 additions & 9 deletions

File tree

src/qqa/pa.py

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,15 @@ def population_annealing(
180180
raise ValueError(f"history_stride must be >= 1, got {history_stride}.")
181181
if resample not in ("systematic", "multinomial"):
182182
raise ValueError(f"resample must be 'systematic' or 'multinomial', got {resample!r}.")
183+
# PA is only well-defined for non-decreasing β: reweighting by exp(-Δβ E)
184+
# with Δβ < 0 would up-weight high-energy replicas, which is the opposite
185+
# of what PA wants. Reject upfront rather than silently skipping resampling.
186+
if beta_end < beta_start:
187+
raise ValueError(
188+
f"Population annealing requires beta_end >= beta_start, got "
189+
f"beta_start={beta_start}, beta_end={beta_end}. Use SA if you "
190+
"actually want a heating schedule."
191+
)
183192

184193
require_cuda_if_requested(device)
185194
device = torch.device(device) if isinstance(device, str) else device
@@ -252,12 +261,14 @@ def population_annealing(
252261
x = _qubo_glauber_sweep(x, q_sym, q_diag, beta, rng)
253262
else:
254263
x = _seq_mh_sweep(x, problem, beta, num_vars, is_spin, rng)
255-
loss_curr = problem.loss_fn(x)
256-
257-
min_val, min_idx = torch.min(loss_curr, dim=0)
258-
if min_val.item() < best_obj:
259-
best_obj = float(min_val.item())
260-
best_sol = x[int(min_idx.item())].detach().clone()
264+
# Track best after EVERY sweep (matches SA semantics): a low-
265+
# energy transient that vanishes by the end of the K-sweep
266+
# batch would otherwise be invisible to ``best_obj``.
267+
loss_curr = problem.loss_fn(x)
268+
min_val, min_idx = torch.min(loss_curr, dim=0)
269+
if min_val.item() < best_obj:
270+
best_obj = float(min_val.item())
271+
best_sol = x[int(min_idx.item())].detach().clone()
261272

262273
if record_history and (step % history_stride == 0 or step == num_temps - 1):
263274
history["loss_mean"].append(float(loss_curr.mean().item()))

tests/test_gui_apptest.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -409,9 +409,7 @@ def test_compare_page_shootout_mode_runs_pqqa_vs_sa_vs_pa():
409409
# since the v0.5.x rename — match by substring rather than literal.
410410
radios = [r for r in at.sidebar.radio if "Compare mode" in r.label]
411411
assert radios, "Compare mode radio missing"
412-
shootout_options = [
413-
opt for opt in radios[0].options if "shootout" in opt.lower()
414-
]
412+
shootout_options = [opt for opt in radios[0].options if "shootout" in opt.lower()]
415413
assert shootout_options, f"shootout option missing; got {radios[0].options!r}"
416414
radios[0].set_value(shootout_options[0])
417415
at.run()

0 commit comments

Comments
 (0)