Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e44ab3669a | |||
| 7a3f89360e |
@@ -6,9 +6,6 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
branches: [main, master]
|
branches: [main, master]
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
env:
|
env:
|
||||||
CARGO_TERM_COLOR: always
|
CARGO_TERM_COLOR: always
|
||||||
|
|
||||||
@@ -278,14 +275,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Publish
|
- name: Publish
|
||||||
if: steps.check.outputs.skip == 'false'
|
if: steps.check.outputs.skip == 'false' && github.ref == 'refs/heads/master'
|
||||||
run: |
|
run: cargo publish
|
||||||
cargo publish --allow-dirty 2>&1 | tee publish_output.txt || {
|
|
||||||
if grep -q "already uploaded" publish_output.txt; then
|
|
||||||
echo "Version already published, skipping"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
env:
|
env:
|
||||||
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
|
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
|
||||||
|
|||||||
@@ -5,9 +5,6 @@ on:
|
|||||||
- cron: "0 6 * * *" # Daily at 6:00 UTC
|
- cron: "0 6 * * *" # Daily at 6:00 UTC
|
||||||
workflow_dispatch: # Allow manual trigger
|
workflow_dispatch: # Allow manual trigger
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
env:
|
env:
|
||||||
CARGO_TERM_COLOR: always
|
CARGO_TERM_COLOR: always
|
||||||
|
|
||||||
|
|||||||
+2
-5
@@ -1,16 +1,13 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "optimizer"
|
name = "optimizer"
|
||||||
version = "0.3.1"
|
version = "0.3.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
rust-version = "1.88"
|
rust-version = "1.88"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
authors = ["Manuel Raimann <raimannma@outlook.de"]
|
authors = ["Manuel Raimann <raimannma@outlook.de"]
|
||||||
description = "A Rust library for optimization algorithms."
|
description = "A Rust library for optimization algorithms."
|
||||||
repository = "https://github.com/raimannma/rust-optimizer"
|
repository = "https://github.com/raimannma/rust-optimizer"
|
||||||
documentation = "https://docs.rs/optimizer"
|
|
||||||
keywords = ["optimization", "hyperparameter", "tpe", "grid-search", "bayesian"]
|
|
||||||
categories = ["algorithm", "science", "data-structures"]
|
|
||||||
readme = "README.md"
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
rand = "0.9"
|
rand = "0.9"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# optimizer
|
# optimizer
|
||||||
|
|
||||||
A Rust library for black-box optimization with multiple sampling strategies.
|
A Rust library for black-box optimization using Tree-Parzen Estimator (TPE).
|
||||||
|
|
||||||
[](https://docs.rs/optimizer)
|
[](https://docs.rs/optimizer)
|
||||||
[](https://crates.io/crates/optimizer)
|
[](https://crates.io/crates/optimizer)
|
||||||
@@ -9,10 +9,6 @@ A Rust library for black-box optimization with multiple sampling strategies.
|
|||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Optuna-like API for hyperparameter optimization
|
- Optuna-like API for hyperparameter optimization
|
||||||
- Multiple sampling strategies:
|
|
||||||
- **Random Search** - Simple random sampling for baseline comparisons
|
|
||||||
- **TPE (Tree-Parzen Estimator)** - Bayesian optimization for efficient search
|
|
||||||
- **Grid Search** - Exhaustive search over a specified parameter grid
|
|
||||||
- Float, integer, and categorical parameter types
|
- Float, integer, and categorical parameter types
|
||||||
- Log-scale and stepped parameter sampling
|
- Log-scale and stepped parameter sampling
|
||||||
- Sync and async optimization with parallel trial evaluation
|
- Sync and async optimization with parallel trial evaluation
|
||||||
@@ -20,16 +16,15 @@ A Rust library for black-box optimization with multiple sampling strategies.
|
|||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
use optimizer::{Direction, Study};
|
use optimizer::{Direction, Study, TpeSampler};
|
||||||
use optimizer::sampler::tpe::TpeSampler;
|
|
||||||
|
|
||||||
let sampler = TpeSampler::builder().seed(42).build().unwrap();
|
let sampler = TpeSampler::builder().seed(42).build();
|
||||||
let study: Study<f64> = Study::with_sampler(Direction::Minimize, sampler);
|
let study: Study<f64> = Study::with_sampler(Direction::Minimize, sampler);
|
||||||
|
|
||||||
study
|
study
|
||||||
.optimize_with_sampler(20, |trial| {
|
.optimize_with_sampler(20, |trial| {
|
||||||
let x = trial.suggest_float("x", -10.0, 10.0)?;
|
let x = trial.suggest_float("x", -10.0, 10.0)?;
|
||||||
Ok::<_, optimizer::Error>(x * x)
|
Ok::<_, optimizer::TpeError>(x * x)
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
@@ -37,50 +32,6 @@ let best = study.best_trial().unwrap();
|
|||||||
println!("Best value: {} at x={:?}", best.value, best.params);
|
println!("Best value: {} at x={:?}", best.value, best.params);
|
||||||
```
|
```
|
||||||
|
|
||||||
## Samplers
|
|
||||||
|
|
||||||
### Random Search
|
|
||||||
|
|
||||||
```rust
|
|
||||||
use optimizer::{Direction, Study};
|
|
||||||
use optimizer::sampler::random::RandomSampler;
|
|
||||||
|
|
||||||
let study: Study<f64> = Study::with_sampler(
|
|
||||||
Direction::Minimize,
|
|
||||||
RandomSampler::with_seed(42),
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
### TPE (Tree-Parzen Estimator)
|
|
||||||
|
|
||||||
```rust
|
|
||||||
use optimizer::{Direction, Study};
|
|
||||||
use optimizer::sampler::tpe::TpeSampler;
|
|
||||||
|
|
||||||
let sampler = TpeSampler::builder()
|
|
||||||
.gamma(0.15) // Quantile for good/bad split
|
|
||||||
.n_startup_trials(20) // Random trials before TPE kicks in
|
|
||||||
.n_ei_candidates(32) // Candidates to evaluate
|
|
||||||
.seed(42)
|
|
||||||
.build()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let study: Study<f64> = Study::with_sampler(Direction::Minimize, sampler);
|
|
||||||
```
|
|
||||||
|
|
||||||
### Grid Search
|
|
||||||
|
|
||||||
```rust
|
|
||||||
use optimizer::{Direction, Study};
|
|
||||||
use optimizer::sampler::grid::GridSearchSampler;
|
|
||||||
|
|
||||||
let sampler = GridSearchSampler::builder()
|
|
||||||
.n_points_per_param(10) // Number of points per parameter dimension
|
|
||||||
.build();
|
|
||||||
|
|
||||||
let study: Study<f64> = Study::with_sampler(Direction::Minimize, sampler);
|
|
||||||
```
|
|
||||||
|
|
||||||
## Feature Flags
|
## Feature Flags
|
||||||
|
|
||||||
- `async` - Enable async optimization methods (requires tokio)
|
- `async` - Enable async optimization methods (requires tokio)
|
||||||
|
|||||||
+9
-3
@@ -1,5 +1,10 @@
|
|||||||
#[derive(Debug, thiserror::Error)]
|
//! Error types for the optimizer library.
|
||||||
pub enum Error {
|
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
/// The error type for TPE operations.
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum TpeError {
|
||||||
/// Returned when the lower bound is greater than the upper bound.
|
/// Returned when the lower bound is greater than the upper bound.
|
||||||
#[error("invalid bounds: low ({low}) must be less than or equal to high ({high})")]
|
#[error("invalid bounds: low ({low}) must be less than or equal to high ({high})")]
|
||||||
InvalidBounds {
|
InvalidBounds {
|
||||||
@@ -56,4 +61,5 @@ pub enum Error {
|
|||||||
TaskError(String),
|
TaskError(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type Result<T> = core::result::Result<T, Error>;
|
/// A specialized Result type for TPE operations.
|
||||||
|
pub type Result<T> = core::result::Result<T, TpeError>;
|
||||||
|
|||||||
+10
-10
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
|
|
||||||
use crate::error::{Error, Result};
|
use crate::error::{Result, TpeError};
|
||||||
|
|
||||||
/// A Gaussian kernel density estimator for continuous distributions.
|
/// A Gaussian kernel density estimator for continuous distributions.
|
||||||
///
|
///
|
||||||
@@ -45,10 +45,10 @@ impl KernelDensityEstimator {
|
|||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
///
|
///
|
||||||
/// Returns `Error::EmptySamples` if `samples` is empty.
|
/// Returns `TpeError::EmptySamples` if `samples` is empty.
|
||||||
pub(crate) fn new(samples: Vec<f64>) -> Result<Self> {
|
pub(crate) fn new(samples: Vec<f64>) -> Result<Self> {
|
||||||
if samples.is_empty() {
|
if samples.is_empty() {
|
||||||
return Err(Error::EmptySamples);
|
return Err(TpeError::EmptySamples);
|
||||||
}
|
}
|
||||||
|
|
||||||
let bandwidth = Self::scotts_rule(&samples);
|
let bandwidth = Self::scotts_rule(&samples);
|
||||||
@@ -61,14 +61,14 @@ impl KernelDensityEstimator {
|
|||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
///
|
///
|
||||||
/// Returns `Error::EmptySamples` if `samples` is empty.
|
/// Returns `TpeError::EmptySamples` if `samples` is empty.
|
||||||
/// Returns `Error::InvalidBandwidth` if `bandwidth` is not positive.
|
/// Returns `TpeError::InvalidBandwidth` if `bandwidth` is not positive.
|
||||||
pub(crate) fn with_bandwidth(samples: Vec<f64>, bandwidth: f64) -> Result<Self> {
|
pub(crate) fn with_bandwidth(samples: Vec<f64>, bandwidth: f64) -> Result<Self> {
|
||||||
if samples.is_empty() {
|
if samples.is_empty() {
|
||||||
return Err(Error::EmptySamples);
|
return Err(TpeError::EmptySamples);
|
||||||
}
|
}
|
||||||
if bandwidth <= 0.0 {
|
if bandwidth <= 0.0 {
|
||||||
return Err(Error::InvalidBandwidth(bandwidth));
|
return Err(TpeError::InvalidBandwidth(bandwidth));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Self { samples, bandwidth })
|
Ok(Self { samples, bandwidth })
|
||||||
@@ -261,20 +261,20 @@ mod tests {
|
|||||||
fn test_kde_empty_samples() {
|
fn test_kde_empty_samples() {
|
||||||
let samples: Vec<f64> = vec![];
|
let samples: Vec<f64> = vec![];
|
||||||
let result = KernelDensityEstimator::new(samples);
|
let result = KernelDensityEstimator::new(samples);
|
||||||
assert!(matches!(result, Err(Error::EmptySamples)));
|
assert!(matches!(result, Err(TpeError::EmptySamples)));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_kde_zero_bandwidth() {
|
fn test_kde_zero_bandwidth() {
|
||||||
let samples = vec![1.0, 2.0, 3.0];
|
let samples = vec![1.0, 2.0, 3.0];
|
||||||
let result = KernelDensityEstimator::with_bandwidth(samples, 0.0);
|
let result = KernelDensityEstimator::with_bandwidth(samples, 0.0);
|
||||||
assert!(matches!(result, Err(Error::InvalidBandwidth(_))));
|
assert!(matches!(result, Err(TpeError::InvalidBandwidth(_))));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_kde_negative_bandwidth() {
|
fn test_kde_negative_bandwidth() {
|
||||||
let samples = vec![1.0, 2.0, 3.0];
|
let samples = vec![1.0, 2.0, 3.0];
|
||||||
let result = KernelDensityEstimator::with_bandwidth(samples, -1.0);
|
let result = KernelDensityEstimator::with_bandwidth(samples, -1.0);
|
||||||
assert!(matches!(result, Err(Error::InvalidBandwidth(_))));
|
assert!(matches!(result, Err(TpeError::InvalidBandwidth(_))));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-42
@@ -9,16 +9,10 @@
|
|||||||
#![deny(clippy::pedantic)]
|
#![deny(clippy::pedantic)]
|
||||||
#![deny(clippy::std_instead_of_core)]
|
#![deny(clippy::std_instead_of_core)]
|
||||||
|
|
||||||
//! A black-box optimization library with multiple sampling strategies.
|
//! A Tree-Parzen Estimator (TPE) library for black-box optimization.
|
||||||
//!
|
//!
|
||||||
//! This library provides an Optuna-like API for hyperparameter optimization
|
//! This library provides an Optuna-like API for hyperparameter optimization
|
||||||
//! with support for multiple sampling algorithms:
|
//! using the Tree-Parzen Estimator algorithm. It supports:
|
||||||
//!
|
|
||||||
//! - **Random Search** - Simple random sampling for baseline comparisons
|
|
||||||
//! - **TPE (Tree-Parzen Estimator)** - Bayesian optimization for efficient search
|
|
||||||
//! - **Grid Search** - Exhaustive search over a specified parameter grid
|
|
||||||
//!
|
|
||||||
//! Additional features include:
|
|
||||||
//!
|
//!
|
||||||
//! - Float, integer, and categorical parameter types
|
//! - Float, integer, and categorical parameter types
|
||||||
//! - Log-scale and stepped parameter sampling
|
//! - Log-scale and stepped parameter sampling
|
||||||
@@ -39,7 +33,7 @@
|
|||||||
//! study
|
//! study
|
||||||
//! .optimize_with_sampler(20, |trial| {
|
//! .optimize_with_sampler(20, |trial| {
|
||||||
//! let x = trial.suggest_float("x", -10.0, 10.0)?;
|
//! let x = trial.suggest_float("x", -10.0, 10.0)?;
|
||||||
//! Ok::<_, optimizer::Error>(x * x)
|
//! Ok::<_, optimizer::TpeError>(x * x)
|
||||||
//! })
|
//! })
|
||||||
//! .unwrap();
|
//! .unwrap();
|
||||||
//!
|
//!
|
||||||
@@ -92,27 +86,14 @@
|
|||||||
//! let optimizer = trial.suggest_categorical("optimizer", &["sgd", "adam", "rmsprop"])?;
|
//! let optimizer = trial.suggest_categorical("optimizer", &["sgd", "adam", "rmsprop"])?;
|
||||||
//!
|
//!
|
||||||
//! // Return objective value
|
//! // Return objective value
|
||||||
//! Ok::<_, optimizer::Error>(x * n as f64)
|
//! Ok::<_, optimizer::TpeError>(x * n as f64)
|
||||||
//! })
|
//! })
|
||||||
//! .unwrap();
|
//! .unwrap();
|
||||||
//! ```
|
//! ```
|
||||||
//!
|
//!
|
||||||
//! # Available Samplers
|
//! # Configuring TPE
|
||||||
//!
|
//!
|
||||||
//! ## Random Search
|
//! The [`sampler::tpe::TpeSampler`] can be configured using the builder pattern:
|
||||||
//!
|
|
||||||
//! The simplest sampling strategy, useful for baselines:
|
|
||||||
//!
|
|
||||||
//! ```
|
|
||||||
//! use optimizer::sampler::random::RandomSampler;
|
|
||||||
//! use optimizer::{Direction, Study};
|
|
||||||
//!
|
|
||||||
//! let study: Study<f64> = Study::with_sampler(Direction::Minimize, RandomSampler::with_seed(42));
|
|
||||||
//! ```
|
|
||||||
//!
|
|
||||||
//! ## TPE (Tree-Parzen Estimator)
|
|
||||||
//!
|
|
||||||
//! Bayesian optimization that learns from previous trials:
|
|
||||||
//!
|
//!
|
||||||
//! ```
|
//! ```
|
||||||
//! use optimizer::sampler::tpe::TpeSampler;
|
//! use optimizer::sampler::tpe::TpeSampler;
|
||||||
@@ -126,21 +107,6 @@
|
|||||||
//! .unwrap();
|
//! .unwrap();
|
||||||
//! ```
|
//! ```
|
||||||
//!
|
//!
|
||||||
//! ## Grid Search
|
|
||||||
//!
|
|
||||||
//! Exhaustive search over a discretized parameter space:
|
|
||||||
//!
|
|
||||||
//! ```
|
|
||||||
//! use optimizer::sampler::grid::GridSearchSampler;
|
|
||||||
//! use optimizer::{Direction, Study};
|
|
||||||
//!
|
|
||||||
//! let sampler = GridSearchSampler::builder()
|
|
||||||
//! .n_points_per_param(10) // Points per parameter dimension
|
|
||||||
//! .build();
|
|
||||||
//!
|
|
||||||
//! let study: Study<f64> = Study::with_sampler(Direction::Minimize, sampler);
|
|
||||||
//! ```
|
|
||||||
//!
|
|
||||||
//! # Async and Parallel Optimization
|
//! # Async and Parallel Optimization
|
||||||
//!
|
//!
|
||||||
//! With the `async` feature enabled, you can run trials asynchronously:
|
//! With the `async` feature enabled, you can run trials asynchronously:
|
||||||
@@ -174,8 +140,8 @@ mod study;
|
|||||||
mod trial;
|
mod trial;
|
||||||
mod types;
|
mod types;
|
||||||
|
|
||||||
pub use error::{Error, Result};
|
pub use error::{Result, TpeError};
|
||||||
pub use param::ParamValue;
|
pub use param::ParamValue;
|
||||||
pub use study::Study;
|
pub use study::Study;
|
||||||
pub use trial::{SuggestableRange, Trial};
|
pub use trial::Trial;
|
||||||
pub use types::{Direction, TrialState};
|
pub use types::{Direction, TrialState};
|
||||||
|
|||||||
-1157
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,5 @@
|
|||||||
//! Sampler trait and implementations for parameter sampling.
|
//! Sampler trait and implementations for parameter sampling.
|
||||||
|
|
||||||
pub mod grid;
|
|
||||||
pub mod random;
|
pub mod random;
|
||||||
pub mod tpe;
|
pub mod tpe;
|
||||||
|
|
||||||
|
|||||||
+12
-12
@@ -9,7 +9,7 @@ use rand::rngs::StdRng;
|
|||||||
use rand::{Rng, SeedableRng};
|
use rand::{Rng, SeedableRng};
|
||||||
|
|
||||||
use crate::distribution::Distribution;
|
use crate::distribution::Distribution;
|
||||||
use crate::error::{Error, Result};
|
use crate::error::{Result, TpeError};
|
||||||
use crate::kde::KernelDensityEstimator;
|
use crate::kde::KernelDensityEstimator;
|
||||||
use crate::param::ParamValue;
|
use crate::param::ParamValue;
|
||||||
use crate::sampler::{CompletedTrial, Sampler};
|
use crate::sampler::{CompletedTrial, Sampler};
|
||||||
@@ -106,8 +106,8 @@ impl TpeSampler {
|
|||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
///
|
///
|
||||||
/// Returns `Error::InvalidGamma` if gamma is not in (0.0, 1.0).
|
/// Returns `TpeError::InvalidGamma` if gamma is not in (0.0, 1.0).
|
||||||
/// Returns `Error::InvalidBandwidth` if `kde_bandwidth` is Some but not positive.
|
/// Returns `TpeError::InvalidBandwidth` if `kde_bandwidth` is Some but not positive.
|
||||||
pub fn with_config(
|
pub fn with_config(
|
||||||
gamma: f64,
|
gamma: f64,
|
||||||
n_startup_trials: usize,
|
n_startup_trials: usize,
|
||||||
@@ -116,12 +116,12 @@ impl TpeSampler {
|
|||||||
seed: Option<u64>,
|
seed: Option<u64>,
|
||||||
) -> Result<Self> {
|
) -> Result<Self> {
|
||||||
if gamma <= 0.0 || gamma >= 1.0 {
|
if gamma <= 0.0 || gamma >= 1.0 {
|
||||||
return Err(Error::InvalidGamma(gamma));
|
return Err(TpeError::InvalidGamma(gamma));
|
||||||
}
|
}
|
||||||
if let Some(bw) = kde_bandwidth
|
if let Some(bw) = kde_bandwidth
|
||||||
&& bw <= 0.0
|
&& bw <= 0.0
|
||||||
{
|
{
|
||||||
return Err(Error::InvalidBandwidth(bw));
|
return Err(TpeError::InvalidBandwidth(bw));
|
||||||
}
|
}
|
||||||
|
|
||||||
let rng = match seed {
|
let rng = match seed {
|
||||||
@@ -484,7 +484,7 @@ impl TpeSamplerBuilder {
|
|||||||
/// # Note
|
/// # Note
|
||||||
///
|
///
|
||||||
/// Validation happens at `build()` time. If gamma is not in (0.0, 1.0),
|
/// Validation happens at `build()` time. If gamma is not in (0.0, 1.0),
|
||||||
/// `build()` will return `Err(Error::InvalidGamma)`.
|
/// `build()` will return `Err(TpeError::InvalidGamma)`.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn gamma(mut self, gamma: f64) -> Self {
|
pub fn gamma(mut self, gamma: f64) -> Self {
|
||||||
self.gamma = gamma;
|
self.gamma = gamma;
|
||||||
@@ -569,7 +569,7 @@ impl TpeSamplerBuilder {
|
|||||||
/// # Note
|
/// # Note
|
||||||
///
|
///
|
||||||
/// Validation happens at `build()` time. If bandwidth is not positive,
|
/// Validation happens at `build()` time. If bandwidth is not positive,
|
||||||
/// `build()` will return `Err(Error::InvalidBandwidth)`.
|
/// `build()` will return `Err(TpeError::InvalidBandwidth)`.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn kde_bandwidth(mut self, bandwidth: f64) -> Self {
|
pub fn kde_bandwidth(mut self, bandwidth: f64) -> Self {
|
||||||
self.kde_bandwidth = Some(bandwidth);
|
self.kde_bandwidth = Some(bandwidth);
|
||||||
@@ -602,8 +602,8 @@ impl TpeSamplerBuilder {
|
|||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
///
|
///
|
||||||
/// Returns `Error::InvalidGamma` if gamma is not in (0.0, 1.0).
|
/// Returns `TpeError::InvalidGamma` if gamma is not in (0.0, 1.0).
|
||||||
/// Returns `Error::InvalidBandwidth` if `kde_bandwidth` is Some but not positive.
|
/// Returns `TpeError::InvalidBandwidth` if `kde_bandwidth` is Some but not positive.
|
||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
///
|
||||||
@@ -817,13 +817,13 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_tpe_sampler_invalid_gamma_zero() {
|
fn test_tpe_sampler_invalid_gamma_zero() {
|
||||||
let result = TpeSampler::with_config(0.0, 10, 24, None, None);
|
let result = TpeSampler::with_config(0.0, 10, 24, None, None);
|
||||||
assert!(matches!(result, Err(Error::InvalidGamma(_))));
|
assert!(matches!(result, Err(TpeError::InvalidGamma(_))));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_tpe_sampler_invalid_gamma_one() {
|
fn test_tpe_sampler_invalid_gamma_one() {
|
||||||
let result = TpeSampler::with_config(1.0, 10, 24, None, None);
|
let result = TpeSampler::with_config(1.0, 10, 24, None, None);
|
||||||
assert!(matches!(result, Err(Error::InvalidGamma(_))));
|
assert!(matches!(result, Err(TpeError::InvalidGamma(_))));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1077,7 +1077,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_tpe_sampler_builder_invalid_gamma() {
|
fn test_tpe_sampler_builder_invalid_gamma() {
|
||||||
let result = TpeSamplerBuilder::new().gamma(1.5).build();
|
let result = TpeSamplerBuilder::new().gamma(1.5).build();
|
||||||
assert!(matches!(result, Err(Error::InvalidGamma(_))));
|
assert!(matches!(result, Err(TpeError::InvalidGamma(_))));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
+38
-38
@@ -275,7 +275,7 @@ where
|
|||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
///
|
///
|
||||||
/// Returns `Error::NoCompletedTrials` if no trials have been completed.
|
/// Returns `TpeError::NoCompletedTrials` if no trials have been completed.
|
||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
///
|
||||||
@@ -305,7 +305,7 @@ where
|
|||||||
let trials = self.completed_trials.read();
|
let trials = self.completed_trials.read();
|
||||||
|
|
||||||
if trials.is_empty() {
|
if trials.is_empty() {
|
||||||
return Err(crate::Error::NoCompletedTrials);
|
return Err(crate::TpeError::NoCompletedTrials);
|
||||||
}
|
}
|
||||||
|
|
||||||
let best = trials
|
let best = trials
|
||||||
@@ -325,7 +325,7 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.ok_or(crate::Error::NoCompletedTrials)?;
|
.ok_or(crate::TpeError::NoCompletedTrials)?;
|
||||||
|
|
||||||
Ok(best.clone())
|
Ok(best.clone())
|
||||||
}
|
}
|
||||||
@@ -338,7 +338,7 @@ where
|
|||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
///
|
///
|
||||||
/// Returns `Error::NoCompletedTrials` if no trials have been completed.
|
/// Returns `TpeError::NoCompletedTrials` if no trials have been completed.
|
||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
///
|
||||||
@@ -387,7 +387,7 @@ where
|
|||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
///
|
///
|
||||||
/// Returns `Error::NoCompletedTrials` if all trials failed (no successful trials).
|
/// Returns `TpeError::NoCompletedTrials` if all trials failed (no successful trials).
|
||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
///
|
||||||
@@ -402,7 +402,7 @@ where
|
|||||||
/// study
|
/// study
|
||||||
/// .optimize(10, |trial| {
|
/// .optimize(10, |trial| {
|
||||||
/// let x = trial.suggest_float("x", -10.0, 10.0)?;
|
/// let x = trial.suggest_float("x", -10.0, 10.0)?;
|
||||||
/// Ok::<_, optimizer::Error>(x * x)
|
/// Ok::<_, optimizer::TpeError>(x * x)
|
||||||
/// })
|
/// })
|
||||||
/// .unwrap();
|
/// .unwrap();
|
||||||
///
|
///
|
||||||
@@ -431,7 +431,7 @@ where
|
|||||||
|
|
||||||
// Return error if no trials succeeded
|
// Return error if no trials succeeded
|
||||||
if self.n_trials() == 0 {
|
if self.n_trials() == 0 {
|
||||||
return Err(crate::Error::NoCompletedTrials);
|
return Err(crate::TpeError::NoCompletedTrials);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -455,7 +455,7 @@ where
|
|||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
///
|
///
|
||||||
/// Returns `Error::NoCompletedTrials` if all trials failed (no successful trials).
|
/// Returns `TpeError::NoCompletedTrials` if all trials failed (no successful trials).
|
||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
///
|
||||||
@@ -474,7 +474,7 @@ where
|
|||||||
/// let x = trial.suggest_float("x", -10.0, 10.0)?;
|
/// let x = trial.suggest_float("x", -10.0, 10.0)?;
|
||||||
/// // Simulate async work (e.g., network request)
|
/// // Simulate async work (e.g., network request)
|
||||||
/// let value = x * x;
|
/// let value = x * x;
|
||||||
/// Ok::<_, optimizer::Error>((trial, value))
|
/// Ok::<_, optimizer::TpeError>((trial, value))
|
||||||
/// })
|
/// })
|
||||||
/// .await?;
|
/// .await?;
|
||||||
///
|
///
|
||||||
@@ -511,7 +511,7 @@ where
|
|||||||
|
|
||||||
// Return error if no trials succeeded
|
// Return error if no trials succeeded
|
||||||
if self.n_trials() == 0 {
|
if self.n_trials() == 0 {
|
||||||
return Err(crate::Error::NoCompletedTrials);
|
return Err(crate::TpeError::NoCompletedTrials);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -536,8 +536,8 @@ where
|
|||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
///
|
///
|
||||||
/// Returns `Error::NoCompletedTrials` if all trials failed (no successful trials).
|
/// Returns `TpeError::NoCompletedTrials` if all trials failed (no successful trials).
|
||||||
/// Returns `Error::TaskError` if the semaphore is closed or a spawned task panics.
|
/// Returns `TpeError::TaskError` if the semaphore is closed or a spawned task panics.
|
||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
///
|
||||||
@@ -556,7 +556,7 @@ where
|
|||||||
/// let x = trial.suggest_float("x", -10.0, 10.0)?;
|
/// let x = trial.suggest_float("x", -10.0, 10.0)?;
|
||||||
/// // Async objective function (e.g., network request)
|
/// // Async objective function (e.g., network request)
|
||||||
/// let value = x * x;
|
/// let value = x * x;
|
||||||
/// Ok::<_, optimizer::Error>((trial, value))
|
/// Ok::<_, optimizer::TpeError>((trial, value))
|
||||||
/// })
|
/// })
|
||||||
/// .await?;
|
/// .await?;
|
||||||
///
|
///
|
||||||
@@ -590,7 +590,7 @@ where
|
|||||||
.clone()
|
.clone()
|
||||||
.acquire_owned()
|
.acquire_owned()
|
||||||
.await
|
.await
|
||||||
.map_err(|e| crate::Error::TaskError(e.to_string()))?;
|
.map_err(|e| crate::TpeError::TaskError(e.to_string()))?;
|
||||||
let trial = self.create_trial();
|
let trial = self.create_trial();
|
||||||
let objective = Arc::clone(&objective);
|
let objective = Arc::clone(&objective);
|
||||||
|
|
||||||
@@ -607,7 +607,7 @@ where
|
|||||||
for handle in handles {
|
for handle in handles {
|
||||||
match handle
|
match handle
|
||||||
.await
|
.await
|
||||||
.map_err(|e| crate::Error::TaskError(e.to_string()))?
|
.map_err(|e| crate::TpeError::TaskError(e.to_string()))?
|
||||||
{
|
{
|
||||||
Ok((trial, value)) => {
|
Ok((trial, value)) => {
|
||||||
self.complete_trial(trial, value);
|
self.complete_trial(trial, value);
|
||||||
@@ -620,7 +620,7 @@ where
|
|||||||
|
|
||||||
// Return error if no trials succeeded
|
// Return error if no trials succeeded
|
||||||
if self.n_trials() == 0 {
|
if self.n_trials() == 0 {
|
||||||
return Err(crate::Error::NoCompletedTrials);
|
return Err(crate::TpeError::NoCompletedTrials);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -643,9 +643,9 @@ where
|
|||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
///
|
///
|
||||||
/// Returns `Error::NoCompletedTrials` if no trials completed successfully
|
/// Returns `TpeError::NoCompletedTrials` if no trials completed successfully
|
||||||
/// before optimization stopped (either by completing all trials or early stopping).
|
/// before optimization stopped (either by completing all trials or early stopping).
|
||||||
/// Returns `Error::Internal` if a completed trial is not found after adding (internal invariant violation).
|
/// Returns `TpeError::Internal` if a completed trial is not found after adding (internal invariant violation).
|
||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
///
|
||||||
@@ -664,7 +664,7 @@ where
|
|||||||
/// 100,
|
/// 100,
|
||||||
/// |trial| {
|
/// |trial| {
|
||||||
/// let x = trial.suggest_float("x", -10.0, 10.0)?;
|
/// let x = trial.suggest_float("x", -10.0, 10.0)?;
|
||||||
/// Ok::<_, optimizer::Error>(x * x)
|
/// Ok::<_, optimizer::TpeError>(x * x)
|
||||||
/// },
|
/// },
|
||||||
/// |_study, completed_trial| {
|
/// |_study, completed_trial| {
|
||||||
/// // Stop early if we find a value less than 1.0
|
/// // Stop early if we find a value less than 1.0
|
||||||
@@ -702,7 +702,7 @@ where
|
|||||||
// Get the just-completed trial for the callback
|
// Get the just-completed trial for the callback
|
||||||
let trials = self.completed_trials.read();
|
let trials = self.completed_trials.read();
|
||||||
let Some(completed) = trials.last() else {
|
let Some(completed) = trials.last() else {
|
||||||
return Err(crate::Error::Internal(
|
return Err(crate::TpeError::Internal(
|
||||||
"completed trial not found after adding",
|
"completed trial not found after adding",
|
||||||
));
|
));
|
||||||
};
|
};
|
||||||
@@ -725,7 +725,7 @@ where
|
|||||||
|
|
||||||
// Return error if no trials succeeded
|
// Return error if no trials succeeded
|
||||||
if self.n_trials() == 0 {
|
if self.n_trials() == 0 {
|
||||||
return Err(crate::Error::NoCompletedTrials);
|
return Err(crate::TpeError::NoCompletedTrials);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -783,7 +783,7 @@ impl Study<f64> {
|
|||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
///
|
///
|
||||||
/// Returns `Error::NoCompletedTrials` if all trials failed (no successful trials).
|
/// Returns `TpeError::NoCompletedTrials` if all trials failed (no successful trials).
|
||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
///
|
||||||
@@ -798,7 +798,7 @@ impl Study<f64> {
|
|||||||
/// study
|
/// study
|
||||||
/// .optimize_with_sampler(10, |trial| {
|
/// .optimize_with_sampler(10, |trial| {
|
||||||
/// let x = trial.suggest_float("x", -10.0, 10.0)?;
|
/// let x = trial.suggest_float("x", -10.0, 10.0)?;
|
||||||
/// Ok::<_, optimizer::Error>(x * x)
|
/// Ok::<_, optimizer::TpeError>(x * x)
|
||||||
/// })
|
/// })
|
||||||
/// .unwrap();
|
/// .unwrap();
|
||||||
///
|
///
|
||||||
@@ -829,7 +829,7 @@ impl Study<f64> {
|
|||||||
|
|
||||||
// Return error if no trials succeeded
|
// Return error if no trials succeeded
|
||||||
if self.n_trials() == 0 {
|
if self.n_trials() == 0 {
|
||||||
return Err(crate::Error::NoCompletedTrials);
|
return Err(crate::TpeError::NoCompletedTrials);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -851,8 +851,8 @@ impl Study<f64> {
|
|||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
///
|
///
|
||||||
/// Returns `Error::NoCompletedTrials` if no trials completed successfully.
|
/// Returns `TpeError::NoCompletedTrials` if no trials completed successfully.
|
||||||
/// Returns `Error::Internal` if a completed trial is not found after adding (internal invariant violation).
|
/// Returns `TpeError::Internal` if a completed trial is not found after adding (internal invariant violation).
|
||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
///
|
||||||
@@ -871,7 +871,7 @@ impl Study<f64> {
|
|||||||
/// 100,
|
/// 100,
|
||||||
/// |trial| {
|
/// |trial| {
|
||||||
/// let x = trial.suggest_float("x", -10.0, 10.0)?;
|
/// let x = trial.suggest_float("x", -10.0, 10.0)?;
|
||||||
/// Ok::<_, optimizer::Error>(x * x)
|
/// Ok::<_, optimizer::TpeError>(x * x)
|
||||||
/// },
|
/// },
|
||||||
/// |study, _completed_trial| {
|
/// |study, _completed_trial| {
|
||||||
/// // Stop after finding 5 good trials
|
/// // Stop after finding 5 good trials
|
||||||
@@ -907,7 +907,7 @@ impl Study<f64> {
|
|||||||
// Get the just-completed trial for the callback
|
// Get the just-completed trial for the callback
|
||||||
let trials = self.completed_trials.read();
|
let trials = self.completed_trials.read();
|
||||||
let Some(completed) = trials.last() else {
|
let Some(completed) = trials.last() else {
|
||||||
return Err(crate::Error::Internal(
|
return Err(crate::TpeError::Internal(
|
||||||
"completed trial not found after adding",
|
"completed trial not found after adding",
|
||||||
));
|
));
|
||||||
};
|
};
|
||||||
@@ -930,7 +930,7 @@ impl Study<f64> {
|
|||||||
|
|
||||||
// Return error if no trials succeeded
|
// Return error if no trials succeeded
|
||||||
if self.n_trials() == 0 {
|
if self.n_trials() == 0 {
|
||||||
return Err(crate::Error::NoCompletedTrials);
|
return Err(crate::TpeError::NoCompletedTrials);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -953,7 +953,7 @@ impl Study<f64> {
|
|||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
///
|
///
|
||||||
/// Returns `Error::NoCompletedTrials` if all trials failed (no successful trials).
|
/// Returns `TpeError::NoCompletedTrials` if all trials failed (no successful trials).
|
||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
///
|
||||||
@@ -972,7 +972,7 @@ impl Study<f64> {
|
|||||||
/// let x = trial.suggest_float("x", -10.0, 10.0)?;
|
/// let x = trial.suggest_float("x", -10.0, 10.0)?;
|
||||||
/// // Simulate async work (e.g., network request)
|
/// // Simulate async work (e.g., network request)
|
||||||
/// let value = x * x;
|
/// let value = x * x;
|
||||||
/// Ok::<_, optimizer::Error>((trial, value))
|
/// Ok::<_, optimizer::TpeError>((trial, value))
|
||||||
/// })
|
/// })
|
||||||
/// .await?;
|
/// .await?;
|
||||||
///
|
///
|
||||||
@@ -1009,7 +1009,7 @@ impl Study<f64> {
|
|||||||
|
|
||||||
// Return error if no trials succeeded
|
// Return error if no trials succeeded
|
||||||
if self.n_trials() == 0 {
|
if self.n_trials() == 0 {
|
||||||
return Err(crate::Error::NoCompletedTrials);
|
return Err(crate::TpeError::NoCompletedTrials);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -1034,8 +1034,8 @@ impl Study<f64> {
|
|||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
///
|
///
|
||||||
/// Returns `Error::NoCompletedTrials` if all trials failed (no successful trials).
|
/// Returns `TpeError::NoCompletedTrials` if all trials failed (no successful trials).
|
||||||
/// Returns `Error::TaskError` if the semaphore is closed or a spawned task panics.
|
/// Returns `TpeError::TaskError` if the semaphore is closed or a spawned task panics.
|
||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
///
|
||||||
@@ -1054,7 +1054,7 @@ impl Study<f64> {
|
|||||||
/// let x = trial.suggest_float("x", -10.0, 10.0)?;
|
/// let x = trial.suggest_float("x", -10.0, 10.0)?;
|
||||||
/// // Async objective function (e.g., network request)
|
/// // Async objective function (e.g., network request)
|
||||||
/// let value = x * x;
|
/// let value = x * x;
|
||||||
/// Ok::<_, optimizer::Error>((trial, value))
|
/// Ok::<_, optimizer::TpeError>((trial, value))
|
||||||
/// })
|
/// })
|
||||||
/// .await?;
|
/// .await?;
|
||||||
///
|
///
|
||||||
@@ -1087,7 +1087,7 @@ impl Study<f64> {
|
|||||||
.clone()
|
.clone()
|
||||||
.acquire_owned()
|
.acquire_owned()
|
||||||
.await
|
.await
|
||||||
.map_err(|e| crate::Error::TaskError(e.to_string()))?;
|
.map_err(|e| crate::TpeError::TaskError(e.to_string()))?;
|
||||||
let trial = self.create_trial_with_sampler();
|
let trial = self.create_trial_with_sampler();
|
||||||
let objective = Arc::clone(&objective);
|
let objective = Arc::clone(&objective);
|
||||||
|
|
||||||
@@ -1104,7 +1104,7 @@ impl Study<f64> {
|
|||||||
for handle in handles {
|
for handle in handles {
|
||||||
match handle
|
match handle
|
||||||
.await
|
.await
|
||||||
.map_err(|e| crate::Error::TaskError(e.to_string()))?
|
.map_err(|e| crate::TpeError::TaskError(e.to_string()))?
|
||||||
{
|
{
|
||||||
Ok((trial, value)) => {
|
Ok((trial, value)) => {
|
||||||
self.complete_trial(trial, value);
|
self.complete_trial(trial, value);
|
||||||
@@ -1117,7 +1117,7 @@ impl Study<f64> {
|
|||||||
|
|
||||||
// Return error if no trials succeeded
|
// Return error if no trials succeeded
|
||||||
if self.n_trials() == 0 {
|
if self.n_trials() == 0 {
|
||||||
return Err(crate::Error::NoCompletedTrials);
|
return Err(crate::TpeError::NoCompletedTrials);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
+32
-176
@@ -1,6 +1,5 @@
|
|||||||
//! 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;
|
||||||
|
|
||||||
@@ -9,69 +8,11 @@ use parking_lot::RwLock;
|
|||||||
use crate::distribution::{
|
use crate::distribution::{
|
||||||
CategoricalDistribution, Distribution, FloatDistribution, IntDistribution,
|
CategoricalDistribution, Distribution, FloatDistribution, IntDistribution,
|
||||||
};
|
};
|
||||||
use crate::error::{Error, Result};
|
use crate::error::{Result, TpeError};
|
||||||
use crate::param::ParamValue;
|
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
|
||||||
@@ -249,7 +190,7 @@ impl Trial {
|
|||||||
/// ```
|
/// ```
|
||||||
pub fn suggest_float(&mut self, name: impl Into<String>, low: f64, high: f64) -> Result<f64> {
|
pub fn suggest_float(&mut self, name: impl Into<String>, low: f64, high: f64) -> Result<f64> {
|
||||||
if low > high {
|
if low > high {
|
||||||
return Err(Error::InvalidBounds { low, high });
|
return Err(TpeError::InvalidBounds { low, high });
|
||||||
}
|
}
|
||||||
|
|
||||||
let name = name.into();
|
let name = name.into();
|
||||||
@@ -275,7 +216,7 @@ impl Trial {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Distribution exists but doesn't match
|
// Distribution exists but doesn't match
|
||||||
return Err(Error::ParameterConflict {
|
return Err(TpeError::ParameterConflict {
|
||||||
name,
|
name,
|
||||||
reason: "parameter was previously sampled with different bounds or type"
|
reason: "parameter was previously sampled with different bounds or type"
|
||||||
.to_string(),
|
.to_string(),
|
||||||
@@ -285,7 +226,7 @@ impl Trial {
|
|||||||
// Sample using the sampler
|
// Sample using the sampler
|
||||||
let dist = Distribution::Float(distribution);
|
let dist = Distribution::Float(distribution);
|
||||||
let ParamValue::Float(value) = self.sample_value(&dist) else {
|
let ParamValue::Float(value) = self.sample_value(&dist) else {
|
||||||
return Err(Error::Internal(
|
return Err(TpeError::Internal(
|
||||||
"Float distribution should return Float value",
|
"Float distribution should return Float value",
|
||||||
));
|
));
|
||||||
};
|
};
|
||||||
@@ -342,11 +283,11 @@ impl Trial {
|
|||||||
high: f64,
|
high: f64,
|
||||||
) -> Result<f64> {
|
) -> Result<f64> {
|
||||||
if low <= 0.0 {
|
if low <= 0.0 {
|
||||||
return Err(Error::InvalidLogBounds);
|
return Err(TpeError::InvalidLogBounds);
|
||||||
}
|
}
|
||||||
|
|
||||||
if low > high {
|
if low > high {
|
||||||
return Err(Error::InvalidBounds { low, high });
|
return Err(TpeError::InvalidBounds { low, high });
|
||||||
}
|
}
|
||||||
|
|
||||||
let name = name.into();
|
let name = name.into();
|
||||||
@@ -372,7 +313,7 @@ impl Trial {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Distribution exists but doesn't match
|
// Distribution exists but doesn't match
|
||||||
return Err(Error::ParameterConflict {
|
return Err(TpeError::ParameterConflict {
|
||||||
name,
|
name,
|
||||||
reason: "parameter was previously sampled with different bounds or type"
|
reason: "parameter was previously sampled with different bounds or type"
|
||||||
.to_string(),
|
.to_string(),
|
||||||
@@ -382,7 +323,7 @@ impl Trial {
|
|||||||
// Sample using the sampler (sampler handles log-scale transformation)
|
// Sample using the sampler (sampler handles log-scale transformation)
|
||||||
let dist = Distribution::Float(distribution);
|
let dist = Distribution::Float(distribution);
|
||||||
let ParamValue::Float(value) = self.sample_value(&dist) else {
|
let ParamValue::Float(value) = self.sample_value(&dist) else {
|
||||||
return Err(Error::Internal(
|
return Err(TpeError::Internal(
|
||||||
"Float distribution should return Float value",
|
"Float distribution should return Float value",
|
||||||
));
|
));
|
||||||
};
|
};
|
||||||
@@ -439,11 +380,11 @@ impl Trial {
|
|||||||
step: f64,
|
step: f64,
|
||||||
) -> Result<f64> {
|
) -> Result<f64> {
|
||||||
if step <= 0.0 {
|
if step <= 0.0 {
|
||||||
return Err(Error::InvalidStep);
|
return Err(TpeError::InvalidStep);
|
||||||
}
|
}
|
||||||
|
|
||||||
if low > high {
|
if low > high {
|
||||||
return Err(Error::InvalidBounds { low, high });
|
return Err(TpeError::InvalidBounds { low, high });
|
||||||
}
|
}
|
||||||
|
|
||||||
let name = name.into();
|
let name = name.into();
|
||||||
@@ -469,7 +410,7 @@ impl Trial {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Distribution exists but doesn't match
|
// Distribution exists but doesn't match
|
||||||
return Err(Error::ParameterConflict {
|
return Err(TpeError::ParameterConflict {
|
||||||
name,
|
name,
|
||||||
reason: "parameter was previously sampled with different bounds or type"
|
reason: "parameter was previously sampled with different bounds or type"
|
||||||
.to_string(),
|
.to_string(),
|
||||||
@@ -479,7 +420,7 @@ impl Trial {
|
|||||||
// Sample using the sampler (sampler handles step-grid)
|
// Sample using the sampler (sampler handles step-grid)
|
||||||
let dist = Distribution::Float(distribution);
|
let dist = Distribution::Float(distribution);
|
||||||
let ParamValue::Float(value) = self.sample_value(&dist) else {
|
let ParamValue::Float(value) = self.sample_value(&dist) else {
|
||||||
return Err(Error::Internal(
|
return Err(TpeError::Internal(
|
||||||
"Float distribution should return Float value",
|
"Float distribution should return Float value",
|
||||||
));
|
));
|
||||||
};
|
};
|
||||||
@@ -525,7 +466,7 @@ impl Trial {
|
|||||||
#[allow(clippy::cast_precision_loss)]
|
#[allow(clippy::cast_precision_loss)]
|
||||||
pub fn suggest_int(&mut self, name: impl Into<String>, low: i64, high: i64) -> Result<i64> {
|
pub fn suggest_int(&mut self, name: impl Into<String>, low: i64, high: i64) -> Result<i64> {
|
||||||
if low > high {
|
if low > high {
|
||||||
return Err(Error::InvalidBounds {
|
return Err(TpeError::InvalidBounds {
|
||||||
low: low as f64,
|
low: low as f64,
|
||||||
high: high as f64,
|
high: high as f64,
|
||||||
});
|
});
|
||||||
@@ -554,7 +495,7 @@ impl Trial {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Distribution exists but doesn't match
|
// Distribution exists but doesn't match
|
||||||
return Err(Error::ParameterConflict {
|
return Err(TpeError::ParameterConflict {
|
||||||
name,
|
name,
|
||||||
reason: "parameter was previously sampled with different bounds or type"
|
reason: "parameter was previously sampled with different bounds or type"
|
||||||
.to_string(),
|
.to_string(),
|
||||||
@@ -564,7 +505,9 @@ impl Trial {
|
|||||||
// Sample using the sampler
|
// Sample using the sampler
|
||||||
let dist = Distribution::Int(distribution);
|
let dist = Distribution::Int(distribution);
|
||||||
let ParamValue::Int(value) = self.sample_value(&dist) else {
|
let ParamValue::Int(value) = self.sample_value(&dist) else {
|
||||||
return Err(Error::Internal("Int distribution should return Int value"));
|
return Err(TpeError::Internal(
|
||||||
|
"Int distribution should return Int value",
|
||||||
|
));
|
||||||
};
|
};
|
||||||
|
|
||||||
// Store distribution and value
|
// Store distribution and value
|
||||||
@@ -611,11 +554,11 @@ impl Trial {
|
|||||||
#[allow(clippy::cast_precision_loss)]
|
#[allow(clippy::cast_precision_loss)]
|
||||||
pub fn suggest_int_log(&mut self, name: impl Into<String>, low: i64, high: i64) -> Result<i64> {
|
pub fn suggest_int_log(&mut self, name: impl Into<String>, low: i64, high: i64) -> Result<i64> {
|
||||||
if low < 1 {
|
if low < 1 {
|
||||||
return Err(Error::InvalidLogBounds);
|
return Err(TpeError::InvalidLogBounds);
|
||||||
}
|
}
|
||||||
|
|
||||||
if low > high {
|
if low > high {
|
||||||
return Err(Error::InvalidBounds {
|
return Err(TpeError::InvalidBounds {
|
||||||
low: low as f64,
|
low: low as f64,
|
||||||
high: high as f64,
|
high: high as f64,
|
||||||
});
|
});
|
||||||
@@ -644,7 +587,7 @@ impl Trial {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Distribution exists but doesn't match
|
// Distribution exists but doesn't match
|
||||||
return Err(Error::ParameterConflict {
|
return Err(TpeError::ParameterConflict {
|
||||||
name,
|
name,
|
||||||
reason: "parameter was previously sampled with different bounds or type"
|
reason: "parameter was previously sampled with different bounds or type"
|
||||||
.to_string(),
|
.to_string(),
|
||||||
@@ -654,7 +597,9 @@ impl Trial {
|
|||||||
// Sample using the sampler (sampler handles log-scale transformation)
|
// Sample using the sampler (sampler handles log-scale transformation)
|
||||||
let dist = Distribution::Int(distribution);
|
let dist = Distribution::Int(distribution);
|
||||||
let ParamValue::Int(value) = self.sample_value(&dist) else {
|
let ParamValue::Int(value) = self.sample_value(&dist) else {
|
||||||
return Err(Error::Internal("Int distribution should return Int value"));
|
return Err(TpeError::Internal(
|
||||||
|
"Int distribution should return Int value",
|
||||||
|
));
|
||||||
};
|
};
|
||||||
|
|
||||||
// Store distribution and value
|
// Store distribution and value
|
||||||
@@ -714,11 +659,11 @@ impl Trial {
|
|||||||
step: i64,
|
step: i64,
|
||||||
) -> Result<i64> {
|
) -> Result<i64> {
|
||||||
if step <= 0 {
|
if step <= 0 {
|
||||||
return Err(Error::InvalidStep);
|
return Err(TpeError::InvalidStep);
|
||||||
}
|
}
|
||||||
|
|
||||||
if low > high {
|
if low > high {
|
||||||
return Err(Error::InvalidBounds {
|
return Err(TpeError::InvalidBounds {
|
||||||
low: low as f64,
|
low: low as f64,
|
||||||
high: high as f64,
|
high: high as f64,
|
||||||
});
|
});
|
||||||
@@ -747,7 +692,7 @@ impl Trial {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Distribution exists but doesn't match
|
// Distribution exists but doesn't match
|
||||||
return Err(Error::ParameterConflict {
|
return Err(TpeError::ParameterConflict {
|
||||||
name,
|
name,
|
||||||
reason: "parameter was previously sampled with different bounds or type"
|
reason: "parameter was previously sampled with different bounds or type"
|
||||||
.to_string(),
|
.to_string(),
|
||||||
@@ -757,7 +702,9 @@ impl Trial {
|
|||||||
// Sample using the sampler (sampler handles step-grid)
|
// Sample using the sampler (sampler handles step-grid)
|
||||||
let dist = Distribution::Int(distribution);
|
let dist = Distribution::Int(distribution);
|
||||||
let ParamValue::Int(value) = self.sample_value(&dist) else {
|
let ParamValue::Int(value) = self.sample_value(&dist) else {
|
||||||
return Err(Error::Internal("Int distribution should return Int value"));
|
return Err(TpeError::Internal(
|
||||||
|
"Int distribution should return Int value",
|
||||||
|
));
|
||||||
};
|
};
|
||||||
|
|
||||||
// Store distribution and value
|
// Store distribution and value
|
||||||
@@ -813,7 +760,7 @@ impl Trial {
|
|||||||
choices: &[T],
|
choices: &[T],
|
||||||
) -> Result<T> {
|
) -> Result<T> {
|
||||||
if choices.is_empty() {
|
if choices.is_empty() {
|
||||||
return Err(Error::EmptyChoices);
|
return Err(TpeError::EmptyChoices);
|
||||||
}
|
}
|
||||||
|
|
||||||
let name = name.into();
|
let name = name.into();
|
||||||
@@ -832,7 +779,7 @@ impl Trial {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Distribution exists but doesn't match
|
// Distribution exists but doesn't match
|
||||||
return Err(Error::ParameterConflict {
|
return Err(TpeError::ParameterConflict {
|
||||||
name,
|
name,
|
||||||
reason: "parameter was previously sampled with different number of choices or type"
|
reason: "parameter was previously sampled with different number of choices or type"
|
||||||
.to_string(),
|
.to_string(),
|
||||||
@@ -842,7 +789,7 @@ impl Trial {
|
|||||||
// Sample using the sampler
|
// Sample using the sampler
|
||||||
let dist = Distribution::Categorical(distribution);
|
let dist = Distribution::Categorical(distribution);
|
||||||
let ParamValue::Categorical(index) = self.sample_value(&dist) else {
|
let ParamValue::Categorical(index) = self.sample_value(&dist) else {
|
||||||
return Err(Error::Internal(
|
return Err(TpeError::Internal(
|
||||||
"Categorical distribution should return Categorical value",
|
"Categorical distribution should return Categorical value",
|
||||||
));
|
));
|
||||||
};
|
};
|
||||||
@@ -853,95 +800,4 @@ 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())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-13
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
use optimizer::sampler::random::RandomSampler;
|
use optimizer::sampler::random::RandomSampler;
|
||||||
use optimizer::sampler::tpe::TpeSampler;
|
use optimizer::sampler::tpe::TpeSampler;
|
||||||
use optimizer::{Direction, Error, Study};
|
use optimizer::{Direction, Study, TpeError};
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_optimize_async_basic() {
|
async fn test_optimize_async_basic() {
|
||||||
@@ -16,7 +16,7 @@ async fn test_optimize_async_basic() {
|
|||||||
study
|
study
|
||||||
.optimize_async(10, |mut trial| async move {
|
.optimize_async(10, |mut trial| async move {
|
||||||
let x = trial.suggest_float("x", -10.0, 10.0)?;
|
let x = trial.suggest_float("x", -10.0, 10.0)?;
|
||||||
Ok::<_, Error>((trial, x * x))
|
Ok::<_, TpeError>((trial, x * x))
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.expect("async optimization should succeed");
|
.expect("async optimization should succeed");
|
||||||
@@ -39,7 +39,7 @@ async fn test_optimize_async_with_sampler() {
|
|||||||
study
|
study
|
||||||
.optimize_async_with_sampler(15, |mut trial| async move {
|
.optimize_async_with_sampler(15, |mut trial| async move {
|
||||||
let x = trial.suggest_float("x", -5.0, 5.0)?;
|
let x = trial.suggest_float("x", -5.0, 5.0)?;
|
||||||
Ok::<_, Error>((trial, x * x))
|
Ok::<_, TpeError>((trial, x * x))
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.expect("async optimization with sampler should succeed");
|
.expect("async optimization with sampler should succeed");
|
||||||
@@ -57,7 +57,7 @@ async fn test_optimize_parallel() {
|
|||||||
study
|
study
|
||||||
.optimize_parallel(20, 4, |mut trial| async move {
|
.optimize_parallel(20, 4, |mut trial| async move {
|
||||||
let x = trial.suggest_float("x", -10.0, 10.0)?;
|
let x = trial.suggest_float("x", -10.0, 10.0)?;
|
||||||
Ok::<_, Error>((trial, x * x))
|
Ok::<_, TpeError>((trial, x * x))
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.expect("parallel optimization should succeed");
|
.expect("parallel optimization should succeed");
|
||||||
@@ -79,7 +79,7 @@ async fn test_optimize_parallel_with_sampler() {
|
|||||||
.optimize_parallel_with_sampler(15, 3, |mut trial| async move {
|
.optimize_parallel_with_sampler(15, 3, |mut trial| async move {
|
||||||
let x = trial.suggest_float("x", -5.0, 5.0)?;
|
let x = trial.suggest_float("x", -5.0, 5.0)?;
|
||||||
let y = trial.suggest_float("y", -5.0, 5.0)?;
|
let y = trial.suggest_float("y", -5.0, 5.0)?;
|
||||||
Ok::<_, Error>((trial, x * x + y * y))
|
Ok::<_, TpeError>((trial, x * x + y * y))
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.expect("parallel optimization with sampler should succeed");
|
.expect("parallel optimization with sampler should succeed");
|
||||||
@@ -99,7 +99,7 @@ async fn test_optimize_async_all_failures() {
|
|||||||
.await;
|
.await;
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
matches!(result, Err(Error::NoCompletedTrials)),
|
matches!(result, Err(TpeError::NoCompletedTrials)),
|
||||||
"should return NoCompletedTrials when all trials fail"
|
"should return NoCompletedTrials when all trials fail"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -116,7 +116,7 @@ async fn test_optimize_async_with_sampler_all_failures() {
|
|||||||
.await;
|
.await;
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
matches!(result, Err(Error::NoCompletedTrials)),
|
matches!(result, Err(TpeError::NoCompletedTrials)),
|
||||||
"should return NoCompletedTrials when all trials fail"
|
"should return NoCompletedTrials when all trials fail"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -133,7 +133,7 @@ async fn test_optimize_parallel_all_failures() {
|
|||||||
.await;
|
.await;
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
matches!(result, Err(Error::NoCompletedTrials)),
|
matches!(result, Err(TpeError::NoCompletedTrials)),
|
||||||
"should return NoCompletedTrials when all trials fail"
|
"should return NoCompletedTrials when all trials fail"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -150,7 +150,7 @@ async fn test_optimize_parallel_with_sampler_all_failures() {
|
|||||||
.await;
|
.await;
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
matches!(result, Err(Error::NoCompletedTrials)),
|
matches!(result, Err(TpeError::NoCompletedTrials)),
|
||||||
"should return NoCompletedTrials when all trials fail"
|
"should return NoCompletedTrials when all trials fail"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -168,9 +168,9 @@ async fn test_optimize_async_partial_failures() {
|
|||||||
async move {
|
async move {
|
||||||
if count.is_multiple_of(2) {
|
if count.is_multiple_of(2) {
|
||||||
let x = trial.suggest_float("x", 0.0, 10.0)?;
|
let x = trial.suggest_float("x", 0.0, 10.0)?;
|
||||||
Ok::<_, Error>((trial, x))
|
Ok::<_, TpeError>((trial, x))
|
||||||
} else {
|
} else {
|
||||||
Err(Error::NoCompletedTrials) // Use as error type
|
Err(TpeError::NoCompletedTrials) // Use as error type
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -190,7 +190,7 @@ async fn test_optimize_parallel_high_concurrency() {
|
|||||||
study
|
study
|
||||||
.optimize_parallel(5, 10, |mut trial| async move {
|
.optimize_parallel(5, 10, |mut trial| async move {
|
||||||
let x = trial.suggest_float("x", 0.0, 10.0)?;
|
let x = trial.suggest_float("x", 0.0, 10.0)?;
|
||||||
Ok::<_, Error>((trial, x))
|
Ok::<_, TpeError>((trial, x))
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.expect("should handle high concurrency");
|
.expect("should handle high concurrency");
|
||||||
@@ -207,7 +207,7 @@ async fn test_optimize_parallel_single_concurrency() {
|
|||||||
study
|
study
|
||||||
.optimize_parallel(10, 1, |mut trial| async move {
|
.optimize_parallel(10, 1, |mut trial| async move {
|
||||||
let x = trial.suggest_float("x", 0.0, 10.0)?;
|
let x = trial.suggest_float("x", 0.0, 10.0)?;
|
||||||
Ok::<_, Error>((trial, x))
|
Ok::<_, TpeError>((trial, x))
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.expect("should work with single concurrency");
|
.expect("should work with single concurrency");
|
||||||
|
|||||||
+56
-285
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
use optimizer::sampler::random::RandomSampler;
|
use optimizer::sampler::random::RandomSampler;
|
||||||
use optimizer::sampler::tpe::TpeSampler;
|
use optimizer::sampler::tpe::TpeSampler;
|
||||||
use optimizer::{Direction, Error, Study, Trial};
|
use optimizer::{Direction, Study, TpeError, Trial};
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// Test: optimize simple quadratic function with TPE, finds near-optimal
|
// Test: optimize simple quadratic function with TPE, finds near-optimal
|
||||||
@@ -30,7 +30,7 @@ fn test_tpe_optimizes_quadratic_function() {
|
|||||||
study
|
study
|
||||||
.optimize_with_sampler(50, |trial| {
|
.optimize_with_sampler(50, |trial| {
|
||||||
let x = trial.suggest_float("x", -10.0, 10.0)?;
|
let x = trial.suggest_float("x", -10.0, 10.0)?;
|
||||||
Ok::<_, Error>((x - 3.0).powi(2))
|
Ok::<_, TpeError>((x - 3.0).powi(2))
|
||||||
})
|
})
|
||||||
.expect("optimization should succeed");
|
.expect("optimization should succeed");
|
||||||
|
|
||||||
@@ -61,7 +61,7 @@ fn test_tpe_optimizes_multivariate_function() {
|
|||||||
.optimize_with_sampler(100, |trial| {
|
.optimize_with_sampler(100, |trial| {
|
||||||
let x = trial.suggest_float("x", -5.0, 5.0)?;
|
let x = trial.suggest_float("x", -5.0, 5.0)?;
|
||||||
let y = trial.suggest_float("y", -5.0, 5.0)?;
|
let y = trial.suggest_float("y", -5.0, 5.0)?;
|
||||||
Ok::<_, Error>(x * x + y * y)
|
Ok::<_, TpeError>(x * x + y * y)
|
||||||
})
|
})
|
||||||
.expect("optimization should succeed");
|
.expect("optimization should succeed");
|
||||||
|
|
||||||
@@ -90,7 +90,7 @@ fn test_tpe_maximization() {
|
|||||||
study
|
study
|
||||||
.optimize_with_sampler(50, |trial| {
|
.optimize_with_sampler(50, |trial| {
|
||||||
let x = trial.suggest_float("x", -10.0, 10.0)?;
|
let x = trial.suggest_float("x", -10.0, 10.0)?;
|
||||||
Ok::<_, Error>(-(x - 2.0).powi(2) + 10.0)
|
Ok::<_, TpeError>(-(x - 2.0).powi(2) + 10.0)
|
||||||
})
|
})
|
||||||
.expect("optimization should succeed");
|
.expect("optimization should succeed");
|
||||||
|
|
||||||
@@ -123,7 +123,7 @@ fn test_random_sampler_uniform_float_distribution() {
|
|||||||
.optimize(n_samples, |trial| {
|
.optimize(n_samples, |trial| {
|
||||||
let x = trial.suggest_float("x", 0.0, 1.0)?;
|
let x = trial.suggest_float("x", 0.0, 1.0)?;
|
||||||
samples.push(x);
|
samples.push(x);
|
||||||
Ok::<_, Error>(x)
|
Ok::<_, TpeError>(x)
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
@@ -152,7 +152,7 @@ fn test_random_sampler_uniform_float_distribution() {
|
|||||||
fn test_random_sampler_uniform_int_distribution() {
|
fn test_random_sampler_uniform_int_distribution() {
|
||||||
let study: Study<f64> = Study::with_sampler(Direction::Minimize, RandomSampler::with_seed(123));
|
let study: Study<f64> = Study::with_sampler(Direction::Minimize, RandomSampler::with_seed(123));
|
||||||
|
|
||||||
let n_samples = 5000;
|
let n_samples = 1000;
|
||||||
let mut counts = [0u32; 10]; // counts for values 1-10
|
let mut counts = [0u32; 10]; // counts for values 1-10
|
||||||
|
|
||||||
study
|
study
|
||||||
@@ -160,18 +160,16 @@ fn test_random_sampler_uniform_int_distribution() {
|
|||||||
let n = trial.suggest_int("n", 1, 10)?;
|
let n = trial.suggest_int("n", 1, 10)?;
|
||||||
assert!((1..=10).contains(&n), "sample {n} out of range [1, 10]");
|
assert!((1..=10).contains(&n), "sample {n} out of range [1, 10]");
|
||||||
counts[(n - 1) as usize] += 1;
|
counts[(n - 1) as usize] += 1;
|
||||||
Ok::<_, Error>(n as f64)
|
Ok::<_, TpeError>(n as f64)
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
// Each value should appear roughly n_samples / 10 times
|
// Each value should appear roughly n_samples / 10 times
|
||||||
// With 5000 samples, expected ~500 per bucket, std dev ~21
|
|
||||||
// 20% tolerance allows for ~4.5 std devs which is very safe
|
|
||||||
let expected = n_samples as f64 / 10.0;
|
let expected = n_samples as f64 / 10.0;
|
||||||
for (i, &count) in counts.iter().enumerate() {
|
for (i, &count) in counts.iter().enumerate() {
|
||||||
let diff = (count as f64 - expected).abs() / expected;
|
let diff = (count as f64 - expected).abs() / expected;
|
||||||
assert!(
|
assert!(
|
||||||
diff < 0.2,
|
diff < 0.3,
|
||||||
"value {} appeared {} times, expected ~{}, diff = {:.1}%",
|
"value {} appeared {} times, expected ~{}, diff = {:.1}%",
|
||||||
i + 1,
|
i + 1,
|
||||||
count,
|
count,
|
||||||
@@ -185,7 +183,7 @@ fn test_random_sampler_uniform_int_distribution() {
|
|||||||
fn test_random_sampler_uniform_categorical_distribution() {
|
fn test_random_sampler_uniform_categorical_distribution() {
|
||||||
let study: Study<f64> = Study::with_sampler(Direction::Minimize, RandomSampler::with_seed(456));
|
let study: Study<f64> = Study::with_sampler(Direction::Minimize, RandomSampler::with_seed(456));
|
||||||
|
|
||||||
let n_samples = 2000;
|
let n_samples = 1000;
|
||||||
let mut counts = [0u32; 4];
|
let mut counts = [0u32; 4];
|
||||||
let choices = ["a", "b", "c", "d"];
|
let choices = ["a", "b", "c", "d"];
|
||||||
|
|
||||||
@@ -194,18 +192,16 @@ fn test_random_sampler_uniform_categorical_distribution() {
|
|||||||
let choice = trial.suggest_categorical("cat", &choices)?;
|
let choice = trial.suggest_categorical("cat", &choices)?;
|
||||||
let idx = choices.iter().position(|&c| c == choice).unwrap();
|
let idx = choices.iter().position(|&c| c == choice).unwrap();
|
||||||
counts[idx] += 1;
|
counts[idx] += 1;
|
||||||
Ok::<_, Error>(idx as f64)
|
Ok::<_, TpeError>(idx as f64)
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
// Each category should appear roughly n_samples / 4 times
|
// Each category should appear roughly n_samples / 4 times
|
||||||
// With 2000 samples, expected ~500 per bucket, std dev ~19
|
|
||||||
// 15% tolerance allows for ~4 std devs which is very safe
|
|
||||||
let expected = n_samples as f64 / 4.0;
|
let expected = n_samples as f64 / 4.0;
|
||||||
for (i, &count) in counts.iter().enumerate() {
|
for (i, &count) in counts.iter().enumerate() {
|
||||||
let diff = (count as f64 - expected).abs() / expected;
|
let diff = (count as f64 - expected).abs() / expected;
|
||||||
assert!(
|
assert!(
|
||||||
diff < 0.15,
|
diff < 0.25,
|
||||||
"category {} appeared {} times, expected ~{}, diff = {:.1}%",
|
"category {} appeared {} times, expected ~{}, diff = {:.1}%",
|
||||||
i,
|
i,
|
||||||
count,
|
count,
|
||||||
@@ -231,7 +227,7 @@ fn test_random_sampler_reproducibility() {
|
|||||||
.optimize_with_sampler(100, |trial| {
|
.optimize_with_sampler(100, |trial| {
|
||||||
let x = trial.suggest_float("x", 0.0, 100.0)?;
|
let x = trial.suggest_float("x", 0.0, 100.0)?;
|
||||||
values1.push(x);
|
values1.push(x);
|
||||||
Ok::<_, Error>(x)
|
Ok::<_, TpeError>(x)
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
@@ -239,7 +235,7 @@ fn test_random_sampler_reproducibility() {
|
|||||||
.optimize_with_sampler(100, |trial| {
|
.optimize_with_sampler(100, |trial| {
|
||||||
let x = trial.suggest_float("x", 0.0, 100.0)?;
|
let x = trial.suggest_float("x", 0.0, 100.0)?;
|
||||||
values2.push(x);
|
values2.push(x);
|
||||||
Ok::<_, Error>(x)
|
Ok::<_, TpeError>(x)
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
@@ -371,7 +367,7 @@ fn test_parameter_conflict_float_different_bounds() {
|
|||||||
trial.suggest_float("x", 0.0, 1.0).unwrap();
|
trial.suggest_float("x", 0.0, 1.0).unwrap();
|
||||||
let result = trial.suggest_float("x", 0.0, 2.0); // Different upper bound
|
let result = trial.suggest_float("x", 0.0, 2.0); // Different upper bound
|
||||||
|
|
||||||
assert!(matches!(result, Err(Error::ParameterConflict { .. })));
|
assert!(matches!(result, Err(TpeError::ParameterConflict { .. })));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -381,7 +377,7 @@ fn test_parameter_conflict_float_vs_log() {
|
|||||||
trial.suggest_float("x", 0.1, 1.0).unwrap();
|
trial.suggest_float("x", 0.1, 1.0).unwrap();
|
||||||
let result = trial.suggest_float_log("x", 0.1, 1.0); // Same bounds but log scale
|
let result = trial.suggest_float_log("x", 0.1, 1.0); // Same bounds but log scale
|
||||||
|
|
||||||
assert!(matches!(result, Err(Error::ParameterConflict { .. })));
|
assert!(matches!(result, Err(TpeError::ParameterConflict { .. })));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -391,7 +387,7 @@ fn test_parameter_conflict_float_vs_step() {
|
|||||||
trial.suggest_float("x", 0.0, 1.0).unwrap();
|
trial.suggest_float("x", 0.0, 1.0).unwrap();
|
||||||
let result = trial.suggest_float_step("x", 0.0, 1.0, 0.1); // Same bounds but with step
|
let result = trial.suggest_float_step("x", 0.0, 1.0, 0.1); // Same bounds but with step
|
||||||
|
|
||||||
assert!(matches!(result, Err(Error::ParameterConflict { .. })));
|
assert!(matches!(result, Err(TpeError::ParameterConflict { .. })));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -401,7 +397,7 @@ fn test_parameter_conflict_int_different_bounds() {
|
|||||||
trial.suggest_int("n", 1, 10).unwrap();
|
trial.suggest_int("n", 1, 10).unwrap();
|
||||||
let result = trial.suggest_int("n", 1, 20); // Different upper bound
|
let result = trial.suggest_int("n", 1, 20); // Different upper bound
|
||||||
|
|
||||||
assert!(matches!(result, Err(Error::ParameterConflict { .. })));
|
assert!(matches!(result, Err(TpeError::ParameterConflict { .. })));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -411,7 +407,7 @@ fn test_parameter_conflict_int_vs_log() {
|
|||||||
trial.suggest_int("n", 1, 100).unwrap();
|
trial.suggest_int("n", 1, 100).unwrap();
|
||||||
let result = trial.suggest_int_log("n", 1, 100); // Same bounds but log scale
|
let result = trial.suggest_int_log("n", 1, 100); // Same bounds but log scale
|
||||||
|
|
||||||
assert!(matches!(result, Err(Error::ParameterConflict { .. })));
|
assert!(matches!(result, Err(TpeError::ParameterConflict { .. })));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -421,7 +417,7 @@ fn test_parameter_conflict_categorical_different_n_choices() {
|
|||||||
trial.suggest_categorical("opt", &["a", "b", "c"]).unwrap();
|
trial.suggest_categorical("opt", &["a", "b", "c"]).unwrap();
|
||||||
let result = trial.suggest_categorical("opt", &["x", "y"]); // Different number of choices
|
let result = trial.suggest_categorical("opt", &["x", "y"]); // Different number of choices
|
||||||
|
|
||||||
assert!(matches!(result, Err(Error::ParameterConflict { .. })));
|
assert!(matches!(result, Err(TpeError::ParameterConflict { .. })));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -431,7 +427,7 @@ fn test_parameter_conflict_float_vs_int() {
|
|||||||
trial.suggest_float("x", 0.0, 10.0).unwrap();
|
trial.suggest_float("x", 0.0, 10.0).unwrap();
|
||||||
let result = trial.suggest_int("x", 0, 10); // Different type
|
let result = trial.suggest_int("x", 0, 10); // Different type
|
||||||
|
|
||||||
assert!(matches!(result, Err(Error::ParameterConflict { .. })));
|
assert!(matches!(result, Err(TpeError::ParameterConflict { .. })));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -442,7 +438,7 @@ fn test_parameter_conflict_returns_name() {
|
|||||||
let result = trial.suggest_float("my_param", 0.0, 2.0);
|
let result = trial.suggest_float("my_param", 0.0, 2.0);
|
||||||
|
|
||||||
match result {
|
match result {
|
||||||
Err(Error::ParameterConflict { name, .. }) => {
|
Err(TpeError::ParameterConflict { name, .. }) => {
|
||||||
assert_eq!(name, "my_param");
|
assert_eq!(name, "my_param");
|
||||||
}
|
}
|
||||||
_ => panic!("expected ParameterConflict error"),
|
_ => panic!("expected ParameterConflict error"),
|
||||||
@@ -460,7 +456,7 @@ fn test_empty_categorical_returns_error() {
|
|||||||
|
|
||||||
let result = trial.suggest_categorical("opt", empty);
|
let result = trial.suggest_categorical("opt", empty);
|
||||||
|
|
||||||
assert!(matches!(result, Err(Error::EmptyChoices)));
|
assert!(matches!(result, Err(TpeError::EmptyChoices)));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -470,7 +466,7 @@ fn test_empty_categorical_vec_returns_error() {
|
|||||||
|
|
||||||
let result = trial.suggest_categorical("numbers", &empty);
|
let result = trial.suggest_categorical("numbers", &empty);
|
||||||
|
|
||||||
assert!(matches!(result, Err(Error::EmptyChoices)));
|
assert!(matches!(result, Err(TpeError::EmptyChoices)));
|
||||||
}
|
}
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
@@ -484,7 +480,7 @@ fn test_study_basic_workflow() {
|
|||||||
study
|
study
|
||||||
.optimize(10, |trial| {
|
.optimize(10, |trial| {
|
||||||
let x = trial.suggest_float("x", -5.0, 5.0)?;
|
let x = trial.suggest_float("x", -5.0, 5.0)?;
|
||||||
Ok::<_, Error>(x * x)
|
Ok::<_, TpeError>(x * x)
|
||||||
})
|
})
|
||||||
.expect("optimization should succeed");
|
.expect("optimization should succeed");
|
||||||
|
|
||||||
@@ -521,7 +517,7 @@ fn test_no_completed_trials_error() {
|
|||||||
let study: Study<f64> = Study::new(Direction::Minimize);
|
let study: Study<f64> = Study::new(Direction::Minimize);
|
||||||
|
|
||||||
let result = study.best_trial();
|
let result = study.best_trial();
|
||||||
assert!(matches!(result, Err(Error::NoCompletedTrials)));
|
assert!(matches!(result, Err(TpeError::NoCompletedTrials)));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -530,11 +526,11 @@ fn test_invalid_bounds_errors() {
|
|||||||
|
|
||||||
// low > high for float
|
// low > high for float
|
||||||
let result = trial.suggest_float("x", 10.0, 5.0);
|
let result = trial.suggest_float("x", 10.0, 5.0);
|
||||||
assert!(matches!(result, Err(Error::InvalidBounds { .. })));
|
assert!(matches!(result, Err(TpeError::InvalidBounds { .. })));
|
||||||
|
|
||||||
// low > high for int
|
// low > high for int
|
||||||
let result = trial.suggest_int("n", 100, 50);
|
let result = trial.suggest_int("n", 100, 50);
|
||||||
assert!(matches!(result, Err(Error::InvalidBounds { .. })));
|
assert!(matches!(result, Err(TpeError::InvalidBounds { .. })));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -543,14 +539,14 @@ fn test_invalid_log_bounds_errors() {
|
|||||||
|
|
||||||
// low <= 0 for log float
|
// low <= 0 for log float
|
||||||
let result = trial.suggest_float_log("x", 0.0, 1.0);
|
let result = trial.suggest_float_log("x", 0.0, 1.0);
|
||||||
assert!(matches!(result, Err(Error::InvalidLogBounds)));
|
assert!(matches!(result, Err(TpeError::InvalidLogBounds)));
|
||||||
|
|
||||||
let result = trial.suggest_float_log("y", -1.0, 1.0);
|
let result = trial.suggest_float_log("y", -1.0, 1.0);
|
||||||
assert!(matches!(result, Err(Error::InvalidLogBounds)));
|
assert!(matches!(result, Err(TpeError::InvalidLogBounds)));
|
||||||
|
|
||||||
// low < 1 for log int
|
// low < 1 for log int
|
||||||
let result = trial.suggest_int_log("n", 0, 100);
|
let result = trial.suggest_int_log("n", 0, 100);
|
||||||
assert!(matches!(result, Err(Error::InvalidLogBounds)));
|
assert!(matches!(result, Err(TpeError::InvalidLogBounds)));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -559,14 +555,14 @@ fn test_invalid_step_errors() {
|
|||||||
|
|
||||||
// step <= 0 for float
|
// step <= 0 for float
|
||||||
let result = trial.suggest_float_step("x", 0.0, 1.0, 0.0);
|
let result = trial.suggest_float_step("x", 0.0, 1.0, 0.0);
|
||||||
assert!(matches!(result, Err(Error::InvalidStep)));
|
assert!(matches!(result, Err(TpeError::InvalidStep)));
|
||||||
|
|
||||||
let result = trial.suggest_float_step("y", 0.0, 1.0, -0.1);
|
let result = trial.suggest_float_step("y", 0.0, 1.0, -0.1);
|
||||||
assert!(matches!(result, Err(Error::InvalidStep)));
|
assert!(matches!(result, Err(TpeError::InvalidStep)));
|
||||||
|
|
||||||
// step <= 0 for int
|
// step <= 0 for int
|
||||||
let result = trial.suggest_int_step("n", 0, 100, 0);
|
let result = trial.suggest_int_step("n", 0, 100, 0);
|
||||||
assert!(matches!(result, Err(Error::InvalidStep)));
|
assert!(matches!(result, Err(TpeError::InvalidStep)));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -592,7 +588,7 @@ fn test_tpe_with_categorical_parameter() {
|
|||||||
"cubic" => -((x - 1.0).powi(2)) + 10.0, // peak at x=1, max value 10
|
"cubic" => -((x - 1.0).powi(2)) + 10.0, // peak at x=1, max value 10
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
};
|
};
|
||||||
Ok::<_, Error>(value)
|
Ok::<_, TpeError>(value)
|
||||||
})
|
})
|
||||||
.expect("optimization should succeed");
|
.expect("optimization should succeed");
|
||||||
|
|
||||||
@@ -619,7 +615,7 @@ fn test_tpe_with_integer_parameters() {
|
|||||||
study
|
study
|
||||||
.optimize_with_sampler(30, |trial| {
|
.optimize_with_sampler(30, |trial| {
|
||||||
let n = trial.suggest_int("n", 1, 10)?;
|
let n = trial.suggest_int("n", 1, 10)?;
|
||||||
Ok::<_, Error>(((n - 7) as f64).powi(2))
|
Ok::<_, TpeError>(((n - 7) as f64).powi(2))
|
||||||
})
|
})
|
||||||
.expect("optimization should succeed");
|
.expect("optimization should succeed");
|
||||||
|
|
||||||
@@ -647,7 +643,7 @@ fn test_callback_early_stopping() {
|
|||||||
|trial| {
|
|trial| {
|
||||||
trials_run.set(trials_run.get() + 1);
|
trials_run.set(trials_run.get() + 1);
|
||||||
let x = trial.suggest_float("x", 0.0, 10.0)?;
|
let x = trial.suggest_float("x", 0.0, 10.0)?;
|
||||||
Ok::<_, Error>(x)
|
Ok::<_, TpeError>(x)
|
||||||
},
|
},
|
||||||
|_study, _trial| {
|
|_study, _trial| {
|
||||||
// Stop after 5 trials
|
// Stop after 5 trials
|
||||||
@@ -670,7 +666,7 @@ fn test_study_trials_iteration() {
|
|||||||
study
|
study
|
||||||
.optimize(5, |trial| {
|
.optimize(5, |trial| {
|
||||||
let x = trial.suggest_float("x", 0.0, 1.0)?;
|
let x = trial.suggest_float("x", 0.0, 1.0)?;
|
||||||
Ok::<_, Error>(x)
|
Ok::<_, TpeError>(x)
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
@@ -762,7 +758,7 @@ fn test_best_value() {
|
|||||||
study
|
study
|
||||||
.optimize(10, |trial| {
|
.optimize(10, |trial| {
|
||||||
let x = trial.suggest_float("x", 0.0, 10.0)?;
|
let x = trial.suggest_float("x", 0.0, 10.0)?;
|
||||||
Ok::<_, Error>(x)
|
Ok::<_, TpeError>(x)
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
@@ -796,7 +792,7 @@ fn test_study_set_sampler() {
|
|||||||
study
|
study
|
||||||
.optimize_with_sampler(10, |trial| {
|
.optimize_with_sampler(10, |trial| {
|
||||||
let x = trial.suggest_float("x", -5.0, 5.0)?;
|
let x = trial.suggest_float("x", -5.0, 5.0)?;
|
||||||
Ok::<_, Error>(x * x)
|
Ok::<_, TpeError>(x * x)
|
||||||
})
|
})
|
||||||
.expect("optimization should succeed with new sampler");
|
.expect("optimization should succeed with new sampler");
|
||||||
|
|
||||||
@@ -811,7 +807,7 @@ fn test_study_with_i32_value_type() {
|
|||||||
study
|
study
|
||||||
.optimize(10, |trial| {
|
.optimize(10, |trial| {
|
||||||
let x = trial.suggest_int("x", -10, 10)?;
|
let x = trial.suggest_int("x", -10, 10)?;
|
||||||
Ok::<_, Error>(x.abs() as i32)
|
Ok::<_, TpeError>(x.abs() as i32)
|
||||||
})
|
})
|
||||||
.expect("optimization should succeed");
|
.expect("optimization should succeed");
|
||||||
|
|
||||||
@@ -828,7 +824,7 @@ fn test_optimize_all_trials_fail() {
|
|||||||
let result = study.optimize(5, |_trial| Err::<f64, &str>("always fails"));
|
let result = study.optimize(5, |_trial| Err::<f64, &str>("always fails"));
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
matches!(result, Err(Error::NoCompletedTrials)),
|
matches!(result, Err(TpeError::NoCompletedTrials)),
|
||||||
"should return NoCompletedTrials when all trials fail"
|
"should return NoCompletedTrials when all trials fail"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -846,7 +842,7 @@ fn test_optimize_with_callback_all_trials_fail() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
matches!(result, Err(Error::NoCompletedTrials)),
|
matches!(result, Err(TpeError::NoCompletedTrials)),
|
||||||
"should return NoCompletedTrials when all trials fail"
|
"should return NoCompletedTrials when all trials fail"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -858,7 +854,7 @@ fn test_optimize_with_sampler_all_trials_fail() {
|
|||||||
let result = study.optimize_with_sampler(5, |_trial| Err::<f64, &str>("always fails"));
|
let result = study.optimize_with_sampler(5, |_trial| Err::<f64, &str>("always fails"));
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
matches!(result, Err(Error::NoCompletedTrials)),
|
matches!(result, Err(TpeError::NoCompletedTrials)),
|
||||||
"should return NoCompletedTrials when all trials fail"
|
"should return NoCompletedTrials when all trials fail"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -876,7 +872,7 @@ fn test_optimize_with_callback_sampler_all_trials_fail() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
matches!(result, Err(Error::NoCompletedTrials)),
|
matches!(result, Err(TpeError::NoCompletedTrials)),
|
||||||
"should return NoCompletedTrials when all trials fail"
|
"should return NoCompletedTrials when all trials fail"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -906,7 +902,7 @@ fn test_tpe_sampler_builder_default_trait() {
|
|||||||
study
|
study
|
||||||
.optimize_with_sampler(5, |trial| {
|
.optimize_with_sampler(5, |trial| {
|
||||||
let x = trial.suggest_float("x", 0.0, 1.0)?;
|
let x = trial.suggest_float("x", 0.0, 1.0)?;
|
||||||
Ok::<_, Error>(x)
|
Ok::<_, TpeError>(x)
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
@@ -921,7 +917,7 @@ fn test_tpe_sampler_default_trait() {
|
|||||||
study
|
study
|
||||||
.optimize_with_sampler(5, |trial| {
|
.optimize_with_sampler(5, |trial| {
|
||||||
let x = trial.suggest_float("x", 0.0, 1.0)?;
|
let x = trial.suggest_float("x", 0.0, 1.0)?;
|
||||||
Ok::<_, Error>(x)
|
Ok::<_, TpeError>(x)
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
@@ -942,7 +938,7 @@ fn test_tpe_with_fixed_kde_bandwidth() {
|
|||||||
study
|
study
|
||||||
.optimize_with_sampler(20, |trial| {
|
.optimize_with_sampler(20, |trial| {
|
||||||
let x = trial.suggest_float("x", -5.0, 5.0)?;
|
let x = trial.suggest_float("x", -5.0, 5.0)?;
|
||||||
Ok::<_, Error>(x * x)
|
Ok::<_, TpeError>(x * x)
|
||||||
})
|
})
|
||||||
.expect("optimization should succeed");
|
.expect("optimization should succeed");
|
||||||
|
|
||||||
@@ -953,7 +949,7 @@ fn test_tpe_with_fixed_kde_bandwidth() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_tpe_sampler_invalid_kde_bandwidth() {
|
fn test_tpe_sampler_invalid_kde_bandwidth() {
|
||||||
let result = TpeSampler::with_config(0.25, 10, 24, Some(-1.0), None);
|
let result = TpeSampler::with_config(0.25, 10, 24, Some(-1.0), None);
|
||||||
assert!(matches!(result, Err(Error::InvalidBandwidth(_))));
|
assert!(matches!(result, Err(TpeError::InvalidBandwidth(_))));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -970,7 +966,7 @@ fn test_tpe_split_trials_with_two_trials() {
|
|||||||
study
|
study
|
||||||
.optimize_with_sampler(5, |trial| {
|
.optimize_with_sampler(5, |trial| {
|
||||||
let x = trial.suggest_float("x", 0.0, 10.0)?;
|
let x = trial.suggest_float("x", 0.0, 10.0)?;
|
||||||
Ok::<_, Error>(x)
|
Ok::<_, TpeError>(x)
|
||||||
})
|
})
|
||||||
.expect("optimization should succeed with small history");
|
.expect("optimization should succeed with small history");
|
||||||
|
|
||||||
@@ -991,7 +987,7 @@ fn test_tpe_with_log_scale_int() {
|
|||||||
.optimize_with_sampler(20, |trial| {
|
.optimize_with_sampler(20, |trial| {
|
||||||
let batch_size = trial.suggest_int_log("batch_size", 1, 1024)?;
|
let batch_size = trial.suggest_int_log("batch_size", 1, 1024)?;
|
||||||
// Optimal around batch_size = 32
|
// Optimal around batch_size = 32
|
||||||
Ok::<_, Error>(((batch_size as f64).log2() - 5.0).powi(2))
|
Ok::<_, TpeError>(((batch_size as f64).log2() - 5.0).powi(2))
|
||||||
})
|
})
|
||||||
.expect("optimization should succeed");
|
.expect("optimization should succeed");
|
||||||
|
|
||||||
@@ -1013,7 +1009,7 @@ fn test_tpe_with_step_distributions() {
|
|||||||
.optimize_with_sampler(20, |trial| {
|
.optimize_with_sampler(20, |trial| {
|
||||||
let x = trial.suggest_float_step("x", 0.0, 10.0, 0.5)?;
|
let x = trial.suggest_float_step("x", 0.0, 10.0, 0.5)?;
|
||||||
let n = trial.suggest_int_step("n", 0, 100, 10)?;
|
let n = trial.suggest_int_step("n", 0, 100, 10)?;
|
||||||
Ok::<_, Error>((x - 5.0).powi(2) + ((n - 50) as f64).powi(2))
|
Ok::<_, TpeError>((x - 5.0).powi(2) + ((n - 50) as f64).powi(2))
|
||||||
})
|
})
|
||||||
.expect("optimization should succeed");
|
.expect("optimization should succeed");
|
||||||
|
|
||||||
@@ -1092,7 +1088,7 @@ fn test_tpe_empty_good_or_bad_values_fallback() {
|
|||||||
study
|
study
|
||||||
.optimize_with_sampler(10, |trial| {
|
.optimize_with_sampler(10, |trial| {
|
||||||
let x = trial.suggest_float("x", 0.0, 10.0)?;
|
let x = trial.suggest_float("x", 0.0, 10.0)?;
|
||||||
Ok::<_, Error>(x)
|
Ok::<_, TpeError>(x)
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
@@ -1100,7 +1096,7 @@ fn test_tpe_empty_good_or_bad_values_fallback() {
|
|||||||
study
|
study
|
||||||
.optimize_with_sampler(5, |trial| {
|
.optimize_with_sampler(5, |trial| {
|
||||||
let y = trial.suggest_float("y", 0.0, 10.0)?;
|
let y = trial.suggest_float("y", 0.0, 10.0)?;
|
||||||
Ok::<_, Error>(y)
|
Ok::<_, TpeError>(y)
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
@@ -1118,7 +1114,7 @@ fn test_callback_early_stopping_on_first_trial() {
|
|||||||
100,
|
100,
|
||||||
|trial| {
|
|trial| {
|
||||||
let x = trial.suggest_float("x", 0.0, 10.0)?;
|
let x = trial.suggest_float("x", 0.0, 10.0)?;
|
||||||
Ok::<_, Error>(x)
|
Ok::<_, TpeError>(x)
|
||||||
},
|
},
|
||||||
|_study, _trial| {
|
|_study, _trial| {
|
||||||
// Stop immediately after first trial
|
// Stop immediately after first trial
|
||||||
@@ -1142,7 +1138,7 @@ fn test_callback_sampler_early_stopping() {
|
|||||||
100,
|
100,
|
||||||
|trial| {
|
|trial| {
|
||||||
let x = trial.suggest_float("x", 0.0, 10.0)?;
|
let x = trial.suggest_float("x", 0.0, 10.0)?;
|
||||||
Ok::<_, Error>(x)
|
Ok::<_, TpeError>(x)
|
||||||
},
|
},
|
||||||
|study, _trial| {
|
|study, _trial| {
|
||||||
if study.n_trials() >= 3 {
|
if study.n_trials() >= 3 {
|
||||||
@@ -1178,7 +1174,7 @@ fn test_best_trial_with_nan_values() {
|
|||||||
study
|
study
|
||||||
.optimize(5, |trial| {
|
.optimize(5, |trial| {
|
||||||
let x = trial.suggest_float("x", 0.0, 10.0)?;
|
let x = trial.suggest_float("x", 0.0, 10.0)?;
|
||||||
Ok::<_, Error>(x)
|
Ok::<_, TpeError>(x)
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
@@ -1186,228 +1182,3 @@ 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