refactor(examples): split multi-concept examples into focused single-topic files

- Split pruning_and_callbacks into pruning and early_stopping
- Split advanced_features into async_parallel, journal_storage, ask_and_tell, multi_objective
- Each example now requires only its own feature flag
- Trim sampler_comparison winner logic and verbose header
- Update CI workflow and README to match new example names
This commit is contained in:
Manuel Raimann
2026-02-12 10:33:51 +01:00
parent 6a4b27c46f
commit ee59c9cdd0
12 changed files with 376 additions and 443 deletions
+14 -6
View File
@@ -66,14 +66,22 @@ jobs:
- uses: Swatinem/rust-cache@v2
- name: Run basic_optimization
run: cargo run --example basic_optimization
- name: Run sampler_comparison
run: cargo run --example sampler_comparison
- name: Run pruning_and_callbacks
run: cargo run --example pruning_and_callbacks
- name: Run parameter_types
run: cargo run --example parameter_types --features derive
- name: Run advanced_features
run: cargo run --example advanced_features --features "async,journal"
- name: Run sampler_comparison
run: cargo run --example sampler_comparison
- name: Run pruning
run: cargo run --example pruning
- name: Run early_stopping
run: cargo run --example early_stopping
- name: Run async_parallel
run: cargo run --example async_parallel --features async
- name: Run journal_storage
run: cargo run --example journal_storage --features journal
- name: Run ask_and_tell
run: cargo run --example ask_and_tell
- name: Run multi_objective
run: cargo run --example multi_objective
docs:
name: Docs
+22 -5
View File
@@ -67,13 +67,30 @@ name = "sampler_comparison"
path = "examples/sampler_comparison.rs"
[[example]]
name = "pruning_and_callbacks"
path = "examples/pruning_and_callbacks.rs"
name = "pruning"
path = "examples/pruning.rs"
[[example]]
name = "advanced_features"
path = "examples/advanced_features.rs"
required-features = ["async", "journal"]
name = "early_stopping"
path = "examples/early_stopping.rs"
[[example]]
name = "async_parallel"
path = "examples/async_parallel.rs"
required-features = ["async"]
[[example]]
name = "journal_storage"
path = "examples/journal_storage.rs"
required-features = ["journal"]
[[example]]
name = "ask_and_tell"
path = "examples/ask_and_tell.rs"
[[example]]
name = "multi_objective"
path = "examples/multi_objective.rs"
[[test]]
name = "journal_tests"
+9 -5
View File
@@ -52,11 +52,15 @@ println!("Best x = {:.4}, f(x) = {:.4}", best.get(&x).unwrap(), best.value);
## Examples
```sh
cargo run --example basic_optimization # Minimize a quadratic — simplest possible usage
cargo run --example sampler_comparison # Compare Random, TPE, and Grid on the same problem
cargo run --example pruning_and_callbacks # Trial pruning with MedianPruner + early stopping
cargo run --example parameter_types --features derive # All 5 param types + #[derive(Categorical)]
cargo run --example advanced_features --features async,journal # Async, journal storage, ask-and-tell, multi-objective
cargo run --example basic_optimization # Minimize a quadratic — simplest possible usage
cargo run --example parameter_types --features derive # All 5 param types + #[derive(Categorical)]
cargo run --example sampler_comparison # Compare Random, TPE, and Grid on the same problem
cargo run --example pruning # Trial pruning with MedianPruner
cargo run --example early_stopping # Halt a study when a target is reached
cargo run --example async_parallel --features async # Evaluate trials concurrently with tokio
cargo run --example journal_storage --features journal # Persist trials to disk and resume later
cargo run --example ask_and_tell # Decouple sampling from evaluation
cargo run --example multi_objective # Optimize competing objectives + Pareto front
```
## Learn More
-277
View File
@@ -1,277 +0,0 @@
//! Advanced Features Example
//!
//! This example demonstrates four advanced capabilities of the optimizer crate:
//!
//! 1. **Async parallel optimization** — evaluate multiple trials concurrently
//! 2. **Journal storage** — persist trials to disk and resume studies later
//! 3. **Ask-and-tell interface** — decouple sampling from evaluation
//! 4. **Multi-objective optimization** — optimize competing objectives simultaneously
//!
//! Run with: `cargo run --example advanced_features --features "async,journal"`
use std::time::Instant;
use optimizer::multi_objective::MultiObjectiveStudy;
use optimizer::prelude::*;
// ============================================================================
// Section 1: Async Parallel Optimization
// ============================================================================
/// Runs multiple trials concurrently using tokio, reducing wall-clock time
/// when the objective function involves I/O or other async work.
async fn async_parallel_optimization() -> optimizer::Result<()> {
println!("=== Section 1: Async Parallel Optimization ===\n");
let sampler = TpeSampler::builder()
.n_startup_trials(5)
.seed(42)
.build()
.expect("Failed to build TPE sampler");
let study: Study<f64> = Study::with_sampler(Direction::Minimize, sampler);
let x = FloatParam::new(-5.0, 5.0).name("x");
let y = FloatParam::new(-5.0, 5.0).name("y");
let n_trials = 30;
let concurrency = 4;
println!("Running {n_trials} trials with {concurrency} concurrent workers...");
let start = Instant::now();
// optimize_parallel spawns up to `concurrency` trials at once.
// The closure must take ownership of Trial and return (Trial, value).
study
.optimize_parallel(n_trials, concurrency, {
let x = x.clone();
let y = y.clone();
move |mut trial| {
let x = x.clone();
let y = y.clone();
async move {
let xv = x.suggest(&mut trial)?;
let yv = y.suggest(&mut trial)?;
// Simulate async I/O (e.g., calling an external service)
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
// Sphere function: minimum at origin
let value = xv * xv + yv * yv;
Ok::<_, optimizer::Error>((trial, value))
}
}
})
.await?;
let elapsed = start.elapsed();
let best = study.best_trial()?;
println!(
"Completed in {elapsed:.2?} (vs ~{:.0?} sequential)",
std::time::Duration::from_millis(10 * n_trials as u64)
);
println!(
"Best: f({:.3}, {:.3}) = {:.6}\n",
best.get(&x).unwrap(),
best.get(&y).unwrap(),
best.value
);
Ok(())
}
// ============================================================================
// Section 2: Journal Storage
// ============================================================================
/// Persists trials to a JSONL file so that a study can be resumed later.
/// Useful for long-running experiments or crash recovery.
fn journal_storage_demo() -> optimizer::Result<()> {
println!("=== Section 2: Journal Storage ===\n");
let path = std::env::temp_dir().join("optimizer_advanced_example.jsonl");
// Clean up from any previous run
let _ = std::fs::remove_file(&path);
let x = FloatParam::new(-5.0, 5.0).name("x");
// --- First run: optimize 20 trials and persist to disk ---
{
let storage = JournalStorage::<f64>::new(&path);
let study: Study<f64> = Study::builder()
.minimize()
.sampler(TpeSampler::new())
.storage(storage)
.build();
study.optimize(20, |trial| {
let xv = x.suggest(trial)?;
Ok::<_, optimizer::Error>(xv * xv)
})?;
println!(
"First run: {} trials saved to {}",
study.n_trials(),
path.display()
);
}
// --- Second run: resume from the journal file ---
{
// JournalStorage::open loads existing trials from disk
let storage = JournalStorage::<f64>::open(&path)?;
let study: Study<f64> = Study::builder()
.minimize()
.sampler(TpeSampler::new())
.storage(storage)
.build();
// The sampler sees the prior 20 trials, so it starts informed
let before = study.n_trials();
study.optimize(10, |trial| {
let xv = x.suggest(trial)?;
Ok::<_, optimizer::Error>(xv * xv)
})?;
let best = study.best_trial()?;
println!(
"Resumed: {}{} trials, best f({:.4}) = {:.6}",
before,
study.n_trials(),
best.get(&x).unwrap(),
best.value
);
}
// Clean up the temporary file
let _ = std::fs::remove_file(&path);
println!();
Ok(())
}
// ============================================================================
// Section 3: Ask-and-Tell Interface
// ============================================================================
/// Decouples trial creation from evaluation. Useful when:
/// - Evaluations happen outside the optimizer (e.g., in a separate process)
/// - You want to batch evaluations before reporting results
/// - You need custom scheduling logic
fn ask_and_tell_demo() -> optimizer::Result<()> {
println!("=== Section 3: Ask-and-Tell Interface ===\n");
let study: Study<f64> = Study::new(Direction::Minimize);
let x = FloatParam::new(-5.0, 5.0).name("x");
let y = FloatParam::new(-5.0, 5.0).name("y");
// Ask for a batch of trials, evaluate externally, then tell results
for batch in 0..3 {
let batch_size = 5;
let mut trials = Vec::with_capacity(batch_size);
// ask() creates trials with sampled parameters
for _ in 0..batch_size {
let mut trial = study.ask();
let xv = x.suggest(&mut trial)?;
let yv = y.suggest(&mut trial)?;
// Store values alongside the trial for later evaluation
trials.push((trial, xv, yv));
}
// Evaluate the batch (could be sent to workers, GPUs, etc.)
for (trial, xv, yv) in trials {
let value = xv * xv + yv * yv;
// tell() reports the result back to the study
study.tell(trial, Ok::<_, &str>(value));
}
println!(
"Batch {}: evaluated {} trials (total: {})",
batch + 1,
batch_size,
study.n_trials()
);
}
let best = study.best_trial()?;
println!(
"Best: f({:.3}, {:.3}) = {:.6}\n",
best.get(&x).unwrap(),
best.get(&y).unwrap(),
best.value
);
Ok(())
}
// ============================================================================
// Section 4: Multi-Objective Optimization
// ============================================================================
/// Optimizes two competing objectives simultaneously.
/// Returns the Pareto front — the set of solutions where no objective can
/// be improved without worsening the other.
fn multi_objective_demo() -> optimizer::Result<()> {
println!("=== Section 4: Multi-Objective Optimization ===\n");
// Two objectives, both minimized
let study = MultiObjectiveStudy::new(vec![Direction::Minimize, Direction::Minimize]);
let x = FloatParam::new(0.0, 1.0).name("x");
// Classic bi-objective problem: f1(x) = x², f2(x) = (x - 1)²
// The Pareto front is the curve where improving f1 worsens f2 and vice versa.
study.optimize(50, |trial| {
let xv = x.suggest(trial)?;
let f1 = xv * xv;
let f2 = (xv - 1.0) * (xv - 1.0);
Ok::<_, optimizer::Error>(vec![f1, f2])
})?;
let front = study.pareto_front();
println!(
"Ran {} trials, Pareto front has {} solutions:",
study.n_trials(),
front.len()
);
// Show a few Pareto-optimal trade-offs
let mut sorted_front = front.clone();
sorted_front.sort_by(|a, b| a.values[0].partial_cmp(&b.values[0]).unwrap());
for (i, trial) in sorted_front.iter().take(5).enumerate() {
println!(
" {}: x={:.3}, f1={:.4}, f2={:.4}",
i + 1,
trial.get(&x).unwrap(),
trial.values[0],
trial.values[1]
);
}
if sorted_front.len() > 5 {
println!(" ... and {} more", sorted_front.len() - 5);
}
println!();
Ok(())
}
// ============================================================================
// Main
// ============================================================================
#[tokio::main]
async fn main() -> optimizer::Result<()> {
async_parallel_optimization().await?;
journal_storage_demo()?;
ask_and_tell_demo()?;
multi_objective_demo()?;
println!("All sections completed successfully!");
Ok(())
}
+51
View File
@@ -0,0 +1,51 @@
//! Ask-and-tell interface — decouple sampling from evaluation.
//!
//! Use `ask()` to get a trial with sampled parameters, evaluate it however
//! you like (workers, GPUs, external processes), then `tell()` the result.
//! This is useful for batch evaluation or custom scheduling.
//!
//! Run with: `cargo run --example ask_and_tell`
use optimizer::prelude::*;
fn main() -> optimizer::Result<()> {
let study: Study<f64> = Study::new(Direction::Minimize);
let x = FloatParam::new(-5.0, 5.0).name("x");
let y = FloatParam::new(-5.0, 5.0).name("y");
for batch in 0..3 {
let batch_size = 5;
let mut trials = Vec::with_capacity(batch_size);
// ask() creates trials with sampled parameters
for _ in 0..batch_size {
let mut trial = study.ask();
let xv = x.suggest(&mut trial)?;
let yv = y.suggest(&mut trial)?;
trials.push((trial, xv, yv));
}
// Evaluate the batch (could be sent to workers, GPUs, etc.)
for (trial, xv, yv) in trials {
let value = xv * xv + yv * yv;
study.tell(trial, Ok::<_, &str>(value));
}
println!(
"Batch {}: evaluated {batch_size} trials (total: {})",
batch + 1,
study.n_trials(),
);
}
let best = study.best_trial()?;
println!(
"Best: f({:.3}, {:.3}) = {:.6}",
best.get(&x).unwrap(),
best.get(&y).unwrap(),
best.value,
);
Ok(())
}
+52
View File
@@ -0,0 +1,52 @@
//! Async parallel optimization — evaluate multiple trials concurrently.
//!
//! Uses `optimize_parallel` with tokio to run several trials at once,
//! reducing wall-clock time when the objective involves I/O or async work.
//!
//! Run with: `cargo run --example async_parallel --features async`
use optimizer::prelude::*;
#[tokio::main]
async fn main() -> optimizer::Result<()> {
let study: Study<f64> = Study::minimize(TpeSampler::new());
let x = FloatParam::new(-5.0, 5.0).name("x");
let y = FloatParam::new(-5.0, 5.0).name("y");
let n_trials = 30;
let concurrency = 4;
println!("Running {n_trials} trials with {concurrency} concurrent workers...");
study
.optimize_parallel(n_trials, concurrency, {
let x = x.clone();
let y = y.clone();
move |mut trial| {
let x = x.clone();
let y = y.clone();
async move {
let xv = x.suggest(&mut trial)?;
let yv = y.suggest(&mut trial)?;
// Simulate async I/O (e.g. calling an external service)
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
let value = xv * xv + yv * yv;
Ok::<_, optimizer::Error>((trial, value))
}
}
})
.await?;
let best = study.best_trial()?;
println!(
"Best: f({:.3}, {:.3}) = {:.6}",
best.get(&x).unwrap(),
best.get(&y).unwrap(),
best.value,
);
Ok(())
}
+43
View File
@@ -0,0 +1,43 @@
//! Early stopping — halt an entire study once a target is reached.
//!
//! Use `optimize_with_callback` to inspect each completed trial and return
//! `ControlFlow::Break(())` when the study should stop (e.g. a quality
//! threshold is met or a time budget is exhausted).
//!
//! Run with: `cargo run --example early_stopping`
use std::ops::ControlFlow;
use optimizer::prelude::*;
fn main() -> optimizer::Result<()> {
let study: Study<f64> = Study::new(Direction::Minimize);
let x = FloatParam::new(-10.0, 10.0).name("x");
let target = 0.01;
study.optimize_with_callback(
100, // upper bound — we expect to stop much earlier
|trial| {
let xv = x.suggest(trial)?;
Ok::<_, Error>((xv - 3.0).powi(2))
},
|_study, completed| {
if completed.value < target {
println!("Target {target} reached at trial #{}", completed.id);
return ControlFlow::Break(());
}
ControlFlow::Continue(())
},
)?;
let best = study.best_trial()?;
println!(
"Stopped after {} trials — best f({:.4}) = {:.6}",
study.n_trials(),
best.get(&x).unwrap(),
best.value,
);
Ok(())
}
+68
View File
@@ -0,0 +1,68 @@
//! Journal storage — persist trials to disk and resume later.
//!
//! `JournalStorage` writes every trial to a JSONL file so that a study can
//! be resumed after a crash or across separate runs.
//!
//! Run with: `cargo run --example journal_storage --features journal`
use optimizer::prelude::*;
fn main() -> optimizer::Result<()> {
let path = std::env::temp_dir().join("optimizer_journal_example.jsonl");
// Clean up from any previous run
let _ = std::fs::remove_file(&path);
let x = FloatParam::new(-5.0, 5.0).name("x");
// --- First run: optimize 20 trials and persist to disk ---
{
let storage = JournalStorage::<f64>::new(&path);
let study: Study<f64> = Study::builder()
.minimize()
.sampler(TpeSampler::new())
.storage(storage)
.build();
study.optimize(20, |trial| {
let xv = x.suggest(trial)?;
Ok::<_, optimizer::Error>(xv * xv)
})?;
println!(
"First run: {} trials saved to {}",
study.n_trials(),
path.display(),
);
}
// --- Second run: resume from the journal file ---
{
let storage = JournalStorage::<f64>::open(&path)?;
let study: Study<f64> = Study::builder()
.minimize()
.sampler(TpeSampler::new())
.storage(storage)
.build();
let before = study.n_trials();
study.optimize(10, |trial| {
let xv = x.suggest(trial)?;
Ok::<_, optimizer::Error>(xv * xv)
})?;
let best = study.best_trial()?;
println!(
"Resumed: {}{} trials, best f({:.4}) = {:.6}",
before,
study.n_trials(),
best.get(&x).unwrap(),
best.value,
);
}
// Clean up
let _ = std::fs::remove_file(&path);
Ok(())
}
+49
View File
@@ -0,0 +1,49 @@
//! Multi-objective optimization — optimize competing objectives simultaneously.
//!
//! `MultiObjectiveStudy` returns the Pareto front: the set of solutions where
//! no objective can be improved without worsening another.
//!
//! Run with: `cargo run --example multi_objective`
use optimizer::multi_objective::MultiObjectiveStudy;
use optimizer::prelude::*;
fn main() -> optimizer::Result<()> {
let study = MultiObjectiveStudy::new(vec![Direction::Minimize, Direction::Minimize]);
let x = FloatParam::new(0.0, 1.0).name("x");
// Classic bi-objective: f1(x) = x², f2(x) = (x-1)²
// The Pareto front is the curve where improving f1 worsens f2.
study.optimize(50, |trial| {
let xv = x.suggest(trial)?;
let f1 = xv * xv;
let f2 = (xv - 1.0) * (xv - 1.0);
Ok::<_, optimizer::Error>(vec![f1, f2])
})?;
let front = study.pareto_front();
println!(
"Ran {} trials, Pareto front has {} solutions:",
study.n_trials(),
front.len(),
);
let mut sorted = front.clone();
sorted.sort_by(|a, b| a.values[0].partial_cmp(&b.values[0]).unwrap());
for (i, trial) in sorted.iter().take(5).enumerate() {
println!(
" {}: x={:.3}, f1={:.4}, f2={:.4}",
i + 1,
trial.get(&x).unwrap(),
trial.values[0],
trial.values[1],
);
}
if sorted.len() > 5 {
println!(" ... and {} more", sorted.len() - 5);
}
Ok(())
}
+67
View File
@@ -0,0 +1,67 @@
//! Trial pruning — stop unpromising trials early with `MedianPruner`.
//!
//! When your objective involves an iterative loop (e.g. training epochs),
//! the pruner compares intermediate values across trials and kills the
//! ones that fall below the median — saving compute on bad configurations.
//!
//! Run with: `cargo run --example pruning`
use optimizer::prelude::*;
fn main() -> optimizer::Result<()> {
// MedianPruner prunes trials whose intermediate value falls below the
// median of previously completed trials at the same step.
let study: Study<f64> = Study::builder()
.minimize()
.sampler(RandomSampler::with_seed(42))
.pruner(
MedianPruner::new(Direction::Minimize)
.n_warmup_steps(3) // run at least 3 epochs before pruning
.n_min_trials(3), // need 3 completed trials before pruning kicks in
)
.build();
let lr = FloatParam::new(1e-4, 1.0).name("learning_rate");
let momentum = FloatParam::new(0.0, 0.99).name("momentum");
let n_epochs: u64 = 20;
study.optimize(30, |trial| {
let lr_val = lr.suggest(trial)?;
let mom = momentum.suggest(trial)?;
// Simulated training loop — good hyperparameters converge to low loss,
// bad ones plateau high, giving the pruner something to cut.
let mut loss = 1.0;
for epoch in 0..n_epochs {
let lr_penalty = (lr_val.log10() - 0.01_f64.log10()).powi(2);
let mom_penalty = (mom - 0.8).powi(2);
let base_loss = 0.02 + 0.05 * lr_penalty + 1.5 * mom_penalty;
let progress = (epoch as f64 + 1.0) / n_epochs as f64;
loss = base_loss + (1.0 - base_loss) * (-3.5 * progress).exp();
// Report intermediate value so the pruner can evaluate this trial.
trial.report(epoch, loss);
// Check whether the pruner recommends stopping early.
if trial.should_prune() {
Err(TrialPruned)?;
}
}
Ok::<_, Error>(loss)
})?;
// --- Results ---
let best = study.best_trial()?;
println!(
"Completed {} trials ({} pruned)",
study.n_trials(),
study.n_pruned_trials()
);
println!("Best trial #{}: loss = {:.6}", best.id, best.value);
println!(" learning_rate = {:.6}", best.get(&lr).unwrap());
println!(" momentum = {:.4}", best.get(&momentum).unwrap());
Ok(())
}
-133
View File
@@ -1,133 +0,0 @@
//! Pruning and early-stopping example — demonstrates trial pruning with `MedianPruner`
//! and early stopping via `optimize_with_callback`.
//!
//! Simulates a training loop where each trial trains for multiple "epochs". The pruner
//! stops unpromising trials early, and a callback halts the entire study once a target
//! loss is reached.
//!
//! Run with: `cargo run --example pruning_and_callbacks`
use std::ops::ControlFlow;
use optimizer::TrialState;
use optimizer::prelude::*;
fn main() -> optimizer::Result<()> {
let n_trials: usize = 30;
let n_epochs: u64 = 20;
let target_loss = 0.15;
// Build a study with a seeded random sampler and MedianPruner.
// MedianPruner compares each trial's intermediate value against the median of
// completed trials at the same step — trials performing below median are pruned.
let study: Study<f64> = Study::builder()
.minimize()
.sampler(RandomSampler::with_seed(42))
.pruner(
MedianPruner::new(Direction::Minimize)
.n_warmup_steps(3) // let every trial run at least 3 epochs before pruning
.n_min_trials(3), // need 3 completed trials before pruning kicks in
)
.build();
let learning_rate = FloatParam::new(1e-4, 1.0).name("learning_rate");
let momentum = FloatParam::new(0.0, 0.99).name("momentum");
// Use optimize_with_callback to get both pruning AND early stopping.
// The callback fires after each completed (or pruned) trial and can halt the study.
study.optimize_with_callback(
n_trials,
// --- Objective function: simulated training loop with pruning ---
|trial| {
let lr = learning_rate.suggest(trial)?;
let mom = momentum.suggest(trial)?;
// Simulate training for n_epochs, reporting intermediate loss each epoch.
// Good hyperparameters (lr ≈ 0.01, momentum ≈ 0.8) converge to low loss;
// bad combos plateau high — giving the pruner something to cut.
let mut loss = 1.0;
for epoch in 0..n_epochs {
let lr_penalty = (lr.log10() - 0.01_f64.log10()).powi(2); // 0 at lr=0.01
let mom_penalty = (mom - 0.8).powi(2); // 0 at momentum=0.8
let base_loss = 0.02 + 0.05 * lr_penalty + 1.5 * mom_penalty;
let progress = (epoch as f64 + 1.0) / n_epochs as f64;
// Loss decays from 1.0 toward base_loss over epochs.
loss = base_loss + (1.0 - base_loss) * (-3.5 * progress).exp();
// Report the intermediate value so the pruner can evaluate this trial.
trial.report(epoch, loss);
// Check whether the pruner recommends stopping this trial early.
if trial.should_prune() {
// Signal that this trial was pruned — the study records it as Pruned.
Err(TrialPruned)?;
}
}
Ok::<_, Error>(loss)
},
// --- Callback: early stopping when we hit the target ---
|study, completed_trial| {
let n_complete = study.n_trials();
let n_pruned = study
.trials()
.iter()
.filter(|t| t.state == TrialState::Pruned)
.count();
match completed_trial.state {
TrialState::Pruned => {
println!(
" Trial {:>3} PRUNED at epoch {} (loss = {:.4}) \
[{n_complete} done, {n_pruned} pruned]",
completed_trial.id,
completed_trial.intermediate_values.len(),
completed_trial
.intermediate_values
.last()
.map_or(f64::NAN, |v| v.1),
);
}
TrialState::Complete => {
println!(
" Trial {:>3} complete: loss = {:.4} \
[{n_complete} done, {n_pruned} pruned]",
completed_trial.id, completed_trial.value,
);
}
_ => {}
}
// Stop the entire study once we find a good enough result.
if completed_trial.state == TrialState::Complete && completed_trial.value < target_loss
{
println!("\n Early stopping: reached target loss {target_loss}!");
return ControlFlow::Break(());
}
ControlFlow::Continue(())
},
)?;
// --- Results ---
let best = study.best_trial().expect("at least one completed trial");
let total = study.n_trials();
let pruned = study
.trials()
.iter()
.filter(|t| t.state == TrialState::Pruned)
.count();
println!("\n--- Results ---");
println!(" Total trials : {total}");
println!(" Pruned : {pruned}");
println!(" Completed : {}", total - pruned);
println!(" Best trial #{}: loss = {:.6}", best.id, best.value);
println!(
" learning_rate = {:.6}",
best.get(&learning_rate).unwrap()
);
println!(" momentum = {:.4}", best.get(&momentum).unwrap());
Ok(())
}
+1 -17
View File
@@ -40,10 +40,7 @@ fn run_study(study: Study<f64>, n_trials: usize) -> f64 {
fn main() {
let n_trials: usize = 100;
println!("Comparing samplers on Sphere(x, y) = x² + y²");
println!(" Search space: x ∈ [-5, 5], y ∈ [-3, 3]");
println!(" Known minimum: f(0, 0) = 0");
println!(" Trials per sampler: {n_trials}");
println!("Comparing samplers on Sphere(x, y) = x² + y² ({n_trials} trials each)");
println!();
// --- Random sampler (baseline) ---
@@ -78,17 +75,4 @@ fn main() {
println!(" Random : {random_best:.6}");
println!(" TPE : {tpe_best:.6}");
println!(" Grid : {grid_best:.6}");
println!();
// Find the winner
let results = [
("Random", random_best),
("TPE", tpe_best),
("Grid", grid_best),
];
let (winner, _) = results
.iter()
.min_by(|a, b| a.1.partial_cmp(&b.1).unwrap())
.unwrap();
println!("Winner: {winner} (closest to known minimum of 0.0)");
}