From 9944e69920fcfcff3d087bf008f443ae34ec3381 Mon Sep 17 00:00:00 2001 From: Manuel Raimann Date: Sat, 31 Jan 2026 11:30:33 +0100 Subject: [PATCH] feat: add SuggestableRange trait and suggest_range method for parameter suggestion from ranges --- src/lib.rs | 2 +- src/trial.rs | 117 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 118 insertions(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 99473ea..a51b2b8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -177,5 +177,5 @@ mod types; pub use error::{Error, Result}; pub use param::ParamValue; pub use study::Study; -pub use trial::Trial; +pub use trial::{SuggestableRange, Trial}; pub use types::{Direction, TrialState}; diff --git a/src/trial.rs b/src/trial.rs index e57302e..bdf256d 100644 --- a/src/trial.rs +++ b/src/trial.rs @@ -1,5 +1,6 @@ //! Trial implementation for tracking sampled parameters and trial state. +use core::ops::{Range, RangeInclusive}; use std::collections::HashMap; use std::sync::Arc; @@ -13,6 +14,64 @@ use crate::param::ParamValue; use crate::sampler::{CompletedTrial, Sampler}; use crate::types::TrialState; +/// A trait for types that can be used with [`Trial::suggest_range`]. +/// +/// This trait is implemented for [`Range`] and [`RangeInclusive`] over `f64` and `i64`. +/// It allows using Rust's range syntax directly with the optimizer. +/// +/// # Supported Range Types +/// +/// | Range Type | Example | Description | +/// |------------|---------|-------------| +/// | `Range` | `0.0..1.0` | Float range (end-exclusive, treated as inclusive for continuous sampling) | +/// | `RangeInclusive` | `0.0..=1.0` | Float range (end-inclusive) | +/// | `Range` | `1..10` | Integer range from 1 to 9 (end-exclusive) | +/// | `RangeInclusive` | `1..=10` | Integer range from 1 to 10 (end-inclusive) | +pub trait SuggestableRange { + /// The output type when suggesting from this range. + type Output; + + /// Suggests a value from this range using the given trial. + /// + /// # Errors + /// + /// Returns an error if the range is invalid (e.g., empty or low > high). + fn suggest(self, trial: &mut Trial, name: String) -> Result; +} + +impl SuggestableRange for Range { + type Output = f64; + + fn suggest(self, trial: &mut Trial, name: String) -> Result { + trial.suggest_float(name, self.start, self.end) + } +} + +impl SuggestableRange for RangeInclusive { + type Output = f64; + + fn suggest(self, trial: &mut Trial, name: String) -> Result { + trial.suggest_float(name, *self.start(), *self.end()) + } +} + +impl SuggestableRange for Range { + type Output = i64; + + fn suggest(self, trial: &mut Trial, name: String) -> Result { + // Range is exclusive on the end, so subtract 1 + trial.suggest_int(name, self.start, self.end.saturating_sub(1)) + } +} + +impl SuggestableRange for RangeInclusive { + type Output = i64; + + fn suggest(self, trial: &mut Trial, name: String) -> Result { + trial.suggest_int(name, *self.start(), *self.end()) + } +} + /// A trial represents a single evaluation of the objective function. /// /// Each trial has a unique ID and stores the sampled parameters along with @@ -827,4 +886,62 @@ impl Trial { pub fn suggest_bool(&mut self, name: impl Into) -> Result { self.suggest_categorical(name, &[false, true]) } + + /// Suggests a parameter value from a range. + /// + /// This method accepts both [`Range`] (`..`) and [`RangeInclusive`] (`..=`) + /// for both `f64` and `i64` types, allowing natural Rust range syntax. + /// + /// For integer ranges, note that `Range` (`..`) is end-exclusive while + /// `RangeInclusive` (`..=`) is end-inclusive, matching Rust's semantics. + /// + /// If the parameter has already been sampled with the same bounds, the cached value is returned. + /// If the parameter was sampled with different bounds, a `ParameterConflict` error is returned. + /// + /// # Arguments + /// + /// * `name` - The name of the parameter. + /// * `range` - The range to sample from. + /// + /// # Type Parameters + /// + /// * `R` - A range type implementing [`SuggestableRange`]. + /// + /// # Errors + /// + /// Returns `InvalidBounds` if the range is invalid (e.g., low > high or empty integer range). + /// Returns `ParameterConflict` if the parameter was previously sampled with different bounds. + /// + /// # Examples + /// + /// ``` + /// use optimizer::Trial; + /// + /// let mut trial = Trial::new(0); + /// + /// // Float ranges + /// let x = trial.suggest_range("x", 0.0..1.0).unwrap(); + /// assert!(x >= 0.0 && x <= 1.0); + /// + /// let y = trial.suggest_range("y", 0.0..=1.0).unwrap(); + /// assert!(y >= 0.0 && y <= 1.0); + /// + /// // Integer ranges + /// let n = trial.suggest_range("n", 1_i64..10).unwrap(); // 1 to 9 inclusive + /// assert!(n >= 1 && n <= 9); + /// + /// let m = trial.suggest_range("m", 1_i64..=10).unwrap(); // 1 to 10 inclusive + /// assert!(m >= 1 && m <= 10); + /// + /// // Calling again with same range returns cached value + /// let x2 = trial.suggest_range("x", 0.0..1.0).unwrap(); + /// assert_eq!(x, x2); + /// ``` + pub fn suggest_range( + &mut self, + name: impl Into, + range: R, + ) -> Result { + range.suggest(self, name.into()) + } }