//! Integration tests for the optimizer library. use optimizer::{Direction, RandomSampler, Study, TpeError, TpeSampler, Trial}; // ============================================================================= // Test: optimize simple quadratic function with TPE, finds near-optimal // ============================================================================= #[test] fn test_tpe_optimizes_quadratic_function() { // Minimize f(x) = (x - 3)^2 where x ∈ [-10, 10] // Optimal: x = 3, f(3) = 0 let sampler = TpeSampler::builder() .seed(42) .n_startup_trials(5) // Quick startup for test .n_ei_candidates(24) .build(); let study: Study = Study::with_sampler(Direction::Minimize, sampler); study .optimize_with_sampler(50, |trial| { let x = trial.suggest_float("x", -10.0, 10.0)?; Ok::<_, TpeError>((x - 3.0).powi(2)) }) .expect("optimization should succeed"); let best = study.best_trial().expect("should have at least one trial"); // TPE should find a value close to optimal (x ≈ 3) // We expect the best value to be small (close to 0) assert!( best.value < 1.0, "TPE should find near-optimal: best value {} should be < 1.0", best.value ); } #[test] fn test_tpe_optimizes_multivariate_function() { // Minimize f(x, y) = x^2 + y^2 where x, y ∈ [-5, 5] // Optimal: (0, 0), f(0, 0) = 0 let sampler = TpeSampler::builder().seed(123).n_startup_trials(10).build(); let study: Study = Study::with_sampler(Direction::Minimize, sampler); study .optimize_with_sampler(100, |trial| { let x = trial.suggest_float("x", -5.0, 5.0)?; let y = trial.suggest_float("y", -5.0, 5.0)?; Ok::<_, TpeError>(x * x + y * y) }) .expect("optimization should succeed"); let best = study.best_trial().expect("should have at least one trial"); // TPE should find a reasonably good solution assert!( best.value < 5.0, "TPE should find near-optimal: best value {} should be < 5.0", best.value ); } #[test] fn test_tpe_maximization() { // Maximize f(x) = -(x - 2)^2 + 10 where x ∈ [-10, 10] // Optimal: x = 2, f(2) = 10 let sampler = TpeSampler::builder().seed(456).n_startup_trials(5).build(); let study: Study = Study::with_sampler(Direction::Maximize, sampler); study .optimize_with_sampler(50, |trial| { let x = trial.suggest_float("x", -10.0, 10.0)?; Ok::<_, TpeError>(-(x - 2.0).powi(2) + 10.0) }) .expect("optimization should succeed"); let best = study.best_trial().expect("should have at least one trial"); // For maximization, best value should be better than a random baseline // The function ranges from -90 (at x=-10 or x=10, when far from x=2) to 10 (at x=2) // A random approach would average around 0, so finding >5 is a reasonable check assert!( best.value > 5.0, "TPE should find reasonably good solution: best value {} should be > 5.0", best.value ); } // ============================================================================= // Test: RandomSampler samples uniformly across range // ============================================================================= #[test] fn test_random_sampler_uniform_float_distribution() { // Test that RandomSampler samples uniformly by running multiple trials // and checking the distribution of sampled values let study: Study = Study::with_sampler(Direction::Minimize, RandomSampler::with_seed(42)); let n_samples = 1000; let mut samples = Vec::with_capacity(n_samples); study .optimize(n_samples, |trial| { let x = trial.suggest_float("x", 0.0, 1.0)?; samples.push(x); Ok::<_, TpeError>(x) }) .unwrap(); // All samples should be in range for &s in &samples { assert!((0.0..=1.0).contains(&s), "sample {s} out of range [0, 1]"); } // Check distribution is roughly uniform by looking at quartiles samples.sort_by(|a, b| a.partial_cmp(b).unwrap()); let q1 = samples[n_samples / 4]; let q2 = samples[n_samples / 2]; let q3 = samples[3 * n_samples / 4]; // For uniform distribution, quartiles should be approximately 0.25, 0.5, 0.75 assert!((q1 - 0.25).abs() < 0.1, "Q1 {q1} should be close to 0.25"); assert!( (q2 - 0.5).abs() < 0.1, "Q2 (median) {q2} should be close to 0.5" ); assert!((q3 - 0.75).abs() < 0.1, "Q3 {q3} should be close to 0.75"); } #[test] fn test_random_sampler_uniform_int_distribution() { let study: Study = Study::with_sampler(Direction::Minimize, RandomSampler::with_seed(123)); let n_samples = 1000; let mut counts = [0u32; 10]; // counts for values 1-10 study .optimize(n_samples, |trial| { let n = trial.suggest_int("n", 1, 10)?; assert!((1..=10).contains(&n), "sample {n} out of range [1, 10]"); counts[(n - 1) as usize] += 1; Ok::<_, TpeError>(n as f64) }) .unwrap(); // Each value should appear roughly n_samples / 10 times let expected = n_samples as f64 / 10.0; for (i, &count) in counts.iter().enumerate() { let diff = (count as f64 - expected).abs() / expected; assert!( diff < 0.3, "value {} appeared {} times, expected ~{}, diff = {:.1}%", i + 1, count, expected, diff * 100.0 ); } } #[test] fn test_random_sampler_uniform_categorical_distribution() { let study: Study = Study::with_sampler(Direction::Minimize, RandomSampler::with_seed(456)); let n_samples = 1000; let mut counts = [0u32; 4]; let choices = ["a", "b", "c", "d"]; study .optimize(n_samples, |trial| { let choice = trial.suggest_categorical("cat", &choices)?; let idx = choices.iter().position(|&c| c == choice).unwrap(); counts[idx] += 1; Ok::<_, TpeError>(idx as f64) }) .unwrap(); // Each category should appear roughly n_samples / 4 times let expected = n_samples as f64 / 4.0; for (i, &count) in counts.iter().enumerate() { let diff = (count as f64 - expected).abs() / expected; assert!( diff < 0.25, "category {} appeared {} times, expected ~{}, diff = {:.1}%", i, count, expected, diff * 100.0 ); } } #[test] fn test_random_sampler_reproducibility() { // Two studies with the same seed should produce the same sequence // NOTE: We must use optimize_with_sampler() for Study to get sampler integration let study1: Study = Study::with_sampler(Direction::Minimize, RandomSampler::with_seed(999)); let study2: Study = Study::with_sampler(Direction::Minimize, RandomSampler::with_seed(999)); let mut values1 = Vec::new(); let mut values2 = Vec::new(); study1 .optimize_with_sampler(100, |trial| { let x = trial.suggest_float("x", 0.0, 100.0)?; values1.push(x); Ok::<_, TpeError>(x) }) .unwrap(); study2 .optimize_with_sampler(100, |trial| { let x = trial.suggest_float("x", 0.0, 100.0)?; values2.push(x); Ok::<_, TpeError>(x) }) .unwrap(); for (i, (v1, v2)) in values1.iter().zip(values2.iter()).enumerate() { assert_eq!( v1, v2, "values at trial {i} should be identical with same seed: {v1} vs {v2}" ); } } // ============================================================================= // Test: suggest_* methods return cached values on repeated calls // ============================================================================= #[test] fn test_suggest_float_caching() { let mut trial = Trial::new(0); let x1 = trial.suggest_float("x", 0.0, 10.0).unwrap(); let x2 = trial.suggest_float("x", 0.0, 10.0).unwrap(); let x3 = trial.suggest_float("x", 0.0, 10.0).unwrap(); assert_eq!(x1, x2, "repeated suggest_float should return cached value"); assert_eq!(x2, x3, "repeated suggest_float should return cached value"); } #[test] fn test_suggest_float_log_caching() { let mut trial = Trial::new(0); let x1 = trial.suggest_float_log("lr", 1e-5, 1e-1).unwrap(); let x2 = trial.suggest_float_log("lr", 1e-5, 1e-1).unwrap(); assert_eq!( x1, x2, "repeated suggest_float_log should return cached value" ); } #[test] fn test_suggest_float_step_caching() { let mut trial = Trial::new(0); let x1 = trial.suggest_float_step("step", 0.0, 1.0, 0.1).unwrap(); let x2 = trial.suggest_float_step("step", 0.0, 1.0, 0.1).unwrap(); assert_eq!( x1, x2, "repeated suggest_float_step should return cached value" ); } #[test] fn test_suggest_int_caching() { let mut trial = Trial::new(0); let n1 = trial.suggest_int("n", 1, 100).unwrap(); let n2 = trial.suggest_int("n", 1, 100).unwrap(); assert_eq!(n1, n2, "repeated suggest_int should return cached value"); } #[test] fn test_suggest_int_log_caching() { let mut trial = Trial::new(0); let n1 = trial.suggest_int_log("batch", 1, 1024).unwrap(); let n2 = trial.suggest_int_log("batch", 1, 1024).unwrap(); assert_eq!( n1, n2, "repeated suggest_int_log should return cached value" ); } #[test] fn test_suggest_int_step_caching() { let mut trial = Trial::new(0); let n1 = trial.suggest_int_step("units", 32, 512, 32).unwrap(); let n2 = trial.suggest_int_step("units", 32, 512, 32).unwrap(); assert_eq!( n1, n2, "repeated suggest_int_step should return cached value" ); } #[test] fn test_suggest_categorical_caching() { let mut trial = Trial::new(0); let choices = ["sgd", "adam", "rmsprop"]; let c1 = trial.suggest_categorical("optimizer", &choices).unwrap(); let c2 = trial.suggest_categorical("optimizer", &choices).unwrap(); assert_eq!( c1, c2, "repeated suggest_categorical should return cached value" ); } #[test] fn test_multiple_parameters_independent_caching() { let mut trial = Trial::new(0); // Suggest multiple parameters let x = trial.suggest_float("x", 0.0, 1.0).unwrap(); let y = trial.suggest_float("y", 0.0, 1.0).unwrap(); let n = trial.suggest_int("n", 1, 10).unwrap(); let opt = trial.suggest_categorical("opt", &["a", "b"]).unwrap(); // All should be cached independently assert_eq!(x, trial.suggest_float("x", 0.0, 1.0).unwrap()); assert_eq!(y, trial.suggest_float("y", 0.0, 1.0).unwrap()); assert_eq!(n, trial.suggest_int("n", 1, 10).unwrap()); assert_eq!(opt, trial.suggest_categorical("opt", &["a", "b"]).unwrap()); } // ============================================================================= // Test: parameter conflict returns error // ============================================================================= #[test] fn test_parameter_conflict_float_different_bounds() { let mut trial = Trial::new(0); trial.suggest_float("x", 0.0, 1.0).unwrap(); let result = trial.suggest_float("x", 0.0, 2.0); // Different upper bound assert!(matches!(result, Err(TpeError::ParameterConflict { .. }))); } #[test] fn test_parameter_conflict_float_vs_log() { let mut trial = Trial::new(0); 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 assert!(matches!(result, Err(TpeError::ParameterConflict { .. }))); } #[test] fn test_parameter_conflict_float_vs_step() { let mut trial = Trial::new(0); 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 assert!(matches!(result, Err(TpeError::ParameterConflict { .. }))); } #[test] fn test_parameter_conflict_int_different_bounds() { let mut trial = Trial::new(0); trial.suggest_int("n", 1, 10).unwrap(); let result = trial.suggest_int("n", 1, 20); // Different upper bound assert!(matches!(result, Err(TpeError::ParameterConflict { .. }))); } #[test] fn test_parameter_conflict_int_vs_log() { let mut trial = Trial::new(0); trial.suggest_int("n", 1, 100).unwrap(); let result = trial.suggest_int_log("n", 1, 100); // Same bounds but log scale assert!(matches!(result, Err(TpeError::ParameterConflict { .. }))); } #[test] fn test_parameter_conflict_categorical_different_n_choices() { let mut trial = Trial::new(0); trial.suggest_categorical("opt", &["a", "b", "c"]).unwrap(); let result = trial.suggest_categorical("opt", &["x", "y"]); // Different number of choices assert!(matches!(result, Err(TpeError::ParameterConflict { .. }))); } #[test] fn test_parameter_conflict_float_vs_int() { let mut trial = Trial::new(0); trial.suggest_float("x", 0.0, 10.0).unwrap(); let result = trial.suggest_int("x", 0, 10); // Different type assert!(matches!(result, Err(TpeError::ParameterConflict { .. }))); } #[test] fn test_parameter_conflict_returns_name() { let mut trial = Trial::new(0); trial.suggest_float("my_param", 0.0, 1.0).unwrap(); let result = trial.suggest_float("my_param", 0.0, 2.0); match result { Err(TpeError::ParameterConflict { name, .. }) => { assert_eq!(name, "my_param"); } _ => panic!("expected ParameterConflict error"), } } // ============================================================================= // Test: empty categorical returns error // ============================================================================= #[test] fn test_empty_categorical_returns_error() { let mut trial = Trial::new(0); let empty: &[&str] = &[]; let result = trial.suggest_categorical("opt", empty); assert!(matches!(result, Err(TpeError::EmptyChoices))); } #[test] fn test_empty_categorical_vec_returns_error() { let mut trial = Trial::new(0); let empty: Vec = vec![]; let result = trial.suggest_categorical("numbers", &empty); assert!(matches!(result, Err(TpeError::EmptyChoices))); } // ============================================================================= // Additional integration tests // ============================================================================= #[test] fn test_study_basic_workflow() { let study: Study = Study::new(Direction::Minimize); study .optimize(10, |trial| { let x = trial.suggest_float("x", -5.0, 5.0)?; Ok::<_, TpeError>(x * x) }) .expect("optimization should succeed"); assert_eq!(study.n_trials(), 10); let best = study.best_trial().expect("should have best trial"); assert!(best.value >= 0.0, "x^2 should be non-negative"); } #[test] fn test_study_with_failures() { let study: Study = Study::new(Direction::Minimize); // Every other trial fails let mut counter = 0; study .optimize(10, |trial| { counter += 1; if counter % 2 == 0 { return Err::("intentional failure"); } let x = trial .suggest_float("x", -5.0, 5.0) .map_err(|_| "param error")?; Ok(x * x) }) .expect("optimization should succeed with some failures"); // Only half the trials should have succeeded assert_eq!(study.n_trials(), 5, "only 5 trials should have completed"); } #[test] fn test_no_completed_trials_error() { let study: Study = Study::new(Direction::Minimize); let result = study.best_trial(); assert!(matches!(result, Err(TpeError::NoCompletedTrials))); } #[test] fn test_invalid_bounds_errors() { let mut trial = Trial::new(0); // low > high for float let result = trial.suggest_float("x", 10.0, 5.0); assert!(matches!(result, Err(TpeError::InvalidBounds { .. }))); // low > high for int let result = trial.suggest_int("n", 100, 50); assert!(matches!(result, Err(TpeError::InvalidBounds { .. }))); } #[test] fn test_invalid_log_bounds_errors() { let mut trial = Trial::new(0); // low <= 0 for log float let result = trial.suggest_float_log("x", 0.0, 1.0); assert!(matches!(result, Err(TpeError::InvalidLogBounds))); let result = trial.suggest_float_log("y", -1.0, 1.0); assert!(matches!(result, Err(TpeError::InvalidLogBounds))); // low < 1 for log int let result = trial.suggest_int_log("n", 0, 100); assert!(matches!(result, Err(TpeError::InvalidLogBounds))); } #[test] fn test_invalid_step_errors() { let mut trial = Trial::new(0); // step <= 0 for float let result = trial.suggest_float_step("x", 0.0, 1.0, 0.0); assert!(matches!(result, Err(TpeError::InvalidStep))); let result = trial.suggest_float_step("y", 0.0, 1.0, -0.1); assert!(matches!(result, Err(TpeError::InvalidStep))); // step <= 0 for int let result = trial.suggest_int_step("n", 0, 100, 0); assert!(matches!(result, Err(TpeError::InvalidStep))); } #[test] fn test_tpe_with_categorical_parameter() { let sampler = TpeSampler::builder().seed(42).n_startup_trials(5).build(); let study: Study = Study::with_sampler(Direction::Maximize, sampler); // Optimization where the best choice depends on the categorical study .optimize_with_sampler(30, |trial| { let choice = trial.suggest_categorical("model", &["linear", "quadratic", "cubic"])?; let x = trial.suggest_float("x", 0.0, 2.0)?; // cubic model is best at x=1 let value = match choice { "linear" => x, "quadratic" => x * x, "cubic" => -((x - 1.0).powi(2)) + 10.0, // peak at x=1, max value 10 _ => unreachable!(), }; Ok::<_, TpeError>(value) }) .expect("optimization should succeed"); let best = study.best_trial().expect("should have best trial"); // The optimizer should find that "cubic" with x≈1 is best assert!( best.value > 5.0, "should find good solution, got {}", best.value ); } #[test] fn test_tpe_with_integer_parameters() { let sampler = TpeSampler::builder().seed(789).n_startup_trials(5).build(); let study: Study = Study::with_sampler(Direction::Minimize, sampler); // Minimize (n - 7)^2 where n ∈ [1, 10] study .optimize_with_sampler(30, |trial| { let n = trial.suggest_int("n", 1, 10)?; Ok::<_, TpeError>(((n - 7) as f64).powi(2)) }) .expect("optimization should succeed"); let best = study.best_trial().expect("should have best trial"); // Best value should be small (n close to 7) assert!( best.value < 5.0, "should find n close to 7, best value = {}", best.value ); } #[test] fn test_callback_early_stopping() { use std::cell::Cell; use std::ops::ControlFlow; let study: Study = Study::new(Direction::Minimize); let trials_run = Cell::new(0); study .optimize_with_callback( 100, |trial| { trials_run.set(trials_run.get() + 1); let x = trial.suggest_float("x", 0.0, 10.0)?; Ok::<_, TpeError>(x) }, |_study, _trial| { // Stop after 5 trials if trials_run.get() >= 5 { ControlFlow::Break(()) } else { ControlFlow::Continue(()) } }, ) .expect("optimization should succeed"); assert_eq!(study.n_trials(), 5, "should have stopped after 5 trials"); } #[test] fn test_study_trials_iteration() { let study: Study = Study::new(Direction::Minimize); study .optimize(5, |trial| { let x = trial.suggest_float("x", 0.0, 1.0)?; Ok::<_, TpeError>(x) }) .unwrap(); let trials = study.trials(); assert_eq!(trials.len(), 5); for trial in &trials { assert!( !trial.params.is_empty(), "each trial should have parameters" ); assert!( trial.params.contains_key("x"), "each trial should have parameter 'x'" ); } } #[test] fn test_study_direction() { let study_min: Study = Study::new(Direction::Minimize); assert_eq!(study_min.direction(), Direction::Minimize); let study_max: Study = Study::new(Direction::Maximize); assert_eq!(study_max.direction(), Direction::Maximize); } #[test] fn test_trial_state() { use optimizer::TrialState; let trial = Trial::new(0); assert_eq!(trial.state(), TrialState::Running); } #[test] fn test_trial_params_access() { let mut trial = Trial::new(0); trial.suggest_float("x", 0.0, 1.0).unwrap(); trial.suggest_int("n", 1, 10).unwrap(); let params = trial.params(); assert_eq!(params.len(), 2); assert!(params.contains_key("x")); assert!(params.contains_key("n")); } #[test] fn test_log_scale_float_range() { let mut trial = Trial::new(0); let lr = trial.suggest_float_log("lr", 1e-5, 1e-1).unwrap(); assert!( (1e-5..=1e-1).contains(&lr), "log-scale value {lr} out of range" ); } #[test] fn test_step_float_snaps_to_grid() { let mut trial = Trial::new(0); let x = trial.suggest_float_step("x", 0.0, 1.0, 0.25).unwrap(); // x should be one of: 0.0, 0.25, 0.5, 0.75, 1.0 let valid_values = [0.0, 0.25, 0.5, 0.75, 1.0]; let is_valid = valid_values.iter().any(|&v| (x - v).abs() < 1e-10); assert!(is_valid, "stepped float {x} should snap to grid"); } #[test] fn test_step_int_snaps_to_grid() { let mut trial = Trial::new(0); let n = trial.suggest_int_step("n", 0, 100, 25).unwrap(); // n should be one of: 0, 25, 50, 75, 100 assert!( n % 25 == 0 && (0..=100).contains(&n), "stepped int {n} should snap to grid" ); } #[test] fn test_best_value() { let study: Study = Study::new(Direction::Minimize); study .optimize(10, |trial| { let x = trial.suggest_float("x", 0.0, 10.0)?; Ok::<_, TpeError>(x) }) .unwrap(); let best_value = study.best_value().expect("should have best value"); let best_trial = study.best_trial().expect("should have best trial"); assert_eq!( best_value, best_trial.value, "best_value should match best_trial.value" ); } // ============================================================================= // Additional coverage tests // ============================================================================= #[test] fn test_study_set_sampler() { // Test that set_sampler allows changing the sampler after study creation let mut study: Study = Study::new(Direction::Minimize); // Initially uses RandomSampler, now switch to TPE let tpe = TpeSampler::builder().seed(42).n_startup_trials(5).build(); study.set_sampler(tpe); // Should work with the new sampler study .optimize_with_sampler(10, |trial| { let x = trial.suggest_float("x", -5.0, 5.0)?; Ok::<_, TpeError>(x * x) }) .expect("optimization should succeed with new sampler"); assert_eq!(study.n_trials(), 10); } #[test] fn test_study_with_i32_value_type() { // Test Study with non-f64 value type let study: Study = Study::new(Direction::Minimize); study .optimize(10, |trial| { let x = trial.suggest_int("x", -10, 10)?; Ok::<_, TpeError>(x.abs() as i32) }) .expect("optimization should succeed"); assert_eq!(study.n_trials(), 10); let best = study.best_trial().expect("should have best trial"); assert!(best.value >= 0, "absolute value should be non-negative"); } #[test] fn test_optimize_all_trials_fail() { let study: Study = Study::new(Direction::Minimize); // All trials fail let result = study.optimize(5, |_trial| Err::("always fails")); assert!( matches!(result, Err(TpeError::NoCompletedTrials)), "should return NoCompletedTrials when all trials fail" ); } #[test] fn test_optimize_with_callback_all_trials_fail() { use std::ops::ControlFlow; let study: Study = Study::new(Direction::Minimize); let result = study.optimize_with_callback( 5, |_trial| Err::("always fails"), |_study, _trial| ControlFlow::Continue(()), ); assert!( matches!(result, Err(TpeError::NoCompletedTrials)), "should return NoCompletedTrials when all trials fail" ); } #[test] fn test_optimize_with_sampler_all_trials_fail() { let study: Study = Study::new(Direction::Minimize); let result = study.optimize_with_sampler(5, |_trial| Err::("always fails")); assert!( matches!(result, Err(TpeError::NoCompletedTrials)), "should return NoCompletedTrials when all trials fail" ); } #[test] fn test_optimize_with_callback_sampler_all_trials_fail() { use std::ops::ControlFlow; let study: Study = Study::new(Direction::Minimize); let result = study.optimize_with_callback_sampler( 5, |_trial| Err::("always fails"), |_study, _trial| ControlFlow::Continue(()), ); assert!( matches!(result, Err(TpeError::NoCompletedTrials)), "should return NoCompletedTrials when all trials fail" ); } #[test] fn test_trial_debug_format() { let mut trial = Trial::new(42); trial.suggest_float("x", 0.0, 1.0).unwrap(); let debug_str = format!("{:?}", trial); // Should contain trial id and other fields assert!(debug_str.contains("Trial")); assert!(debug_str.contains("42")); assert!(debug_str.contains("has_sampler")); } #[test] fn test_tpe_sampler_builder_default_trait() { use optimizer::TpeSamplerBuilder; let builder = TpeSamplerBuilder::default(); let sampler = builder.build(); // Should have default values let study: Study = Study::with_sampler(Direction::Minimize, sampler); study .optimize_with_sampler(5, |trial| { let x = trial.suggest_float("x", 0.0, 1.0)?; Ok::<_, TpeError>(x) }) .unwrap(); assert_eq!(study.n_trials(), 5); } #[test] fn test_tpe_sampler_default_trait() { let sampler = TpeSampler::default(); let study: Study = Study::with_sampler(Direction::Minimize, sampler); study .optimize_with_sampler(5, |trial| { let x = trial.suggest_float("x", 0.0, 1.0)?; Ok::<_, TpeError>(x) }) .unwrap(); assert_eq!(study.n_trials(), 5); } #[test] fn test_tpe_with_fixed_kde_bandwidth() { let sampler = TpeSampler::builder() .seed(42) .n_startup_trials(5) .kde_bandwidth(0.5) .build(); let study: Study = Study::with_sampler(Direction::Minimize, sampler); study .optimize_with_sampler(20, |trial| { let x = trial.suggest_float("x", -5.0, 5.0)?; Ok::<_, TpeError>(x * x) }) .expect("optimization should succeed"); let best = study.best_trial().unwrap(); assert!(best.value < 10.0, "should find reasonable solution"); } #[test] #[should_panic(expected = "kde_bandwidth must be positive")] fn test_tpe_sampler_invalid_kde_bandwidth() { TpeSampler::with_config(0.25, 10, 24, Some(-1.0), None); } #[test] fn test_tpe_split_trials_with_two_trials() { // Edge case: exactly 2 trials in history let sampler = TpeSampler::builder() .seed(42) .n_startup_trials(2) // TPE kicks in after 2 trials .build(); let study: Study = Study::with_sampler(Direction::Minimize, sampler); study .optimize_with_sampler(5, |trial| { let x = trial.suggest_float("x", 0.0, 10.0)?; Ok::<_, TpeError>(x) }) .expect("optimization should succeed with small history"); assert_eq!(study.n_trials(), 5); } #[test] fn test_tpe_with_log_scale_int() { let sampler = TpeSampler::builder().seed(42).n_startup_trials(5).build(); let study: Study = Study::with_sampler(Direction::Minimize, sampler); study .optimize_with_sampler(20, |trial| { let batch_size = trial.suggest_int_log("batch_size", 1, 1024)?; // Optimal around batch_size = 32 Ok::<_, TpeError>(((batch_size as f64).log2() - 5.0).powi(2)) }) .expect("optimization should succeed"); let best = study.best_trial().unwrap(); assert!(best.value < 10.0, "should find reasonable solution"); } #[test] fn test_tpe_with_step_distributions() { let sampler = TpeSampler::builder().seed(42).n_startup_trials(5).build(); let study: Study = Study::with_sampler(Direction::Minimize, sampler); study .optimize_with_sampler(20, |trial| { let x = trial.suggest_float_step("x", 0.0, 10.0, 0.5)?; let n = trial.suggest_int_step("n", 0, 100, 10)?; Ok::<_, TpeError>((x - 5.0).powi(2) + ((n - 50) as f64).powi(2)) }) .expect("optimization should succeed"); let best = study.best_trial().unwrap(); assert!(best.value < 100.0, "should find reasonable solution"); } #[test] fn test_create_trial_vs_create_trial_with_sampler() { let sampler = RandomSampler::with_seed(42); let study: Study = Study::with_sampler(Direction::Minimize, sampler); // create_trial() creates trial without sampler integration let trial1 = study.create_trial(); assert_eq!(trial1.id(), 0); // create_trial_with_sampler() creates trial with sampler let trial2 = study.create_trial_with_sampler(); assert_eq!(trial2.id(), 1); // Both should work for suggesting parameters let mut trial3 = study.create_trial(); let x = trial3.suggest_float("x", 0.0, 1.0).unwrap(); assert!((0.0..=1.0).contains(&x)); } #[test] fn test_manual_trial_completion() { let study: Study = Study::new(Direction::Minimize); // Manually create and complete trials let mut trial = study.create_trial(); let x = trial.suggest_float("x", 0.0, 10.0).unwrap(); study.complete_trial(trial, x * x); let mut trial2 = study.create_trial(); let y = trial2.suggest_float("x", 0.0, 10.0).unwrap(); study.complete_trial(trial2, y * y); // Manually fail a trial let trial3 = study.create_trial(); study.fail_trial(trial3, "test failure"); // Only 2 completed trials assert_eq!(study.n_trials(), 2); } #[test] fn test_distributions_access() { let mut trial = Trial::new(0); trial.suggest_float("x", 0.0, 1.0).unwrap(); trial.suggest_int("n", 1, 10).unwrap(); trial.suggest_categorical("opt", &["a", "b", "c"]).unwrap(); let dists = trial.distributions(); assert_eq!(dists.len(), 3); assert!(dists.contains_key("x")); assert!(dists.contains_key("n")); assert!(dists.contains_key("opt")); } #[test] fn test_tpe_empty_good_or_bad_values_fallback() { // When TPE can't find values in the good/bad groups, it falls back to random let sampler = TpeSampler::builder() .seed(42) .n_startup_trials(5) .gamma(0.1) // Very small gamma means few "good" trials .build(); let study: Study = Study::with_sampler(Direction::Minimize, sampler); // First optimize with one parameter study .optimize_with_sampler(10, |trial| { let x = trial.suggest_float("x", 0.0, 10.0)?; Ok::<_, TpeError>(x) }) .unwrap(); // Now try with a different parameter - TPE won't have history for "y" study .optimize_with_sampler(5, |trial| { let y = trial.suggest_float("y", 0.0, 10.0)?; Ok::<_, TpeError>(y) }) .unwrap(); assert_eq!(study.n_trials(), 15); } #[test] fn test_callback_early_stopping_on_first_trial() { use std::ops::ControlFlow; let study: Study = Study::new(Direction::Minimize); study .optimize_with_callback( 100, |trial| { let x = trial.suggest_float("x", 0.0, 10.0)?; Ok::<_, TpeError>(x) }, |_study, _trial| { // Stop immediately after first trial ControlFlow::Break(()) }, ) .expect("optimization should succeed"); assert_eq!(study.n_trials(), 1, "should have stopped after 1 trial"); } #[test] fn test_callback_sampler_early_stopping() { use std::ops::ControlFlow; let sampler = RandomSampler::with_seed(42); let study: Study = Study::with_sampler(Direction::Minimize, sampler); study .optimize_with_callback_sampler( 100, |trial| { let x = trial.suggest_float("x", 0.0, 10.0)?; Ok::<_, TpeError>(x) }, |study, _trial| { if study.n_trials() >= 3 { ControlFlow::Break(()) } else { ControlFlow::Continue(()) } }, ) .expect("optimization should succeed"); assert_eq!(study.n_trials(), 3); } #[test] fn test_int_bounds_with_low_equals_high() { let mut trial = Trial::new(0); // When low == high, should return that exact value let n = trial.suggest_int("n", 5, 5).unwrap(); assert_eq!(n, 5); let x = trial.suggest_float("x", 3.0, 3.0).unwrap(); assert_eq!(x, 3.0); } #[test] fn test_best_trial_with_nan_values() { // Test behavior when comparing with NaN values (PartialOrd edge case) let study: Study = Study::new(Direction::Minimize); // Complete some normal trials study .optimize(5, |trial| { let x = trial.suggest_float("x", 0.0, 10.0)?; Ok::<_, TpeError>(x) }) .unwrap(); // best_trial should still work let best = study.best_trial(); assert!(best.is_ok()); }