From fc0559a02d90e760d2104a8935a00c88c70cdccc Mon Sep 17 00:00:00 2001 From: Manuel Raimann Date: Fri, 30 Jan 2026 18:38:43 +0100 Subject: [PATCH] Remove Serde --- .github/workflows/ci.yml | 2 - Cargo.toml | 3 - README.md | 2 - src/distribution.rs | 7 -- src/lib.rs | 19 --- src/param.rs | 4 - src/sampler/mod.rs | 3 - src/study.rs | 252 --------------------------------------- src/trial.rs | 5 - src/types.rs | 5 - tests/integration.rs | 141 ---------------------- 11 files changed, 443 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 736f187..4a38f88 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -42,9 +42,7 @@ jobs: matrix: features: - "" - - "serde" - "async" - - "serde,async" steps: - uses: actions/checkout@v6 - name: Install Rust diff --git a/Cargo.toml b/Cargo.toml index bde0aab..646a32d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,14 +13,11 @@ repository = "https://github.com/raimannma/rust-optimizer" rand = "0.9" thiserror = "2" parking_lot = "0.12" -serde = { version = "1", features = ["derive"], optional = true } tokio = { version = "1", features = ["sync", "rt-multi-thread"], optional = true } [features] default = [] -serde = ["dep:serde"] async = ["dep:tokio"] [dev-dependencies] -serde_json = "1" tokio = { version = "1", features = ["rt-multi-thread", "macros"] } diff --git a/README.md b/README.md index 20b1697..7949823 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,6 @@ A Rust library for black-box optimization using Tree-Parzen Estimator (TPE). - Float, integer, and categorical parameter types - Log-scale and stepped parameter sampling - Sync and async optimization with parallel trial evaluation -- Serialization support for saving/loading study state ## Quick Start @@ -35,7 +34,6 @@ println!("Best value: {} at x={:?}", best.value, best.params); ## Feature Flags -- `serde` - Enable serialization/deserialization of studies and trials - `async` - Enable async optimization methods (requires tokio) ## Documentation diff --git a/src/distribution.rs b/src/distribution.rs index ca97532..fd64f80 100644 --- a/src/distribution.rs +++ b/src/distribution.rs @@ -1,11 +1,7 @@ //! Parameter distribution types. -#[cfg(feature = "serde")] -use serde::{Deserialize, Serialize}; - /// Distribution for floating-point parameters. #[derive(Clone, Debug, PartialEq)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct FloatDistribution { /// Lower bound (inclusive). pub low: f64, @@ -19,7 +15,6 @@ pub struct FloatDistribution { /// Distribution for integer parameters. #[derive(Clone, Debug, PartialEq)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct IntDistribution { /// Lower bound (inclusive). pub low: i64, @@ -33,7 +28,6 @@ pub struct IntDistribution { /// Distribution for categorical parameters. #[derive(Clone, Debug, PartialEq)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct CategoricalDistribution { /// Number of choices available. pub n_choices: usize, @@ -41,7 +35,6 @@ pub struct CategoricalDistribution { /// Enum wrapping all parameter distribution types. #[derive(Clone, Debug, PartialEq)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum Distribution { /// A floating-point distribution. Float(FloatDistribution), diff --git a/src/lib.rs b/src/lib.rs index bed8faf..66ea1b1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,7 +7,6 @@ //! - Log-scale and stepped parameter sampling //! - Synchronous and async optimization //! - Parallel trial evaluation with bounded concurrency -//! - Serialization for saving/loading study state //! //! # Quick Start //! @@ -113,26 +112,8 @@ //! }).await?; //! ``` //! -//! # Serialization -//! -//! With the `serde` feature enabled, studies can be serialized: -//! -//! ```ignore -//! use optimizer::{Study, Direction, TpeSampler}; -//! -//! // Save study state -//! let study: Study = Study::new(Direction::Minimize); -//! let json = serde_json::to_string(&study)?; -//! -//! // Load and continue -//! let mut study: Study = serde_json::from_str(&json)?; -//! study.set_sampler(TpeSampler::new()); // Restore sampler -//! study.optimize_with_sampler(10, |trial| { /* ... */ }).unwrap(); -//! ``` -//! //! # Feature Flags //! -//! - `serde`: Enable serialization/deserialization of studies and trials //! - `async`: Enable async optimization methods (requires tokio) mod distribution; diff --git a/src/param.rs b/src/param.rs index 201f6b1..c5a5797 100644 --- a/src/param.rs +++ b/src/param.rs @@ -1,15 +1,11 @@ //! Parameter value storage types. -#[cfg(feature = "serde")] -use serde::{Deserialize, Serialize}; - /// Represents a sampled parameter value. /// /// This enum stores different parameter value types uniformly. /// For categorical parameters, the `Categorical` variant stores /// the index into the choices array. #[derive(Clone, Debug, PartialEq)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum ParamValue { /// A floating-point parameter value. Float(f64), diff --git a/src/sampler/mod.rs b/src/sampler/mod.rs index 501b1d5..ba2979e 100644 --- a/src/sampler/mod.rs +++ b/src/sampler/mod.rs @@ -6,8 +6,6 @@ pub mod tpe; use std::collections::HashMap; pub use random::RandomSampler; -#[cfg(feature = "serde")] -use serde::{Deserialize, Serialize}; pub use tpe::{TpeSampler, TpeSamplerBuilder}; use crate::distribution::Distribution; @@ -19,7 +17,6 @@ use crate::param::ParamValue; /// parameter values, their distributions, and the objective value returned /// by the objective function. #[derive(Clone, Debug)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct CompletedTrial { /// The unique identifier for this trial. pub id: u64, diff --git a/src/study.rs b/src/study.rs index be8a10a..5203ab0 100644 --- a/src/study.rs +++ b/src/study.rs @@ -7,19 +7,11 @@ use std::sync::Arc; use std::sync::atomic::{AtomicU64, Ordering}; use parking_lot::RwLock; -#[cfg(feature = "serde")] -use serde::{Deserialize, Serialize}; use crate::sampler::{CompletedTrial, RandomSampler, Sampler}; use crate::trial::Trial; use crate::types::Direction; -/// Helper function to create default sampler for serde deserialization. -#[cfg(feature = "serde")] -fn default_sampler() -> Arc { - Arc::new(RandomSampler::new()) -} - /// 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`. @@ -29,13 +21,6 @@ fn default_sampler() -> Arc { /// When `V = f64`, the study passes trial history to the sampler for informed /// 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 /// /// ``` @@ -115,9 +100,6 @@ where /// 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 /// /// * `sampler` - The sampler to use for parameter sampling. @@ -127,7 +109,6 @@ where /// ``` /// use optimizer::{Direction, Study, TpeSampler}; /// - /// // After deserializing a study, restore the TPE sampler /// let mut study: Study = Study::new(Direction::Minimize); /// study.set_sampler(TpeSampler::new()); /// ``` @@ -1105,236 +1086,3 @@ impl Study { Ok(()) } } - -// Manual Serialize implementation for Study when serde feature is enabled. -#[cfg(feature = "serde")] -impl Serialize for Study -where - V: PartialOrd + Serialize, -{ - fn serialize(&self, serializer: S) -> Result - 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> - 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 when serde feature is enabled. -#[cfg(feature = "serde")] -impl<'de, V> Deserialize<'de> for Study -where - V: PartialOrd + Deserialize<'de>, -{ - fn deserialize(deserializer: D) -> Result - 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(PhantomData); - - impl<'de, V> Visitor<'de> for StudyVisitor - where - V: PartialOrd + Deserialize<'de>, - { - type Value = Study; - - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str("struct Study") - } - - fn visit_map(self, mut map: A) -> Result - where - A: MapAccess<'de>, - { - let mut direction = None; - let mut completed_trials: Option>> = 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 = 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 = 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 = 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 = 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 = 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); - } -} diff --git a/src/trial.rs b/src/trial.rs index bac8d69..bb1ecda 100644 --- a/src/trial.rs +++ b/src/trial.rs @@ -4,8 +4,6 @@ use std::collections::HashMap; use std::sync::Arc; use parking_lot::RwLock; -#[cfg(feature = "serde")] -use serde::{Deserialize, Serialize}; use crate::distribution::{ CategoricalDistribution, Distribution, FloatDistribution, IntDistribution, @@ -24,7 +22,6 @@ use crate::types::TrialState; /// `Study::create_trial()`, the trial receives the study's sampler and access /// to the history of completed trials for informed sampling. #[derive(Clone)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct Trial { /// Unique identifier for this trial. id: u64, @@ -35,10 +32,8 @@ pub struct Trial { /// Parameter distributions, keyed by parameter name. distributions: HashMap, /// The sampler to use for generating parameter values. - #[cfg_attr(feature = "serde", serde(skip))] sampler: Option>, /// Access to the history of completed trials (shared with Study). - #[cfg_attr(feature = "serde", serde(skip))] history: Option>>>>, } diff --git a/src/types.rs b/src/types.rs index 6ceedce..1d61969 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1,11 +1,7 @@ //! Core types for the optimizer library. -#[cfg(feature = "serde")] -use serde::{Deserialize, Serialize}; - /// The direction of optimization. #[derive(Clone, Copy, Debug, PartialEq, Eq)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum Direction { /// Minimize the objective value. Minimize, @@ -15,7 +11,6 @@ pub enum Direction { /// The state of a trial in its lifecycle. #[derive(Clone, Copy, Debug, PartialEq, Eq)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum TrialState { /// The trial is currently running. Running, diff --git a/tests/integration.rs b/tests/integration.rs index e608ee9..c49cb3c 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -1142,144 +1142,3 @@ fn test_best_trial_with_nan_values() { let best = study.best_trial(); 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 = 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 = 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 = 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 = 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 = Study::new(Direction::Minimize); - - let json = serde_json::to_string(&study).unwrap(); - let loaded: Study = 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 = 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 = 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 = 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 = 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 - } - } -}