Files
foxhunt/crates/ml-hyperopt/src/observer.rs
jgrusewski 9b4395b0df fix: trial budget observer — shared AtomicUsize counter enforces trial limit
increment_trial() was never called — budget enforcement was broken because
TrialBudgetObserver had its own internal Arc<AtomicUsize> disconnected from
the trial_counter used by evaluate_point() and cost(). With num_trials=2,
PSO ran 20 particle evaluations instead of stopping at 2.

Now TrialBudgetObserver::new() takes the caller-owned Arc<AtomicUsize> so
all evaluation paths (LHS via evaluate_point + PSO via cost()) share one
counter. ObjectiveFunction::cost() and ParallelObjectiveFunction::cost()
increment trial_counter directly (fetch_add SeqCst) and check budget inline;
observe_iter() reads the same counter to terminate PSO between iterations.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 01:18:19 +01:00

59 lines
1.8 KiB
Rust

use argmin::core::observers::Observe;
use argmin::core::{Error, State};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
/// Observer that enforces strict trial budget limits
///
/// Argmin's PSO does not reliably respect max_iters when set to small values (e.g., 1).
/// This observer shares the same `Arc<AtomicUsize>` counter that the cost function and
/// LHS evaluation phase use, so every evaluation (LHS + PSO) counts toward the budget.
#[derive(Clone, Debug)]
pub struct TrialBudgetObserver {
max_trials: usize,
trial_counter: Arc<AtomicUsize>,
}
impl TrialBudgetObserver {
/// Create a new observer that shares the caller-owned trial counter.
///
/// The same `trial_counter` must be passed to the cost function and incremented
/// on every evaluation (LHS and PSO) so the budget is accurately tracked.
pub fn new(max_trials: usize, trial_counter: Arc<AtomicUsize>) -> Self {
Self {
max_trials,
trial_counter,
}
}
pub fn should_terminate(&self) -> bool {
self.trial_counter.load(Ordering::SeqCst) >= self.max_trials
}
pub fn get_trials_used(&self) -> usize {
self.trial_counter.load(Ordering::SeqCst)
}
}
impl<I> Observe<I> for TrialBudgetObserver
where
I: State,
{
fn observe_iter(&mut self, _state: &I, _kv: &argmin::core::KV) -> Result<(), Error> {
let used = self.trial_counter.load(Ordering::SeqCst);
if used >= self.max_trials {
tracing::warn!(
"Trial budget exhausted: {}/{} trials used. Terminating optimization.",
used,
self.max_trials
);
return Err(Error::msg(format!(
"Trial budget exhausted: {used}/{}",
self.max_trials
)));
}
Ok(())
}
}