refactor: LHS is the correct algorithm for small budgets, not a fallback

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
jgrusewski
2026-03-29 19:18:47 +02:00
parent 3e1f3fd0cc
commit 8a42af0bc6

View File

@@ -354,12 +354,15 @@ impl ArgminOptimizer {
let trials_used = trials.len();
let remaining_trials = self.max_trials.saturating_sub(trials_used);
// For small budgets (< 2× swarm size), PSO can't complete a full iteration.
// Fall back to additional LHS samples which give better space coverage.
// Algorithm selection by budget:
// - Budget < 2× swarm → LHS (space-filling, optimal for small budgets)
// - Budget ≥ 2× swarm → PSO (swarm optimization, needs full iterations)
// PSO with a partial swarm is worse than random — 75% of particles
// have no cost value, so gbest is computed from incomplete data.
if remaining_trials < self.n_particles * 2 {
info!(
"Budget too small for PSO ({} remaining < {} particles × 2). Using additional LHS.",
remaining_trials, self.n_particles
"Using LHS for remaining {} trials (budget < {} particles × 2 = need PSO budget ≥ {})",
remaining_trials, self.n_particles, self.n_particles * 2
);
let extra_samples = Self::latin_hypercube_sampling(remaining_trials, &bounds, &mut rng);
let mut model_guard = cost_fn.model.lock().map_err(|e| anyhow::anyhow!("lock: {e}"))?;
@@ -379,7 +382,7 @@ impl ArgminOptimizer {
let max_iters_by_budget =
((remaining_trials as f64) / (self.n_particles as f64)).ceil() as usize;
let max_iters = if remaining_trials < self.n_particles * 2 {
0 // Already handled by LHS fallback above
0 // LHS already handled above
} else {
max_iters_by_budget.min(self.max_iters_per_restart)
};