diff --git a/src/study.rs b/src/study.rs index 6009246..5705200 100644 --- a/src/study.rs +++ b/src/study.rs @@ -398,6 +398,58 @@ where // They could be stored in a separate list for debugging if needed } + /// Request a new trial with suggested parameters. + /// + /// This is the first half of the ask-and-tell interface. After calling + /// `ask()`, use parameter types to suggest values on the returned trial, + /// evaluate your objective externally, then pass the trial back to + /// [`tell()`](Self::tell) with the result. + /// + /// # Examples + /// + /// ``` + /// use optimizer::parameter::{FloatParam, Parameter}; + /// use optimizer::{Direction, Study}; + /// + /// let study: Study = Study::new(Direction::Minimize); + /// let x = FloatParam::new(0.0, 10.0); + /// + /// let mut trial = study.ask(); + /// let x_val = x.suggest(&mut trial).unwrap(); + /// let value = x_val * x_val; + /// study.tell(trial, Ok::<_, &str>(value)); + /// ``` + pub fn ask(&self) -> Trial { + self.create_trial() + } + + /// Report the result of a trial obtained from [`ask()`](Self::ask). + /// + /// Pass `Ok(value)` for a successful evaluation or `Err(reason)` for a + /// failure. Failed trials are not stored in the study's history. + /// + /// # Examples + /// + /// ``` + /// use optimizer::{Direction, Study}; + /// + /// let study: Study = Study::new(Direction::Minimize); + /// + /// let trial = study.ask(); + /// study.tell(trial, Ok::<_, &str>(42.0)); + /// assert_eq!(study.n_trials(), 1); + /// + /// let trial = study.ask(); + /// study.tell(trial, Err::("evaluation failed")); + /// assert_eq!(study.n_trials(), 1); // failed trials not counted + /// ``` + pub fn tell(&self, trial: Trial, value: core::result::Result) { + match value { + Ok(v) => self.complete_trial(trial, v), + Err(e) => self.fail_trial(trial, e), + } + } + /// Records a pruned trial, preserving its intermediate values. /// /// Pruned trials are stored alongside completed trials so that samplers diff --git a/tests/integration.rs b/tests/integration.rs index 0e484bc..8cfd84d 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -1601,3 +1601,102 @@ fn test_top_trials_excludes_pruned() { assert_eq!(top.len(), 3, "pruned trial should be excluded"); assert_eq!(top[0].value, 1.0); } + +// ============================================================================= +// Test: ask-and-tell interface +// ============================================================================= + +#[test] +fn test_ask_and_tell_basic() { + let study: Study = Study::new(Direction::Minimize); + let x_param = FloatParam::new(0.0, 10.0); + + for _ in 0..10 { + let mut trial = study.ask(); + let x = x_param.suggest(&mut trial).unwrap(); + let value = x * x; + study.tell(trial, Ok::<_, &str>(value)); + } + + assert_eq!(study.n_trials(), 10); + assert!(study.best_value().unwrap() >= 0.0); +} + +#[test] +fn test_ask_and_tell_with_failures() { + let study: Study = Study::new(Direction::Minimize); + let x_param = FloatParam::new(-5.0, 5.0); + + // Alternate success and failure + for i in 0..10 { + let mut trial = study.ask(); + let x = x_param.suggest(&mut trial).unwrap(); + if i % 2 == 0 { + study.tell(trial, Ok::<_, &str>(x * x)); + } else { + study.tell(trial, Err::("simulated failure")); + } + } + + // Only successful trials are counted + assert_eq!(study.n_trials(), 5); +} + +#[test] +fn test_ask_and_tell_with_tpe_sampler() { + let sampler = TpeSampler::builder() + .seed(42) + .n_startup_trials(5) + .build() + .unwrap(); + let study: Study = Study::minimize(sampler); + let x_param = FloatParam::new(-10.0, 10.0); + + for _ in 0..30 { + let mut trial = study.ask(); + let x = x_param.suggest(&mut trial).unwrap(); + study.tell(trial, Ok::<_, &str>((x - 3.0).powi(2))); + } + + assert_eq!(study.n_trials(), 30); + assert!( + study.best_value().unwrap() < 5.0, + "TPE ask-and-tell should find a reasonable value" + ); +} + +#[test] +fn test_ask_and_tell_batch() { + let study: Study = Study::new(Direction::Minimize); + let x_param = FloatParam::new(0.0, 10.0); + + // Ask a batch of trials + let batch: Vec<_> = (0..5) + .map(|_| { + let mut t = study.ask(); + let x = x_param.suggest(&mut t).unwrap(); + (t, x) + }) + .collect(); + + // Tell results for the batch + for (trial, x) in batch { + study.tell(trial, Ok::<_, &str>(x * x)); + } + + assert_eq!(study.n_trials(), 5); +} + +#[test] +fn test_ask_and_tell_with_custom_value_type() { + // Ask-and-tell works with non-f64 value types too + let study: Study = Study::new(Direction::Maximize); + + for i in 0..5 { + let trial = study.ask(); + study.tell(trial, Ok::<_, &str>(i * 10)); + } + + assert_eq!(study.n_trials(), 5); + assert_eq!(study.best_value().unwrap(), 40); +}