fix(hyperopt): LHS fallback when budget < 2× swarm size
With 10 trials and n_initial=5, PSO got 5 remaining evals for a 20-particle swarm. Only 5 of 20 particles were evaluated before the budget observer killed the run — 75% of the swarm had no cost value. PSO can't compute a proper gbest from partial data. Fix: when remaining_trials < n_particles × 2, skip PSO entirely and use additional LHS samples instead. LHS gives better space coverage than an incomplete swarm iteration. For 10 trials: 5 initial LHS + 5 additional LHS = 10 independent space-filling samples. Much better than 5 LHS + 5 broken PSO particles. PSO still runs when budget is sufficient (≥40 remaining for 20 particles). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -354,15 +354,35 @@ impl ArgminOptimizer {
|
||||
let trials_used = trials.len();
|
||||
let remaining_trials = self.max_trials.saturating_sub(trials_used);
|
||||
|
||||
// FIX: Each PSO iteration evaluates n_particles candidates (sequentially via mutex)
|
||||
// PSO evaluates ALL particles in swarm per iteration, so divide remaining budget
|
||||
// by swarm size to prevent trial count overflow (fixes 962 trial bug)
|
||||
// CRITICAL FIX (2025-11-07): Use CEILING division to ensure all trials complete
|
||||
// Example: 8 remaining ÷ 20 particles = 0.4 → ceil to 1 iteration (not 0)
|
||||
// For small budgets (< 2× swarm size), PSO can't complete a full iteration.
|
||||
// Fall back to additional LHS samples which give better space coverage.
|
||||
if remaining_trials < self.n_particles * 2 {
|
||||
info!(
|
||||
"Budget too small for PSO ({} remaining < {} particles × 2). Using additional LHS.",
|
||||
remaining_trials, self.n_particles
|
||||
);
|
||||
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}"))?;
|
||||
for i in 0..remaining_trials {
|
||||
let continuous_vec: Vec<f64> = extra_samples.row(i).to_vec();
|
||||
Self::evaluate_point(
|
||||
&continuous_vec,
|
||||
&mut *model_guard,
|
||||
&trial_results,
|
||||
&trial_counter,
|
||||
¶m_names,
|
||||
)?;
|
||||
}
|
||||
drop(model_guard);
|
||||
}
|
||||
|
||||
let max_iters_by_budget =
|
||||
((remaining_trials as f64) / (self.n_particles as f64)).ceil() as usize;
|
||||
|
||||
let max_iters = max_iters_by_budget.min(self.max_iters_per_restart);
|
||||
let max_iters = if remaining_trials < self.n_particles * 2 {
|
||||
0 // Already handled by LHS fallback above
|
||||
} else {
|
||||
max_iters_by_budget.min(self.max_iters_per_restart)
|
||||
};
|
||||
|
||||
info!(
|
||||
"PSO Budget: {} iterations ({} remaining trials ÷ {} particles = {} max iters)",
|
||||
|
||||
Reference in New Issue
Block a user