Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 762f1b3cde | |||
| b3534b3a3b | |||
| 9944e69920 | |||
| e8e446fb0e |
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "optimizer"
|
name = "optimizer"
|
||||||
version = "0.3.0"
|
version = "0.3.1"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
rust-version = "1.88"
|
rust-version = "1.88"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|||||||
+1
-1
@@ -177,5 +177,5 @@ mod types;
|
|||||||
pub use error::{Error, Result};
|
pub use error::{Error, Result};
|
||||||
pub use param::ParamValue;
|
pub use param::ParamValue;
|
||||||
pub use study::Study;
|
pub use study::Study;
|
||||||
pub use trial::Trial;
|
pub use trial::{SuggestableRange, Trial};
|
||||||
pub use types::{Direction, TrialState};
|
pub use types::{Direction, TrialState};
|
||||||
|
|||||||
+150
@@ -1,5 +1,6 @@
|
|||||||
//! Trial implementation for tracking sampled parameters and trial state.
|
//! Trial implementation for tracking sampled parameters and trial state.
|
||||||
|
|
||||||
|
use core::ops::{Range, RangeInclusive};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
@@ -13,6 +14,64 @@ use crate::param::ParamValue;
|
|||||||
use crate::sampler::{CompletedTrial, Sampler};
|
use crate::sampler::{CompletedTrial, Sampler};
|
||||||
use crate::types::TrialState;
|
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<f64>` | `0.0..1.0` | Float range (end-exclusive, treated as inclusive for continuous sampling) |
|
||||||
|
/// | `RangeInclusive<f64>` | `0.0..=1.0` | Float range (end-inclusive) |
|
||||||
|
/// | `Range<i64>` | `1..10` | Integer range from 1 to 9 (end-exclusive) |
|
||||||
|
/// | `RangeInclusive<i64>` | `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<Self::Output>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SuggestableRange for Range<f64> {
|
||||||
|
type Output = f64;
|
||||||
|
|
||||||
|
fn suggest(self, trial: &mut Trial, name: String) -> Result<f64> {
|
||||||
|
trial.suggest_float(name, self.start, self.end)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SuggestableRange for RangeInclusive<f64> {
|
||||||
|
type Output = f64;
|
||||||
|
|
||||||
|
fn suggest(self, trial: &mut Trial, name: String) -> Result<f64> {
|
||||||
|
trial.suggest_float(name, *self.start(), *self.end())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SuggestableRange for Range<i64> {
|
||||||
|
type Output = i64;
|
||||||
|
|
||||||
|
fn suggest(self, trial: &mut Trial, name: String) -> Result<i64> {
|
||||||
|
// Range is exclusive on the end, so subtract 1
|
||||||
|
trial.suggest_int(name, self.start, self.end.saturating_sub(1))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SuggestableRange for RangeInclusive<i64> {
|
||||||
|
type Output = i64;
|
||||||
|
|
||||||
|
fn suggest(self, trial: &mut Trial, name: String) -> Result<i64> {
|
||||||
|
trial.suggest_int(name, *self.start(), *self.end())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// A trial represents a single evaluation of the objective function.
|
/// A trial represents a single evaluation of the objective function.
|
||||||
///
|
///
|
||||||
/// Each trial has a unique ID and stores the sampled parameters along with
|
/// Each trial has a unique ID and stores the sampled parameters along with
|
||||||
@@ -794,4 +853,95 @@ impl Trial {
|
|||||||
|
|
||||||
Ok(choices[index].clone())
|
Ok(choices[index].clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Suggests a boolean parameter.
|
||||||
|
///
|
||||||
|
/// The value is selected uniformly at random from `{false, true}`.
|
||||||
|
/// This is equivalent to calling `suggest_categorical(name, &[false, true])`.
|
||||||
|
///
|
||||||
|
/// If the parameter has already been sampled, the cached value is returned.
|
||||||
|
/// If the parameter was sampled with a different type, a `ParameterConflict` error is returned.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `name` - The name of the parameter.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns `ParameterConflict` if the parameter was previously sampled with a different type.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use optimizer::Trial;
|
||||||
|
///
|
||||||
|
/// let mut trial = Trial::new(0);
|
||||||
|
/// let use_dropout = trial.suggest_bool("use_dropout").unwrap();
|
||||||
|
/// assert!(use_dropout == true || use_dropout == false);
|
||||||
|
///
|
||||||
|
/// // Calling again returns cached value
|
||||||
|
/// let use_dropout2 = trial.suggest_bool("use_dropout").unwrap();
|
||||||
|
/// assert_eq!(use_dropout, use_dropout2);
|
||||||
|
/// ```
|
||||||
|
pub fn suggest_bool(&mut self, name: impl Into<String>) -> Result<bool> {
|
||||||
|
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<R: SuggestableRange>(
|
||||||
|
&mut self,
|
||||||
|
name: impl Into<String>,
|
||||||
|
range: R,
|
||||||
|
) -> Result<R::Output> {
|
||||||
|
range.suggest(self, name.into())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1186,3 +1186,228 @@ fn test_best_trial_with_nan_values() {
|
|||||||
let best = study.best_trial();
|
let best = study.best_trial();
|
||||||
assert!(best.is_ok());
|
assert!(best.is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Tests for suggest_bool
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_suggest_bool_returns_boolean() {
|
||||||
|
let mut trial = Trial::new(0);
|
||||||
|
|
||||||
|
let flag = trial.suggest_bool("use_feature").unwrap();
|
||||||
|
assert!(flag == true || flag == false);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_suggest_bool_caching() {
|
||||||
|
let mut trial = Trial::new(0);
|
||||||
|
|
||||||
|
let b1 = trial.suggest_bool("flag").unwrap();
|
||||||
|
let b2 = trial.suggest_bool("flag").unwrap();
|
||||||
|
|
||||||
|
assert_eq!(b1, b2, "repeated suggest_bool should return cached value");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_suggest_bool_multiple_parameters() {
|
||||||
|
let mut trial = Trial::new(0);
|
||||||
|
|
||||||
|
let a = trial.suggest_bool("use_dropout").unwrap();
|
||||||
|
let b = trial.suggest_bool("use_batchnorm").unwrap();
|
||||||
|
let c = trial.suggest_bool("use_skip_connections").unwrap();
|
||||||
|
|
||||||
|
// All should be cached independently
|
||||||
|
assert_eq!(a, trial.suggest_bool("use_dropout").unwrap());
|
||||||
|
assert_eq!(b, trial.suggest_bool("use_batchnorm").unwrap());
|
||||||
|
assert_eq!(c, trial.suggest_bool("use_skip_connections").unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_suggest_bool_in_optimization() {
|
||||||
|
let study: Study<f64> = Study::new(Direction::Minimize);
|
||||||
|
|
||||||
|
study
|
||||||
|
.optimize(10, |trial| {
|
||||||
|
let use_feature = trial.suggest_bool("use_feature")?;
|
||||||
|
let x = trial.suggest_float("x", 0.0, 10.0)?;
|
||||||
|
|
||||||
|
// Objective depends on boolean flag
|
||||||
|
let value = if use_feature { x } else { x * 2.0 };
|
||||||
|
Ok::<_, Error>(value)
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(study.n_trials(), 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_suggest_bool_with_tpe() {
|
||||||
|
let sampler = TpeSampler::builder()
|
||||||
|
.seed(42)
|
||||||
|
.n_startup_trials(5)
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let study: Study<f64> = Study::with_sampler(Direction::Minimize, sampler);
|
||||||
|
|
||||||
|
study
|
||||||
|
.optimize_with_sampler(20, |trial| {
|
||||||
|
let use_large = trial.suggest_bool("use_large")?;
|
||||||
|
let base = if use_large { 10.0 } else { 1.0 };
|
||||||
|
let x = trial.suggest_float("x", 0.0, base)?;
|
||||||
|
Ok::<_, Error>(x)
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let best = study.best_trial().unwrap();
|
||||||
|
// Best should prefer use_large=false for smaller range
|
||||||
|
assert!(best.value < 5.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Tests for suggest_range
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_suggest_range_float_exclusive() {
|
||||||
|
let mut trial = Trial::new(0);
|
||||||
|
|
||||||
|
let x = trial.suggest_range("x", 0.0..1.0).unwrap();
|
||||||
|
assert!(x >= 0.0 && x <= 1.0, "value {x} out of range 0.0..1.0");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_suggest_range_float_inclusive() {
|
||||||
|
let mut trial = Trial::new(0);
|
||||||
|
|
||||||
|
let x = trial.suggest_range("x", 0.0..=1.0).unwrap();
|
||||||
|
assert!(x >= 0.0 && x <= 1.0, "value {x} out of range 0.0..=1.0");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_suggest_range_int_exclusive() {
|
||||||
|
let mut trial = Trial::new(0);
|
||||||
|
|
||||||
|
// 1..10 means 1 to 9 inclusive
|
||||||
|
let n = trial.suggest_range("n", 1_i64..10).unwrap();
|
||||||
|
assert!(n >= 1 && n <= 9, "value {n} out of range 1..10 (should be 1-9)");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_suggest_range_int_inclusive() {
|
||||||
|
let mut trial = Trial::new(0);
|
||||||
|
|
||||||
|
// 1..=10 means 1 to 10 inclusive
|
||||||
|
let n = trial.suggest_range("n", 1_i64..=10).unwrap();
|
||||||
|
assert!(
|
||||||
|
n >= 1 && n <= 10,
|
||||||
|
"value {n} out of range 1..=10 (should be 1-10)"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_suggest_range_caching_float() {
|
||||||
|
let mut trial = Trial::new(0);
|
||||||
|
|
||||||
|
let x1 = trial.suggest_range("x", 0.0..1.0).unwrap();
|
||||||
|
let x2 = trial.suggest_range("x", 0.0..1.0).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(x1, x2, "repeated suggest_range should return cached value");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_suggest_range_caching_int() {
|
||||||
|
let mut trial = Trial::new(0);
|
||||||
|
|
||||||
|
let n1 = trial.suggest_range("n", 1_i64..=100).unwrap();
|
||||||
|
let n2 = trial.suggest_range("n", 1_i64..=100).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(n1, n2, "repeated suggest_range should return cached value");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_suggest_range_multiple_parameters() {
|
||||||
|
let mut trial = Trial::new(0);
|
||||||
|
|
||||||
|
let x = trial.suggest_range("x", 0.0..1.0).unwrap();
|
||||||
|
let y = trial.suggest_range("y", -5.0..=5.0).unwrap();
|
||||||
|
let n = trial.suggest_range("n", 1_i64..10).unwrap();
|
||||||
|
let m = trial.suggest_range("m", 100_i64..=200).unwrap();
|
||||||
|
|
||||||
|
// All should be cached independently
|
||||||
|
assert_eq!(x, trial.suggest_range("x", 0.0..1.0).unwrap());
|
||||||
|
assert_eq!(y, trial.suggest_range("y", -5.0..=5.0).unwrap());
|
||||||
|
assert_eq!(n, trial.suggest_range("n", 1_i64..10).unwrap());
|
||||||
|
assert_eq!(m, trial.suggest_range("m", 100_i64..=200).unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_suggest_range_in_optimization() {
|
||||||
|
let study: Study<f64> = Study::new(Direction::Minimize);
|
||||||
|
|
||||||
|
study
|
||||||
|
.optimize(10, |trial| {
|
||||||
|
let x = trial.suggest_range("x", -10.0..10.0)?;
|
||||||
|
let n = trial.suggest_range("n", 1_i64..=5)?;
|
||||||
|
Ok::<_, Error>(x * x + n as f64)
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(study.n_trials(), 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_suggest_range_with_tpe() {
|
||||||
|
let sampler = TpeSampler::builder()
|
||||||
|
.seed(42)
|
||||||
|
.n_startup_trials(5)
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let study: Study<f64> = Study::with_sampler(Direction::Minimize, sampler);
|
||||||
|
|
||||||
|
study
|
||||||
|
.optimize_with_sampler(30, |trial| {
|
||||||
|
let x = trial.suggest_range("x", -5.0..=5.0)?;
|
||||||
|
let n = trial.suggest_range("n", 1_i64..=10)?;
|
||||||
|
Ok::<_, Error>(x * x + (n as f64 - 5.0).powi(2))
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let best = study.best_trial().unwrap();
|
||||||
|
// TPE should find near-optimal solution
|
||||||
|
assert!(best.value < 10.0, "TPE should find good solution");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_suggest_range_empty_int_range_error() {
|
||||||
|
let mut trial = Trial::new(0);
|
||||||
|
|
||||||
|
// 5..5 is empty (no valid integers)
|
||||||
|
let result = trial.suggest_range("n", 5_i64..5);
|
||||||
|
assert!(
|
||||||
|
matches!(result, Err(Error::InvalidBounds { .. })),
|
||||||
|
"empty range should return InvalidBounds error"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_suggest_range_single_value_int() {
|
||||||
|
let mut trial = Trial::new(0);
|
||||||
|
|
||||||
|
// 5..=5 has exactly one value: 5
|
||||||
|
let n = trial.suggest_range("n", 5_i64..=5).unwrap();
|
||||||
|
assert_eq!(n, 5, "single-value range should return that value");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_suggest_range_single_value_float() {
|
||||||
|
let mut trial = Trial::new(0);
|
||||||
|
|
||||||
|
let x = trial.suggest_range("x", 3.14..=3.14).unwrap();
|
||||||
|
assert!(
|
||||||
|
(x - 3.14).abs() < f64::EPSILON,
|
||||||
|
"single-value range should return that value"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user