Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7c364ecfad | |||
| a488a6303e |
@@ -42,7 +42,9 @@ jobs:
|
|||||||
matrix:
|
matrix:
|
||||||
features:
|
features:
|
||||||
- ""
|
- ""
|
||||||
|
- "serde"
|
||||||
- "async"
|
- "async"
|
||||||
|
- "serde,async"
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v6
|
||||||
- name: Install Rust
|
- name: Install Rust
|
||||||
@@ -129,19 +131,6 @@ jobs:
|
|||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v6
|
||||||
- uses: EmbarkStudios/cargo-deny-action@v2
|
- uses: EmbarkStudios/cargo-deny-action@v2
|
||||||
|
|
||||||
machete:
|
|
||||||
name: Unused Dependencies
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v6
|
|
||||||
- name: Install Rust
|
|
||||||
run: |
|
|
||||||
rustup override set stable
|
|
||||||
rustup update stable
|
|
||||||
- uses: Swatinem/rust-cache@v2
|
|
||||||
- name: Machete
|
|
||||||
uses: bnjbvr/cargo-machete@7959c845782fed02ee69303126d4a12d64f1db18
|
|
||||||
|
|
||||||
semver:
|
semver:
|
||||||
name: Semver Check
|
name: Semver Check
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -210,50 +199,3 @@ jobs:
|
|||||||
tool: typos-cli
|
tool: typos-cli
|
||||||
- name: Typos Check
|
- name: Typos Check
|
||||||
run: typos src/
|
run: typos src/
|
||||||
|
|
||||||
publish:
|
|
||||||
name: Publish to crates.io
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
if: github.event_name == 'push'
|
|
||||||
needs:
|
|
||||||
- fmt
|
|
||||||
- clippy
|
|
||||||
- test
|
|
||||||
- docs
|
|
||||||
- feature-check
|
|
||||||
- coverage
|
|
||||||
- msrv
|
|
||||||
- deny
|
|
||||||
- machete
|
|
||||||
- semver
|
|
||||||
- minimal-versions
|
|
||||||
- cross-platform
|
|
||||||
- typos
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v6
|
|
||||||
- name: Install Rust
|
|
||||||
run: |
|
|
||||||
rustup override set stable
|
|
||||||
rustup update stable
|
|
||||||
- uses: Swatinem/rust-cache@v2
|
|
||||||
|
|
||||||
- name: Check if version already published
|
|
||||||
id: check
|
|
||||||
run: |
|
|
||||||
CARGO_VERSION=$(cargo metadata --no-deps --format-version 1 | jq -r '.packages[0].version')
|
|
||||||
echo "version=$CARGO_VERSION" >> "$GITHUB_OUTPUT"
|
|
||||||
|
|
||||||
HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" "https://crates.io/api/v1/crates/optimizer/$CARGO_VERSION")
|
|
||||||
if [ "$HTTP_STATUS" = "200" ]; then
|
|
||||||
echo "Version $CARGO_VERSION already exists on crates.io, skipping publish"
|
|
||||||
echo "skip=true" >> "$GITHUB_OUTPUT"
|
|
||||||
else
|
|
||||||
echo "Version $CARGO_VERSION not found on crates.io, will publish"
|
|
||||||
echo "skip=false" >> "$GITHUB_OUTPUT"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Publish
|
|
||||||
if: steps.check.outputs.skip == 'false' && github.ref == 'refs/heads/master'
|
|
||||||
run: cargo publish
|
|
||||||
env:
|
|
||||||
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
|
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
name: Publish
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- "v*"
|
||||||
|
|
||||||
|
env:
|
||||||
|
CARGO_TERM_COLOR: always
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
publish:
|
||||||
|
name: Publish to crates.io
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
- name: Install Rust
|
||||||
|
run: |
|
||||||
|
rustup override set stable
|
||||||
|
rustup update stable
|
||||||
|
- uses: Swatinem/rust-cache@v2
|
||||||
|
|
||||||
|
- name: Verify version matches tag
|
||||||
|
run: |
|
||||||
|
CARGO_VERSION=$(cargo metadata --no-deps --format-version 1 | jq -r '.packages[0].version')
|
||||||
|
TAG_VERSION="${GITHUB_REF_NAME#v}"
|
||||||
|
if [ "$CARGO_VERSION" != "$TAG_VERSION" ]; then
|
||||||
|
echo "Error: Cargo.toml version ($CARGO_VERSION) doesn't match tag ($TAG_VERSION)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Publish
|
||||||
|
run: cargo publish
|
||||||
|
env:
|
||||||
|
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
|
||||||
+5
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "optimizer"
|
name = "optimizer"
|
||||||
version = "0.2.0"
|
version = "0.1.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
rust-version = "1.88"
|
rust-version = "1.88"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
@@ -13,11 +13,15 @@ repository = "https://github.com/raimannma/rust-optimizer"
|
|||||||
rand = "0.9"
|
rand = "0.9"
|
||||||
thiserror = "2"
|
thiserror = "2"
|
||||||
parking_lot = "0.12"
|
parking_lot = "0.12"
|
||||||
|
ordered-float = "5"
|
||||||
|
serde = { version = "1", features = ["derive"], optional = true }
|
||||||
tokio = { version = "1", features = ["sync", "rt-multi-thread"], optional = true }
|
tokio = { version = "1", features = ["sync", "rt-multi-thread"], optional = true }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = []
|
default = []
|
||||||
|
serde = ["dep:serde", "ordered-float/serde"]
|
||||||
async = ["dep:tokio"]
|
async = ["dep:tokio"]
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
serde_json = "1"
|
||||||
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
|
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ A Rust library for black-box optimization using Tree-Parzen Estimator (TPE).
|
|||||||
- 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
|
||||||
|
- Serialization support for saving/loading study state
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
@@ -34,6 +35,7 @@ println!("Best value: {} at x={:?}", best.value, best.params);
|
|||||||
|
|
||||||
## Feature Flags
|
## Feature Flags
|
||||||
|
|
||||||
|
- `serde` - Enable serialization/deserialization of studies and trials
|
||||||
- `async` - Enable async optimization methods (requires tokio)
|
- `async` - Enable async optimization methods (requires tokio)
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
//! Parameter distribution types.
|
//! Parameter distribution types.
|
||||||
|
|
||||||
|
#[cfg(feature = "serde")]
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
/// Distribution for floating-point parameters.
|
/// Distribution for floating-point parameters.
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||||
pub struct FloatDistribution {
|
pub struct FloatDistribution {
|
||||||
/// Lower bound (inclusive).
|
/// Lower bound (inclusive).
|
||||||
pub low: f64,
|
pub low: f64,
|
||||||
@@ -15,6 +19,7 @@ pub struct FloatDistribution {
|
|||||||
|
|
||||||
/// Distribution for integer parameters.
|
/// Distribution for integer parameters.
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||||
pub struct IntDistribution {
|
pub struct IntDistribution {
|
||||||
/// Lower bound (inclusive).
|
/// Lower bound (inclusive).
|
||||||
pub low: i64,
|
pub low: i64,
|
||||||
@@ -28,6 +33,7 @@ pub struct IntDistribution {
|
|||||||
|
|
||||||
/// Distribution for categorical parameters.
|
/// Distribution for categorical parameters.
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||||
pub struct CategoricalDistribution {
|
pub struct CategoricalDistribution {
|
||||||
/// Number of choices available.
|
/// Number of choices available.
|
||||||
pub n_choices: usize,
|
pub n_choices: usize,
|
||||||
@@ -35,6 +41,7 @@ pub struct CategoricalDistribution {
|
|||||||
|
|
||||||
/// Enum wrapping all parameter distribution types.
|
/// Enum wrapping all parameter distribution types.
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||||
pub enum Distribution {
|
pub enum Distribution {
|
||||||
/// A floating-point distribution.
|
/// A floating-point distribution.
|
||||||
Float(FloatDistribution),
|
Float(FloatDistribution),
|
||||||
|
|||||||
+19
@@ -7,6 +7,7 @@
|
|||||||
//! - Log-scale and stepped parameter sampling
|
//! - Log-scale and stepped parameter sampling
|
||||||
//! - Synchronous and async optimization
|
//! - Synchronous and async optimization
|
||||||
//! - Parallel trial evaluation with bounded concurrency
|
//! - Parallel trial evaluation with bounded concurrency
|
||||||
|
//! - Serialization for saving/loading study state
|
||||||
//!
|
//!
|
||||||
//! # Quick Start
|
//! # Quick Start
|
||||||
//!
|
//!
|
||||||
@@ -112,8 +113,26 @@
|
|||||||
//! }).await?;
|
//! }).await?;
|
||||||
//! ```
|
//! ```
|
||||||
//!
|
//!
|
||||||
|
//! # Serialization
|
||||||
|
//!
|
||||||
|
//! With the `serde` feature enabled, studies can be serialized:
|
||||||
|
//!
|
||||||
|
//! ```ignore
|
||||||
|
//! use optimizer::{Study, Direction, TpeSampler};
|
||||||
|
//!
|
||||||
|
//! // Save study state
|
||||||
|
//! let study: Study<f64> = Study::new(Direction::Minimize);
|
||||||
|
//! let json = serde_json::to_string(&study)?;
|
||||||
|
//!
|
||||||
|
//! // Load and continue
|
||||||
|
//! let mut study: Study<f64> = serde_json::from_str(&json)?;
|
||||||
|
//! study.set_sampler(TpeSampler::new()); // Restore sampler
|
||||||
|
//! study.optimize_with_sampler(10, |trial| { /* ... */ }).unwrap();
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
//! # Feature Flags
|
//! # Feature Flags
|
||||||
//!
|
//!
|
||||||
|
//! - `serde`: Enable serialization/deserialization of studies and trials
|
||||||
//! - `async`: Enable async optimization methods (requires tokio)
|
//! - `async`: Enable async optimization methods (requires tokio)
|
||||||
|
|
||||||
mod distribution;
|
mod distribution;
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
//! Parameter value storage types.
|
//! Parameter value storage types.
|
||||||
|
|
||||||
|
#[cfg(feature = "serde")]
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
/// Represents a sampled parameter value.
|
/// Represents a sampled parameter value.
|
||||||
///
|
///
|
||||||
/// This enum stores different parameter value types uniformly.
|
/// This enum stores different parameter value types uniformly.
|
||||||
/// For categorical parameters, the `Categorical` variant stores
|
/// For categorical parameters, the `Categorical` variant stores
|
||||||
/// the index into the choices array.
|
/// the index into the choices array.
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||||
pub enum ParamValue {
|
pub enum ParamValue {
|
||||||
/// A floating-point parameter value.
|
/// A floating-point parameter value.
|
||||||
Float(f64),
|
Float(f64),
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ pub mod tpe;
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
pub use random::RandomSampler;
|
pub use random::RandomSampler;
|
||||||
|
#[cfg(feature = "serde")]
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
pub use tpe::{TpeSampler, TpeSamplerBuilder};
|
pub use tpe::{TpeSampler, TpeSamplerBuilder};
|
||||||
|
|
||||||
use crate::distribution::Distribution;
|
use crate::distribution::Distribution;
|
||||||
@@ -17,6 +19,7 @@ use crate::param::ParamValue;
|
|||||||
/// parameter values, their distributions, and the objective value returned
|
/// parameter values, their distributions, and the objective value returned
|
||||||
/// by the objective function.
|
/// by the objective function.
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
|
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||||
pub struct CompletedTrial<V = f64> {
|
pub struct CompletedTrial<V = f64> {
|
||||||
/// The unique identifier for this trial.
|
/// The unique identifier for this trial.
|
||||||
pub id: u64,
|
pub id: u64,
|
||||||
|
|||||||
+253
-1
@@ -7,11 +7,19 @@ use std::sync::Arc;
|
|||||||
use std::sync::atomic::{AtomicU64, Ordering};
|
use std::sync::atomic::{AtomicU64, Ordering};
|
||||||
|
|
||||||
use parking_lot::RwLock;
|
use parking_lot::RwLock;
|
||||||
|
#[cfg(feature = "serde")]
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::sampler::{CompletedTrial, RandomSampler, Sampler};
|
use crate::sampler::{CompletedTrial, RandomSampler, Sampler};
|
||||||
use crate::trial::Trial;
|
use crate::trial::Trial;
|
||||||
use crate::types::Direction;
|
use crate::types::Direction;
|
||||||
|
|
||||||
|
/// Helper function to create default sampler for serde deserialization.
|
||||||
|
#[cfg(feature = "serde")]
|
||||||
|
fn default_sampler() -> Arc<dyn Sampler> {
|
||||||
|
Arc::new(RandomSampler::new())
|
||||||
|
}
|
||||||
|
|
||||||
/// A study manages the optimization process, tracking trials and their results.
|
/// A study manages the optimization process, tracking trials and their results.
|
||||||
///
|
///
|
||||||
/// The study is parameterized by the objective value type `V`, which defaults to `f64`.
|
/// The study is parameterized by the objective value type `V`, which defaults to `f64`.
|
||||||
@@ -21,6 +29,13 @@ use crate::types::Direction;
|
|||||||
/// When `V = f64`, the study passes trial history to the sampler for informed
|
/// When `V = f64`, the study passes trial history to the sampler for informed
|
||||||
/// parameter suggestions (e.g., TPE sampler uses history to guide sampling).
|
/// parameter suggestions (e.g., TPE sampler uses history to guide sampling).
|
||||||
///
|
///
|
||||||
|
/// # Serialization
|
||||||
|
///
|
||||||
|
/// When the `serde` feature is enabled, the study can be serialized and deserialized.
|
||||||
|
/// The completed trials and trial ID counter are preserved, allowing optimization to
|
||||||
|
/// continue after deserialization. The sampler is not serialized; upon deserialization,
|
||||||
|
/// a default `RandomSampler` is used. Use `Study::set_sampler()` to restore a custom sampler.
|
||||||
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
@@ -100,6 +115,9 @@ where
|
|||||||
|
|
||||||
/// Sets a new sampler for the study.
|
/// Sets a new sampler for the study.
|
||||||
///
|
///
|
||||||
|
/// This method is useful after deserializing a study when you want to use
|
||||||
|
/// a custom sampler (e.g., TPE) instead of the default `RandomSampler`.
|
||||||
|
///
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
///
|
///
|
||||||
/// * `sampler` - The sampler to use for parameter sampling.
|
/// * `sampler` - The sampler to use for parameter sampling.
|
||||||
@@ -109,6 +127,7 @@ where
|
|||||||
/// ```
|
/// ```
|
||||||
/// use optimizer::{Direction, Study, TpeSampler};
|
/// use optimizer::{Direction, Study, TpeSampler};
|
||||||
///
|
///
|
||||||
|
/// // After deserializing a study, restore the TPE sampler
|
||||||
/// let mut study: Study<f64> = Study::new(Direction::Minimize);
|
/// let mut study: Study<f64> = Study::new(Direction::Minimize);
|
||||||
/// study.set_sampler(TpeSampler::new());
|
/// study.set_sampler(TpeSampler::new());
|
||||||
/// ```
|
/// ```
|
||||||
@@ -1024,7 +1043,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::TpeError>((trial, value))
|
/// Ok::<_, optimize::TpeError>((trial, value))
|
||||||
/// })
|
/// })
|
||||||
/// .await?;
|
/// .await?;
|
||||||
///
|
///
|
||||||
@@ -1086,3 +1105,236 @@ impl Study<f64> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Manual Serialize implementation for Study<V> when serde feature is enabled.
|
||||||
|
#[cfg(feature = "serde")]
|
||||||
|
impl<V> Serialize for Study<V>
|
||||||
|
where
|
||||||
|
V: PartialOrd + Serialize,
|
||||||
|
{
|
||||||
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: serde::Serializer,
|
||||||
|
{
|
||||||
|
use serde::ser::SerializeStruct;
|
||||||
|
|
||||||
|
let mut state = serializer.serialize_struct("Study", 3)?;
|
||||||
|
state.serialize_field("direction", &self.direction)?;
|
||||||
|
// Serialize the Vec inside the Arc<RwLock<>>
|
||||||
|
let trials = self.completed_trials.read();
|
||||||
|
state.serialize_field("completed_trials", &*trials)?;
|
||||||
|
state.serialize_field("next_trial_id", &self.next_trial_id.load(Ordering::SeqCst))?;
|
||||||
|
state.end()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Manual Deserialize implementation for Study<V> when serde feature is enabled.
|
||||||
|
#[cfg(feature = "serde")]
|
||||||
|
impl<'de, V> Deserialize<'de> for Study<V>
|
||||||
|
where
|
||||||
|
V: PartialOrd + Deserialize<'de>,
|
||||||
|
{
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
use std::fmt;
|
||||||
|
use std::marker::PhantomData;
|
||||||
|
|
||||||
|
use serde::de::{self, MapAccess, Visitor};
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize)]
|
||||||
|
#[serde(field_identifier, rename_all = "snake_case")]
|
||||||
|
enum Field {
|
||||||
|
Direction,
|
||||||
|
CompletedTrials,
|
||||||
|
NextTrialId,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct StudyVisitor<V>(PhantomData<V>);
|
||||||
|
|
||||||
|
impl<'de, V> Visitor<'de> for StudyVisitor<V>
|
||||||
|
where
|
||||||
|
V: PartialOrd + Deserialize<'de>,
|
||||||
|
{
|
||||||
|
type Value = Study<V>;
|
||||||
|
|
||||||
|
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
formatter.write_str("struct Study")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
|
||||||
|
where
|
||||||
|
A: MapAccess<'de>,
|
||||||
|
{
|
||||||
|
let mut direction = None;
|
||||||
|
let mut completed_trials: Option<Vec<CompletedTrial<V>>> = None;
|
||||||
|
let mut next_trial_id = None;
|
||||||
|
|
||||||
|
while let Some(key) = map.next_key()? {
|
||||||
|
match key {
|
||||||
|
Field::Direction => {
|
||||||
|
if direction.is_some() {
|
||||||
|
return Err(de::Error::duplicate_field("direction"));
|
||||||
|
}
|
||||||
|
direction = Some(map.next_value()?);
|
||||||
|
}
|
||||||
|
Field::CompletedTrials => {
|
||||||
|
if completed_trials.is_some() {
|
||||||
|
return Err(de::Error::duplicate_field("completed_trials"));
|
||||||
|
}
|
||||||
|
completed_trials = Some(map.next_value()?);
|
||||||
|
}
|
||||||
|
Field::NextTrialId => {
|
||||||
|
if next_trial_id.is_some() {
|
||||||
|
return Err(de::Error::duplicate_field("next_trial_id"));
|
||||||
|
}
|
||||||
|
next_trial_id = Some(map.next_value()?);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let direction = direction.ok_or_else(|| de::Error::missing_field("direction"))?;
|
||||||
|
let completed_trials =
|
||||||
|
completed_trials.ok_or_else(|| de::Error::missing_field("completed_trials"))?;
|
||||||
|
let next_trial_id: u64 =
|
||||||
|
next_trial_id.ok_or_else(|| de::Error::missing_field("next_trial_id"))?;
|
||||||
|
|
||||||
|
Ok(Study {
|
||||||
|
direction,
|
||||||
|
sampler: default_sampler(),
|
||||||
|
completed_trials: Arc::new(RwLock::new(completed_trials)),
|
||||||
|
next_trial_id: AtomicU64::new(next_trial_id),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const FIELDS: &[&str] = &["direction", "completed_trials", "next_trial_id"];
|
||||||
|
deserializer.deserialize_struct("Study", FIELDS, StudyVisitor(PhantomData))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(all(test, feature = "serde"))]
|
||||||
|
mod serde_tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_study_serde_round_trip() {
|
||||||
|
// Create a study and add some trials
|
||||||
|
let study: Study<f64> = Study::new(Direction::Minimize);
|
||||||
|
|
||||||
|
// Run some optimization
|
||||||
|
study
|
||||||
|
.optimize(5, |trial| {
|
||||||
|
let x = trial.suggest_float("x", 0.0, 10.0)?;
|
||||||
|
let y = trial.suggest_int("y", 1, 5)?;
|
||||||
|
Ok::<_, crate::TpeError>(x + y as f64)
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Serialize to JSON
|
||||||
|
let serialized = serde_json::to_string(&study).unwrap();
|
||||||
|
|
||||||
|
// Deserialize from JSON
|
||||||
|
let deserialized: Study<f64> = serde_json::from_str(&serialized).unwrap();
|
||||||
|
|
||||||
|
// Verify the data is preserved
|
||||||
|
assert_eq!(deserialized.direction(), study.direction());
|
||||||
|
assert_eq!(deserialized.n_trials(), study.n_trials());
|
||||||
|
|
||||||
|
// Verify the best trial is the same
|
||||||
|
let original_best = study.best_trial().unwrap();
|
||||||
|
let deserialized_best = deserialized.best_trial().unwrap();
|
||||||
|
assert_eq!(original_best.id, deserialized_best.id);
|
||||||
|
// Use approximate comparison for floats due to JSON serialization precision
|
||||||
|
assert!((original_best.value - deserialized_best.value).abs() < 1e-10);
|
||||||
|
// Check that all param keys match
|
||||||
|
assert_eq!(original_best.params.len(), deserialized_best.params.len());
|
||||||
|
for (key, original_val) in &original_best.params {
|
||||||
|
let deserialized_val = deserialized_best.params.get(key).unwrap();
|
||||||
|
match (original_val, deserialized_val) {
|
||||||
|
(crate::param::ParamValue::Float(a), crate::param::ParamValue::Float(b)) => {
|
||||||
|
assert!((a - b).abs() < 1e-10, "Float param {key} differs");
|
||||||
|
}
|
||||||
|
_ => assert_eq!(original_val, deserialized_val),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify we can continue optimization on the deserialized study
|
||||||
|
let initial_count = deserialized.n_trials();
|
||||||
|
deserialized
|
||||||
|
.optimize(3, |trial| {
|
||||||
|
let x = trial.suggest_float("x", 0.0, 10.0)?;
|
||||||
|
let y = trial.suggest_int("y", 1, 5)?;
|
||||||
|
Ok::<_, crate::TpeError>(x + y as f64)
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Verify new trials were added
|
||||||
|
assert_eq!(deserialized.n_trials(), initial_count + 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_study_serde_preserves_trial_ids() {
|
||||||
|
let study: Study<f64> = Study::new(Direction::Maximize);
|
||||||
|
|
||||||
|
// Add 5 trials
|
||||||
|
study
|
||||||
|
.optimize(5, |trial| {
|
||||||
|
let x = trial.suggest_float("x", -1.0, 1.0)?;
|
||||||
|
Ok::<_, crate::TpeError>(x * x)
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Serialize and deserialize
|
||||||
|
let serialized = serde_json::to_string(&study).unwrap();
|
||||||
|
let deserialized: Study<f64> = serde_json::from_str(&serialized).unwrap();
|
||||||
|
|
||||||
|
// Create a new trial - its ID should continue from where we left off
|
||||||
|
let new_trial = deserialized.create_trial();
|
||||||
|
assert_eq!(new_trial.id(), 5); // Next trial should be ID 5
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_completed_trial_serde() {
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use crate::distribution::{Distribution, FloatDistribution, IntDistribution};
|
||||||
|
use crate::param::ParamValue;
|
||||||
|
|
||||||
|
let mut params = HashMap::new();
|
||||||
|
params.insert("x".to_string(), ParamValue::Float(0.5));
|
||||||
|
params.insert("n".to_string(), ParamValue::Int(42));
|
||||||
|
|
||||||
|
let mut distributions = HashMap::new();
|
||||||
|
distributions.insert(
|
||||||
|
"x".to_string(),
|
||||||
|
Distribution::Float(FloatDistribution {
|
||||||
|
low: 0.0,
|
||||||
|
high: 1.0,
|
||||||
|
log_scale: false,
|
||||||
|
step: None,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
distributions.insert(
|
||||||
|
"n".to_string(),
|
||||||
|
Distribution::Int(IntDistribution {
|
||||||
|
low: 1,
|
||||||
|
high: 100,
|
||||||
|
log_scale: false,
|
||||||
|
step: None,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
let completed = CompletedTrial::new(42, params.clone(), distributions.clone(), 0.75);
|
||||||
|
|
||||||
|
// Serialize and deserialize
|
||||||
|
let serialized = serde_json::to_string(&completed).unwrap();
|
||||||
|
let deserialized: CompletedTrial<f64> = serde_json::from_str(&serialized).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(deserialized.id, 42);
|
||||||
|
assert_eq!(deserialized.value, 0.75);
|
||||||
|
assert_eq!(deserialized.params, params);
|
||||||
|
assert_eq!(deserialized.distributions, distributions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ use std::collections::HashMap;
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use parking_lot::RwLock;
|
use parking_lot::RwLock;
|
||||||
|
#[cfg(feature = "serde")]
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::distribution::{
|
use crate::distribution::{
|
||||||
CategoricalDistribution, Distribution, FloatDistribution, IntDistribution,
|
CategoricalDistribution, Distribution, FloatDistribution, IntDistribution,
|
||||||
@@ -22,6 +24,7 @@ use crate::types::TrialState;
|
|||||||
/// `Study::create_trial()`, the trial receives the study's sampler and access
|
/// `Study::create_trial()`, the trial receives the study's sampler and access
|
||||||
/// to the history of completed trials for informed sampling.
|
/// to the history of completed trials for informed sampling.
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
|
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||||
pub struct Trial {
|
pub struct Trial {
|
||||||
/// Unique identifier for this trial.
|
/// Unique identifier for this trial.
|
||||||
id: u64,
|
id: u64,
|
||||||
@@ -32,8 +35,10 @@ pub struct Trial {
|
|||||||
/// Parameter distributions, keyed by parameter name.
|
/// Parameter distributions, keyed by parameter name.
|
||||||
distributions: HashMap<String, Distribution>,
|
distributions: HashMap<String, Distribution>,
|
||||||
/// The sampler to use for generating parameter values.
|
/// The sampler to use for generating parameter values.
|
||||||
|
#[cfg_attr(feature = "serde", serde(skip))]
|
||||||
sampler: Option<Arc<dyn Sampler>>,
|
sampler: Option<Arc<dyn Sampler>>,
|
||||||
/// Access to the history of completed trials (shared with Study).
|
/// Access to the history of completed trials (shared with Study).
|
||||||
|
#[cfg_attr(feature = "serde", serde(skip))]
|
||||||
history: Option<Arc<RwLock<Vec<CompletedTrial<f64>>>>>,
|
history: Option<Arc<RwLock<Vec<CompletedTrial<f64>>>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
//! Core types for the optimizer library.
|
//! Core types for the optimizer library.
|
||||||
|
|
||||||
|
#[cfg(feature = "serde")]
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
/// The direction of optimization.
|
/// The direction of optimization.
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||||
pub enum Direction {
|
pub enum Direction {
|
||||||
/// Minimize the objective value.
|
/// Minimize the objective value.
|
||||||
Minimize,
|
Minimize,
|
||||||
@@ -11,6 +15,7 @@ pub enum Direction {
|
|||||||
|
|
||||||
/// The state of a trial in its lifecycle.
|
/// The state of a trial in its lifecycle.
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||||
pub enum TrialState {
|
pub enum TrialState {
|
||||||
/// The trial is currently running.
|
/// The trial is currently running.
|
||||||
Running,
|
Running,
|
||||||
|
|||||||
@@ -1142,3 +1142,144 @@ 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());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Serde tests (only run when serde feature is enabled)
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
#[cfg(feature = "serde")]
|
||||||
|
mod serde_tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_direction_serde() {
|
||||||
|
// Test Direction serialization
|
||||||
|
let min = Direction::Minimize;
|
||||||
|
let max = Direction::Maximize;
|
||||||
|
|
||||||
|
let min_json = serde_json::to_string(&min).unwrap();
|
||||||
|
let max_json = serde_json::to_string(&max).unwrap();
|
||||||
|
|
||||||
|
let min_deser: Direction = serde_json::from_str(&min_json).unwrap();
|
||||||
|
let max_deser: Direction = serde_json::from_str(&max_json).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(min, min_deser);
|
||||||
|
assert_eq!(max, max_deser);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_study_serde_with_categorical() {
|
||||||
|
let study: Study<f64> = Study::new(Direction::Minimize);
|
||||||
|
|
||||||
|
study
|
||||||
|
.optimize(5, |trial| {
|
||||||
|
let x = trial.suggest_float("x", 0.0, 10.0)?;
|
||||||
|
let opt = trial.suggest_categorical("opt", &["a", "b", "c"])?;
|
||||||
|
let _ = opt;
|
||||||
|
Ok::<_, TpeError>(x)
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Serialize
|
||||||
|
let json = serde_json::to_string(&study).unwrap();
|
||||||
|
|
||||||
|
// Deserialize
|
||||||
|
let loaded: Study<f64> = serde_json::from_str(&json).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(loaded.n_trials(), 5);
|
||||||
|
assert_eq!(loaded.direction(), Direction::Minimize);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_study_serde_with_all_param_types() {
|
||||||
|
let study: Study<f64> = Study::new(Direction::Maximize);
|
||||||
|
|
||||||
|
study
|
||||||
|
.optimize(3, |trial| {
|
||||||
|
let x = trial.suggest_float("x", 0.0, 10.0)?;
|
||||||
|
let y = trial.suggest_float_log("y", 0.001, 1.0)?;
|
||||||
|
let z = trial.suggest_float_step("z", 0.0, 1.0, 0.1)?;
|
||||||
|
let a = trial.suggest_int("a", 1, 10)?;
|
||||||
|
let b = trial.suggest_int_log("b", 1, 100)?;
|
||||||
|
let c = trial.suggest_int_step("c", 0, 100, 10)?;
|
||||||
|
let d = trial.suggest_categorical("d", &["p", "q"])?;
|
||||||
|
let _ = (y, z, b, c, d);
|
||||||
|
Ok::<_, TpeError>(x + a as f64)
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let json = serde_json::to_string(&study).unwrap();
|
||||||
|
let loaded: Study<f64> = serde_json::from_str(&json).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(loaded.n_trials(), 3);
|
||||||
|
assert_eq!(loaded.direction(), Direction::Maximize);
|
||||||
|
|
||||||
|
// Verify we can continue optimization
|
||||||
|
loaded
|
||||||
|
.optimize(2, |trial| {
|
||||||
|
let x = trial.suggest_float("x", 0.0, 10.0)?;
|
||||||
|
Ok::<_, TpeError>(x)
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(loaded.n_trials(), 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_study_serde_empty() {
|
||||||
|
// Test serializing a study with no trials
|
||||||
|
let study: Study<f64> = Study::new(Direction::Minimize);
|
||||||
|
|
||||||
|
let json = serde_json::to_string(&study).unwrap();
|
||||||
|
let loaded: Study<f64> = serde_json::from_str(&json).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(loaded.n_trials(), 0);
|
||||||
|
assert_eq!(loaded.direction(), Direction::Minimize);
|
||||||
|
assert!(loaded.best_trial().is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_study_serde_with_custom_value_type() {
|
||||||
|
// Test Study with i32 value type
|
||||||
|
let study: Study<i32> = Study::new(Direction::Minimize);
|
||||||
|
|
||||||
|
study
|
||||||
|
.optimize(5, |trial| {
|
||||||
|
let n = trial.suggest_int("n", 1, 100)?;
|
||||||
|
Ok::<_, TpeError>(n as i32)
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let json = serde_json::to_string(&study).unwrap();
|
||||||
|
let loaded: Study<i32> = serde_json::from_str(&json).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(loaded.n_trials(), 5);
|
||||||
|
let best = loaded.best_trial().unwrap();
|
||||||
|
assert!(best.value >= 1 && best.value <= 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_completed_trial_access_after_serde() {
|
||||||
|
let study: Study<f64> = Study::new(Direction::Minimize);
|
||||||
|
|
||||||
|
study
|
||||||
|
.optimize(3, |trial| {
|
||||||
|
let x = trial.suggest_float("x", 0.0, 10.0)?;
|
||||||
|
Ok::<_, TpeError>(x * x)
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let json = serde_json::to_string(&study).unwrap();
|
||||||
|
let loaded: Study<f64> = serde_json::from_str(&json).unwrap();
|
||||||
|
|
||||||
|
// Access all trials
|
||||||
|
let trials = loaded.trials();
|
||||||
|
assert_eq!(trials.len(), 3);
|
||||||
|
|
||||||
|
for trial in &trials {
|
||||||
|
assert!(trial.params.contains_key("x"));
|
||||||
|
assert!(trial.distributions.contains_key("x"));
|
||||||
|
assert!(trial.value >= 0.0); // x^2 is non-negative
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
[default.extend-words]
|
|
||||||
Tpe = "Tpe"
|
|
||||||
Reference in New Issue
Block a user